mirror of
https://github.com/bitwarden/web
synced 2025-12-06 00:03:28 +00:00
Compare commits
119 Commits
snyk-fix-4
...
feature/ap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1258989f8a | ||
|
|
58d9ac5ebc | ||
|
|
eab478da0c | ||
|
|
5a78853de5 | ||
|
|
b4ddce1da2 | ||
|
|
6ee47f0057 | ||
|
|
7b55c8ad1a | ||
|
|
30057d2ac4 | ||
|
|
6f7b712bc7 | ||
|
|
45da771404 | ||
|
|
902c620eb6 | ||
|
|
ca35ccbd35 | ||
|
|
85aa4274f3 | ||
|
|
ffb63a1cc7 | ||
|
|
3501be9484 | ||
|
|
5d1522b77a | ||
|
|
be30d47038 | ||
|
|
888892b3e7 | ||
|
|
f4f3e8c574 | ||
|
|
3367736c7e | ||
|
|
e3e7fce70a | ||
|
|
74bdfe2602 | ||
|
|
9e01d47a3f | ||
|
|
67de7d4bfb | ||
|
|
f5245a280e | ||
|
|
1dc9502676 | ||
|
|
ccf0d64a7b | ||
|
|
dc2078ae58 | ||
|
|
da470ad709 | ||
|
|
9f977cfc68 | ||
|
|
9627782a04 | ||
|
|
c5877cd063 | ||
|
|
136e8897ae | ||
|
|
81c6a4b1df | ||
|
|
da62cec6f0 | ||
|
|
474df5ba5e | ||
|
|
2c609fc6fd | ||
|
|
f8a2fae82b | ||
|
|
2f04c07262 | ||
|
|
f81195c920 | ||
|
|
d031b53c74 | ||
|
|
468007a984 | ||
|
|
bc054236ad | ||
|
|
1c31d090a3 | ||
|
|
f8d942c02c | ||
|
|
248938ca00 | ||
|
|
06d95bb224 | ||
|
|
446f2027b4 | ||
|
|
1f0d496f21 | ||
|
|
2b03162bfd | ||
|
|
f586359610 | ||
|
|
96641cf195 | ||
|
|
572758c598 | ||
|
|
df7db8ad07 | ||
|
|
0439d37c14 | ||
|
|
97f38aa654 | ||
|
|
0444b78ad1 | ||
|
|
705251fbe2 | ||
|
|
27853481d8 | ||
|
|
c0511f25ca | ||
|
|
1be62ac222 | ||
|
|
8304104a7a | ||
|
|
56808a7dbb | ||
|
|
609c13faf4 | ||
|
|
62b20a5c6d | ||
|
|
d56bf1211e | ||
|
|
8831f96fc2 | ||
|
|
f26dc27515 | ||
|
|
cb8a40d9cd | ||
|
|
2652a2deae | ||
|
|
e1c0c9f009 | ||
|
|
612442c1bb | ||
|
|
23b02a770a | ||
|
|
42ececbcf5 | ||
|
|
11034de7d1 | ||
|
|
571aaf31c4 | ||
|
|
0884e2d761 | ||
|
|
00975e6896 | ||
|
|
2c43249e98 | ||
|
|
575847f252 | ||
|
|
d6c181c997 | ||
|
|
9bb004923c | ||
|
|
e08726463e | ||
|
|
fdf93b610c | ||
|
|
144038ed1c | ||
|
|
5cb5e37270 | ||
|
|
e266a740ba | ||
|
|
3b0fc94239 | ||
|
|
32e27b5f08 | ||
|
|
317c40386f | ||
|
|
c9eeca7def | ||
|
|
902c568c09 | ||
|
|
153870693b | ||
|
|
a8cd2a6cf7 | ||
|
|
7404da9b3c | ||
|
|
9b40ce1024 | ||
|
|
80ffa965e1 | ||
|
|
57f1a5e380 | ||
|
|
18f1929f65 | ||
|
|
5cb3941190 | ||
|
|
0e515bc6c1 | ||
|
|
e103ddf02f | ||
|
|
8242989b9d | ||
|
|
5e7d94efb8 | ||
|
|
3bc8955dd5 | ||
|
|
bc05d27082 | ||
|
|
e93c155885 | ||
|
|
1076749635 | ||
|
|
06e1af6d48 | ||
|
|
cf9a90d10e | ||
|
|
6e8c15bccd | ||
|
|
7d018e4b59 | ||
|
|
f832cb4138 | ||
|
|
b8a23cf014 | ||
|
|
d0c0e80b6c | ||
|
|
98fb71fcb6 | ||
|
|
1b52b5a98a | ||
|
|
c3e5c74253 | ||
|
|
df5b175cdf |
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -21,10 +21,6 @@
|
|||||||
|
|
||||||
<!--Required for any UI changes. Delete if not applicable-->
|
<!--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
|
## Before you submit
|
||||||
|
|
||||||
- [ ] I have checked for **linting** errors (`npm run lint`) (required)
|
- [ ] I have checked for **linting** errors (`npm run lint`) (required)
|
||||||
|
|||||||
191
.github/workflows/build.yml
vendored
191
.github/workflows/build.yml
vendored
@@ -11,6 +11,10 @@ on:
|
|||||||
branches-ignore:
|
branches-ignore:
|
||||||
- "l10n_master"
|
- "l10n_master"
|
||||||
- "gh-pages"
|
- "gh-pages"
|
||||||
|
- "deploy"
|
||||||
|
paths-ignore:
|
||||||
|
- '.github/workflows/**'
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cloc:
|
cloc:
|
||||||
@@ -28,6 +32,28 @@ jobs:
|
|||||||
- name: Print lines of code
|
- name: Print lines of code
|
||||||
run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
|
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:
|
setup:
|
||||||
name: Setup
|
name: Setup
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
@@ -41,24 +67,25 @@ jobs:
|
|||||||
id: version
|
id: version
|
||||||
run: echo "::set-output name=value::${GITHUB_SHA:0:7}"
|
run: echo "::set-output name=value::${GITHUB_SHA:0:7}"
|
||||||
|
|
||||||
|
|
||||||
build-oss-selfhost:
|
build-oss-selfhost:
|
||||||
name: Build OSS zip
|
name: Build OSS zip
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: setup
|
needs:
|
||||||
|
- setup
|
||||||
|
- lint
|
||||||
env:
|
env:
|
||||||
_VERSION: ${{ needs.setup.outputs.version }}
|
_VERSION: ${{ needs.setup.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Node
|
- name: Checkout repo
|
||||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||||
with:
|
|
||||||
node-version: "16"
|
|
||||||
|
|
||||||
- name: Cache npm
|
- name: Set up Node
|
||||||
id: npm-cache
|
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
|
||||||
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
|
||||||
with:
|
with:
|
||||||
path: "~/.npm"
|
cache: 'npm'
|
||||||
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
node-version: "16"
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
@@ -70,9 +97,6 @@ jobs:
|
|||||||
echo "GitHub ref: $GITHUB_REF"
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
@@ -88,24 +112,25 @@ jobs:
|
|||||||
path: ./web-${{ env._VERSION }}-selfhosted-open-source.zip
|
path: ./web-${{ env._VERSION }}-selfhosted-open-source.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
|
||||||
build-cloud:
|
build-cloud:
|
||||||
name: Build Cloud zip
|
name: Build Cloud zip
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: setup
|
needs:
|
||||||
|
- setup
|
||||||
|
- lint
|
||||||
env:
|
env:
|
||||||
_VERSION: ${{ needs.setup.outputs.version }}
|
_VERSION: ${{ needs.setup.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Node
|
- name: Checkout repo
|
||||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||||
with:
|
|
||||||
node-version: "16"
|
|
||||||
|
|
||||||
- name: Cache npm
|
- name: Set up Node
|
||||||
id: npm-cache
|
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
|
||||||
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
|
||||||
with:
|
with:
|
||||||
path: "~/.npm"
|
cache: 'npm'
|
||||||
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
node-version: "16"
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
@@ -117,9 +142,6 @@ jobs:
|
|||||||
echo "GitHub ref: $GITHUB_REF"
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
@@ -135,24 +157,25 @@ jobs:
|
|||||||
path: ./web-${{ env._VERSION }}-cloud-COMMERCIAL.zip
|
path: ./web-${{ env._VERSION }}-cloud-COMMERCIAL.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
|
||||||
build-commercial-selfhost:
|
build-commercial-selfhost:
|
||||||
name: Build SelfHost Docker image
|
name: Build SelfHost Docker image
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: setup
|
needs:
|
||||||
|
- setup
|
||||||
|
- lint
|
||||||
env:
|
env:
|
||||||
_VERSION: ${{ needs.setup.outputs.version }}
|
_VERSION: ${{ needs.setup.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Node
|
- name: Checkout repo
|
||||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||||
with:
|
|
||||||
node-version: "16"
|
|
||||||
|
|
||||||
- name: Cache npm
|
- name: Set up Node
|
||||||
id: npm-cache
|
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
|
||||||
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
|
||||||
with:
|
with:
|
||||||
path: "~/.npm"
|
cache: 'npm'
|
||||||
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
node-version: "16"
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
@@ -165,16 +188,13 @@ jobs:
|
|||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
- name: Setup DCT
|
- name: Setup DCT
|
||||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix'
|
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
|
||||||
id: setup-dct
|
id: setup-dct
|
||||||
uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff
|
uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff
|
||||||
with:
|
with:
|
||||||
azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||||
azure-keyvault-name: "bitwarden-prod-kv"
|
azure-keyvault-name: "bitwarden-prod-kv"
|
||||||
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
@@ -209,11 +229,11 @@ jobs:
|
|||||||
run: docker tag bitwarden/web bitwarden/web:dev
|
run: docker tag bitwarden/web bitwarden/web:dev
|
||||||
|
|
||||||
- name: Tag hotfix branch
|
- name: Tag hotfix branch
|
||||||
if: github.ref == 'refs/heads/hotfix'
|
if: github.ref == 'refs/heads/hotfix-rc'
|
||||||
run: docker tag bitwarden/web bitwarden/web:hotfix
|
run: docker tag bitwarden/web bitwarden/web:hotfix-rc
|
||||||
|
|
||||||
- name: List Docker images
|
- name: List Docker images
|
||||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix'
|
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
|
||||||
run: docker images
|
run: docker images
|
||||||
|
|
||||||
- name: Push rc image
|
- name: Push rc image
|
||||||
@@ -231,31 +251,58 @@ jobs:
|
|||||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
|
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
|
||||||
|
|
||||||
- name: Push hotfix image
|
- name: Push hotfix image
|
||||||
if: github.ref == 'refs/heads/hotfix'
|
if: github.ref == 'refs/heads/hotfix-rc'
|
||||||
run: docker push bitwarden/web:hotfix
|
run: docker push bitwarden/web:hotfix-rc
|
||||||
env:
|
env:
|
||||||
DOCKER_CONTENT_TRUST: 1
|
DOCKER_CONTENT_TRUST: 1
|
||||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
|
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
|
||||||
|
|
||||||
- name: Log out of Docker
|
- name: Log out of Docker
|
||||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix'
|
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
|
||||||
run: docker logout
|
run: docker logout
|
||||||
|
|
||||||
|
|
||||||
build-qa:
|
build-qa:
|
||||||
name: Build Docker images for QA environment
|
name: Build Docker images for QA environment
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
needs:
|
||||||
|
- setup
|
||||||
|
- lint
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Node
|
- name: Checkout repo
|
||||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||||
with:
|
|
||||||
node-version: "16"
|
|
||||||
|
|
||||||
- name: Cache npm
|
- name: Set up Node
|
||||||
id: npm-cache
|
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
|
||||||
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
|
||||||
with:
|
with:
|
||||||
path: "~/.npm"
|
cache: 'npm'
|
||||||
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
cache-dependency-path: '**/package-lock.json'
|
||||||
|
node-version: "16"
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
@@ -275,9 +322,6 @@ jobs:
|
|||||||
- name: Log into container registry
|
- name: Log into container registry
|
||||||
run: az acr login -n bitwardenqa
|
run: az acr login -n bitwardenqa
|
||||||
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
@@ -333,34 +377,29 @@ jobs:
|
|||||||
- name: Log out of Docker
|
- name: Log out of Docker
|
||||||
run: docker logout
|
run: docker logout
|
||||||
|
|
||||||
|
|
||||||
windows:
|
windows:
|
||||||
name: Test code on Windows
|
name: Test code on Windows
|
||||||
runs-on: windows-2019
|
runs-on: windows-2019
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||||
|
|
||||||
- name: Set up NuGet
|
- name: Set up NuGet
|
||||||
uses: nuget/setup-nuget@04b0c2b8d1b97922f67eca497d7cf0bf17b8ffe1
|
uses: nuget/setup-nuget@04b0c2b8d1b97922f67eca497d7cf0bf17b8ffe1
|
||||||
with:
|
with:
|
||||||
nuget-version: "latest"
|
nuget-version: "latest"
|
||||||
|
|
||||||
- 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
|
- name: Set up Node
|
||||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
|
||||||
with:
|
with:
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: '**/package-lock.json'
|
||||||
node-version: "16"
|
node-version: "16"
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
nuget help | grep Version
|
nuget help | grep Version
|
||||||
msbuild -version
|
|
||||||
node --version
|
node --version
|
||||||
npm --version
|
npm --version
|
||||||
echo "GitHub ref: $GITHUB_REF"
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
@@ -369,18 +408,13 @@ jobs:
|
|||||||
GITHUB_REF: ${{ github.ref }}
|
GITHUB_REF: ${{ github.ref }}
|
||||||
GITHUB_EVENT: ${{ github.event_name }}
|
GITHUB_EVENT: ${{ github.event_name }}
|
||||||
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Run linter
|
|
||||||
run: npm run lint
|
|
||||||
|
|
||||||
- name: NPM build
|
- name: NPM build
|
||||||
run: npm run build:bit:cloud
|
run: npm run build:bit:cloud
|
||||||
|
|
||||||
|
|
||||||
crowdin-push:
|
crowdin-push:
|
||||||
name: Crowdin Push
|
name: Crowdin Push
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/master'
|
||||||
@@ -419,6 +453,7 @@ jobs:
|
|||||||
upload_sources: true
|
upload_sources: true
|
||||||
upload_translations: false
|
upload_translations: false
|
||||||
|
|
||||||
|
|
||||||
check-failures:
|
check-failures:
|
||||||
name: Check for failures
|
name: Check for failures
|
||||||
if: always()
|
if: always()
|
||||||
@@ -426,6 +461,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- cloc
|
- cloc
|
||||||
- setup
|
- setup
|
||||||
|
- lint
|
||||||
- build-oss-selfhost
|
- build-oss-selfhost
|
||||||
- build-cloud
|
- build-cloud
|
||||||
- build-commercial-selfhost
|
- build-commercial-selfhost
|
||||||
@@ -437,6 +473,7 @@ jobs:
|
|||||||
if: ${{ (github.ref == 'refs/heads/master') || (github.ref == 'refs/heads/rc') }}
|
if: ${{ (github.ref == 'refs/heads/master') || (github.ref == 'refs/heads/rc') }}
|
||||||
env:
|
env:
|
||||||
CLOC_STATUS: ${{ needs.cloc.result }}
|
CLOC_STATUS: ${{ needs.cloc.result }}
|
||||||
|
LINT_STATUS: ${{ needs.lint.result }}
|
||||||
SETUP_STATUS: ${{ needs.setup.result }}
|
SETUP_STATUS: ${{ needs.setup.result }}
|
||||||
BUILD_OSS_SELFHOST_STATUS: ${{ needs.build-oss-selfhost.result }}
|
BUILD_OSS_SELFHOST_STATUS: ${{ needs.build-oss-selfhost.result }}
|
||||||
BUILD_CLOUD_STATUS: ${{ needs.build-cloud.result }}
|
BUILD_CLOUD_STATUS: ${{ needs.build-cloud.result }}
|
||||||
@@ -447,6 +484,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
if [ "$CLOC_STATUS" = "failure" ]; then
|
if [ "$CLOC_STATUS" = "failure" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
|
elif [ "$LINT_STATUS" = "failure" ]; then
|
||||||
|
exit 1
|
||||||
elif [ "$SETUP_STATUS" = "failure" ]; then
|
elif [ "$SETUP_STATUS" = "failure" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
elif [ "$BUILD_OSS_SELFHOST_STATUS" = "failure" ]; then
|
elif [ "$BUILD_OSS_SELFHOST_STATUS" = "failure" ]; then
|
||||||
|
|||||||
192
.github/workflows/release.yml
vendored
192
.github/workflows/release.yml
vendored
@@ -12,6 +12,7 @@ on:
|
|||||||
options:
|
options:
|
||||||
- Initial Release
|
- Initial Release
|
||||||
- Redeploy
|
- Redeploy
|
||||||
|
- Dry Run
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
setup:
|
setup:
|
||||||
@@ -23,16 +24,17 @@ jobs:
|
|||||||
branch_name: ${{ steps.branch.outputs.branch_name }}
|
branch_name: ${{ steps.branch.outputs.branch_name }}
|
||||||
steps:
|
steps:
|
||||||
- name: Branch check
|
- name: Branch check
|
||||||
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
run: |
|
run: |
|
||||||
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix" ]]; then
|
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then
|
||||||
echo "==================================="
|
echo "==================================="
|
||||||
echo "[!] Can only release from the 'rc' or 'hotfix' branches"
|
echo "[!] Can only release from the 'rc' or 'hotfix-rc' branches"
|
||||||
echo "==================================="
|
echo "==================================="
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # 2.3.4
|
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # 2.4.0
|
||||||
|
|
||||||
- name: Check Release Version
|
- name: Check Release Version
|
||||||
id: version
|
id: version
|
||||||
@@ -57,6 +59,7 @@ jobs:
|
|||||||
BRANCH_NAME=$(basename ${{ github.ref }})
|
BRANCH_NAME=$(basename ${{ github.ref }})
|
||||||
echo "::set-output name=branch_name::$BRANCH_NAME"
|
echo "::set-output name=branch_name::$BRANCH_NAME"
|
||||||
|
|
||||||
|
|
||||||
self-host:
|
self-host:
|
||||||
name: Release self-host docker
|
name: Release self-host docker
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
@@ -64,6 +67,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
_BRANCH_NAME: ${{ needs.setup.outputs.branch_name }}
|
_BRANCH_NAME: ${{ needs.setup.outputs.branch_name }}
|
||||||
_RELEASE_VERSION: ${{ needs.setup.outputs.release_version }}
|
_RELEASE_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||||
|
_RELEASE_OPTION: ${{ github.event.inputs.release_type }}
|
||||||
steps:
|
steps:
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
@@ -71,7 +75,12 @@ jobs:
|
|||||||
docker --version
|
docker --version
|
||||||
echo "GitHub ref: $GITHUB_REF"
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
echo "Github Release Option: $_RELEASE_OPTION"
|
||||||
|
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||||
|
|
||||||
|
########## DockerHub ##########
|
||||||
- name: Setup DCT
|
- name: Setup DCT
|
||||||
id: setup-dct
|
id: setup-dct
|
||||||
uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff
|
uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff
|
||||||
@@ -79,21 +88,25 @@ jobs:
|
|||||||
azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||||
azure-keyvault-name: "bitwarden-prod-kv"
|
azure-keyvault-name: "bitwarden-prod-kv"
|
||||||
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
|
||||||
|
|
||||||
- name: Pull latest selfhost image
|
- name: Pull latest selfhost image
|
||||||
run: docker pull bitwarden/web:$_BRANCH_NAME
|
run: |
|
||||||
|
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
|
||||||
|
docker pull bitwarden/web:latest
|
||||||
|
else
|
||||||
|
docker pull bitwarden/web:$_BRANCH_NAME
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Tag version and latest
|
- name: Tag version and latest
|
||||||
run: |
|
run: |
|
||||||
docker tag bitwarden/web:$_BRANCH_NAME bitwarden/web:$_RELEASE_VERSION
|
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
|
||||||
docker tag bitwarden/web:$_BRANCH_NAME bitwarden/web:latest
|
docker tag bitwarden/web:latest bitwarden/web:dryrun
|
||||||
|
else
|
||||||
- name: List Docker images
|
docker tag bitwarden/web:$_BRANCH_NAME bitwarden/web:$_RELEASE_VERSION
|
||||||
run: docker images
|
docker tag bitwarden/web:$_BRANCH_NAME bitwarden/web:latest
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Push version and latest image
|
- name: Push version and latest image
|
||||||
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
env:
|
env:
|
||||||
DOCKER_CONTENT_TRUST: 1
|
DOCKER_CONTENT_TRUST: 1
|
||||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
|
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
|
||||||
@@ -101,11 +114,51 @@ jobs:
|
|||||||
docker push bitwarden/web:$_RELEASE_VERSION
|
docker push bitwarden/web:$_RELEASE_VERSION
|
||||||
docker push bitwarden/web:latest
|
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
|
- name: Log out of Docker
|
||||||
run: docker logout
|
run: docker logout
|
||||||
|
|
||||||
|
|
||||||
ghpages-deploy:
|
ghpages-deploy:
|
||||||
name: Deploy Web Vault
|
name: Deploy Web Vault to GitHub Pages
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs:
|
needs:
|
||||||
- setup
|
- setup
|
||||||
@@ -115,17 +168,17 @@ jobs:
|
|||||||
_TAG_VERSION: ${{ needs.setup.outputs.tag_version }}
|
_TAG_VERSION: ${{ needs.setup.outputs.tag_version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repo
|
- name: Checkout Repo
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
|
||||||
with:
|
with:
|
||||||
ref: gh-pages
|
ref: gh-pages
|
||||||
|
|
||||||
- name: Create deploy branch
|
- name: Create gh-pages-deploy branch
|
||||||
run: |
|
run: |
|
||||||
git switch -c deploy-$_TAG_VERSION
|
git switch -c gh-pages-deploy-$_TAG_VERSION
|
||||||
git push -u origin deploy-$_TAG_VERSION
|
git push -u origin gh-pages-deploy-$_TAG_VERSION
|
||||||
|
|
||||||
- name: Checkout Repo
|
- name: Checkout Repo
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
|
||||||
|
|
||||||
- name: Setup git config
|
- name: Setup git config
|
||||||
run: |
|
run: |
|
||||||
@@ -135,7 +188,7 @@ jobs:
|
|||||||
git config --global url."https://".insteadOf ssh://
|
git config --global url."https://".insteadOf ssh://
|
||||||
|
|
||||||
- name: Download latest cloud asset
|
- name: Download latest cloud asset
|
||||||
uses: bitwarden/gh-actions/download-artifacts@23433be15ed6fd046ce12b6889c5184a8d9c8783
|
uses: bitwarden/gh-actions/download-artifacts@c1fa8e09871a860862d6bbe36184b06d2c7e35a8
|
||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
@@ -147,25 +200,92 @@ jobs:
|
|||||||
run: unzip web-*-cloud-COMMERCIAL.zip
|
run: unzip web-*-cloud-COMMERCIAL.zip
|
||||||
|
|
||||||
- name: Deploy GitHub Pages
|
- name: Deploy GitHub Pages
|
||||||
uses: crazy-max/ghaction-github-pages@db4476a01402e1a7ce05f41832040eef16d14925 # v2.5.0
|
uses: crazy-max/ghaction-github-pages@a117e4aa1fb4854d021546d2abdfac95be568a3a # v2.6.0
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
target_branch: deploy-${{ needs.setup.outputs.tag_version }}
|
target_branch: gh-pages-deploy-${{ needs.setup.outputs.tag_version }}
|
||||||
build_dir: build
|
build_dir: build
|
||||||
keep_history: true
|
keep_history: true
|
||||||
commit_message: "Staging deploy ${{ needs.setup.outputs.release_version }}"
|
commit_message: "Staging deploy ${{ needs.setup.outputs.release_version }}"
|
||||||
|
dry_run: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||||
|
|
||||||
- name: Create Deploy PR
|
- name: Create GitHub Pages Deploy PR
|
||||||
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
env:
|
env:
|
||||||
PR_BRANCH: deploy-${{ env._TAG_VERSION }}
|
PR_BRANCH: gh-pages-deploy-${{ env._TAG_VERSION }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
gh pr create --title "Deploy $_RELEASE_VERSION" \
|
gh pr create --title "Deploy $_RELEASE_VERSION to GitHub Pages" \
|
||||||
--body "Deploying $_RELEASE_VERSION" \
|
--body "Deploying $_RELEASE_VERSION" \
|
||||||
--base gh-pages \
|
--base gh-pages \
|
||||||
--head "$PR_BRANCH"
|
--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:
|
release:
|
||||||
name: Create GitHub Release
|
name: Create GitHub Release
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
@@ -173,6 +293,7 @@ jobs:
|
|||||||
- setup
|
- setup
|
||||||
- self-host
|
- self-host
|
||||||
- ghpages-deploy
|
- ghpages-deploy
|
||||||
|
- cfpages-deploy
|
||||||
steps:
|
steps:
|
||||||
- name: Download latest build artifacts
|
- name: Download latest build artifacts
|
||||||
uses: bitwarden/gh-actions/download-artifacts@23433be15ed6fd046ce12b6889c5184a8d9c8783
|
uses: bitwarden/gh-actions/download-artifacts@23433be15ed6fd046ce12b6889c5184a8d9c8783
|
||||||
@@ -189,7 +310,8 @@ jobs:
|
|||||||
mv web-*-selfhosted-open-source.zip web-${{ needs.setup.outputs.release_version }}-selfhosted-open-source.zip
|
mv web-*-selfhosted-open-source.zip web-${{ needs.setup.outputs.release_version }}-selfhosted-open-source.zip
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
uses: ncipollo/release-action@95215a3cb6e6a1908b3c44e00b4fdb15548b1e09
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
|
uses: ncipollo/release-action@40bb172bd05f266cf9ba4ff965cb61e9ee5f6d01
|
||||||
with:
|
with:
|
||||||
name: "Version ${{ needs.setup.outputs.release_version }}"
|
name: "Version ${{ needs.setup.outputs.release_version }}"
|
||||||
commit: ${{ github.sha }}
|
commit: ${{ github.sha }}
|
||||||
@@ -199,3 +321,23 @@ jobs:
|
|||||||
web-${{ needs.setup.outputs.release_version }}-selfhosted-open-source.zip"
|
web-${{ needs.setup.outputs.release_version }}-selfhosted-open-source.zip"
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
draft: true
|
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
|
||||||
|
|||||||
@@ -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
|
- **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
|
- **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)
|
- **Write documentation:** Submit a pull request to the [Bitwarden help repository](https://github.com/bitwarden/help)
|
||||||
- **Help other users:** Go to the [User-to-User Support category](https://community.bitwarden.com/c/support/) on the Community Forums
|
- **Help other users:** Go to the [Ask the Bitwarden Community category](https://community.bitwarden.com/c/support/) on the Community Forums
|
||||||
- **Translate:** See the localization (l10n) section below
|
- **Translate:** See the localization (l10n) section below
|
||||||
|
|
||||||
## Contributor Agreement
|
## 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 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/kspearrin).
|
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).
|
||||||
|
|
||||||
You can read Crowdin's getting started guide for translators here: https://support.crowdin.com/crowdin-intro/
|
You can read Crowdin's getting started guide for translators here: https://support.crowdin.com/crowdin-intro/
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
> **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">
|
<p align="center">
|
||||||
<img src="https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/web-vault-macbook.png" alt="" width="600" height="358" />
|
<img src="https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/web-vault-macbook.png" alt="" width="600" height="358" />
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
42
SECURITY.md
42
SECURITY.md
@@ -1,39 +1,11 @@
|
|||||||
Bitwarden believes that working with security researchers across the globe is crucial to keeping our
|
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!
|
||||||
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
|
# Disclosure Policy
|
||||||
|
|
||||||
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every
|
- 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.
|
||||||
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.
|
||||||
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a
|
- 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.
|
||||||
third-party. We may publicly disclose the issue before resolving it, if appropriate.
|
- If you would like to encrypt your report, please use the PGP key with long ID `0xDE6887086F892325FEC04CC0D847525B6931381F` (available in the public keyserver pool).
|
||||||
- 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:
|
While researching, we'd like to ask you to refrain from:
|
||||||
|
|
||||||
@@ -42,4 +14,8 @@ While researching, we'd like to ask you to refrain from:
|
|||||||
- Social engineering (including phishing) of Bitwarden staff or contractors
|
- Social engineering (including phishing) of Bitwarden staff or contractors
|
||||||
- Any physical attempts against Bitwarden property or data centers
|
- 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!
|
Thank you for helping keep Bitwarden and our users safe!
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { DragDropModule } from "@angular/cdk/drag-drop";
|
import { DragDropModule } from "@angular/cdk/drag-drop";
|
||||||
|
import { OverlayModule } from "@angular/cdk/overlay";
|
||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||||
import { RouterModule } from "@angular/router";
|
import { RouterModule } from "@angular/router";
|
||||||
import { InfiniteScrollModule } from "ngx-infinite-scroll";
|
import { InfiniteScrollModule } from "ngx-infinite-scroll";
|
||||||
|
|
||||||
import { BitwardenToastModule } from "jslib-angular/components/toastr.component";
|
import { JslibModule } from "jslib-angular/jslib.module";
|
||||||
|
|
||||||
import { OssRoutingModule } from "src/app/oss-routing.module";
|
import { OssRoutingModule } from "src/app/oss-routing.module";
|
||||||
import { OssModule } from "src/app/oss.module";
|
import { OssModule } from "src/app/oss.module";
|
||||||
@@ -20,28 +21,25 @@ import { MaximumVaultTimeoutPolicyComponent } from "./policies/maximum-vault-tim
|
|||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
OverlayModule,
|
||||||
OssModule,
|
OssModule,
|
||||||
|
JslibModule,
|
||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
ServicesModule,
|
ServicesModule,
|
||||||
BitwardenToastModule.forRoot({
|
|
||||||
maxOpened: 5,
|
|
||||||
autoDismiss: true,
|
|
||||||
closeButton: true,
|
|
||||||
}),
|
|
||||||
InfiniteScrollModule,
|
InfiniteScrollModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
OssRoutingModule,
|
OssRoutingModule,
|
||||||
OrganizationsModule,
|
OrganizationsModule, // Must be after OssRoutingModule for competing routes to resolve properly
|
||||||
RouterModule,
|
RouterModule,
|
||||||
WildcardRoutingModule, // Needs to be last to catch all non-existing routes
|
WildcardRoutingModule, // Needs to be last to catch all non-existing routes
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
MaximumVaultTimeoutPolicyComponent,
|
|
||||||
DisablePersonalVaultExportPolicyComponent,
|
DisablePersonalVaultExportPolicyComponent,
|
||||||
|
MaximumVaultTimeoutPolicyComponent,
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import "jquery";
|
|||||||
import "popper.js";
|
import "popper.js";
|
||||||
|
|
||||||
require("src/scss/styles.scss");
|
require("src/scss/styles.scss");
|
||||||
|
require("src/scss/tailwind.css");
|
||||||
|
|
||||||
import { AppModule } from "./app.module";
|
import { AppModule } from "./app.module";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
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 {}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
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, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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[];
|
||||||
|
}
|
||||||
@@ -14,10 +14,9 @@
|
|||||||
<form
|
<form
|
||||||
#form
|
#form
|
||||||
(ngSubmit)="submit()"
|
(ngSubmit)="submit()"
|
||||||
[formGroup]="data"
|
[formGroup]="ssoConfigForm"
|
||||||
[appApiAction]="formPromise"
|
[appApiAction]="formPromise"
|
||||||
*ngIf="!loading"
|
*ngIf="!loading"
|
||||||
ngNativeValidate
|
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
{{ "ssoPolicyHelpStart" | i18n }}
|
{{ "ssoPolicyHelpStart" | i18n }}
|
||||||
@@ -27,462 +26,407 @@
|
|||||||
{{ "ssoPolicyHelpKeyConnector" | i18n }}
|
{{ "ssoPolicyHelpKeyConnector" | i18n }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="form-group">
|
<!-- Root form -->
|
||||||
<div class="form-check">
|
<ng-container>
|
||||||
<input
|
<app-input-checkbox
|
||||||
class="form-check-input"
|
controlId="enabled"
|
||||||
type="checkbox"
|
[formControl]="enabled"
|
||||||
id="enabled"
|
[label]="'allowSso' | i18n"
|
||||||
[formControl]="enabled"
|
[helperText]="'allowSsoDesc' | i18n"
|
||||||
name="Enabled"
|
></app-input-checkbox>
|
||||||
/>
|
|
||||||
<label class="form-check-label" for="enabled">{{ "allowSso" | i18n }}</label>
|
|
||||||
</div>
|
|
||||||
<small class="form-text text-muted">{{ "allowSsoDesc" | i18n }}</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<ng-container *ngIf="data.value.keyConnectorEnabled">
|
|
||||||
<app-callout type="warning" [useAlertRole]="true">
|
|
||||||
{{ "keyConnectorWarning" | i18n }}
|
|
||||||
</app-callout>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="keyConnectorUrl">{{ "keyConnectorUrl" | i18n }}</label>
|
<label>{{ "memberDecryptionOption" | i18n }}</label>
|
||||||
<div class="input-group">
|
<div class="form-check form-check-block">
|
||||||
<input
|
<input
|
||||||
class="form-control"
|
class="form-check-input"
|
||||||
formControlName="keyConnectorUrl"
|
type="radio"
|
||||||
id="keyConnectorUrl"
|
id="memberDecryptionPass"
|
||||||
required
|
[value]="false"
|
||||||
|
formControlName="keyConnectorEnabled"
|
||||||
/>
|
/>
|
||||||
<div class="input-group-append">
|
<label class="form-check-label" for="memberDecryptionPass">
|
||||||
<button
|
{{ "masterPass" | i18n }}
|
||||||
type="button"
|
<small>{{ "memberDecryptionPassDesc" | i18n }}</small>
|
||||||
class="btn btn-outline-secondary"
|
</label>
|
||||||
(click)="validateKeyConnectorUrl()"
|
</div>
|
||||||
[disabled]="!enableTestKeyConnector"
|
<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
|
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||||
class="bwi bwi-spinner bwi-spin"
|
</a>
|
||||||
title="{{ 'loading' | i18n }}"
|
<small>{{ "memberDecryptionKeyConnectorDesc" | i18n }}</small>
|
||||||
aria-hidden="true"
|
</label>
|
||||||
*ngIf="keyConnectorUrl.pending"
|
</div>
|
||||||
></i>
|
</div>
|
||||||
<span *ngIf="!keyConnectorUrl.pending">
|
|
||||||
{{ "keyConnectorTest" | i18n }}
|
<!-- Key Connector -->
|
||||||
</span>
|
<ng-container *ngIf="ssoConfigForm.get('keyConnectorEnabled').value">
|
||||||
</button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="keyConnectorUrl.pristine && !keyConnectorUrl.pending">
|
</ng-container>
|
||||||
<div class="text-danger" *ngIf="keyConnectorUrl.hasError('invalidUrl')" role="alert">
|
|
||||||
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
|
<app-select
|
||||||
{{ "keyConnectorTestFail" | i18n }}
|
controlId="type"
|
||||||
</div>
|
[label]="'type' | i18n"
|
||||||
<div class="text-success" *ngIf="!keyConnectorUrl.hasError('invalidUrl')" role="alert">
|
[selectOptions]="ssoTypeOptions"
|
||||||
<i class="bwi bwi-check-circle" aria-hidden="true"></i>
|
formControlName="configType"
|
||||||
{{ "keyConnectorTestSuccess" | i18n }}
|
>
|
||||||
</div>
|
</app-select>
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="type">{{ "type" | i18n }}</label>
|
|
||||||
<select class="form-control" id="type" formControlName="configType">
|
|
||||||
<option [ngValue]="0" disabled>{{ "selectType" | i18n }}</option>
|
|
||||||
<option [ngValue]="1">OpenID Connect</option>
|
|
||||||
<option [ngValue]="2">SAML 2.0</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- OIDC -->
|
<!-- OIDC -->
|
||||||
<div *ngIf="data.value.configType == 1">
|
<div
|
||||||
|
*ngIf="ssoConfigForm.get('configType').value === ssoType.OpenIdConnect"
|
||||||
|
[formGroup]="openIdForm"
|
||||||
|
>
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<h2>{{ "openIdConnectConfig" | i18n }}</h2>
|
<h2 class="secondary-header">{{ "openIdConnectConfig" | i18n }}</h2>
|
||||||
<div class="form-group">
|
|
||||||
<label>{{ "callbackPath" | i18n }}</label>
|
<app-input-text-readonly
|
||||||
<div class="input-group">
|
[label]="'callbackPath' | i18n"
|
||||||
<input class="form-control" readonly [value]="callbackPath" />
|
[controlValue]="callbackPath"
|
||||||
<div class="input-group-append">
|
></app-input-text-readonly>
|
||||||
<button
|
|
||||||
type="button"
|
<app-input-text-readonly
|
||||||
class="btn btn-outline-secondary"
|
[label]="'signedOutCallbackPath' | i18n"
|
||||||
appA11yTitle="{{ 'copyValue' | i18n }}"
|
[controlValue]="signedOutCallbackPath"
|
||||||
(click)="copy(callbackPath)"
|
></app-input-text-readonly>
|
||||||
>
|
|
||||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
<app-input-text
|
||||||
</button>
|
[label]="'authority' | i18n"
|
||||||
</div>
|
controlId="authority"
|
||||||
</div>
|
[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>
|
||||||
<div class="form-group">
|
<div id="customizations" [hidden]="!showOpenIdCustomizations">
|
||||||
<label>{{ "signedOutCallbackPath" | i18n }}</label>
|
<app-input-text
|
||||||
<div class="input-group">
|
[label]="'additionalScopes' | i18n"
|
||||||
<input class="form-control" readonly [value]="signedOutCallbackPath" />
|
controlId="additionalScopes"
|
||||||
<div class="input-group-append">
|
[helperText]="'separateMultipleWithComma' | i18n"
|
||||||
<button
|
formControlName="additionalScopes"
|
||||||
type="button"
|
></app-input-text>
|
||||||
class="btn btn-outline-secondary"
|
|
||||||
appA11yTitle="{{ 'copyValue' | i18n }}"
|
<app-input-text
|
||||||
(click)="copy(signedOutCallbackPath)"
|
[label]="'additionalUserIdClaimTypes' | i18n"
|
||||||
>
|
controlId="additionalUserIdClaimTypes"
|
||||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
[helperText]="'separateMultipleWithComma' | i18n"
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="authority">{{ "authority" | i18n }}</label>
|
|
||||||
<input class="form-control" formControlName="authority" id="authority" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="clientId">{{ "clientId" | i18n }}</label>
|
|
||||||
<input class="form-control" formControlName="clientId" id="clientId" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="clientSecret">{{ "clientSecret" | i18n }}</label>
|
|
||||||
<input class="form-control" formControlName="clientSecret" id="clientSecret" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="metadataAddress">{{ "metadataAddress" | i18n }}</label>
|
|
||||||
<input class="form-control" formControlName="metadataAddress" id="metadataAddress" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="redirectBehavior">{{ "oidcRedirectBehavior" | i18n }}</label>
|
|
||||||
<select class="form-control" formControlName="redirectBehavior" id="redirectBehavior">
|
|
||||||
<option [ngValue]="0">Redirect GET</option>
|
|
||||||
<option [ngValue]="1">Form POST</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="form-check">
|
|
||||||
<input
|
|
||||||
class="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
id="getClaimsFromUserInfoEndpoint"
|
|
||||||
formControlName="getClaimsFromUserInfoEndpoint"
|
|
||||||
/>
|
|
||||||
<label class="form-check-label" for="getClaimsFromUserInfoEndpoint">
|
|
||||||
{{ "getClaimsFromUserInfoEndpoint" | i18n }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="additionalScopes">{{ "additionalScopes" | i18n }}</label>
|
|
||||||
<input class="form-control" formControlName="additionalScopes" id="additionalScopes" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="additionalUserIdClaimTypes">{{ "additionalUserIdClaimTypes" | i18n }}</label>
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
formControlName="additionalUserIdClaimTypes"
|
formControlName="additionalUserIdClaimTypes"
|
||||||
id="additionalUserIdClaimTypes"
|
></app-input-text>
|
||||||
/>
|
|
||||||
</div>
|
<app-input-text
|
||||||
<div class="form-group">
|
[label]="'additionalEmailClaimTypes' | i18n"
|
||||||
<label for="additionalEmailClaimTypes">{{ "additionalEmailClaimTypes" | i18n }}</label>
|
controlId="additionalEmailClaimTypes"
|
||||||
<input
|
[helperText]="'separateMultipleWithComma' | i18n"
|
||||||
class="form-control"
|
|
||||||
formControlName="additionalEmailClaimTypes"
|
formControlName="additionalEmailClaimTypes"
|
||||||
id="additionalEmailClaimTypes"
|
></app-input-text>
|
||||||
/>
|
|
||||||
</div>
|
<app-input-text
|
||||||
<div class="form-group">
|
[label]="'additionalNameClaimTypes' | i18n"
|
||||||
<label for="additionalNameClaimTypes">{{ "additionalNameClaimTypes" | i18n }}</label>
|
controlId="additionalNameClaimTypes"
|
||||||
<input
|
[helperText]="'separateMultipleWithComma' | i18n"
|
||||||
class="form-control"
|
|
||||||
formControlName="additionalNameClaimTypes"
|
formControlName="additionalNameClaimTypes"
|
||||||
id="additionalNameClaimTypes"
|
></app-input-text>
|
||||||
/>
|
|
||||||
</div>
|
<app-input-text
|
||||||
<div class="form-group">
|
[label]="'acrValues' | i18n"
|
||||||
<label for="acrValues">{{ "acrValues" | i18n }}</label>
|
controlId="acrValues"
|
||||||
<input class="form-control" formControlName="acrValues" id="acrValues" />
|
helperText="acr_values"
|
||||||
</div>
|
formControlName="acrValues"
|
||||||
<div class="form-group">
|
></app-input-text>
|
||||||
<label for="expectedReturnAcrValue">{{ "expectedReturnAcrValue" | i18n }}</label>
|
|
||||||
<input
|
<app-input-text
|
||||||
class="form-control"
|
[label]="'expectedReturnAcrValue' | i18n"
|
||||||
|
controlId="expectedReturnAcrValue"
|
||||||
|
helperText="acr_validation"
|
||||||
formControlName="expectedReturnAcrValue"
|
formControlName="expectedReturnAcrValue"
|
||||||
id="expectedReturnAcrValue"
|
></app-input-text>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="data.value.configType == 2">
|
<!-- SAML2 SP -->
|
||||||
|
<div *ngIf="ssoConfigForm.get('configType').value === ssoType.Saml2" [formGroup]="samlForm">
|
||||||
<!-- SAML2 SP -->
|
<!-- SAML2 SP -->
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<h2>{{ "samlSpConfig" | i18n }}</h2>
|
<h2 class="secondary-header">{{ "samlSpConfig" | i18n }}</h2>
|
||||||
<div class="form-group">
|
|
||||||
<label>{{ "spEntityId" | i18n }}</label>
|
<app-input-text-readonly
|
||||||
<div class="input-group">
|
[label]="'spEntityId' | i18n"
|
||||||
<input class="form-control" readonly [value]="spEntityId" />
|
[controlValue]="spEntityId"
|
||||||
<div class="input-group-append">
|
></app-input-text-readonly>
|
||||||
<button
|
|
||||||
type="button"
|
<app-input-text-readonly
|
||||||
class="btn btn-outline-secondary"
|
[label]="'spMetadataUrl' | i18n"
|
||||||
appA11yTitle="{{ 'copyValue' | i18n }}"
|
[controlValue]="spMetadataUrl"
|
||||||
(click)="copy(spEntityId)"
|
[showLaunch]="true"
|
||||||
>
|
></app-input-text-readonly>
|
||||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
|
||||||
</button>
|
<app-input-text-readonly
|
||||||
</div>
|
[label]="'spAcsUrl' | i18n"
|
||||||
</div>
|
[controlValue]="spAcsUrl"
|
||||||
</div>
|
></app-input-text-readonly>
|
||||||
<div class="form-group">
|
|
||||||
<label>{{ "spMetadataUrl" | i18n }}</label>
|
<app-select
|
||||||
<div class="input-group">
|
controlId="spNameIdFormat"
|
||||||
<input class="form-control" readonly [value]="spMetadataUrl" />
|
[label]="'spNameIdFormat' | i18n"
|
||||||
<div class="input-group-append">
|
[selectOptions]="saml2NameIdFormatOptions"
|
||||||
<button
|
formControlName="spNameIdFormat"
|
||||||
type="button"
|
>
|
||||||
class="btn btn-outline-secondary"
|
</app-select>
|
||||||
appA11yTitle="{{ 'launch' | i18n }}"
|
|
||||||
(click)="launchUri(spMetadataUrl)"
|
<app-select
|
||||||
>
|
controlId="spOutboundSigningAlgorithm"
|
||||||
<i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i>
|
[label]="'spOutboundSigningAlgorithm' | i18n"
|
||||||
</button>
|
[selectOptions]="samlSigningAlgorithmOptions"
|
||||||
<button
|
formControlName="spOutboundSigningAlgorithm"
|
||||||
type="button"
|
>
|
||||||
class="btn btn-outline-secondary"
|
</app-select>
|
||||||
appA11yTitle="{{ 'copyValue' | i18n }}"
|
|
||||||
(click)="copy(spMetadataUrl)"
|
<app-select
|
||||||
>
|
controlId="spSigningBehavior"
|
||||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
[label]="'spSigningBehavior' | i18n"
|
||||||
</button>
|
[selectOptions]="saml2SigningBehaviourOptions"
|
||||||
</div>
|
formControlName="spSigningBehavior"
|
||||||
</div>
|
>
|
||||||
</div>
|
</app-select>
|
||||||
<div class="form-group">
|
|
||||||
<label>{{ "spAcsUrl" | i18n }}</label>
|
<app-select
|
||||||
<div class="input-group">
|
controlId="spMinIncomingSigningAlgorithm"
|
||||||
<input class="form-control" readonly [value]="spAcsUrl" />
|
[label]="'spMinIncomingSigningAlgorithm' | i18n"
|
||||||
<div class="input-group-append">
|
[selectOptions]="samlSigningAlgorithmOptions"
|
||||||
<button
|
formControlName="spMinIncomingSigningAlgorithm"
|
||||||
type="button"
|
>
|
||||||
class="btn btn-outline-secondary"
|
</app-select>
|
||||||
appA11yTitle="{{ 'copyValue' | i18n }}"
|
|
||||||
(click)="copy(spAcsUrl)"
|
<app-input-checkbox
|
||||||
>
|
controlId="spWantAssertionsSigned"
|
||||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
formControlName="spWantAssertionsSigned"
|
||||||
</button>
|
[label]="'spWantAssertionsSigned' | i18n"
|
||||||
</div>
|
></app-input-checkbox>
|
||||||
</div>
|
|
||||||
</div>
|
<app-input-checkbox
|
||||||
<div class="form-group">
|
controlId="spValidateCertificates"
|
||||||
<label for="spNameIdFormat">{{ "spNameIdFormat" | i18n }}</label>
|
formControlName="spValidateCertificates"
|
||||||
<select class="form-control" formControlName="spNameIdFormat" id="spNameIdFormat">
|
[label]="'spValidateCertificates' | i18n"
|
||||||
<option [ngValue]="0">Not Configured</option>
|
></app-input-checkbox>
|
||||||
<option [ngValue]="1">Unspecified</option>
|
|
||||||
<option [ngValue]="2">Email Address</option>
|
|
||||||
<option [ngValue]="3">X.509 Subject Name</option>
|
|
||||||
<option [ngValue]="4">Windows Domain Qualified Name</option>
|
|
||||||
<option [ngValue]="5">Kerberos Principal Name</option>
|
|
||||||
<option [ngValue]="6">Entity Identifier</option>
|
|
||||||
<option [ngValue]="7">Persistent</option>
|
|
||||||
<option [ngValue]="8">Transient</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="spOutboundSigningAlgorithm">{{ "spOutboundSigningAlgorithm" | i18n }}</label>
|
|
||||||
<select
|
|
||||||
class="form-control"
|
|
||||||
formControlName="spOutboundSigningAlgorithm"
|
|
||||||
id="spOutboundSigningAlgorithm"
|
|
||||||
>
|
|
||||||
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{ o }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="spSigningBehavior">{{ "spSigningBehavior" | i18n }}</label>
|
|
||||||
<select class="form-control" formControlName="spSigningBehavior" id="spSigningBehavior">
|
|
||||||
<option [ngValue]="0">If IdP Wants Authn Requests Signed</option>
|
|
||||||
<option [ngValue]="1">Always</option>
|
|
||||||
<option [ngValue]="3">Never</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="spMinIncomingSigningAlgorithm">{{
|
|
||||||
"spMinIncomingSigningAlgorithm" | i18n
|
|
||||||
}}</label>
|
|
||||||
<select
|
|
||||||
class="form-control"
|
|
||||||
formControlName="spMinIncomingSigningAlgorithm"
|
|
||||||
id="spMinIncomingSigningAlgorithm"
|
|
||||||
>
|
|
||||||
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{ o }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="form-check">
|
|
||||||
<input
|
|
||||||
class="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
id="spWantAssertionsSigned"
|
|
||||||
formControlName="spWantAssertionsSigned"
|
|
||||||
/>
|
|
||||||
<label class="form-check-label" for="spWantAssertionsSigned">
|
|
||||||
{{ "spWantAssertionsSigned" | i18n }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="form-check">
|
|
||||||
<input
|
|
||||||
class="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
id="spValidateCertificates"
|
|
||||||
formControlName="spValidateCertificates"
|
|
||||||
/>
|
|
||||||
<label class="form-check-label" for="spValidateCertificates">
|
|
||||||
{{ "spValidateCertificates" | i18n }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- SAML2 IDP -->
|
<!-- SAML2 IDP -->
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<h2>{{ "samlIdpConfig" | i18n }}</h2>
|
<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">
|
<div class="form-group">
|
||||||
<label for="idpEntityId">{{ "idpEntityId" | i18n }}</label>
|
<label for="idpX509PublicCert">
|
||||||
<input class="form-control" formControlName="idpEntityId" id="idpEntityId" />
|
{{ "idpX509PublicCert" | i18n }}
|
||||||
</div>
|
<small class="text-muted form-text d-inline">({{ "required" | i18n }})</small>
|
||||||
<div class="form-group">
|
</label>
|
||||||
<label for="idpBindingType">{{ "idpBindingType" | i18n }}</label>
|
|
||||||
<select class="form-control" formControlName="idpBindingType" id="idpBindingType">
|
|
||||||
<option [ngValue]="1">Redirect</option>
|
|
||||||
<option [ngValue]="2">HTTP POST</option>
|
|
||||||
<option [ngValue]="4">Artifact</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="idpSingleSignOnServiceUrl">{{ "idpSingleSignOnServiceUrl" | i18n }}</label>
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
formControlName="idpSingleSignOnServiceUrl"
|
|
||||||
id="idpSingleSignOnServiceUrl"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="idpSingleLogoutServiceUrl">{{ "idpSingleLogoutServiceUrl" | i18n }}</label>
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
formControlName="idpSingleLogoutServiceUrl"
|
|
||||||
id="idpSingleLogoutServiceUrl"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="idpArtifactResolutionServiceUrl">{{
|
|
||||||
"idpArtifactResolutionServiceUrl" | i18n
|
|
||||||
}}</label>
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
formControlName="idpArtifactResolutionServiceUrl"
|
|
||||||
id="idpArtifactResolutionServiceUrl"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="idpX509PublicCert">{{ "idpX509PublicCert" | i18n }}</label>
|
|
||||||
<textarea
|
<textarea
|
||||||
formControlName="idpX509PublicCert"
|
formControlName="idpX509PublicCert"
|
||||||
class="form-control form-control-sm text-monospace"
|
class="form-control form-control-sm text-monospace"
|
||||||
rows="6"
|
rows="6"
|
||||||
id="idpX509PublicCert"
|
id="idpX509PublicCert"
|
||||||
|
appA11yInvalid
|
||||||
|
aria-describedby="idpX509PublicCertDesc"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
<small
|
||||||
<div class="form-group">
|
id="idpX509PublicCertDesc"
|
||||||
<label for="idpOutboundSigningAlgorithm">{{ "idpOutboundSigningAlgorithm" | i18n }}</label>
|
class="error-inline"
|
||||||
<select
|
role="alert"
|
||||||
class="form-control"
|
*ngIf="samlForm.get('idpX509PublicCert').hasError('required')"
|
||||||
formControlName="idpOutboundSigningAlgorithm"
|
|
||||||
id="idpOutboundSigningAlgorithm"
|
|
||||||
>
|
>
|
||||||
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{ o }}</option>
|
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
|
||||||
</select>
|
<span class="sr-only">{{ "error" | i18n }}:</span>
|
||||||
</div>
|
{{ "fieldRequiredError" | i18n: ("idpX509PublicCert" | i18n) }}
|
||||||
<div class="form-group" [hidden]="true">
|
</small>
|
||||||
<!--TODO: Unhide once Unsolicited IdP Response is supported-->
|
|
||||||
<div class="form-check">
|
|
||||||
<input
|
|
||||||
class="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
id="idpAllowUnsolicitedAuthnResponse"
|
|
||||||
formControlName="idpAllowUnsolicitedAuthnResponse"
|
|
||||||
/>
|
|
||||||
<label class="form-check-label" for="idpAllowUnsolicitedAuthnResponse">
|
|
||||||
{{ "idpAllowUnsolicitedAuthnResponse" | i18n }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="form-check">
|
|
||||||
<input
|
|
||||||
class="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
id="idpDisableOutboundLogoutRequests"
|
|
||||||
formControlName="idpDisableOutboundLogoutRequests"
|
|
||||||
/>
|
|
||||||
<label class="form-check-label" for="idpDisableOutboundLogoutRequests">
|
|
||||||
{{ "idpDisableOutboundLogoutRequests" | i18n }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="form-check">
|
|
||||||
<input
|
|
||||||
class="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
id="idpWantAuthnRequestsSigned"
|
|
||||||
formControlName="idpWantAuthnRequestsSigned"
|
|
||||||
/>
|
|
||||||
<label class="form-check-label" for="idpWantAuthnRequestsSigned">
|
|
||||||
{{ "idpWantAuthnRequestsSigned" | i18n }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -490,4 +434,15 @@
|
|||||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||||
<span>{{ "save" | i18n }}</span>
|
<span>{{ "save" | i18n }}</span>
|
||||||
</button>
|
</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>
|
</form>
|
||||||
|
|||||||
@@ -1,27 +1,82 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { FormBuilder } from "@angular/forms";
|
import { AbstractControl, FormBuilder, FormGroup } from "@angular/forms";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
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 { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.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 { Organization } from "jslib-common/models/domain/organization";
|
||||||
import { OrganizationSsoRequest } from "jslib-common/models/request/organization/organizationSsoRequest";
|
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({
|
@Component({
|
||||||
selector: "app-org-manage-sso",
|
selector: "app-org-manage-sso",
|
||||||
templateUrl: "sso.component.html",
|
templateUrl: "sso.component.html",
|
||||||
})
|
})
|
||||||
export class SsoComponent implements OnInit {
|
export class SsoComponent implements OnInit {
|
||||||
samlSigningAlgorithms = [
|
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/2001/04/xmldsig-more#rsa-sha256",
|
||||||
"http://www.w3.org/2000/09/xmldsig#rsa-sha384",
|
"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-sha512",
|
||||||
"http://www.w3.org/2000/09/xmldsig#rsa-sha1",
|
"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;
|
loading = true;
|
||||||
|
haveTestedKeyConnector = false;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
organization: Organization;
|
organization: Organization;
|
||||||
formPromise: Promise<any>;
|
formPromise: Promise<any>;
|
||||||
@@ -33,44 +88,57 @@ export class SsoComponent implements OnInit {
|
|||||||
spAcsUrl: string;
|
spAcsUrl: string;
|
||||||
|
|
||||||
enabled = this.formBuilder.control(false);
|
enabled = this.formBuilder.control(false);
|
||||||
data = this.formBuilder.group({
|
|
||||||
configType: [],
|
|
||||||
|
|
||||||
keyConnectorEnabled: [],
|
openIdForm = this.formBuilder.group(
|
||||||
keyConnectorUrl: [],
|
{
|
||||||
|
authority: ["", dirtyRequired],
|
||||||
|
clientId: ["", dirtyRequired],
|
||||||
|
clientSecret: ["", dirtyRequired],
|
||||||
|
metadataAddress: [],
|
||||||
|
redirectBehavior: [OpenIdConnectRedirectBehavior.RedirectGet, dirtyRequired],
|
||||||
|
getClaimsFromUserInfoEndpoint: [],
|
||||||
|
additionalScopes: [],
|
||||||
|
additionalUserIdClaimTypes: [],
|
||||||
|
additionalEmailClaimTypes: [],
|
||||||
|
additionalNameClaimTypes: [],
|
||||||
|
acrValues: [],
|
||||||
|
expectedReturnAcrValue: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
updateOn: "blur",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// OpenId
|
samlForm = this.formBuilder.group(
|
||||||
authority: [],
|
{
|
||||||
clientId: [],
|
spNameIdFormat: [Saml2NameIdFormat.NotConfigured],
|
||||||
clientSecret: [],
|
spOutboundSigningAlgorithm: [defaultSigningAlgorithm],
|
||||||
metadataAddress: [],
|
spSigningBehavior: [Saml2SigningBehavior.IfIdpWantAuthnRequestsSigned],
|
||||||
redirectBehavior: [],
|
spMinIncomingSigningAlgorithm: [defaultSigningAlgorithm],
|
||||||
getClaimsFromUserInfoEndpoint: [],
|
spWantAssertionsSigned: [],
|
||||||
additionalScopes: [],
|
spValidateCertificates: [],
|
||||||
additionalUserIdClaimTypes: [],
|
|
||||||
additionalEmailClaimTypes: [],
|
|
||||||
additionalNameClaimTypes: [],
|
|
||||||
acrValues: [],
|
|
||||||
expectedReturnAcrValue: [],
|
|
||||||
|
|
||||||
// SAML
|
idpEntityId: ["", dirtyRequired],
|
||||||
spNameIdFormat: [],
|
idpBindingType: [Saml2BindingType.HttpRedirect],
|
||||||
spOutboundSigningAlgorithm: [],
|
idpSingleSignOnServiceUrl: [],
|
||||||
spSigningBehavior: [],
|
idpSingleLogoutServiceUrl: [],
|
||||||
spMinIncomingSigningAlgorithm: [],
|
idpX509PublicCert: ["", dirtyRequired],
|
||||||
spWantAssertionsSigned: [],
|
idpOutboundSigningAlgorithm: [defaultSigningAlgorithm],
|
||||||
spValidateCertificates: [],
|
idpAllowUnsolicitedAuthnResponse: [],
|
||||||
|
idpAllowOutboundLogoutRequests: [true],
|
||||||
|
idpWantAuthnRequestsSigned: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
updateOn: "blur",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
idpEntityId: [],
|
ssoConfigForm = this.formBuilder.group({
|
||||||
idpBindingType: [],
|
configType: [SsoType.None],
|
||||||
idpSingleSignOnServiceUrl: [],
|
keyConnectorEnabled: [false],
|
||||||
idpSingleLogoutServiceUrl: [],
|
keyConnectorUrl: [""],
|
||||||
idpArtifactResolutionServiceUrl: [],
|
openId: this.openIdForm,
|
||||||
idpX509PublicCert: [],
|
saml: this.samlForm,
|
||||||
idpOutboundSigningAlgorithm: [],
|
|
||||||
idpAllowUnsolicitedAuthnResponse: [],
|
|
||||||
idpDisableOutboundLogoutRequests: [],
|
|
||||||
idpWantAuthnRequestsSigned: [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -83,6 +151,25 @@ export class SsoComponent implements OnInit {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
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.route.parent.parent.params.subscribe(async (params) => {
|
||||||
this.organizationId = params.organizationId;
|
this.organizationId = params.organizationId;
|
||||||
await this.load();
|
await this.load();
|
||||||
@@ -92,9 +179,7 @@ export class SsoComponent implements OnInit {
|
|||||||
async load() {
|
async load() {
|
||||||
this.organization = await this.organizationService.get(this.organizationId);
|
this.organization = await this.organizationService.get(this.organizationId);
|
||||||
const ssoSettings = await this.apiService.getOrganizationSso(this.organizationId);
|
const ssoSettings = await this.apiService.getOrganizationSso(this.organizationId);
|
||||||
|
this.populateForm(ssoSettings);
|
||||||
this.data.patchValue(ssoSettings.data);
|
|
||||||
this.enabled.setValue(ssoSettings.enabled);
|
|
||||||
|
|
||||||
this.callbackPath = ssoSettings.urls.callbackPath;
|
this.callbackPath = ssoSettings.urls.callbackPath;
|
||||||
this.signedOutCallbackPath = ssoSettings.urls.signedOutCallbackPath;
|
this.signedOutCallbackPath = ssoSettings.urls.signedOutCallbackPath;
|
||||||
@@ -102,28 +187,30 @@ export class SsoComponent implements OnInit {
|
|||||||
this.spMetadataUrl = ssoSettings.urls.spMetadataUrl;
|
this.spMetadataUrl = ssoSettings.urls.spMetadataUrl;
|
||||||
this.spAcsUrl = ssoSettings.urls.spAcsUrl;
|
this.spAcsUrl = ssoSettings.urls.spAcsUrl;
|
||||||
|
|
||||||
this.keyConnectorUrl.markAsDirty();
|
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
copy(value: string) {
|
|
||||||
this.platformUtilsService.copyToClipboard(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
launchUri(url: string) {
|
|
||||||
this.platformUtilsService.launchUri(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
async submit() {
|
async submit() {
|
||||||
this.formPromise = this.postData();
|
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 {
|
try {
|
||||||
const response = await this.formPromise;
|
const response = await this.formPromise;
|
||||||
|
this.populateForm(response);
|
||||||
this.data.patchValue(response.data);
|
|
||||||
this.enabled.setValue(response.enabled);
|
|
||||||
|
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("ssoSettingsSaved"));
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("ssoSettingsSaved"));
|
||||||
} catch {
|
} catch {
|
||||||
// Logged by appApiAction, do nothing
|
// Logged by appApiAction, do nothing
|
||||||
@@ -132,24 +219,8 @@ export class SsoComponent implements OnInit {
|
|||||||
this.formPromise = null;
|
this.formPromise = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async postData() {
|
|
||||||
if (this.data.get("keyConnectorEnabled").value) {
|
|
||||||
await this.validateKeyConnectorUrl();
|
|
||||||
|
|
||||||
if (this.keyConnectorUrl.hasError("invalidUrl")) {
|
|
||||||
throw new Error(this.i18nService.t("keyConnectorTestFail"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const request = new OrganizationSsoRequest();
|
|
||||||
request.enabled = this.enabled.value;
|
|
||||||
request.data = this.data.value;
|
|
||||||
|
|
||||||
return this.apiService.postOrganizationSso(this.organizationId, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
async validateKeyConnectorUrl() {
|
async validateKeyConnectorUrl() {
|
||||||
if (this.keyConnectorUrl.pristine) {
|
if (this.haveTestedKeyConnector) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,18 +235,84 @@ export class SsoComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.keyConnectorUrl.markAsPristine();
|
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() {
|
get enableTestKeyConnector() {
|
||||||
return (
|
return (
|
||||||
this.data.get("keyConnectorEnabled").value &&
|
this.ssoConfigForm.get("keyConnectorEnabled").value &&
|
||||||
this.keyConnectorUrl != null &&
|
!Utils.isNullOrWhitespace(this.keyConnectorUrl?.value)
|
||||||
this.keyConnectorUrl.value !== ""
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get keyConnectorUrl() {
|
get keyConnectorUrl() {
|
||||||
return this.data.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
import { RouterModule, Routes } from "@angular/router";
|
import { RouterModule, Routes } from "@angular/router";
|
||||||
|
|
||||||
import { AuthGuardService } from "jslib-angular/services/auth-guard.service";
|
import { AuthGuard } from "jslib-angular/guards/auth.guard";
|
||||||
import { Permissions } from "jslib-common/enums/permissions";
|
import { Permissions } from "jslib-common/enums/permissions";
|
||||||
|
|
||||||
import { OrganizationLayoutComponent } from "src/app/layouts/organization-layout.component";
|
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 { ManageComponent } from "src/app/organizations/manage/manage.component";
|
||||||
import { OrganizationGuardService } from "src/app/services/organization-guard.service";
|
import { NavigationPermissionsService } from "src/app/organizations/services/navigation-permissions.service";
|
||||||
import { OrganizationTypeGuardService } from "src/app/services/organization-type-guard.service";
|
|
||||||
|
|
||||||
import { SsoComponent } from "./manage/sso.component";
|
import { SsoComponent } from "./manage/sso.component";
|
||||||
|
|
||||||
@@ -15,25 +15,16 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: "organizations/:organizationId",
|
path: "organizations/:organizationId",
|
||||||
component: OrganizationLayoutComponent,
|
component: OrganizationLayoutComponent,
|
||||||
canActivate: [AuthGuardService, OrganizationGuardService],
|
canActivate: [AuthGuard, PermissionsGuard],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "manage",
|
path: "manage",
|
||||||
component: ManageComponent,
|
component: ManageComponent,
|
||||||
canActivate: [OrganizationTypeGuardService],
|
canActivate: [PermissionsGuard],
|
||||||
data: {
|
data: {
|
||||||
permissions: [
|
permissions: NavigationPermissionsService.getPermissions("manage").concat(
|
||||||
Permissions.CreateNewCollections,
|
Permissions.ManageSso
|
||||||
Permissions.EditAnyCollection,
|
),
|
||||||
Permissions.DeleteAnyCollection,
|
|
||||||
Permissions.EditAssignedCollections,
|
|
||||||
Permissions.DeleteAssignedCollections,
|
|
||||||
Permissions.AccessEventLogs,
|
|
||||||
Permissions.ManageGroups,
|
|
||||||
Permissions.ManageUsers,
|
|
||||||
Permissions.ManagePolicies,
|
|
||||||
Permissions.ManageSso,
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,13 +2,31 @@ import { CommonModule } from "@angular/common";
|
|||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||||
|
|
||||||
import { OssModule } from "src/app/oss.module";
|
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 { SsoComponent } from "./manage/sso.component";
|
||||||
import { OrganizationsRoutingModule } from "./organizations-routing.module";
|
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({
|
@NgModule({
|
||||||
imports: [CommonModule, FormsModule, ReactiveFormsModule, OssModule, OrganizationsRoutingModule],
|
imports: [
|
||||||
declarations: [SsoComponent],
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
JslibModule,
|
||||||
|
OrganizationsRoutingModule,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
InputCheckboxComponent,
|
||||||
|
InputTextComponent,
|
||||||
|
InputTextReadOnlyComponent,
|
||||||
|
SelectComponent,
|
||||||
|
SsoComponent,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class OrganizationsModule {}
|
export class OrganizationsModule {}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ProviderService } from "jslib-common/abstractions/provider.service";
|
|||||||
import { Permissions } from "jslib-common/enums/permissions";
|
import { Permissions } from "jslib-common/enums/permissions";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ProviderTypeGuardService implements CanActivate {
|
export class PermissionsGuard implements CanActivate {
|
||||||
constructor(private providerService: ProviderService, private router: Router) {}
|
constructor(private providerService: ProviderService, private router: Router) {}
|
||||||
|
|
||||||
async canActivate(route: ActivatedRouteSnapshot) {
|
async canActivate(route: ActivatedRouteSnapshot) {
|
||||||
@@ -6,7 +6,7 @@ import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.se
|
|||||||
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ProviderGuardService implements CanActivate {
|
export class ProviderGuard implements CanActivate {
|
||||||
constructor(
|
constructor(
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
@@ -24,7 +24,11 @@
|
|||||||
<p>{{ "joinProviderDesc" | i18n }}</p>
|
<p>{{ "joinProviderDesc" | i18n }}</p>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<a routerLink="/" [queryParams]="{ email: email }" class="btn btn-primary btn-block">
|
<a
|
||||||
|
routerLink="/login"
|
||||||
|
[queryParams]="{ email: email }"
|
||||||
|
class="btn btn-primary btn-block"
|
||||||
|
>
|
||||||
{{ "logIn" | i18n }}
|
{{ "logIn" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
import { RouterModule, Routes } from "@angular/router";
|
import { RouterModule, Routes } from "@angular/router";
|
||||||
|
|
||||||
import { AuthGuardService } from "jslib-angular/services/auth-guard.service";
|
import { AuthGuard } from "jslib-angular/guards/auth.guard";
|
||||||
import { Permissions } from "jslib-common/enums/permissions";
|
import { Permissions } from "jslib-common/enums/permissions";
|
||||||
|
|
||||||
import { FrontendLayoutComponent } from "src/app/layouts/frontend-layout.component";
|
import { FrontendLayoutComponent } from "src/app/layouts/frontend-layout.component";
|
||||||
@@ -9,13 +9,13 @@ import { ProvidersComponent } from "src/app/providers/providers.component";
|
|||||||
|
|
||||||
import { ClientsComponent } from "./clients/clients.component";
|
import { ClientsComponent } from "./clients/clients.component";
|
||||||
import { CreateOrganizationComponent } from "./clients/create-organization.component";
|
import { CreateOrganizationComponent } from "./clients/create-organization.component";
|
||||||
|
import { PermissionsGuard } from "./guards/provider-type.guard";
|
||||||
|
import { ProviderGuard } from "./guards/provider.guard";
|
||||||
import { AcceptProviderComponent } from "./manage/accept-provider.component";
|
import { AcceptProviderComponent } from "./manage/accept-provider.component";
|
||||||
import { EventsComponent } from "./manage/events.component";
|
import { EventsComponent } from "./manage/events.component";
|
||||||
import { ManageComponent } from "./manage/manage.component";
|
import { ManageComponent } from "./manage/manage.component";
|
||||||
import { PeopleComponent } from "./manage/people.component";
|
import { PeopleComponent } from "./manage/people.component";
|
||||||
import { ProvidersLayoutComponent } from "./providers-layout.component";
|
import { ProvidersLayoutComponent } from "./providers-layout.component";
|
||||||
import { ProviderGuardService } from "./services/provider-guard.service";
|
|
||||||
import { ProviderTypeGuardService } from "./services/provider-type-guard.service";
|
|
||||||
import { AccountComponent } from "./settings/account.component";
|
import { AccountComponent } from "./settings/account.component";
|
||||||
import { SettingsComponent } from "./settings/settings.component";
|
import { SettingsComponent } from "./settings/settings.component";
|
||||||
import { SetupProviderComponent } from "./setup/setup-provider.component";
|
import { SetupProviderComponent } from "./setup/setup-provider.component";
|
||||||
@@ -24,7 +24,7 @@ import { SetupComponent } from "./setup/setup.component";
|
|||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
canActivate: [AuthGuardService],
|
canActivate: [AuthGuard],
|
||||||
component: ProvidersComponent,
|
component: ProvidersComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -45,7 +45,7 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
canActivate: [AuthGuardService],
|
canActivate: [AuthGuard],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "setup",
|
path: "setup",
|
||||||
@@ -54,7 +54,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: ":providerId",
|
path: ":providerId",
|
||||||
component: ProvidersLayoutComponent,
|
component: ProvidersLayoutComponent,
|
||||||
canActivate: [ProviderGuardService],
|
canActivate: [ProviderGuard],
|
||||||
children: [
|
children: [
|
||||||
{ path: "", pathMatch: "full", redirectTo: "clients" },
|
{ path: "", pathMatch: "full", redirectTo: "clients" },
|
||||||
{ path: "clients/create", component: CreateOrganizationComponent },
|
{ path: "clients/create", component: CreateOrganizationComponent },
|
||||||
@@ -71,7 +71,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: "people",
|
path: "people",
|
||||||
component: PeopleComponent,
|
component: PeopleComponent,
|
||||||
canActivate: [ProviderTypeGuardService],
|
canActivate: [PermissionsGuard],
|
||||||
data: {
|
data: {
|
||||||
titleId: "people",
|
titleId: "people",
|
||||||
permissions: [Permissions.ManageUsers],
|
permissions: [Permissions.ManageUsers],
|
||||||
@@ -80,7 +80,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: "events",
|
path: "events",
|
||||||
component: EventsComponent,
|
component: EventsComponent,
|
||||||
canActivate: [ProviderTypeGuardService],
|
canActivate: [PermissionsGuard],
|
||||||
data: {
|
data: {
|
||||||
titleId: "eventLogs",
|
titleId: "eventLogs",
|
||||||
permissions: [Permissions.AccessEventLogs],
|
permissions: [Permissions.AccessEventLogs],
|
||||||
@@ -100,7 +100,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: "account",
|
path: "account",
|
||||||
component: AccountComponent,
|
component: AccountComponent,
|
||||||
canActivate: [ProviderTypeGuardService],
|
canActivate: [PermissionsGuard],
|
||||||
data: {
|
data: {
|
||||||
titleId: "myProvider",
|
titleId: "myProvider",
|
||||||
permissions: [Permissions.ManageProvider],
|
permissions: [Permissions.ManageProvider],
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { CommonModule } from "@angular/common";
|
|||||||
import { ComponentFactoryResolver, NgModule } from "@angular/core";
|
import { ComponentFactoryResolver, NgModule } from "@angular/core";
|
||||||
import { FormsModule } from "@angular/forms";
|
import { FormsModule } from "@angular/forms";
|
||||||
|
|
||||||
|
import { JslibModule } from "jslib-angular/jslib.module";
|
||||||
import { ModalService } from "jslib-angular/services/modal.service";
|
import { ModalService } from "jslib-angular/services/modal.service";
|
||||||
|
|
||||||
import { OssModule } from "src/app/oss.module";
|
import { OssModule } from "src/app/oss.module";
|
||||||
@@ -9,6 +10,8 @@ import { OssModule } from "src/app/oss.module";
|
|||||||
import { AddOrganizationComponent } from "./clients/add-organization.component";
|
import { AddOrganizationComponent } from "./clients/add-organization.component";
|
||||||
import { ClientsComponent } from "./clients/clients.component";
|
import { ClientsComponent } from "./clients/clients.component";
|
||||||
import { CreateOrganizationComponent } from "./clients/create-organization.component";
|
import { CreateOrganizationComponent } from "./clients/create-organization.component";
|
||||||
|
import { PermissionsGuard } from "./guards/provider-type.guard";
|
||||||
|
import { ProviderGuard } from "./guards/provider.guard";
|
||||||
import { AcceptProviderComponent } from "./manage/accept-provider.component";
|
import { AcceptProviderComponent } from "./manage/accept-provider.component";
|
||||||
import { BulkConfirmComponent } from "./manage/bulk/bulk-confirm.component";
|
import { BulkConfirmComponent } from "./manage/bulk/bulk-confirm.component";
|
||||||
import { BulkRemoveComponent } from "./manage/bulk/bulk-remove.component";
|
import { BulkRemoveComponent } from "./manage/bulk/bulk-remove.component";
|
||||||
@@ -18,8 +21,6 @@ import { PeopleComponent } from "./manage/people.component";
|
|||||||
import { UserAddEditComponent } from "./manage/user-add-edit.component";
|
import { UserAddEditComponent } from "./manage/user-add-edit.component";
|
||||||
import { ProvidersLayoutComponent } from "./providers-layout.component";
|
import { ProvidersLayoutComponent } from "./providers-layout.component";
|
||||||
import { ProvidersRoutingModule } from "./providers-routing.module";
|
import { ProvidersRoutingModule } from "./providers-routing.module";
|
||||||
import { ProviderGuardService } from "./services/provider-guard.service";
|
|
||||||
import { ProviderTypeGuardService } from "./services/provider-type-guard.service";
|
|
||||||
import { WebProviderService } from "./services/webProvider.service";
|
import { WebProviderService } from "./services/webProvider.service";
|
||||||
import { AccountComponent } from "./settings/account.component";
|
import { AccountComponent } from "./settings/account.component";
|
||||||
import { SettingsComponent } from "./settings/settings.component";
|
import { SettingsComponent } from "./settings/settings.component";
|
||||||
@@ -27,7 +28,7 @@ import { SetupProviderComponent } from "./setup/setup-provider.component";
|
|||||||
import { SetupComponent } from "./setup/setup.component";
|
import { SetupComponent } from "./setup/setup.component";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [CommonModule, FormsModule, OssModule, ProvidersRoutingModule],
|
imports: [CommonModule, FormsModule, OssModule, JslibModule, ProvidersRoutingModule],
|
||||||
declarations: [
|
declarations: [
|
||||||
AcceptProviderComponent,
|
AcceptProviderComponent,
|
||||||
AccountComponent,
|
AccountComponent,
|
||||||
@@ -45,7 +46,7 @@ import { SetupComponent } from "./setup/setup.component";
|
|||||||
SetupProviderComponent,
|
SetupProviderComponent,
|
||||||
UserAddEditComponent,
|
UserAddEditComponent,
|
||||||
],
|
],
|
||||||
providers: [WebProviderService, ProviderGuardService, ProviderTypeGuardService],
|
providers: [WebProviderService, ProviderGuard, PermissionsGuard],
|
||||||
})
|
})
|
||||||
export class ProvidersModule {
|
export class ProvidersModule {
|
||||||
constructor(modalService: ModalService, componentFactoryResolver: ComponentFactoryResolver) {
|
constructor(modalService: ModalService, componentFactoryResolver: ComponentFactoryResolver) {
|
||||||
|
|||||||
@@ -20,7 +20,11 @@
|
|||||||
<p>{{ "setupProviderLoginDesc" | i18n }}</p>
|
<p>{{ "setupProviderLoginDesc" | i18n }}</p>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<a routerLink="/" [queryParams]="{ email: email }" class="btn btn-primary btn-block">
|
<a
|
||||||
|
routerLink="/login"
|
||||||
|
[queryParams]="{ email: email }"
|
||||||
|
class="btn btn-primary btn-block"
|
||||||
|
>
|
||||||
{{ "logIn" | i18n }}
|
{{ "logIn" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"buttonAction": "https://www.sandbox.paypal.com/cgi-bin/webscr"
|
"buttonAction": "https://www.sandbox.paypal.com/cgi-bin/webscr"
|
||||||
},
|
},
|
||||||
"dev": {
|
"dev": {
|
||||||
|
"port": 8080,
|
||||||
"allowedHosts": "auto"
|
"allowedHosts": "auto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,9 @@
|
|||||||
{}
|
{
|
||||||
|
"dev": {
|
||||||
|
"proxyApi": "http://localhost:4001",
|
||||||
|
"proxyIdentity": "http://localhost:33657",
|
||||||
|
"proxyEvents": "http://localhost:46274",
|
||||||
|
"proxyNotifications": "http://localhost:61841",
|
||||||
|
"port": 8081
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
2
jslib
2
jslib
Submodule jslib updated: 60878cd4ed...3cb94623e2
7169
package-lock.json
generated
7169
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@bitwarden/web-vault",
|
"name": "@bitwarden/web-vault",
|
||||||
"version": "2.26.1",
|
"version": "2.28.1",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"repository": "https://github.com/bitwarden/web",
|
"repository": "https://github.com/bitwarden/web",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -43,6 +43,7 @@
|
|||||||
"@types/webpack": "^5.28.0",
|
"@types/webpack": "^5.28.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
||||||
"@typescript-eslint/parser": "^5.10.1",
|
"@typescript-eslint/parser": "^5.10.1",
|
||||||
|
"autoprefixer": "^10.4.2",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"clean-webpack-plugin": "^4.0.0",
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
"copy-webpack-plugin": "^10.0.0",
|
"copy-webpack-plugin": "^10.0.0",
|
||||||
@@ -59,12 +60,15 @@
|
|||||||
"husky": "^7.0.4",
|
"husky": "^7.0.4",
|
||||||
"lint-staged": "^12.1.2",
|
"lint-staged": "^12.1.2",
|
||||||
"mini-css-extract-plugin": "^2.4.5",
|
"mini-css-extract-plugin": "^2.4.5",
|
||||||
|
"postcss": "^8.4.6",
|
||||||
|
"postcss-loader": "^6.2.1",
|
||||||
"prettier": "2.5.1",
|
"prettier": "2.5.1",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"sass": "^1.32.10",
|
"sass": "^1.32.10",
|
||||||
"sass-loader": "^12.4.0",
|
"sass-loader": "^12.4.0",
|
||||||
"style-loader": "^3.3.1",
|
"style-loader": "^3.3.1",
|
||||||
|
"tailwindcss": "^3.0.18",
|
||||||
"terser-webpack-plugin": "^5.2.5",
|
"terser-webpack-plugin": "^5.2.5",
|
||||||
"ts-loader": "^9.2.5",
|
"ts-loader": "^9.2.5",
|
||||||
"typescript": "4.3.5",
|
"typescript": "4.3.5",
|
||||||
@@ -86,20 +90,22 @@
|
|||||||
"@bitwarden/jslib-angular": "file:jslib/angular",
|
"@bitwarden/jslib-angular": "file:jslib/angular",
|
||||||
"@bitwarden/jslib-common": "file:jslib/common",
|
"@bitwarden/jslib-common": "file:jslib/common",
|
||||||
"bootstrap": "4.6.0",
|
"bootstrap": "4.6.0",
|
||||||
"braintree-web-drop-in": "1.30.1",
|
"braintree-web-drop-in": "1.33.1",
|
||||||
"browser-hrtime": "^1.1.8",
|
"browser-hrtime": "^1.1.8",
|
||||||
"core-js": "^3.11.0",
|
"core-js": "^3.11.0",
|
||||||
"date-input-polyfill": "^2.14.0",
|
"date-input-polyfill": "^2.14.0",
|
||||||
"jquery": "3.6.0",
|
"jquery": "3.6.0",
|
||||||
|
"jszip": "^3.7.1",
|
||||||
"ngx-infinite-scroll": "^10.0.1",
|
"ngx-infinite-scroll": "^10.0.1",
|
||||||
"ngx-toastr": "14.1.4",
|
"ngx-toastr": "14.1.4",
|
||||||
"node-forge": "^0.10.0",
|
"node-forge": "^1.3.1",
|
||||||
"popper.js": "1.16.1",
|
"popper.js": "1.16.1",
|
||||||
"qrious": "4.0.2",
|
"qrious": "4.0.2",
|
||||||
"rxjs": "^7.4.0",
|
"rxjs": "^7.4.0",
|
||||||
"sweetalert2": "^10.16.6",
|
"sweetalert2": "^10.16.6",
|
||||||
"webcrypto-shim": "0.1.7",
|
"webcrypto-shim": "0.1.7",
|
||||||
"whatwg-fetch": "3.6.2"
|
"whatwg-fetch": "3.6.2",
|
||||||
|
"zone.js": "0.11.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "~16",
|
"node": "~16",
|
||||||
|
|||||||
4
postcss.config.js
Normal file
4
postcss.config.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/* eslint-disable no-undef */
|
||||||
|
module.exports = {
|
||||||
|
plugins: [require("tailwindcss"), require("autoprefixer"), require("postcss-nested")],
|
||||||
|
};
|
||||||
@@ -23,7 +23,11 @@
|
|||||||
<p>{{ "acceptEmergencyAccess" | i18n }}</p>
|
<p>{{ "acceptEmergencyAccess" | i18n }}</p>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<a routerLink="/" [queryParams]="{ email: email }" class="btn btn-primary btn-block">
|
<a
|
||||||
|
routerLink="/login"
|
||||||
|
[queryParams]="{ email: email }"
|
||||||
|
class="btn btn-primary btn-block"
|
||||||
|
>
|
||||||
{{ "logIn" | i18n }}
|
{{ "logIn" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -24,7 +24,11 @@
|
|||||||
<p>{{ "joinOrganizationDesc" | i18n }}</p>
|
<p>{{ "joinOrganizationDesc" | i18n }}</p>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<a routerLink="/" [queryParams]="{ email: email }" class="btn btn-primary btn-block">
|
<a
|
||||||
|
routerLink="/login"
|
||||||
|
[queryParams]="{ email: email }"
|
||||||
|
class="btn btn-primary btn-block"
|
||||||
|
>
|
||||||
{{ "logIn" | i18n }}
|
{{ "logIn" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></i>
|
></i>
|
||||||
</button>
|
</button>
|
||||||
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
||||||
{{ "cancel" | i18n }}
|
{{ "cancel" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -55,10 +55,10 @@ export class LockComponent extends BaseLockComponent {
|
|||||||
await super.ngOnInit();
|
await super.ngOnInit();
|
||||||
this.onSuccessfulSubmit = async () => {
|
this.onSuccessfulSubmit = async () => {
|
||||||
const previousUrl = this.routerService.getPreviousUrl();
|
const previousUrl = this.routerService.getPreviousUrl();
|
||||||
if (previousUrl !== "/" && previousUrl.indexOf("lock") === -1) {
|
if (previousUrl && previousUrl !== "/" && previousUrl.indexOf("lock") === -1) {
|
||||||
this.successRoute = previousUrl;
|
this.successRoute = previousUrl;
|
||||||
}
|
}
|
||||||
this.router.navigate([this.successRoute]);
|
this.router.navigateByUrl(this.successRoute);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.
|
|||||||
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
|
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { LogService } from "jslib-common/abstractions/log.service";
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
|
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||||
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
|
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
|
||||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
import { PolicyService } from "jslib-common/abstractions/policy.service";
|
import { PolicyService } from "jslib-common/abstractions/policy.service";
|
||||||
@@ -19,6 +20,7 @@ import { ListResponse } from "jslib-common/models/response/listResponse";
|
|||||||
import { PolicyResponse } from "jslib-common/models/response/policyResponse";
|
import { PolicyResponse } from "jslib-common/models/response/policyResponse";
|
||||||
|
|
||||||
import { StateService } from "../../abstractions/state.service";
|
import { StateService } from "../../abstractions/state.service";
|
||||||
|
import { RouterService } from "../services/router.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-login",
|
selector: "app-login",
|
||||||
@@ -42,7 +44,9 @@ export class LoginComponent extends BaseLoginComponent {
|
|||||||
private policyService: PolicyService,
|
private policyService: PolicyService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
ngZone: NgZone,
|
ngZone: NgZone,
|
||||||
protected stateService: StateService
|
protected stateService: StateService,
|
||||||
|
private messagingService: MessagingService,
|
||||||
|
private routerService: RouterService
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
authService,
|
authService,
|
||||||
@@ -56,6 +60,9 @@ export class LoginComponent extends BaseLoginComponent {
|
|||||||
logService,
|
logService,
|
||||||
ngZone
|
ngZone
|
||||||
);
|
);
|
||||||
|
this.onSuccessfulLogin = async () => {
|
||||||
|
this.messagingService.send("setFullWidth");
|
||||||
|
};
|
||||||
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,21 +72,20 @@ export class LoginComponent extends BaseLoginComponent {
|
|||||||
this.email = qParams.email;
|
this.email = qParams.email;
|
||||||
}
|
}
|
||||||
if (qParams.premium != null) {
|
if (qParams.premium != null) {
|
||||||
this.stateService.setLoginRedirect({ route: "/settings/premium" });
|
this.routerService.setPreviousUrl("/settings/premium");
|
||||||
} else if (qParams.org != null) {
|
} else if (qParams.org != null) {
|
||||||
this.stateService.setLoginRedirect({
|
const route = this.router.createUrlTree(["create-organization"], {
|
||||||
route: "/settings/create-organization",
|
queryParams: { plan: qParams.org },
|
||||||
qParams: { plan: qParams.org },
|
|
||||||
});
|
});
|
||||||
|
this.routerService.setPreviousUrl(route.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Are they coming from an email for sponsoring a families organization
|
// Are they coming from an email for sponsoring a families organization
|
||||||
if (qParams.sponsorshipToken != null) {
|
if (qParams.sponsorshipToken != null) {
|
||||||
// After logging in redirect them to setup the families sponsorship
|
const route = this.router.createUrlTree(["setup/families-for-enterprise"], {
|
||||||
this.stateService.setLoginRedirect({
|
queryParams: { token: qParams.sponsorshipToken },
|
||||||
route: "/setup/families-for-enterprise",
|
|
||||||
qParams: { token: qParams.sponsorshipToken },
|
|
||||||
});
|
});
|
||||||
|
this.routerService.setPreviousUrl(route.toString());
|
||||||
}
|
}
|
||||||
await super.ngOnInit();
|
await super.ngOnInit();
|
||||||
this.rememberEmail = await this.stateService.getRememberEmail();
|
this.rememberEmail = await this.stateService.getRememberEmail();
|
||||||
@@ -140,10 +146,9 @@ export class LoginComponent extends BaseLoginComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loginRedirect = await this.stateService.getLoginRedirect();
|
const previousUrl = this.routerService.getPreviousUrl();
|
||||||
if (loginRedirect != null) {
|
if (previousUrl) {
|
||||||
this.router.navigate([loginRedirect.route], { queryParams: loginRedirect.qParams });
|
this.router.navigateByUrl(previousUrl);
|
||||||
await this.stateService.setLoginRedirect(null);
|
|
||||||
} else {
|
} else {
|
||||||
this.router.navigate([this.successRoute]);
|
this.router.navigate([this.successRoute]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></i>
|
></i>
|
||||||
</button>
|
</button>
|
||||||
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
||||||
{{ "cancel" | i18n }}
|
{{ "cancel" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></i>
|
></i>
|
||||||
</button>
|
</button>
|
||||||
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
||||||
{{ "cancel" | i18n }}
|
{{ "cancel" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -258,7 +258,7 @@
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></i>
|
></i>
|
||||||
</button>
|
</button>
|
||||||
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
||||||
{{ "cancel" | i18n }}
|
{{ "cancel" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { MasterPasswordPolicyOptions } from "jslib-common/models/domain/masterPa
|
|||||||
import { Policy } from "jslib-common/models/domain/policy";
|
import { Policy } from "jslib-common/models/domain/policy";
|
||||||
import { ReferenceEventRequest } from "jslib-common/models/request/referenceEventRequest";
|
import { ReferenceEventRequest } from "jslib-common/models/request/referenceEventRequest";
|
||||||
|
|
||||||
|
import { RouterService } from "../services/router.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-register",
|
selector: "app-register",
|
||||||
templateUrl: "register.component.html",
|
templateUrl: "register.component.html",
|
||||||
@@ -41,7 +43,8 @@ export class RegisterComponent extends BaseRegisterComponent {
|
|||||||
passwordGenerationService: PasswordGenerationService,
|
passwordGenerationService: PasswordGenerationService,
|
||||||
private policyService: PolicyService,
|
private policyService: PolicyService,
|
||||||
environmentService: EnvironmentService,
|
environmentService: EnvironmentService,
|
||||||
logService: LogService
|
logService: LogService,
|
||||||
|
private routerService: RouterService
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
authService,
|
authService,
|
||||||
@@ -64,14 +67,14 @@ export class RegisterComponent extends BaseRegisterComponent {
|
|||||||
this.email = qParams.email;
|
this.email = qParams.email;
|
||||||
}
|
}
|
||||||
if (qParams.premium != null) {
|
if (qParams.premium != null) {
|
||||||
this.stateService.setLoginRedirect({ route: "/settings/premium" });
|
this.routerService.setPreviousUrl("/settings/premium");
|
||||||
} else if (qParams.org != null) {
|
} else if (qParams.org != null) {
|
||||||
this.showCreateOrgMessage = true;
|
this.showCreateOrgMessage = true;
|
||||||
this.referenceData.flow = qParams.org;
|
this.referenceData.flow = qParams.org;
|
||||||
this.stateService.setLoginRedirect({
|
const route = this.router.createUrlTree(["create-organization"], {
|
||||||
route: "/settings/create-organization",
|
queryParams: { plan: qParams.org },
|
||||||
qParams: { plan: qParams.org },
|
|
||||||
});
|
});
|
||||||
|
this.routerService.setPreviousUrl(route.toString());
|
||||||
}
|
}
|
||||||
if (qParams.layout != null) {
|
if (qParams.layout != null) {
|
||||||
this.layout = this.referenceData.layout = qParams.layout;
|
this.layout = this.referenceData.layout = qParams.layout;
|
||||||
@@ -88,10 +91,10 @@ export class RegisterComponent extends BaseRegisterComponent {
|
|||||||
// Are they coming from an email for sponsoring a families organization
|
// Are they coming from an email for sponsoring a families organization
|
||||||
if (qParams.sponsorshipToken != null) {
|
if (qParams.sponsorshipToken != null) {
|
||||||
// After logging in redirect them to setup the families sponsorship
|
// After logging in redirect them to setup the families sponsorship
|
||||||
this.stateService.setLoginRedirect({
|
const route = this.router.createUrlTree(["setup/families-for-enterprise"], {
|
||||||
route: "/setup/families-for-enterprise",
|
queryParams: { plan: qParams.sponsorshipToken },
|
||||||
qParams: { token: qParams.sponsorshipToken },
|
|
||||||
});
|
});
|
||||||
|
this.routerService.setPreviousUrl(route.toString());
|
||||||
}
|
}
|
||||||
if (this.referenceData.id === "") {
|
if (this.referenceData.id === "") {
|
||||||
this.referenceData.id = null;
|
this.referenceData.id = null;
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></i>
|
></i>
|
||||||
</button>
|
</button>
|
||||||
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
||||||
{{ "cancel" | i18n }}
|
{{ "cancel" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -114,6 +114,9 @@
|
|||||||
<p>{{ "noTwoStepProviders2" | i18n }}</p>
|
<p>{{ "noTwoStepProviders2" | i18n }}</p>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<hr />
|
<hr />
|
||||||
|
<div [hidden]="!showCaptcha()">
|
||||||
|
<iframe id="hcaptcha_iframe" height="80"></iframe>
|
||||||
|
</div>
|
||||||
<div class="d-flex mb-3">
|
<div class="d-flex mb-3">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -135,7 +138,7 @@
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></i>
|
></i>
|
||||||
</button>
|
</button>
|
||||||
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
||||||
{{ "cancel" | i18n }}
|
{{ "cancel" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ActivatedRoute, Router } from "@angular/router";
|
|||||||
import { TwoFactorComponent as BaseTwoFactorComponent } from "jslib-angular/components/two-factor.component";
|
import { TwoFactorComponent as BaseTwoFactorComponent } from "jslib-angular/components/two-factor.component";
|
||||||
import { ModalService } from "jslib-angular/services/modal.service";
|
import { ModalService } from "jslib-angular/services/modal.service";
|
||||||
import { ApiService } from "jslib-common/abstractions/api.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 { AuthService } from "jslib-common/abstractions/auth.service";
|
||||||
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
|
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
@@ -13,6 +14,8 @@ import { StateService } from "jslib-common/abstractions/state.service";
|
|||||||
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
|
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
|
||||||
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
|
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
|
||||||
|
|
||||||
|
import { RouterService } from "../services/router.service";
|
||||||
|
|
||||||
import { TwoFactorOptionsComponent } from "./two-factor-options.component";
|
import { TwoFactorOptionsComponent } from "./two-factor-options.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -34,7 +37,9 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
|||||||
private modalService: ModalService,
|
private modalService: ModalService,
|
||||||
route: ActivatedRoute,
|
route: ActivatedRoute,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
twoFactorService: TwoFactorService
|
twoFactorService: TwoFactorService,
|
||||||
|
appIdService: AppIdService,
|
||||||
|
private routerService: RouterService
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
authService,
|
authService,
|
||||||
@@ -47,7 +52,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
|||||||
stateService,
|
stateService,
|
||||||
route,
|
route,
|
||||||
logService,
|
logService,
|
||||||
twoFactorService
|
twoFactorService,
|
||||||
|
appIdService
|
||||||
);
|
);
|
||||||
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||||
}
|
}
|
||||||
@@ -70,10 +76,9 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async goAfterLogIn() {
|
async goAfterLogIn() {
|
||||||
const loginRedirect = await this.stateService.getLoginRedirect();
|
const previousUrl = this.routerService.getPreviousUrl();
|
||||||
if (loginRedirect != null) {
|
if (previousUrl) {
|
||||||
this.router.navigate([loginRedirect.route], { queryParams: loginRedirect.qParams });
|
this.router.navigateByUrl(previousUrl);
|
||||||
await this.stateService.setLoginRedirect(null);
|
|
||||||
} else {
|
} else {
|
||||||
this.router.navigate([this.successRoute], {
|
this.router.navigate([this.successRoute], {
|
||||||
queryParams: {
|
queryParams: {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></i>
|
></i>
|
||||||
</button>
|
</button>
|
||||||
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
||||||
{{ "cancel" | i18n }}
|
{{ "cancel" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -91,11 +91,17 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
this.ngZone.run(async () => {
|
this.ngZone.run(async () => {
|
||||||
switch (message.command) {
|
switch (message.command) {
|
||||||
case "loggedIn":
|
case "loggedIn":
|
||||||
|
this.notificationsService.updateConnection(false);
|
||||||
|
break;
|
||||||
case "loggedOut":
|
case "loggedOut":
|
||||||
|
this.routerService.setPreviousUrl(null);
|
||||||
|
this.notificationsService.updateConnection(false);
|
||||||
|
break;
|
||||||
case "unlocked":
|
case "unlocked":
|
||||||
this.notificationsService.updateConnection(false);
|
this.notificationsService.updateConnection(false);
|
||||||
break;
|
break;
|
||||||
case "authBlocked":
|
case "authBlocked":
|
||||||
|
this.routerService.setPreviousUrl(message.url);
|
||||||
this.router.navigate(["/"]);
|
this.router.navigate(["/"]);
|
||||||
break;
|
break;
|
||||||
case "logout":
|
case "logout":
|
||||||
@@ -109,7 +115,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
this.router.navigate(["lock"]);
|
this.router.navigate(["lock"]);
|
||||||
break;
|
break;
|
||||||
case "lockedUrl":
|
case "lockedUrl":
|
||||||
window.setTimeout(() => this.routerService.setPreviousUrl(message.url), 500);
|
this.routerService.setPreviousUrl(message.url);
|
||||||
break;
|
break;
|
||||||
case "syncStarted":
|
case "syncStarted":
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import { FormsModule } from "@angular/forms";
|
|||||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||||
import { InfiniteScrollModule } from "ngx-infinite-scroll";
|
import { InfiniteScrollModule } from "ngx-infinite-scroll";
|
||||||
|
|
||||||
import { BitwardenToastModule } from "jslib-angular/components/toastr.component";
|
|
||||||
|
|
||||||
import { AppComponent } from "./app.component";
|
import { AppComponent } from "./app.component";
|
||||||
import { OssRoutingModule } from "./oss-routing.module";
|
import { OssRoutingModule } from "./oss-routing.module";
|
||||||
import { OssModule } from "./oss.module";
|
import { OssModule } from "./oss.module";
|
||||||
@@ -18,11 +16,6 @@ import { WildcardRoutingModule } from "./wildcard-routing.module";
|
|||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ServicesModule,
|
ServicesModule,
|
||||||
BitwardenToastModule.forRoot({
|
|
||||||
maxOpened: 5,
|
|
||||||
autoDismiss: true,
|
|
||||||
closeButton: true,
|
|
||||||
}),
|
|
||||||
InfiniteScrollModule,
|
InfiniteScrollModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
OssRoutingModule,
|
OssRoutingModule,
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export abstract class BaseAcceptComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||||
await this.stateService.setLoginRedirect(null);
|
|
||||||
let error = this.requiredParameters.some((e) => qParams?.[e] == null || qParams[e] === "");
|
let error = this.requiredParameters.some((e) => qParams?.[e] == null || qParams[e] === "");
|
||||||
let errorMessage: string = null;
|
let errorMessage: string = null;
|
||||||
if (!error) {
|
if (!error) {
|
||||||
@@ -44,11 +43,6 @@ export abstract class BaseAcceptComponent implements OnInit {
|
|||||||
errorMessage = e.message;
|
errorMessage = e.message;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await this.stateService.setLoginRedirect({
|
|
||||||
route: this.getRedirectRoute(),
|
|
||||||
qParams: qParams,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.email = qParams.email;
|
this.email = qParams.email;
|
||||||
await this.unauthedHandler(qParams);
|
await this.unauthedHandler(qParams);
|
||||||
}
|
}
|
||||||
@@ -66,10 +60,4 @@ export abstract class BaseAcceptComponent implements OnInit {
|
|||||||
this.loading = false;
|
this.loading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getRedirectRoute() {
|
|
||||||
const urlTree = this.router.parseUrl(this.router.url);
|
|
||||||
urlTree.queryParams = {};
|
|
||||||
return urlTree.toString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,17 +122,20 @@ export abstract class BaseEventsComponent {
|
|||||||
const userId = r.actingUserId == null ? r.userId : r.actingUserId;
|
const userId = r.actingUserId == null ? r.userId : r.actingUserId;
|
||||||
const eventInfo = await this.eventService.getEventInfo(r);
|
const eventInfo = await this.eventService.getEventInfo(r);
|
||||||
const user = this.getUserName(r, userId);
|
const user = this.getUserName(r, userId);
|
||||||
|
const userName = user != null ? user.name : this.i18nService.t("unknown");
|
||||||
|
|
||||||
return new EventView({
|
return new EventView({
|
||||||
message: eventInfo.message,
|
message: eventInfo.message,
|
||||||
humanReadableMessage: eventInfo.humanReadableMessage,
|
humanReadableMessage: eventInfo.humanReadableMessage,
|
||||||
appIcon: eventInfo.appIcon,
|
appIcon: eventInfo.appIcon,
|
||||||
appName: eventInfo.appName,
|
appName: eventInfo.appName,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
userName: user != null ? user.name : this.i18nService.t("unknown"),
|
userName: r.installationId != null ? `Installation: ${r.installationId}` : userName,
|
||||||
userEmail: user != null ? user.email : "",
|
userEmail: user != null ? user.email : "",
|
||||||
date: r.date,
|
date: r.date,
|
||||||
ip: r.ipAddress,
|
ip: r.ipAddress,
|
||||||
type: r.type,
|
type: r.type,
|
||||||
|
installationId: r.installationId,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
68
src/app/components/organization-switcher.component.html
Normal file
68
src/app/components/organization-switcher.component.html
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<div *ngIf="loaded && activeOrganization != null" class="tw-flex">
|
||||||
|
<button
|
||||||
|
class="tw-flex tw-items-center tw-bg-background-alt tw-border-none"
|
||||||
|
type="button"
|
||||||
|
id="pickerButton"
|
||||||
|
[appA11yTitle]="'organizationPicker' | i18n"
|
||||||
|
[bitMenuTriggerFor]="orgPickerMenu"
|
||||||
|
>
|
||||||
|
<app-avatar
|
||||||
|
[data]="activeOrganization.name"
|
||||||
|
size="45"
|
||||||
|
[circle]="true"
|
||||||
|
[dynamic]="true"
|
||||||
|
></app-avatar>
|
||||||
|
<div class="tw-flex">
|
||||||
|
<div class="org-name tw-ml-3">
|
||||||
|
<span>{{ activeOrganization.name }}</span>
|
||||||
|
<small class="tw-text-muted">{{ "organization" | i18n }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="tw-ml-3">
|
||||||
|
<i class="bwi bwi-angle-down tw-text-main" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="tw-ml-3 tw-border tw-border-solid tw-rounded tw-border-danger-500 tw-text-danger"
|
||||||
|
*ngIf="!activeOrganization.enabled"
|
||||||
|
>
|
||||||
|
<div class="tw-py-2 tw-px-5">
|
||||||
|
<i class="bwi bwi-exclamation-triangle" aria-hidden="true"></i>
|
||||||
|
{{ "organizationIsDisabled" | i18n }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="tw-ml-3 tw-border tw-border-solid tw-rounded tw-border-info-500 tw-text-info"
|
||||||
|
*ngIf="activeOrganization.isProviderUser"
|
||||||
|
>
|
||||||
|
<div class="tw-py-2 tw-px-5">
|
||||||
|
<i class="bwi bwi-exclamation-triangle" aria-hidden="true"></i>
|
||||||
|
{{ "accessingUsingProvider" | i18n: activeOrganization.providerName }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<bit-menu #orgPickerMenu>
|
||||||
|
<ul aria-labelledby="pickerButton" class="tw-p-0 tw-m-0">
|
||||||
|
<li *ngFor="let org of organizations" class="tw-list-none tw-flex tw-flex-col" role="none">
|
||||||
|
<a bit-menu-item [routerLink]="['/organizations', org.id]">
|
||||||
|
<i
|
||||||
|
class="bwi bwi-check mr-2"
|
||||||
|
[ngClass]="org.id === activeOrganization.id ? 'visible' : 'invisible'"
|
||||||
|
>
|
||||||
|
<span class="tw-sr-only">{{ "currentOrganization" | i18n }}</span>
|
||||||
|
</i>
|
||||||
|
{{ org.name }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<bit-menu-divider></bit-menu-divider>
|
||||||
|
<li class="tw-list-none" role="none">
|
||||||
|
<a bit-menu-item routerLink="/create-organization">
|
||||||
|
<i class="bwi bwi-plus mr-2"></i>
|
||||||
|
{{ "newOrganization" | i18n }}</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</bit-menu>
|
||||||
|
</div>
|
||||||
34
src/app/components/organization-switcher.component.ts
Normal file
34
src/app/components/organization-switcher.component.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Component, Input, OnInit } from "@angular/core";
|
||||||
|
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||||
|
import { Utils } from "jslib-common/misc/utils";
|
||||||
|
import { Organization } from "jslib-common/models/domain/organization";
|
||||||
|
|
||||||
|
import { NavigationPermissionsService } from "../organizations/services/navigation-permissions.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-organization-switcher",
|
||||||
|
templateUrl: "organization-switcher.component.html",
|
||||||
|
})
|
||||||
|
export class OrganizationSwitcherComponent implements OnInit {
|
||||||
|
constructor(private organizationService: OrganizationService, private i18nService: I18nService) {}
|
||||||
|
|
||||||
|
@Input() activeOrganization: Organization = null;
|
||||||
|
organizations: Organization[] = [];
|
||||||
|
|
||||||
|
loaded = false;
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
await this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
const orgs = await this.organizationService.getAll();
|
||||||
|
this.organizations = orgs
|
||||||
|
.filter((org) => NavigationPermissionsService.canAccessAdmin(org))
|
||||||
|
.sort(Utils.getSortFunction(this.i18nService, "name"));
|
||||||
|
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/app/components/premium-badge.component.ts
Normal file
19
src/app/components/premium-badge.component.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
|
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-premium-badge",
|
||||||
|
template: `
|
||||||
|
<button *appNotPremium bit-badge badgeType="success" (click)="premiumRequired()">
|
||||||
|
{{ "premium" | i18n }}
|
||||||
|
</button>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class PremiumBadgeComponent {
|
||||||
|
constructor(private messagingService: MessagingService) {}
|
||||||
|
|
||||||
|
premiumRequired() {
|
||||||
|
this.messagingService.send("premiumRequired");
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/app/guards/home.guard.ts
Normal file
22
src/app/guards/home.guard.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { ActivatedRouteSnapshot, CanActivate, Router } from "@angular/router";
|
||||||
|
|
||||||
|
import { AuthService } from "jslib-common/abstractions/auth.service";
|
||||||
|
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class HomeGuard implements CanActivate {
|
||||||
|
constructor(private router: Router, private authService: AuthService) {}
|
||||||
|
|
||||||
|
async canActivate(route: ActivatedRouteSnapshot) {
|
||||||
|
const authStatus = await this.authService.getAuthStatus();
|
||||||
|
|
||||||
|
if (authStatus === AuthenticationStatus.LoggedOut) {
|
||||||
|
return this.router.createUrlTree(["/login"], { queryParams: route.queryParams });
|
||||||
|
}
|
||||||
|
if (authStatus === AuthenticationStatus.Locked) {
|
||||||
|
return this.router.createUrlTree(["/lock"], { queryParams: route.queryParams });
|
||||||
|
}
|
||||||
|
return this.router.createUrlTree(["/vault"], { queryParams: route.queryParams });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="container footer text-muted">
|
<div class="container footer text-muted">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">© {{ year }}, Bitwarden Inc.</div>
|
<div class="col">© {{ year }} Bitwarden Inc.</div>
|
||||||
<div class="col text-center"></div>
|
<div class="col text-center"></div>
|
||||||
<div class="col text-right">
|
<div class="col text-right">
|
||||||
{{ "versionNumber" | i18n: version }}
|
{{ "versionNumber" | i18n: version }}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
<div class="container my-5 text-muted text-center">
|
<div class="container my-5 text-muted text-center">
|
||||||
© {{ year }}, Bitwarden Inc. <br />
|
© {{ year }} Bitwarden Inc. <br />
|
||||||
{{ "versionNumber" | i18n: version }}
|
{{ "versionNumber" | i18n: version }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,11 +6,22 @@
|
|||||||
<div class="collapse navbar-collapse">
|
<div class="collapse navbar-collapse">
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
<li class="nav-item" routerLinkActive="active">
|
<li class="nav-item" routerLinkActive="active">
|
||||||
<a class="nav-link" routerLink="/vault">{{ "myVault" | i18n }}</a>
|
<a class="nav-link" routerLink="/vault">{{ "vaults" | i18n }}</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" routerLinkActive="active">
|
<li class="nav-item" routerLinkActive="active">
|
||||||
<a class="nav-link" routerLink="/sends">{{ "send" | i18n }}</a>
|
<a class="nav-link" routerLink="/sends">{{ "send" | i18n }}</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item" routerLinkActive="active">
|
||||||
|
<a class="nav-link" routerLink="/tools">{{ "tools" | i18n }}</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" routerLinkActive="active">
|
||||||
|
<a class="nav-link" routerLink="/reports">{{ "reports" | i18n }}</a>
|
||||||
|
</li>
|
||||||
|
<li *ngIf="organizations.length >= 1" class="nav-item" routerLinkActive="active">
|
||||||
|
<a class="nav-link" [routerLink]="['/organizations', organizations[0].id]">{{
|
||||||
|
"organizations" | i18n
|
||||||
|
}}</a>
|
||||||
|
</li>
|
||||||
<ng-container *ngIf="providers.length >= 1">
|
<ng-container *ngIf="providers.length >= 1">
|
||||||
<li class="nav-item" routerLinkActive="active" *ngIf="providers.length == 1">
|
<li class="nav-item" routerLinkActive="active" *ngIf="providers.length == 1">
|
||||||
<a class="nav-link" [routerLink]="['/providers', providers[0].id]">{{
|
<a class="nav-link" [routerLink]="['/providers', providers[0].id]">{{
|
||||||
@@ -21,73 +32,62 @@
|
|||||||
<a class="nav-link" routerLink="/providers">{{ "provider" | i18n }}</a>
|
<a class="nav-link" routerLink="/providers">{{ "provider" | i18n }}</a>
|
||||||
</li>
|
</li>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<li class="nav-item" routerLinkActive="active">
|
|
||||||
<a class="nav-link" routerLink="/tools">{{ "tools" | i18n }}</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item" routerLinkActive="active">
|
|
||||||
<a class="nav-link" routerLink="/settings">{{ "settings" | i18n }}</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<ul class="navbar-nav flex-row ml-md-auto d-none d-md-flex">
|
<ul class="navbar-nav flex-row ml-md-auto d-none d-md-flex">
|
||||||
<li class="nav-item dropdown">
|
<li>
|
||||||
<a
|
<button
|
||||||
class="nav-item nav-link dropdown-toggle"
|
[bitMenuTriggerFor]="accountMenu"
|
||||||
href="#"
|
class="tw-border-0 tw-bg-transparent tw-text-alt2 tw-opacity-70 hover:tw-opacity-90"
|
||||||
id="nav-profile"
|
|
||||||
data-toggle="dropdown"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
>
|
||||||
<i class="bwi bwi-user-circle bwi-lg" aria-hidden="true"></i>
|
<i class="bwi bwi-user-circle bwi-lg" aria-hidden="true"></i>
|
||||||
</a>
|
<i class="bwi bwi-caret-down bwi-sm" aria-hidden="true"></i>
|
||||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="nav-profile">
|
</button>
|
||||||
<div class="dropdown-item-text d-flex align-items-center" *ngIf="name" appStopProp>
|
<bit-menu class="dropdown-menu" #accountMenu>
|
||||||
<app-avatar
|
<div class="tw-max-w-[300px] tw-min-w-[200px] tw-flex tw-flex-col">
|
||||||
[data]="name"
|
<div
|
||||||
[email]="email"
|
class="tw-flex tw-items-center tw-leading-tight tw-text-info tw-py-1 tw-px-4"
|
||||||
size="25"
|
*ngIf="name"
|
||||||
fontSize="14"
|
appStopProp
|
||||||
[circle]="true"
|
>
|
||||||
></app-avatar>
|
<app-avatar
|
||||||
<div class="ml-2 overflow-hidden">
|
[data]="name"
|
||||||
<span>{{ "loggedInAs" | i18n }}</span>
|
[email]="email"
|
||||||
<small class="text-muted">{{ name }}</small>
|
size="25"
|
||||||
|
fontSize="14"
|
||||||
|
[circle]="true"
|
||||||
|
></app-avatar>
|
||||||
|
<div class="tw-ml-2 tw-block tw-overflow-hidden tw-whitespace-nowrap">
|
||||||
|
<span>{{ "loggedInAs" | i18n }}</span>
|
||||||
|
<small class="tw-text-muted tw-block tw-overflow-hidden tw-whitespace-nowrap">{{
|
||||||
|
name
|
||||||
|
}}</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<bit-menu-divider></bit-menu-divider>
|
||||||
|
<a bit-menu-item routerLink="/settings/account">
|
||||||
|
<i class="bwi bwi-fw bwi-user" aria-hidden="true"></i>
|
||||||
|
{{ "accountSettings" | i18n }}
|
||||||
|
</a>
|
||||||
|
<a bit-menu-item href="https://bitwarden.com/help/" target="_blank" rel="noopener">
|
||||||
|
<i class="bwi bwi-fw bwi-question-circle" aria-hidden="true"></i>
|
||||||
|
{{ "getHelp" | i18n }}
|
||||||
|
</a>
|
||||||
|
<a bit-menu-item href="https://bitwarden.com/download/" target="_blank" rel="noopener">
|
||||||
|
<i class="bwi bwi-fw bwi-download" aria-hidden="true"></i>
|
||||||
|
{{ "getApps" | i18n }}
|
||||||
|
</a>
|
||||||
|
<bit-menu-divider></bit-menu-divider>
|
||||||
|
<button bit-menu-item type="button" (click)="lock()">
|
||||||
|
<i class="bwi bwi-fw bwi-lock" aria-hidden="true"></i>
|
||||||
|
{{ "lockNow" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button bit-menu-item type="button" (click)="logOut()">
|
||||||
|
<i class="bwi bwi-fw bwi-sign-out" aria-hidden="true"></i>
|
||||||
|
{{ "logOut" | i18n }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-divider"></div>
|
</bit-menu>
|
||||||
<a class="dropdown-item" href="#" routerLink="/settings/account">
|
|
||||||
<i class="bwi bwi-fw bwi-user" aria-hidden="true"></i>
|
|
||||||
{{ "myAccount" | i18n }}
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="dropdown-item"
|
|
||||||
href="https://bitwarden.com/help/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-fw bwi-question-circle" aria-hidden="true"></i>
|
|
||||||
{{ "getHelp" | i18n }}
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="dropdown-item"
|
|
||||||
href="https://bitwarden.com/download/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-fw bwi-download" aria-hidden="true"></i>
|
|
||||||
{{ "getApps" | i18n }}
|
|
||||||
</a>
|
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
<button type="button" class="dropdown-item" (click)="lock()">
|
|
||||||
<i class="bwi bwi-fw bwi-lock" aria-hidden="true"></i>
|
|
||||||
{{ "lockNow" | i18n }}
|
|
||||||
</button>
|
|
||||||
<button type="button" class="dropdown-item" (click)="logOut()">
|
|
||||||
<i class="bwi bwi-fw bwi-sign-out" aria-hidden="true"></i>
|
|
||||||
{{ "logOut" | i18n }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
import { Component, NgZone, OnInit } from "@angular/core";
|
||||||
|
|
||||||
|
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||||
|
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
||||||
import { SyncService } from "jslib-common/abstractions/sync.service";
|
import { SyncService } from "jslib-common/abstractions/sync.service";
|
||||||
import { TokenService } from "jslib-common/abstractions/token.service";
|
import { TokenService } from "jslib-common/abstractions/token.service";
|
||||||
|
import { Utils } from "jslib-common/misc/utils";
|
||||||
|
import { Organization } from "jslib-common/models/domain/organization";
|
||||||
import { Provider } from "jslib-common/models/domain/provider";
|
import { Provider } from "jslib-common/models/domain/provider";
|
||||||
|
|
||||||
|
import { NavigationPermissionsService as OrgNavigationPermissionsService } from "../organizations/services/navigation-permissions.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-navbar",
|
selector: "app-navbar",
|
||||||
templateUrl: "navbar.component.html",
|
templateUrl: "navbar.component.html",
|
||||||
@@ -16,13 +23,18 @@ export class NavbarComponent implements OnInit {
|
|||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
providers: Provider[] = [];
|
providers: Provider[] = [];
|
||||||
|
organizations: Organization[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private tokenService: TokenService,
|
private tokenService: TokenService,
|
||||||
private providerService: ProviderService,
|
private providerService: ProviderService,
|
||||||
private syncService: SyncService
|
private syncService: SyncService,
|
||||||
|
private organizationService: OrganizationService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private broadcasterService: BroadcasterService,
|
||||||
|
private ngZone: NgZone
|
||||||
) {
|
) {
|
||||||
this.selfHosted = this.platformUtilsService.isSelfHost();
|
this.selfHosted = this.platformUtilsService.isSelfHost();
|
||||||
}
|
}
|
||||||
@@ -34,11 +46,32 @@ export class NavbarComponent implements OnInit {
|
|||||||
this.name = this.email;
|
this.name = this.email;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure provides are loaded
|
// Ensure providers and organizations are loaded
|
||||||
if ((await this.syncService.getLastSync()) == null) {
|
if ((await this.syncService.getLastSync()) == null) {
|
||||||
await this.syncService.fullSync(false);
|
await this.syncService.fullSync(false);
|
||||||
}
|
}
|
||||||
this.providers = await this.providerService.getAll();
|
this.providers = await this.providerService.getAll();
|
||||||
|
|
||||||
|
this.organizations = await this.buildOrganizations();
|
||||||
|
|
||||||
|
this.broadcasterService.subscribe(this.constructor.name, async (message: any) => {
|
||||||
|
this.ngZone.run(async () => {
|
||||||
|
switch (message.command) {
|
||||||
|
case "organizationCreated":
|
||||||
|
if (this.organizations.length < 1) {
|
||||||
|
this.organizations = await this.buildOrganizations();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async buildOrganizations() {
|
||||||
|
const allOrgs = await this.organizationService.getAll();
|
||||||
|
return allOrgs
|
||||||
|
.filter((org) => OrgNavigationPermissionsService.canAccessAdmin(org))
|
||||||
|
.sort(Utils.getSortFunction(this.i18nService, "name"));
|
||||||
}
|
}
|
||||||
|
|
||||||
lock() {
|
lock() {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import "jquery";
|
|||||||
import "popper.js";
|
import "popper.js";
|
||||||
|
|
||||||
require("../scss/styles.scss");
|
require("../scss/styles.scss");
|
||||||
|
require("../scss/tailwind.css");
|
||||||
|
|
||||||
import { AppModule } from "./app.module";
|
import { AppModule } from "./app.module";
|
||||||
|
|
||||||
|
|||||||
496
src/app/modules/loose-components.module.ts
Normal file
496
src/app/modules/loose-components.module.ts
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { UserVerificationComponent } from "jslib-angular/components/user-verification.component";
|
||||||
|
|
||||||
|
import { AcceptEmergencyComponent } from "../accounts/accept-emergency.component";
|
||||||
|
import { AcceptOrganizationComponent } from "../accounts/accept-organization.component";
|
||||||
|
import { HintComponent } from "../accounts/hint.component";
|
||||||
|
import { LockComponent } from "../accounts/lock.component";
|
||||||
|
import { LoginComponent } from "../accounts/login.component";
|
||||||
|
import { RecoverDeleteComponent } from "../accounts/recover-delete.component";
|
||||||
|
import { RecoverTwoFactorComponent } from "../accounts/recover-two-factor.component";
|
||||||
|
import { RegisterComponent } from "../accounts/register.component";
|
||||||
|
import { RemovePasswordComponent } from "../accounts/remove-password.component";
|
||||||
|
import { SetPasswordComponent } from "../accounts/set-password.component";
|
||||||
|
import { SsoComponent } from "../accounts/sso.component";
|
||||||
|
import { TwoFactorOptionsComponent } from "../accounts/two-factor-options.component";
|
||||||
|
import { TwoFactorComponent } from "../accounts/two-factor.component";
|
||||||
|
import { UpdatePasswordComponent } from "../accounts/update-password.component";
|
||||||
|
import { UpdateTempPasswordComponent } from "../accounts/update-temp-password.component";
|
||||||
|
import { VerifyEmailTokenComponent } from "../accounts/verify-email-token.component";
|
||||||
|
import { VerifyRecoverDeleteComponent } from "../accounts/verify-recover-delete.component";
|
||||||
|
import { NestedCheckboxComponent } from "../components/nested-checkbox.component";
|
||||||
|
import { OrganizationSwitcherComponent } from "../components/organization-switcher.component";
|
||||||
|
import { PasswordRepromptComponent } from "../components/password-reprompt.component";
|
||||||
|
import { PasswordStrengthComponent } from "../components/password-strength.component";
|
||||||
|
import { PremiumBadgeComponent } from "../components/premium-badge.component";
|
||||||
|
import { FooterComponent } from "../layouts/footer.component";
|
||||||
|
import { FrontendLayoutComponent } from "../layouts/frontend-layout.component";
|
||||||
|
import { NavbarComponent } from "../layouts/navbar.component";
|
||||||
|
import { UserLayoutComponent } from "../layouts/user-layout.component";
|
||||||
|
import { OrganizationLayoutComponent } from "../organizations/layouts/organization-layout.component";
|
||||||
|
import { BulkConfirmComponent as OrgBulkConfirmComponent } from "../organizations/manage/bulk/bulk-confirm.component";
|
||||||
|
import { BulkRemoveComponent as OrgBulkRemoveComponent } from "../organizations/manage/bulk/bulk-remove.component";
|
||||||
|
import { BulkStatusComponent as OrgBulkStatusComponent } from "../organizations/manage/bulk/bulk-status.component";
|
||||||
|
import { CollectionAddEditComponent as OrgCollectionAddEditComponent } from "../organizations/manage/collection-add-edit.component";
|
||||||
|
import { CollectionsComponent as OrgManageCollectionsComponent } from "../organizations/manage/collections.component";
|
||||||
|
import { EntityEventsComponent as OrgEntityEventsComponent } from "../organizations/manage/entity-events.component";
|
||||||
|
import { EventsComponent as OrgEventsComponent } from "../organizations/manage/events.component";
|
||||||
|
import { GroupAddEditComponent as OrgGroupAddEditComponent } from "../organizations/manage/group-add-edit.component";
|
||||||
|
import { GroupsComponent as OrgGroupsComponent } from "../organizations/manage/groups.component";
|
||||||
|
import { ManageComponent as OrgManageComponent } from "../organizations/manage/manage.component";
|
||||||
|
import { PeopleComponent as OrgPeopleComponent } from "../organizations/manage/people.component";
|
||||||
|
import { PoliciesComponent as OrgPoliciesComponent } from "../organizations/manage/policies.component";
|
||||||
|
import { PolicyEditComponent as OrgPolicyEditComponent } from "../organizations/manage/policy-edit.component";
|
||||||
|
import { ResetPasswordComponent as OrgResetPasswordComponent } from "../organizations/manage/reset-password.component";
|
||||||
|
import { UserAddEditComponent as OrgUserAddEditComponent } from "../organizations/manage/user-add-edit.component";
|
||||||
|
import { UserConfirmComponent as OrgUserConfirmComponent } from "../organizations/manage/user-confirm.component";
|
||||||
|
import { UserGroupsComponent as OrgUserGroupsComponent } from "../organizations/manage/user-groups.component";
|
||||||
|
import { DisableSendPolicyComponent } from "../organizations/policies/disable-send.component";
|
||||||
|
import { MasterPasswordPolicyComponent } from "../organizations/policies/master-password.component";
|
||||||
|
import { PasswordGeneratorPolicyComponent } from "../organizations/policies/password-generator.component";
|
||||||
|
import { PersonalOwnershipPolicyComponent } from "../organizations/policies/personal-ownership.component";
|
||||||
|
import { RequireSsoPolicyComponent } from "../organizations/policies/require-sso.component";
|
||||||
|
import { ResetPasswordPolicyComponent } from "../organizations/policies/reset-password.component";
|
||||||
|
import { SendOptionsPolicyComponent } from "../organizations/policies/send-options.component";
|
||||||
|
import { SingleOrgPolicyComponent } from "../organizations/policies/single-org.component";
|
||||||
|
import { TwoFactorAuthenticationPolicyComponent } from "../organizations/policies/two-factor-authentication.component";
|
||||||
|
import { AccountComponent as OrgAccountComponent } from "../organizations/settings/account.component";
|
||||||
|
import { AdjustSubscription } from "../organizations/settings/adjust-subscription.component";
|
||||||
|
import { BillingSyncApiKeyComponent } from "../organizations/settings/billing-sync-api-key.component";
|
||||||
|
import { ChangePlanComponent } from "../organizations/settings/change-plan.component";
|
||||||
|
import { DeleteOrganizationComponent } from "../organizations/settings/delete-organization.component";
|
||||||
|
import { DownloadLicenseComponent } from "../organizations/settings/download-license.component";
|
||||||
|
import { ImageSubscriptionHiddenComponent as OrgSubscriptionHiddenComponent } from "../organizations/settings/image-subscription-hidden.component";
|
||||||
|
import { OrganizationBillingComponent } from "../organizations/settings/organization-billing.component";
|
||||||
|
import { OrganizationSubscriptionComponent } from "../organizations/settings/organization-subscription.component";
|
||||||
|
import { SettingsComponent as OrgSettingComponent } from "../organizations/settings/settings.component";
|
||||||
|
import { TwoFactorSetupComponent as OrgTwoFactorSetupComponent } from "../organizations/settings/two-factor-setup.component";
|
||||||
|
import { AcceptFamilySponsorshipComponent } from "../organizations/sponsorships/accept-family-sponsorship.component";
|
||||||
|
import { FamiliesForEnterpriseSetupComponent } from "../organizations/sponsorships/families-for-enterprise-setup.component";
|
||||||
|
import { ExportComponent as OrgExportComponent } from "../organizations/tools/export.component";
|
||||||
|
import { ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent } from "../organizations/tools/exposed-passwords-report.component";
|
||||||
|
import { ImportComponent as OrgImportComponent } from "../organizations/tools/import.component";
|
||||||
|
import { InactiveTwoFactorReportComponent as OrgInactiveTwoFactorReportComponent } from "../organizations/tools/inactive-two-factor-report.component";
|
||||||
|
import { ReusedPasswordsReportComponent as OrgReusedPasswordsReportComponent } from "../organizations/tools/reused-passwords-report.component";
|
||||||
|
import { ToolsComponent as OrgToolsComponent } from "../organizations/tools/tools.component";
|
||||||
|
import { UnsecuredWebsitesReportComponent as OrgUnsecuredWebsitesReportComponent } from "../organizations/tools/unsecured-websites-report.component";
|
||||||
|
import { WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent } from "../organizations/tools/weak-passwords-report.component";
|
||||||
|
import { AddEditComponent as OrgAddEditComponent } from "../organizations/vault/add-edit.component";
|
||||||
|
import { AttachmentsComponent as OrgAttachmentsComponent } from "../organizations/vault/attachments.component";
|
||||||
|
import { CiphersComponent as OrgCiphersComponent } from "../organizations/vault/ciphers.component";
|
||||||
|
import { CollectionsComponent as OrgCollectionsComponent } from "../organizations/vault/collections.component";
|
||||||
|
import { ProvidersComponent } from "../providers/providers.component";
|
||||||
|
import { BreachReportComponent } from "../reports/breach-report.component";
|
||||||
|
import { ExposedPasswordsReportComponent } from "../reports/exposed-passwords-report.component";
|
||||||
|
import { InactiveTwoFactorReportComponent } from "../reports/inactive-two-factor-report.component";
|
||||||
|
import { ReportCardComponent } from "../reports/report-card.component";
|
||||||
|
import { ReportListComponent } from "../reports/report-list.component";
|
||||||
|
import { ReportsComponent } from "../reports/reports.component";
|
||||||
|
import { ReusedPasswordsReportComponent } from "../reports/reused-passwords-report.component";
|
||||||
|
import { UnsecuredWebsitesReportComponent } from "../reports/unsecured-websites-report.component";
|
||||||
|
import { WeakPasswordsReportComponent } from "../reports/weak-passwords-report.component";
|
||||||
|
import { AccessComponent } from "../send/access.component";
|
||||||
|
import { AddEditComponent as SendAddEditComponent } from "../send/add-edit.component";
|
||||||
|
import { EffluxDatesComponent as SendEffluxDatesComponent } from "../send/efflux-dates.component";
|
||||||
|
import { SendComponent } from "../send/send.component";
|
||||||
|
import { AccountComponent } from "../settings/account.component";
|
||||||
|
import { AddCreditComponent } from "../settings/add-credit.component";
|
||||||
|
import { AdjustPaymentComponent } from "../settings/adjust-payment.component";
|
||||||
|
import { AdjustStorageComponent } from "../settings/adjust-storage.component";
|
||||||
|
import { ApiKeyComponent } from "../settings/api-key.component";
|
||||||
|
import { BillingSyncKeyComponent } from "../settings/billing-sync-key.component";
|
||||||
|
import { ChangeEmailComponent } from "../settings/change-email.component";
|
||||||
|
import { ChangeKdfComponent } from "../settings/change-kdf.component";
|
||||||
|
import { ChangePasswordComponent } from "../settings/change-password.component";
|
||||||
|
import { CreateOrganizationComponent } from "../settings/create-organization.component";
|
||||||
|
import { DeauthorizeSessionsComponent } from "../settings/deauthorize-sessions.component";
|
||||||
|
import { DeleteAccountComponent } from "../settings/delete-account.component";
|
||||||
|
import { DomainRulesComponent } from "../settings/domain-rules.component";
|
||||||
|
import { EmergencyAccessAddEditComponent } from "../settings/emergency-access-add-edit.component";
|
||||||
|
import { EmergencyAccessAttachmentsComponent } from "../settings/emergency-access-attachments.component";
|
||||||
|
import { EmergencyAccessConfirmComponent } from "../settings/emergency-access-confirm.component";
|
||||||
|
import { EmergencyAccessTakeoverComponent } from "../settings/emergency-access-takeover.component";
|
||||||
|
import { EmergencyAccessViewComponent } from "../settings/emergency-access-view.component";
|
||||||
|
import { EmergencyAccessComponent } from "../settings/emergency-access.component";
|
||||||
|
import { EmergencyAddEditComponent } from "../settings/emergency-add-edit.component";
|
||||||
|
import { LinkSsoComponent } from "../settings/link-sso.component";
|
||||||
|
import { OrganizationPlansComponent } from "../settings/organization-plans.component";
|
||||||
|
import { PaymentMethodComponent } from "../settings/payment-method.component";
|
||||||
|
import { PaymentComponent } from "../settings/payment.component";
|
||||||
|
import { PreferencesComponent } from "../settings/preferences.component";
|
||||||
|
import { PremiumComponent } from "../settings/premium.component";
|
||||||
|
import { ProfileComponent } from "../settings/profile.component";
|
||||||
|
import { PurgeVaultComponent } from "../settings/purge-vault.component";
|
||||||
|
import { SecurityKeysComponent } from "../settings/security-keys.component";
|
||||||
|
import { SecurityComponent } from "../settings/security.component";
|
||||||
|
import { SettingsComponent } from "../settings/settings.component";
|
||||||
|
import { SponsoredFamiliesComponent } from "../settings/sponsored-families.component";
|
||||||
|
import { SponsoringOrgRowComponent } from "../settings/sponsoring-org-row.component";
|
||||||
|
import { SubscriptionComponent } from "../settings/subscription.component";
|
||||||
|
import { TaxInfoComponent } from "../settings/tax-info.component";
|
||||||
|
import { TwoFactorAuthenticatorComponent } from "../settings/two-factor-authenticator.component";
|
||||||
|
import { TwoFactorDuoComponent } from "../settings/two-factor-duo.component";
|
||||||
|
import { TwoFactorEmailComponent } from "../settings/two-factor-email.component";
|
||||||
|
import { TwoFactorRecoveryComponent } from "../settings/two-factor-recovery.component";
|
||||||
|
import { TwoFactorSetupComponent } from "../settings/two-factor-setup.component";
|
||||||
|
import { TwoFactorVerifyComponent } from "../settings/two-factor-verify.component";
|
||||||
|
import { TwoFactorWebAuthnComponent } from "../settings/two-factor-webauthn.component";
|
||||||
|
import { TwoFactorYubiKeyComponent } from "../settings/two-factor-yubikey.component";
|
||||||
|
import { UpdateKeyComponent } from "../settings/update-key.component";
|
||||||
|
import { UpdateLicenseComponent } from "../settings/update-license.component";
|
||||||
|
import { UserBillingHistoryComponent } from "../settings/user-billing-history.component";
|
||||||
|
import { UserSubscriptionComponent } from "../settings/user-subscription.component";
|
||||||
|
import { VaultTimeoutInputComponent } from "../settings/vault-timeout-input.component";
|
||||||
|
import { VerifyEmailComponent } from "../settings/verify-email.component";
|
||||||
|
import { ExportComponent } from "../tools/export.component";
|
||||||
|
import { GeneratorComponent } from "../tools/generator.component";
|
||||||
|
import { ImportComponent } from "../tools/import.component";
|
||||||
|
import { PasswordGeneratorHistoryComponent } from "../tools/password-generator-history.component";
|
||||||
|
import { ToolsComponent } from "../tools/tools.component";
|
||||||
|
import { AddEditCustomFieldsComponent } from "../vault/add-edit-custom-fields.component";
|
||||||
|
import { AddEditComponent } from "../vault/add-edit.component";
|
||||||
|
import { AttachmentsComponent } from "../vault/attachments.component";
|
||||||
|
import { BulkActionsComponent } from "../vault/bulk-actions.component";
|
||||||
|
import { BulkDeleteComponent } from "../vault/bulk-delete.component";
|
||||||
|
import { BulkMoveComponent } from "../vault/bulk-move.component";
|
||||||
|
import { BulkRestoreComponent } from "../vault/bulk-restore.component";
|
||||||
|
import { BulkShareComponent } from "../vault/bulk-share.component";
|
||||||
|
import { CiphersComponent } from "../vault/ciphers.component";
|
||||||
|
import { CollectionsComponent } from "../vault/collections.component";
|
||||||
|
import { FolderAddEditComponent } from "../vault/folder-add-edit.component";
|
||||||
|
import { ShareComponent } from "../vault/share.component";
|
||||||
|
|
||||||
|
import { PipesModule } from "./pipes/pipes.module";
|
||||||
|
import { SharedModule } from "./shared.module";
|
||||||
|
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||||
|
import { OrganizationBadgeModule } from "./vault/modules/organization-badge/organization-badge.module";
|
||||||
|
|
||||||
|
// Please do not add to this list of declarations - we should refactor these into modules when doing so makes sense until there are none left.
|
||||||
|
// If you are building new functionality, please create or extend a feature module instead.
|
||||||
|
@NgModule({
|
||||||
|
imports: [SharedModule, VaultFilterModule, OrganizationBadgeModule, PipesModule],
|
||||||
|
declarations: [
|
||||||
|
PremiumBadgeComponent,
|
||||||
|
AcceptEmergencyComponent,
|
||||||
|
AcceptFamilySponsorshipComponent,
|
||||||
|
AcceptOrganizationComponent,
|
||||||
|
AccessComponent,
|
||||||
|
AccountComponent,
|
||||||
|
AddCreditComponent,
|
||||||
|
AddEditComponent,
|
||||||
|
AddEditCustomFieldsComponent,
|
||||||
|
AddEditCustomFieldsComponent,
|
||||||
|
AdjustPaymentComponent,
|
||||||
|
AdjustStorageComponent,
|
||||||
|
AdjustSubscription,
|
||||||
|
ApiKeyComponent,
|
||||||
|
AttachmentsComponent,
|
||||||
|
BillingSyncApiKeyComponent,
|
||||||
|
BillingSyncKeyComponent,
|
||||||
|
BreachReportComponent,
|
||||||
|
BulkActionsComponent,
|
||||||
|
BulkDeleteComponent,
|
||||||
|
BulkMoveComponent,
|
||||||
|
BulkRestoreComponent,
|
||||||
|
BulkShareComponent,
|
||||||
|
ChangeEmailComponent,
|
||||||
|
ChangeKdfComponent,
|
||||||
|
ChangePasswordComponent,
|
||||||
|
ChangePlanComponent,
|
||||||
|
CiphersComponent,
|
||||||
|
CollectionsComponent,
|
||||||
|
CreateOrganizationComponent,
|
||||||
|
DeauthorizeSessionsComponent,
|
||||||
|
DeleteAccountComponent,
|
||||||
|
DeleteOrganizationComponent,
|
||||||
|
DisableSendPolicyComponent,
|
||||||
|
DomainRulesComponent,
|
||||||
|
DownloadLicenseComponent,
|
||||||
|
EmergencyAccessAddEditComponent,
|
||||||
|
EmergencyAccessAttachmentsComponent,
|
||||||
|
EmergencyAccessComponent,
|
||||||
|
EmergencyAccessConfirmComponent,
|
||||||
|
EmergencyAccessTakeoverComponent,
|
||||||
|
EmergencyAccessViewComponent,
|
||||||
|
EmergencyAddEditComponent,
|
||||||
|
ExportComponent,
|
||||||
|
ExposedPasswordsReportComponent,
|
||||||
|
FamiliesForEnterpriseSetupComponent,
|
||||||
|
FolderAddEditComponent,
|
||||||
|
FooterComponent,
|
||||||
|
FrontendLayoutComponent,
|
||||||
|
HintComponent,
|
||||||
|
ImportComponent,
|
||||||
|
InactiveTwoFactorReportComponent,
|
||||||
|
LinkSsoComponent,
|
||||||
|
LockComponent,
|
||||||
|
LoginComponent,
|
||||||
|
MasterPasswordPolicyComponent,
|
||||||
|
NavbarComponent,
|
||||||
|
NestedCheckboxComponent,
|
||||||
|
OrganizationSwitcherComponent,
|
||||||
|
OrgAccountComponent,
|
||||||
|
OrgAddEditComponent,
|
||||||
|
OrganizationBillingComponent,
|
||||||
|
OrganizationLayoutComponent,
|
||||||
|
OrganizationPlansComponent,
|
||||||
|
OrganizationSubscriptionComponent,
|
||||||
|
OrgAttachmentsComponent,
|
||||||
|
OrgBulkConfirmComponent,
|
||||||
|
OrgBulkRemoveComponent,
|
||||||
|
OrgBulkStatusComponent,
|
||||||
|
OrgCiphersComponent,
|
||||||
|
OrgCollectionAddEditComponent,
|
||||||
|
OrgCollectionsComponent,
|
||||||
|
OrgEntityEventsComponent,
|
||||||
|
OrgEventsComponent,
|
||||||
|
OrgExportComponent,
|
||||||
|
OrgExposedPasswordsReportComponent,
|
||||||
|
OrgGroupAddEditComponent,
|
||||||
|
OrgGroupsComponent,
|
||||||
|
OrgImportComponent,
|
||||||
|
OrgInactiveTwoFactorReportComponent,
|
||||||
|
OrgManageCollectionsComponent,
|
||||||
|
OrgManageComponent,
|
||||||
|
OrgPeopleComponent,
|
||||||
|
OrgPoliciesComponent,
|
||||||
|
OrgPolicyEditComponent,
|
||||||
|
OrgResetPasswordComponent,
|
||||||
|
OrgReusedPasswordsReportComponent,
|
||||||
|
OrgSettingComponent,
|
||||||
|
OrgToolsComponent,
|
||||||
|
OrgTwoFactorSetupComponent,
|
||||||
|
OrgSubscriptionHiddenComponent,
|
||||||
|
OrgUnsecuredWebsitesReportComponent,
|
||||||
|
OrgUserAddEditComponent,
|
||||||
|
OrgUserConfirmComponent,
|
||||||
|
OrgUserGroupsComponent,
|
||||||
|
OrgWeakPasswordsReportComponent,
|
||||||
|
GeneratorComponent,
|
||||||
|
PasswordGeneratorHistoryComponent,
|
||||||
|
PasswordGeneratorPolicyComponent,
|
||||||
|
PasswordRepromptComponent,
|
||||||
|
PasswordStrengthComponent,
|
||||||
|
PaymentComponent,
|
||||||
|
PaymentMethodComponent,
|
||||||
|
PersonalOwnershipPolicyComponent,
|
||||||
|
PreferencesComponent,
|
||||||
|
PremiumBadgeComponent,
|
||||||
|
PremiumComponent,
|
||||||
|
ProfileComponent,
|
||||||
|
ProvidersComponent,
|
||||||
|
PurgeVaultComponent,
|
||||||
|
RecoverDeleteComponent,
|
||||||
|
RecoverTwoFactorComponent,
|
||||||
|
RegisterComponent,
|
||||||
|
RemovePasswordComponent,
|
||||||
|
ReportCardComponent,
|
||||||
|
ReportListComponent,
|
||||||
|
ReportsComponent,
|
||||||
|
RequireSsoPolicyComponent,
|
||||||
|
ResetPasswordPolicyComponent,
|
||||||
|
ReusedPasswordsReportComponent,
|
||||||
|
SecurityComponent,
|
||||||
|
SecurityKeysComponent,
|
||||||
|
SendAddEditComponent,
|
||||||
|
SendComponent,
|
||||||
|
SendEffluxDatesComponent,
|
||||||
|
SendOptionsPolicyComponent,
|
||||||
|
SetPasswordComponent,
|
||||||
|
SettingsComponent,
|
||||||
|
ShareComponent,
|
||||||
|
SingleOrgPolicyComponent,
|
||||||
|
SponsoredFamiliesComponent,
|
||||||
|
SponsoringOrgRowComponent,
|
||||||
|
SsoComponent,
|
||||||
|
SubscriptionComponent,
|
||||||
|
TaxInfoComponent,
|
||||||
|
ToolsComponent,
|
||||||
|
TwoFactorAuthenticationPolicyComponent,
|
||||||
|
TwoFactorAuthenticatorComponent,
|
||||||
|
TwoFactorComponent,
|
||||||
|
TwoFactorDuoComponent,
|
||||||
|
TwoFactorEmailComponent,
|
||||||
|
TwoFactorOptionsComponent,
|
||||||
|
TwoFactorRecoveryComponent,
|
||||||
|
TwoFactorSetupComponent,
|
||||||
|
TwoFactorVerifyComponent,
|
||||||
|
TwoFactorWebAuthnComponent,
|
||||||
|
TwoFactorYubiKeyComponent,
|
||||||
|
UnsecuredWebsitesReportComponent,
|
||||||
|
UpdateKeyComponent,
|
||||||
|
UpdateLicenseComponent,
|
||||||
|
UpdatePasswordComponent,
|
||||||
|
UpdateTempPasswordComponent,
|
||||||
|
UserBillingHistoryComponent,
|
||||||
|
UserLayoutComponent,
|
||||||
|
UserSubscriptionComponent,
|
||||||
|
UserVerificationComponent,
|
||||||
|
VaultTimeoutInputComponent,
|
||||||
|
VerifyEmailComponent,
|
||||||
|
VerifyEmailTokenComponent,
|
||||||
|
VerifyRecoverDeleteComponent,
|
||||||
|
WeakPasswordsReportComponent,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
PremiumBadgeComponent,
|
||||||
|
AcceptEmergencyComponent,
|
||||||
|
AcceptOrganizationComponent,
|
||||||
|
AccessComponent,
|
||||||
|
AccountComponent,
|
||||||
|
AddCreditComponent,
|
||||||
|
AddEditComponent,
|
||||||
|
AddEditCustomFieldsComponent,
|
||||||
|
AddEditCustomFieldsComponent,
|
||||||
|
AdjustPaymentComponent,
|
||||||
|
AdjustStorageComponent,
|
||||||
|
AdjustSubscription,
|
||||||
|
ApiKeyComponent,
|
||||||
|
AttachmentsComponent,
|
||||||
|
BreachReportComponent,
|
||||||
|
BulkActionsComponent,
|
||||||
|
BulkDeleteComponent,
|
||||||
|
BulkMoveComponent,
|
||||||
|
BulkRestoreComponent,
|
||||||
|
BulkShareComponent,
|
||||||
|
ChangeEmailComponent,
|
||||||
|
ChangeKdfComponent,
|
||||||
|
ChangePasswordComponent,
|
||||||
|
ChangePlanComponent,
|
||||||
|
CiphersComponent,
|
||||||
|
CollectionsComponent,
|
||||||
|
CreateOrganizationComponent,
|
||||||
|
DeauthorizeSessionsComponent,
|
||||||
|
DeleteAccountComponent,
|
||||||
|
DeleteOrganizationComponent,
|
||||||
|
DisableSendPolicyComponent,
|
||||||
|
DomainRulesComponent,
|
||||||
|
DownloadLicenseComponent,
|
||||||
|
EmergencyAccessAddEditComponent,
|
||||||
|
EmergencyAccessAttachmentsComponent,
|
||||||
|
EmergencyAccessComponent,
|
||||||
|
EmergencyAccessConfirmComponent,
|
||||||
|
EmergencyAccessTakeoverComponent,
|
||||||
|
EmergencyAccessViewComponent,
|
||||||
|
EmergencyAddEditComponent,
|
||||||
|
ExportComponent,
|
||||||
|
ExposedPasswordsReportComponent,
|
||||||
|
FamiliesForEnterpriseSetupComponent,
|
||||||
|
FolderAddEditComponent,
|
||||||
|
FooterComponent,
|
||||||
|
FrontendLayoutComponent,
|
||||||
|
HintComponent,
|
||||||
|
ImportComponent,
|
||||||
|
InactiveTwoFactorReportComponent,
|
||||||
|
LinkSsoComponent,
|
||||||
|
LockComponent,
|
||||||
|
LoginComponent,
|
||||||
|
MasterPasswordPolicyComponent,
|
||||||
|
NavbarComponent,
|
||||||
|
NestedCheckboxComponent,
|
||||||
|
OrganizationSwitcherComponent,
|
||||||
|
OrgAccountComponent,
|
||||||
|
OrgAddEditComponent,
|
||||||
|
OrganizationBillingComponent,
|
||||||
|
OrganizationLayoutComponent,
|
||||||
|
OrganizationPlansComponent,
|
||||||
|
OrganizationSubscriptionComponent,
|
||||||
|
OrgAttachmentsComponent,
|
||||||
|
OrgBulkConfirmComponent,
|
||||||
|
OrgBulkRemoveComponent,
|
||||||
|
OrgBulkStatusComponent,
|
||||||
|
OrgCiphersComponent,
|
||||||
|
OrgCollectionAddEditComponent,
|
||||||
|
OrgCollectionsComponent,
|
||||||
|
OrgEntityEventsComponent,
|
||||||
|
OrgEventsComponent,
|
||||||
|
OrgExportComponent,
|
||||||
|
OrgExposedPasswordsReportComponent,
|
||||||
|
OrgGroupAddEditComponent,
|
||||||
|
OrgGroupsComponent,
|
||||||
|
OrgImportComponent,
|
||||||
|
OrgInactiveTwoFactorReportComponent,
|
||||||
|
OrgManageCollectionsComponent,
|
||||||
|
OrgManageComponent,
|
||||||
|
OrgPeopleComponent,
|
||||||
|
OrgPoliciesComponent,
|
||||||
|
OrgPolicyEditComponent,
|
||||||
|
OrgResetPasswordComponent,
|
||||||
|
OrgReusedPasswordsReportComponent,
|
||||||
|
OrgSettingComponent,
|
||||||
|
OrgToolsComponent,
|
||||||
|
OrgTwoFactorSetupComponent,
|
||||||
|
OrgUnsecuredWebsitesReportComponent,
|
||||||
|
OrgUserAddEditComponent,
|
||||||
|
OrgUserConfirmComponent,
|
||||||
|
OrgUserGroupsComponent,
|
||||||
|
OrgWeakPasswordsReportComponent,
|
||||||
|
GeneratorComponent,
|
||||||
|
PasswordGeneratorHistoryComponent,
|
||||||
|
PasswordGeneratorPolicyComponent,
|
||||||
|
PasswordRepromptComponent,
|
||||||
|
PasswordStrengthComponent,
|
||||||
|
PaymentComponent,
|
||||||
|
PaymentMethodComponent,
|
||||||
|
PersonalOwnershipPolicyComponent,
|
||||||
|
PreferencesComponent,
|
||||||
|
PremiumBadgeComponent,
|
||||||
|
PremiumComponent,
|
||||||
|
ProfileComponent,
|
||||||
|
ProvidersComponent,
|
||||||
|
PurgeVaultComponent,
|
||||||
|
RecoverDeleteComponent,
|
||||||
|
RecoverTwoFactorComponent,
|
||||||
|
RegisterComponent,
|
||||||
|
RemovePasswordComponent,
|
||||||
|
ReportCardComponent,
|
||||||
|
ReportListComponent,
|
||||||
|
ReportsComponent,
|
||||||
|
RequireSsoPolicyComponent,
|
||||||
|
ResetPasswordPolicyComponent,
|
||||||
|
ReusedPasswordsReportComponent,
|
||||||
|
SecurityComponent,
|
||||||
|
SecurityKeysComponent,
|
||||||
|
SendAddEditComponent,
|
||||||
|
SendComponent,
|
||||||
|
SendEffluxDatesComponent,
|
||||||
|
SendOptionsPolicyComponent,
|
||||||
|
SetPasswordComponent,
|
||||||
|
SettingsComponent,
|
||||||
|
ShareComponent,
|
||||||
|
SingleOrgPolicyComponent,
|
||||||
|
SponsoredFamiliesComponent,
|
||||||
|
SponsoringOrgRowComponent,
|
||||||
|
SsoComponent,
|
||||||
|
SubscriptionComponent,
|
||||||
|
TaxInfoComponent,
|
||||||
|
ToolsComponent,
|
||||||
|
TwoFactorAuthenticationPolicyComponent,
|
||||||
|
TwoFactorAuthenticatorComponent,
|
||||||
|
TwoFactorComponent,
|
||||||
|
TwoFactorDuoComponent,
|
||||||
|
TwoFactorEmailComponent,
|
||||||
|
TwoFactorOptionsComponent,
|
||||||
|
TwoFactorRecoveryComponent,
|
||||||
|
TwoFactorSetupComponent,
|
||||||
|
TwoFactorVerifyComponent,
|
||||||
|
TwoFactorWebAuthnComponent,
|
||||||
|
TwoFactorYubiKeyComponent,
|
||||||
|
UnsecuredWebsitesReportComponent,
|
||||||
|
UpdateKeyComponent,
|
||||||
|
UpdateLicenseComponent,
|
||||||
|
UpdatePasswordComponent,
|
||||||
|
UpdateTempPasswordComponent,
|
||||||
|
UserBillingHistoryComponent,
|
||||||
|
UserLayoutComponent,
|
||||||
|
UserSubscriptionComponent,
|
||||||
|
UserVerificationComponent,
|
||||||
|
VaultTimeoutInputComponent,
|
||||||
|
VerifyEmailComponent,
|
||||||
|
VerifyEmailTokenComponent,
|
||||||
|
VerifyRecoverDeleteComponent,
|
||||||
|
WeakPasswordsReportComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class LooseComponentsModule {}
|
||||||
@@ -29,52 +29,52 @@
|
|||||||
></i>
|
></i>
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<cdk-virtual-scroll-viewport
|
||||||
class="modal-body"
|
itemSize="46"
|
||||||
*ngIf="
|
minBufferPx="600"
|
||||||
!loading && users && (users | search: searchText:'name':'email':'id') as searchedUsers
|
maxBufferPx="1200"
|
||||||
"
|
[style]="scrollViewportStyle"
|
||||||
>
|
>
|
||||||
<div class="d-flex">
|
<div class="modal-body" *ngIf="!loading && users && searchedUsers">
|
||||||
<div class="mr-3">
|
<div class="d-flex">
|
||||||
<label class="sr-only" for="search">{{ "search" | i18n }}</label>
|
<div class="mr-3">
|
||||||
<input
|
<label class="sr-only" for="search">{{ "search" | i18n }}</label>
|
||||||
type="search"
|
<input
|
||||||
class="form-control form-control-sm"
|
type="search"
|
||||||
id="search"
|
class="form-control form-control-sm"
|
||||||
placeholder="{{ 'search' | i18n }}"
|
id="search"
|
||||||
name="SearchText"
|
placeholder="{{ 'search' | i18n }}"
|
||||||
[(ngModel)]="searchText"
|
name="SearchText"
|
||||||
/>
|
[(ngModel)]="searchText"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
[ngClass]="{ active: !showSelected }"
|
||||||
|
(click)="filterSelected(false)"
|
||||||
|
>
|
||||||
|
{{ "all" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
[ngClass]="{ active: showSelected }"
|
||||||
|
(click)="filterSelected(true)"
|
||||||
|
>
|
||||||
|
{{ "selected" | i18n }}
|
||||||
|
<span class="badge badge-pill badge-info" *ngIf="selectedCount">{{
|
||||||
|
selectedCount
|
||||||
|
}}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group btn-group-sm" role="group">
|
<ng-container *ngIf="!searchedUsers.length">
|
||||||
<button
|
<hr />
|
||||||
type="button"
|
{{ "noUsersInList" | i18n }}
|
||||||
class="btn btn-outline-secondary"
|
</ng-container>
|
||||||
[ngClass]="{ active: !showSelected }"
|
<table class="table table-hover table-list mb-0" [hidden]="!searchedUsers.length">
|
||||||
(click)="filterSelected(false)"
|
|
||||||
>
|
|
||||||
{{ "all" | i18n }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-secondary"
|
|
||||||
[ngClass]="{ active: showSelected }"
|
|
||||||
(click)="filterSelected(true)"
|
|
||||||
>
|
|
||||||
{{ "selected" | i18n }}
|
|
||||||
<span class="badge badge-pill badge-info" *ngIf="selectedCount">{{
|
|
||||||
selectedCount
|
|
||||||
}}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ng-container *ngIf="!searchedUsers.length">
|
|
||||||
<hr />
|
|
||||||
{{ "noUsersInList" | i18n }}
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="searchedUsers.length">
|
|
||||||
<table class="table table-hover table-list mb-0">
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th> </th>
|
<th> </th>
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let u of searchedUsers">
|
<tr *cdkVirtualFor="let u of searchedUsers" class="">
|
||||||
<td class="table-list-checkbox" (click)="check(u)">
|
<td class="table-list-checkbox" (click)="check(u)">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -164,8 +164,8 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</ng-container>
|
</div>
|
||||||
</div>
|
</cdk-virtual-scroll-viewport>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
<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>
|
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||||
|
|
||||||
|
import { SearchPipe } from "jslib-angular/pipes/search.pipe";
|
||||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { LogService } from "jslib-common/abstractions/log.service";
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
@@ -13,6 +14,7 @@ import { OrganizationUserUserDetailsResponse } from "jslib-common/models/respons
|
|||||||
@Component({
|
@Component({
|
||||||
selector: "app-entity-users",
|
selector: "app-entity-users",
|
||||||
templateUrl: "entity-users.component.html",
|
templateUrl: "entity-users.component.html",
|
||||||
|
providers: [SearchPipe],
|
||||||
})
|
})
|
||||||
export class EntityUsersComponent implements OnInit {
|
export class EntityUsersComponent implements OnInit {
|
||||||
@Input() entity: "group" | "collection";
|
@Input() entity: "group" | "collection";
|
||||||
@@ -33,6 +35,7 @@ export class EntityUsersComponent implements OnInit {
|
|||||||
private allUsers: OrganizationUserUserDetailsResponse[] = [];
|
private allUsers: OrganizationUserUserDetailsResponse[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private search: SearchPipe,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
@@ -52,6 +55,14 @@ export class EntityUsersComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get searchedUsers() {
|
||||||
|
return this.search.transform(this.users, this.searchText, "name", "email", "id");
|
||||||
|
}
|
||||||
|
|
||||||
|
get scrollViewportStyle() {
|
||||||
|
return `min-height: 120px; height: ${120 + this.searchedUsers.length * 46}px`;
|
||||||
|
}
|
||||||
|
|
||||||
async loadUsers() {
|
async loadUsers() {
|
||||||
const users = await this.apiService.getOrganizationUsers(this.organizationId);
|
const users = await this.apiService.getOrganizationUsers(this.organizationId);
|
||||||
this.allUsers = users.data.map((r) => r).sort(Utils.getSortFunction(this.i18nService, "email"));
|
this.allUsers = users.data.map((r) => r).sort(Utils.getSortFunction(this.i18nService, "email"));
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { ScrollingModule } from "@angular/cdk/scrolling";
|
||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { SharedModule } from "../../shared.module";
|
||||||
|
|
||||||
|
import { EntityUsersComponent } from "./entity-users.component";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [SharedModule, ScrollingModule],
|
||||||
|
declarations: [EntityUsersComponent],
|
||||||
|
exports: [EntityUsersComponent],
|
||||||
|
})
|
||||||
|
export class OrganizationManageModule {}
|
||||||
14
src/app/modules/pipes/get-organization-name.pipe.ts
Normal file
14
src/app/modules/pipes/get-organization-name.pipe.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Pipe, PipeTransform } from "@angular/core";
|
||||||
|
|
||||||
|
import { Organization } from "jslib-common/models/domain/organization";
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: "orgNameFromId",
|
||||||
|
pure: true,
|
||||||
|
})
|
||||||
|
export class GetOrgNameFromIdPipe implements PipeTransform {
|
||||||
|
transform(value: string, organizations: Organization[]) {
|
||||||
|
const orgName = organizations.find((o) => o.id === value)?.name;
|
||||||
|
return orgName;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/app/modules/pipes/pipes.module.ts
Normal file
10
src/app/modules/pipes/pipes.module.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { GetOrgNameFromIdPipe } from "./get-organization-name.pipe";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [],
|
||||||
|
declarations: [GetOrgNameFromIdPipe],
|
||||||
|
exports: [GetOrgNameFromIdPipe],
|
||||||
|
})
|
||||||
|
export class PipesModule {}
|
||||||
149
src/app/modules/shared.module.ts
Normal file
149
src/app/modules/shared.module.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { DragDropModule } from "@angular/cdk/drag-drop";
|
||||||
|
import { DatePipe, registerLocaleData, CommonModule } from "@angular/common";
|
||||||
|
import localeAf from "@angular/common/locales/af";
|
||||||
|
import localeAz from "@angular/common/locales/az";
|
||||||
|
import localeBe from "@angular/common/locales/be";
|
||||||
|
import localeBg from "@angular/common/locales/bg";
|
||||||
|
import localeBn from "@angular/common/locales/bn";
|
||||||
|
import localeBs from "@angular/common/locales/bs";
|
||||||
|
import localeCa from "@angular/common/locales/ca";
|
||||||
|
import localeCs from "@angular/common/locales/cs";
|
||||||
|
import localeDa from "@angular/common/locales/da";
|
||||||
|
import localeDe from "@angular/common/locales/de";
|
||||||
|
import localeEl from "@angular/common/locales/el";
|
||||||
|
import localeEnGb from "@angular/common/locales/en-GB";
|
||||||
|
import localeEnIn from "@angular/common/locales/en-IN";
|
||||||
|
import localeEo from "@angular/common/locales/eo";
|
||||||
|
import localeEs from "@angular/common/locales/es";
|
||||||
|
import localeEt from "@angular/common/locales/et";
|
||||||
|
import localeFi from "@angular/common/locales/fi";
|
||||||
|
import localeFil from "@angular/common/locales/fil";
|
||||||
|
import localeFr from "@angular/common/locales/fr";
|
||||||
|
import localeHe from "@angular/common/locales/he";
|
||||||
|
import localeHi from "@angular/common/locales/hi";
|
||||||
|
import localeHr from "@angular/common/locales/hr";
|
||||||
|
import localeHu from "@angular/common/locales/hu";
|
||||||
|
import localeId from "@angular/common/locales/id";
|
||||||
|
import localeIt from "@angular/common/locales/it";
|
||||||
|
import localeJa from "@angular/common/locales/ja";
|
||||||
|
import localeKa from "@angular/common/locales/ka";
|
||||||
|
import localeKm from "@angular/common/locales/km";
|
||||||
|
import localeKn from "@angular/common/locales/kn";
|
||||||
|
import localeKo from "@angular/common/locales/ko";
|
||||||
|
import localeLv from "@angular/common/locales/lv";
|
||||||
|
import localeMl from "@angular/common/locales/ml";
|
||||||
|
import localeNb from "@angular/common/locales/nb";
|
||||||
|
import localeNl from "@angular/common/locales/nl";
|
||||||
|
import localeNn from "@angular/common/locales/nn";
|
||||||
|
import localePl from "@angular/common/locales/pl";
|
||||||
|
import localePtBr from "@angular/common/locales/pt";
|
||||||
|
import localePtPt from "@angular/common/locales/pt-PT";
|
||||||
|
import localeRo from "@angular/common/locales/ro";
|
||||||
|
import localeRu from "@angular/common/locales/ru";
|
||||||
|
import localeSi from "@angular/common/locales/si";
|
||||||
|
import localeSk from "@angular/common/locales/sk";
|
||||||
|
import localeSl from "@angular/common/locales/sl";
|
||||||
|
import localeSr from "@angular/common/locales/sr";
|
||||||
|
import localeSv from "@angular/common/locales/sv";
|
||||||
|
import localeTr from "@angular/common/locales/tr";
|
||||||
|
import localeUk from "@angular/common/locales/uk";
|
||||||
|
import localeVi from "@angular/common/locales/vi";
|
||||||
|
import localeZhCn from "@angular/common/locales/zh-Hans";
|
||||||
|
import localeZhTw from "@angular/common/locales/zh-Hant";
|
||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||||
|
import { RouterModule } from "@angular/router";
|
||||||
|
import { BadgeModule, ButtonModule, CalloutModule, MenuModule } from "@bitwarden/components";
|
||||||
|
import { InfiniteScrollModule } from "ngx-infinite-scroll";
|
||||||
|
import { ToastrModule } from "ngx-toastr";
|
||||||
|
|
||||||
|
import { JslibModule } from "jslib-angular/jslib.module";
|
||||||
|
|
||||||
|
registerLocaleData(localeAf, "af");
|
||||||
|
registerLocaleData(localeAz, "az");
|
||||||
|
registerLocaleData(localeBe, "be");
|
||||||
|
registerLocaleData(localeBg, "bg");
|
||||||
|
registerLocaleData(localeBn, "bn");
|
||||||
|
registerLocaleData(localeBs, "bs");
|
||||||
|
registerLocaleData(localeCa, "ca");
|
||||||
|
registerLocaleData(localeCs, "cs");
|
||||||
|
registerLocaleData(localeDa, "da");
|
||||||
|
registerLocaleData(localeDe, "de");
|
||||||
|
registerLocaleData(localeEl, "el");
|
||||||
|
registerLocaleData(localeEnGb, "en-GB");
|
||||||
|
registerLocaleData(localeEnIn, "en-IN");
|
||||||
|
registerLocaleData(localeEo, "eo");
|
||||||
|
registerLocaleData(localeEs, "es");
|
||||||
|
registerLocaleData(localeEt, "et");
|
||||||
|
registerLocaleData(localeFi, "fi");
|
||||||
|
registerLocaleData(localeFil, "fil");
|
||||||
|
registerLocaleData(localeFr, "fr");
|
||||||
|
registerLocaleData(localeHe, "he");
|
||||||
|
registerLocaleData(localeHi, "hi");
|
||||||
|
registerLocaleData(localeHr, "hr");
|
||||||
|
registerLocaleData(localeHu, "hu");
|
||||||
|
registerLocaleData(localeId, "id");
|
||||||
|
registerLocaleData(localeIt, "it");
|
||||||
|
registerLocaleData(localeJa, "ja");
|
||||||
|
registerLocaleData(localeKa, "ka");
|
||||||
|
registerLocaleData(localeKm, "km");
|
||||||
|
registerLocaleData(localeKn, "kn");
|
||||||
|
registerLocaleData(localeKo, "ko");
|
||||||
|
registerLocaleData(localeLv, "lv");
|
||||||
|
registerLocaleData(localeMl, "ml");
|
||||||
|
registerLocaleData(localeNb, "nb");
|
||||||
|
registerLocaleData(localeNl, "nl");
|
||||||
|
registerLocaleData(localeNn, "nn");
|
||||||
|
registerLocaleData(localePl, "pl");
|
||||||
|
registerLocaleData(localePtBr, "pt-BR");
|
||||||
|
registerLocaleData(localePtPt, "pt-PT");
|
||||||
|
registerLocaleData(localeRo, "ro");
|
||||||
|
registerLocaleData(localeRu, "ru");
|
||||||
|
registerLocaleData(localeSi, "si");
|
||||||
|
registerLocaleData(localeSk, "sk");
|
||||||
|
registerLocaleData(localeSl, "sl");
|
||||||
|
registerLocaleData(localeSr, "sr");
|
||||||
|
registerLocaleData(localeSv, "sv");
|
||||||
|
registerLocaleData(localeTr, "tr");
|
||||||
|
registerLocaleData(localeUk, "uk");
|
||||||
|
registerLocaleData(localeVi, "vi");
|
||||||
|
registerLocaleData(localeZhCn, "zh-CN");
|
||||||
|
registerLocaleData(localeZhTw, "zh-TW");
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
DragDropModule,
|
||||||
|
FormsModule,
|
||||||
|
InfiniteScrollModule,
|
||||||
|
JslibModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
RouterModule,
|
||||||
|
BadgeModule,
|
||||||
|
ButtonModule,
|
||||||
|
CalloutModule,
|
||||||
|
ToastrModule,
|
||||||
|
BadgeModule,
|
||||||
|
ButtonModule,
|
||||||
|
MenuModule,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
CommonModule,
|
||||||
|
DragDropModule,
|
||||||
|
FormsModule,
|
||||||
|
InfiniteScrollModule,
|
||||||
|
JslibModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
RouterModule,
|
||||||
|
BadgeModule,
|
||||||
|
ButtonModule,
|
||||||
|
CalloutModule,
|
||||||
|
ToastrModule,
|
||||||
|
BadgeModule,
|
||||||
|
ButtonModule,
|
||||||
|
MenuModule,
|
||||||
|
],
|
||||||
|
providers: [DatePipe],
|
||||||
|
bootstrap: [],
|
||||||
|
})
|
||||||
|
export class SharedModule {}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<ng-container *ngIf="show">
|
||||||
|
<div class="filter-heading">
|
||||||
|
<button
|
||||||
|
(click)="toggleCollapse(collectionsGrouping)"
|
||||||
|
[attr.aria-expanded]="!isCollapsed(collectionsGrouping)"
|
||||||
|
aria-controls="collection-filters"
|
||||||
|
title="{{ 'toggleCollapse' | i18n }}"
|
||||||
|
class="toggle-button"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="bwi bwi-fw"
|
||||||
|
[ngClass]="{
|
||||||
|
'bwi-angle-right': isCollapsed(collectionsGrouping),
|
||||||
|
'bwi-angle-down': !isCollapsed(collectionsGrouping)
|
||||||
|
}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
<h3 class="filter-title"> {{ collectionsGrouping.name | i18n }}</h3>
|
||||||
|
</div>
|
||||||
|
<ul id="collection-filters" *ngIf="!isCollapsed(collectionsGrouping)" class="filter-options">
|
||||||
|
<ng-template #recursiveCollections let-collections>
|
||||||
|
<li
|
||||||
|
*ngFor="let c of collections"
|
||||||
|
[ngClass]="{
|
||||||
|
active: c.node.id === activeFilter.selectedCollectionId
|
||||||
|
}"
|
||||||
|
class="filter-option"
|
||||||
|
>
|
||||||
|
<span class="filter-buttons">
|
||||||
|
<button
|
||||||
|
class="toggle-button"
|
||||||
|
*ngIf="c.children.length"
|
||||||
|
(click)="collapse(c.node)"
|
||||||
|
title="{{ 'toggleCollapse' | i18n }}"
|
||||||
|
[attr.aria-expanded]="!isCollapsed(c.node)"
|
||||||
|
[attr.aria-controls]="c.node.name + '_children'"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="bwi bwi-fw"
|
||||||
|
[ngClass]="{
|
||||||
|
'bwi-angle-right': isCollapsed(c.node),
|
||||||
|
'bwi-angle-down': !isCollapsed(c.node)
|
||||||
|
}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
<button class="filter-button" (click)="applyFilter(c.node)">
|
||||||
|
<i
|
||||||
|
*ngIf="c.children.length === 0"
|
||||||
|
class="bwi bwi-collection bwi-fw"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i
|
||||||
|
> {{ c.node.name }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<ul
|
||||||
|
[id]="c.node.name + '_children'"
|
||||||
|
class="nested-filter-options"
|
||||||
|
*ngIf="c.children.length && !isCollapsed(c.node)"
|
||||||
|
>
|
||||||
|
<ng-container
|
||||||
|
*ngTemplateOutlet="recursiveCollections; context: { $implicit: c.children }"
|
||||||
|
>
|
||||||
|
</ng-container>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ng-template>
|
||||||
|
<ng-container
|
||||||
|
*ngTemplateOutlet="recursiveCollections; context: { $implicit: nestedCollections }"
|
||||||
|
>
|
||||||
|
</ng-container>
|
||||||
|
</ul>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
|
import { CollectionFilterComponent as BaseCollectionFilterComponent } from "jslib-angular/modules/vault-filter/components/collection-filter.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-collection-filter",
|
||||||
|
templateUrl: "collection-filter.component.html",
|
||||||
|
})
|
||||||
|
export class CollectionFilterComponent extends BaseCollectionFilterComponent {}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<ng-container *ngIf="!hide">
|
||||||
|
<div class="filter-heading">
|
||||||
|
<button
|
||||||
|
class="toggle-button"
|
||||||
|
(click)="toggleCollapse(foldersGrouping)"
|
||||||
|
[attr.aria-expanded]="!isCollapsed(foldersGrouping)"
|
||||||
|
aria-controls="folder-filters"
|
||||||
|
title="{{ 'toggleCollapse' | i18n }}"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="bwi bwi-fw"
|
||||||
|
aria-hidden="true"
|
||||||
|
[ngClass]="{
|
||||||
|
'bwi-angle-right': isCollapsed(foldersGrouping),
|
||||||
|
'bwi-angle-down': !isCollapsed(foldersGrouping)
|
||||||
|
}"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
<h3 class="filter-title"> {{ "folders" | i18n }}</h3>
|
||||||
|
<button
|
||||||
|
class="text-muted ml-auto add-button"
|
||||||
|
(click)="addFolder()"
|
||||||
|
appA11yTitle="{{ 'addFolder' | i18n }}"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ul id="folder-filters" *ngIf="!isCollapsed(foldersGrouping)" class="filter-options">
|
||||||
|
<ng-template #recursiveFolders let-folders>
|
||||||
|
<li
|
||||||
|
*ngFor="let f of folders"
|
||||||
|
[ngClass]="{
|
||||||
|
active: f.node.id === activeFilter.selectedFolderId && activeFilter.selectedFolder
|
||||||
|
}"
|
||||||
|
class="filter-option"
|
||||||
|
>
|
||||||
|
<span class="filter-buttons">
|
||||||
|
<button
|
||||||
|
*ngIf="f.children.length"
|
||||||
|
title="{{ 'toggleCollapse' | i18n }}"
|
||||||
|
(click)="toggleCollapse(f.node)"
|
||||||
|
[attr.aria-expanded]="!isCollapsed(f.node)"
|
||||||
|
[attr.aria-controls]="f.node.name + '_children'"
|
||||||
|
class="toggle-button"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="bwi bwi-fw"
|
||||||
|
[ngClass]="{
|
||||||
|
'bwi-angle-right': isCollapsed(f.node),
|
||||||
|
'bwi-angle-down': !isCollapsed(f.node)
|
||||||
|
}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
<button class="filter-button" (click)="applyFilter(f.node)">
|
||||||
|
<i *ngIf="f.children.length === 0" class="bwi bwi-fw bwi-folder" aria-hidden="true"></i
|
||||||
|
> {{ f.node.name }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="edit-button"
|
||||||
|
(click)="editFolder(f.node)"
|
||||||
|
appA11yTitle="{{ 'editFolder' | i18n }}"
|
||||||
|
*ngIf="f.node.id"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-pencil bwi-fw" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<ul
|
||||||
|
[id]="f.node.name + '_children'"
|
||||||
|
class="nested-filter-options"
|
||||||
|
*ngIf="f.children.length && !isCollapsed(f.node)"
|
||||||
|
>
|
||||||
|
<ng-container *ngTemplateOutlet="recursiveFolders; context: { $implicit: f.children }">
|
||||||
|
</ng-container>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ng-template>
|
||||||
|
<ng-container
|
||||||
|
*ngTemplateOutlet="recursiveFolders; context: { $implicit: nestedFolders }"
|
||||||
|
></ng-container>
|
||||||
|
</ul>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
|
import { FolderFilterComponent as BaseFolderFilterComponent } from "jslib-angular/modules/vault-filter/components/folder-filter.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-folder-filter",
|
||||||
|
templateUrl: "folder-filter.component.html",
|
||||||
|
})
|
||||||
|
export class FolderFilterComponent extends BaseFolderFilterComponent {}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<ng-container *ngIf="!hide">
|
||||||
|
<ng-container [ngSwitch]="displayMode">
|
||||||
|
<ng-container *ngSwitchCase="'noOrganizations'">
|
||||||
|
<ul class="filter-options">
|
||||||
|
<li class="filter-option active">
|
||||||
|
<span class="filter-buttons">
|
||||||
|
<button class="filter-button">
|
||||||
|
<i class="bwi bwi-fw bwi-user" aria-hidden="true"></i>
|
||||||
|
{{ "myVault" | i18n }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="filter-option">
|
||||||
|
<span class="filter-buttons">
|
||||||
|
<a href="#" routerLink="/create-organization" class="filter-button">
|
||||||
|
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||||
|
{{ "newOrganization" | i18n }}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'personalOwnershipPolicy'">
|
||||||
|
<div class="filter-heading">
|
||||||
|
<button
|
||||||
|
(click)="toggleCollapse()"
|
||||||
|
title="{{ 'toggleCollapse' | i18n }}"
|
||||||
|
class="toggle-button"
|
||||||
|
[attr.aria-expanded]="!isCollapsed"
|
||||||
|
aria-controls="organization-filters"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="bwi bwi-fw"
|
||||||
|
aria-hidden="true"
|
||||||
|
[ngClass]="{
|
||||||
|
'bwi-angle-right': isCollapsed,
|
||||||
|
'bwi-angle-down': !isCollapsed
|
||||||
|
}"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="filter-button"
|
||||||
|
(click)="clearFilter()"
|
||||||
|
[ngClass]="{ active: !hasActiveFilter }"
|
||||||
|
>
|
||||||
|
{{ organizationGrouping.name | i18n }}
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
routerLink="/create-organization"
|
||||||
|
class="text-muted ml-auto create-organization-link"
|
||||||
|
appA11yTitle="{{ 'newOrganization' | i18n }}"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<ul id="organization-filters" *ngIf="!isCollapsed" class="filter-options">
|
||||||
|
<li
|
||||||
|
class="filter-option"
|
||||||
|
*ngFor="let organization of organizations"
|
||||||
|
[ngClass]="{ active: organization.id === activeFilter.selectedOrganizationId }"
|
||||||
|
>
|
||||||
|
<span class="filter-buttons">
|
||||||
|
<button class="filter-button" (click)="applyOrganizationFilter(organization)">
|
||||||
|
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
|
||||||
|
{{ organization.name }}
|
||||||
|
</button>
|
||||||
|
<ng-container *ngIf="organization.id === activeFilter.selectedOrganizationId">
|
||||||
|
<button [bitMenuTriggerFor]="orgMenu" class="org-options ml-auto">
|
||||||
|
<i class="bwi bwi-ellipsis-v" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<bit-menu class="filter-organization-options" #orgMenu>
|
||||||
|
<app-organization-options [organization]="organization"></app-organization-options>
|
||||||
|
</bit-menu>
|
||||||
|
</ng-container>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'singleOrganizationAndPersonalOwnershipPolicies'">
|
||||||
|
<div class="filter-heading">
|
||||||
|
<button class="filter-button active">
|
||||||
|
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
|
||||||
|
{{ organizations[0].name }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchDefault>
|
||||||
|
<div class="filter-heading">
|
||||||
|
<button
|
||||||
|
class="toggle-button"
|
||||||
|
title="{{ 'toggleCollapse' | i18n }}"
|
||||||
|
(click)="toggleCollapse()"
|
||||||
|
[attr.aria-expanded]="!isCollapsed"
|
||||||
|
aria-controls="organization-filters"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="bwi bwi-fw"
|
||||||
|
aria-hidden="true"
|
||||||
|
[ngClass]="{
|
||||||
|
'bwi-angle-right': isCollapsed,
|
||||||
|
'bwi-angle-down': !isCollapsed
|
||||||
|
}"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="filter-button"
|
||||||
|
(click)="clearFilter()"
|
||||||
|
[ngClass]="{ active: !hasActiveFilter }"
|
||||||
|
>
|
||||||
|
{{ organizationGrouping.name | i18n }}
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
routerLink="/create-organization"
|
||||||
|
class="text-muted ml-auto create-organization-link"
|
||||||
|
appA11yTitle="{{ 'newOrganization' | i18n }}"
|
||||||
|
*ngIf="!(displayMode === 'singleOrganizationPolicy')"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<ul id="organization-filters" *ngIf="!isCollapsed" class="filter-options">
|
||||||
|
<li class="filter-option" [ngClass]="{ active: activeFilter.myVaultOnly }">
|
||||||
|
<span class="filter-buttons">
|
||||||
|
<button class="filter-button" (click)="applyMyVaultFilter()">
|
||||||
|
<i class="bwi bwi-fw bwi-user" aria-hidden="true"></i>
|
||||||
|
{{ "myVault" | i18n }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="filter-option"
|
||||||
|
*ngFor="let organization of organizations"
|
||||||
|
[ngClass]="{ active: organization.id === activeFilter.selectedOrganizationId }"
|
||||||
|
>
|
||||||
|
<span class="filter-buttons">
|
||||||
|
<button class="filter-button" (click)="applyOrganizationFilter(organization)">
|
||||||
|
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
|
||||||
|
{{ organization.name }}
|
||||||
|
</button>
|
||||||
|
<ng-container>
|
||||||
|
<button [bitMenuTriggerFor]="orgMenu" class="org-options ml-auto">
|
||||||
|
<i class="bwi bwi-ellipsis-v" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<bit-menu class="filter-organization-options" #orgMenu>
|
||||||
|
<app-organization-options [organization]="organization"></app-organization-options>
|
||||||
|
</bit-menu>
|
||||||
|
</ng-container>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
<hr />
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
|
import { OrganizationFilterComponent as BaseOrganizationFilterComponent } from "jslib-angular/modules/vault-filter/components/organization-filter.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-organization-filter",
|
||||||
|
templateUrl: "organization-filter.component.html",
|
||||||
|
})
|
||||||
|
export class OrganizationFilterComponent extends BaseOrganizationFilterComponent {
|
||||||
|
displayText = "allVaults";
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<ng-container *ngIf="!loaded">
|
||||||
|
<i
|
||||||
|
class="bwi bwi-spinner bwi-spin text-muted tw-m-2"
|
||||||
|
title="{{ 'loading' | i18n }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||||
|
</ng-container>
|
||||||
|
<div *ngIf="loaded" class="tw-max-w-[300px] tw-min-w-[200px] tw-flex tw-flex-col">
|
||||||
|
<button
|
||||||
|
*ngIf="allowEnrollmentChanges(organization) && !organization.resetPasswordEnrolled"
|
||||||
|
class="dropdown-item"
|
||||||
|
(click)="toggleResetPasswordEnrollment(organization)"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-fw bwi-key" aria-hidden="true"></i>
|
||||||
|
{{ "enrollPasswordReset" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="allowEnrollmentChanges(organization) && organization.resetPasswordEnrolled"
|
||||||
|
class="dropdown-item"
|
||||||
|
(click)="toggleResetPasswordEnrollment(organization)"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
|
||||||
|
{{ "withdrawPasswordReset" | i18n }}
|
||||||
|
</button>
|
||||||
|
<ng-container *ngIf="organization.useSso && organization.identifier">
|
||||||
|
<button
|
||||||
|
*ngIf="organization.ssoBound; else linkSso"
|
||||||
|
class="dropdown-item"
|
||||||
|
(click)="unlinkSso(organization)"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-fw bwi-chain-broken" aria-hidden="true"></i>
|
||||||
|
{{ "unlinkSso" | i18n }}
|
||||||
|
</button>
|
||||||
|
<ng-template #linkSso>
|
||||||
|
<app-link-sso [organization]="organization"> </app-link-sso>
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
<button class="dropdown-item text-danger" (click)="leave(organization)">
|
||||||
|
<i class="bwi bwi-fw bwi-sign-out" aria-hidden="true"></i>
|
||||||
|
{{ "leave" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import { Component, Input, OnInit } from "@angular/core";
|
import { Component, Input } from "@angular/core";
|
||||||
|
|
||||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { LogService } from "jslib-common/abstractions/log.service";
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
|
||||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
import { PolicyService } from "jslib-common/abstractions/policy.service";
|
import { PolicyService } from "jslib-common/abstractions/policy.service";
|
||||||
import { SyncService } from "jslib-common/abstractions/sync.service";
|
import { SyncService } from "jslib-common/abstractions/sync.service";
|
||||||
@@ -15,19 +14,17 @@ import { Policy } from "jslib-common/models/domain/policy";
|
|||||||
import { OrganizationUserResetPasswordEnrollmentRequest } from "jslib-common/models/request/organizationUserResetPasswordEnrollmentRequest";
|
import { OrganizationUserResetPasswordEnrollmentRequest } from "jslib-common/models/request/organizationUserResetPasswordEnrollmentRequest";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-organizations",
|
selector: "app-organization-options",
|
||||||
templateUrl: "organizations.component.html",
|
templateUrl: "organization-options.component.html",
|
||||||
})
|
})
|
||||||
export class OrganizationsComponent implements OnInit {
|
export class OrganizationOptionsComponent {
|
||||||
@Input() vault = false;
|
actionPromise: Promise<any>;
|
||||||
|
|
||||||
organizations: Organization[];
|
|
||||||
policies: Policy[];
|
policies: Policy[];
|
||||||
loaded = false;
|
loaded = false;
|
||||||
actionPromise: Promise<any>;
|
|
||||||
|
@Input() organization: Organization;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private organizationService: OrganizationService,
|
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
@@ -38,16 +35,10 @@ export class OrganizationsComponent implements OnInit {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
if (!this.vault) {
|
await this.load();
|
||||||
await this.syncService.fullSync(true);
|
|
||||||
await this.load();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
const orgs = await this.organizationService.getAll();
|
|
||||||
orgs.sort(Utils.getSortFunction(this.i18nService, "name"));
|
|
||||||
this.organizations = orgs;
|
|
||||||
this.policies = await this.policyService.getAll(PolicyType.ResetPassword);
|
this.policies = await this.policyService.getAll(PolicyType.ResetPassword);
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
}
|
}
|
||||||
@@ -91,6 +82,7 @@ export class OrganizationsComponent implements OnInit {
|
|||||||
this.platformUtilsService.showToast("success", null, "Unlinked SSO");
|
this.platformUtilsService.showToast("success", null, "Unlinked SSO");
|
||||||
await this.load();
|
await this.load();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message);
|
||||||
this.logService.error(e);
|
this.logService.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,6 +107,7 @@ export class OrganizationsComponent implements OnInit {
|
|||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("leftOrganization"));
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("leftOrganization"));
|
||||||
await this.load();
|
await this.load();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message);
|
||||||
this.logService.error(e);
|
this.logService.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,6 +175,7 @@ export class OrganizationsComponent implements OnInit {
|
|||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t(toastStringRef));
|
this.platformUtilsService.showToast("success", null, this.i18nService.t(toastStringRef));
|
||||||
await this.load();
|
await this.load();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message);
|
||||||
this.logService.error(e);
|
this.logService.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<ng-container *ngIf="show">
|
||||||
|
<ul class="filter-options">
|
||||||
|
<li class="filter-option" [ngClass]="{ active: activeFilter.status === 'all' }">
|
||||||
|
<span class="filter-buttons">
|
||||||
|
<button class="filter-button" (click)="applyFilter('all')">
|
||||||
|
<i class="bwi bwi-fw bwi-filter" aria-hidden="true"></i> {{ "allItems" | i18n }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
*ngIf="!hideFavorites"
|
||||||
|
class="filter-option"
|
||||||
|
[ngClass]="{ active: activeFilter.status === 'favorites' }"
|
||||||
|
>
|
||||||
|
<span class="filter-buttons">
|
||||||
|
<button class="filter-button" (click)="applyFilter('favorites')">
|
||||||
|
<i class="bwi bwi-fw bwi-star" aria-hidden="true"></i> {{ "favorites" | i18n }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
*ngIf="!hideTrash"
|
||||||
|
class="filter-option"
|
||||||
|
[ngClass]="{ active: activeFilter.status === 'trash' }"
|
||||||
|
>
|
||||||
|
<span class="filter-buttons">
|
||||||
|
<button class="filter-button" (click)="applyFilter('trash')">
|
||||||
|
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i> {{ "trash" | i18n }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
|
import { StatusFilterComponent as BaseStatusFilterComponent } from "jslib-angular/modules/vault-filter/components/status-filter.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-status-filter",
|
||||||
|
templateUrl: "status-filter.component.html",
|
||||||
|
})
|
||||||
|
export class StatusFilterComponent extends BaseStatusFilterComponent {}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<div class="filter-heading">
|
||||||
|
<button
|
||||||
|
class="toggle-button"
|
||||||
|
[attr.aria-expanded]="!isCollapsed"
|
||||||
|
aria-controls="type-filters"
|
||||||
|
(click)="toggleCollapse()"
|
||||||
|
title="{{ 'toggleCollapse' | i18n }}"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="bwi bwi-fw"
|
||||||
|
aria-hidden="true"
|
||||||
|
[ngClass]="{
|
||||||
|
'bwi-angle-right': isCollapsed,
|
||||||
|
'bwi-angle-down': !isCollapsed
|
||||||
|
}"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
<h3> {{ "types" | i18n }}</h3>
|
||||||
|
</div>
|
||||||
|
<ul id="type-filters" *ngIf="!isCollapsed" class="filter-options">
|
||||||
|
<li
|
||||||
|
class="filter-option"
|
||||||
|
[ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.Login }"
|
||||||
|
>
|
||||||
|
<span class="filter-buttons">
|
||||||
|
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.Login)">
|
||||||
|
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i> {{ "typeLogin" | i18n }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="filter-option" [ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.Card }">
|
||||||
|
<span class="filter-buttons">
|
||||||
|
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.Card)">
|
||||||
|
<i class="bwi bwi-fw bwi-credit-card" aria-hidden="true"></i> {{ "typeCard" | i18n }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="filter-option"
|
||||||
|
[ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.Identity }"
|
||||||
|
>
|
||||||
|
<span class="filter-buttons">
|
||||||
|
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.Identity)">
|
||||||
|
<i class="bwi bwi-fw bwi-id-card" aria-hidden="true"></i> {{ "typeIdentity" | i18n }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="filter-option"
|
||||||
|
[ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.SecureNote }"
|
||||||
|
>
|
||||||
|
<span class="filter-buttons">
|
||||||
|
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.SecureNote)">
|
||||||
|
<i class="bwi bwi-fw bwi-sticky-note" aria-hidden="true"></i> {{
|
||||||
|
"typeSecureNote" | i18n
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
|
import { TypeFilterComponent as BaseTypeFilterComponent } from "jslib-angular/modules/vault-filter/components/type-filter.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-type-filter",
|
||||||
|
templateUrl: "type-filter.component.html",
|
||||||
|
})
|
||||||
|
export class TypeFilterComponent extends BaseTypeFilterComponent {}
|
||||||
80
src/app/modules/vault-filter/vault-filter.component.html
Normal file
80
src/app/modules/vault-filter/vault-filter.component.html
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<div class="card vault-filters">
|
||||||
|
<div class="container loading-spinner" *ngIf="!isLoaded">
|
||||||
|
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="isLoaded">
|
||||||
|
<div class="card-header d-flex">
|
||||||
|
{{ "filters" | i18n }}
|
||||||
|
<a
|
||||||
|
class="ml-auto"
|
||||||
|
href="https://bitwarden.com/help/searching-vault/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
placeholder="{{ (searchPlaceholder | i18n) || ('searchVault' | i18n) }}"
|
||||||
|
id="search"
|
||||||
|
class="form-control"
|
||||||
|
[(ngModel)]="searchText"
|
||||||
|
(input)="searchTextChanged()"
|
||||||
|
autocomplete="off"
|
||||||
|
appAutofocus
|
||||||
|
/>
|
||||||
|
<app-organization-filter
|
||||||
|
*ngIf="showOrgFilter"
|
||||||
|
[hide]="hideOrganizations"
|
||||||
|
[activeFilter]="activeFilter"
|
||||||
|
[collapsedFilterNodes]="collapsedFilterNodes"
|
||||||
|
[organizations]="organizations"
|
||||||
|
[activePersonalOwnershipPolicy]="activePersonalOwnershipPolicy"
|
||||||
|
[activeSingleOrganizationPolicy]="activeSingleOrganizationPolicy"
|
||||||
|
(onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)"
|
||||||
|
(onFilterChange)="applyFilter($event)"
|
||||||
|
></app-organization-filter>
|
||||||
|
<div class="filter">
|
||||||
|
<app-status-filter
|
||||||
|
[hideFavorites]="!showFavorites"
|
||||||
|
[hideTrash]="hideTrash"
|
||||||
|
[activeFilter]="activeFilter"
|
||||||
|
(onFilterChange)="applyFilter($event)"
|
||||||
|
></app-status-filter>
|
||||||
|
</div>
|
||||||
|
<div class="filter">
|
||||||
|
<app-type-filter
|
||||||
|
[activeFilter]="activeFilter"
|
||||||
|
[collapsedFilterNodes]="collapsedFilterNodes"
|
||||||
|
(onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)"
|
||||||
|
(onFilterChange)="applyFilter($event)"
|
||||||
|
></app-type-filter>
|
||||||
|
</div>
|
||||||
|
<div class="filter">
|
||||||
|
<app-folder-filter
|
||||||
|
[hide]="!showFolders"
|
||||||
|
[activeFilter]="activeFilter"
|
||||||
|
[collapsedFilterNodes]="collapsedFilterNodes"
|
||||||
|
[folderNodes]="folders"
|
||||||
|
(onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)"
|
||||||
|
(onFilterChange)="applyFilter($event)"
|
||||||
|
(onAddFolder)="addFolder()"
|
||||||
|
(onEditFolder)="editFolder($event)"
|
||||||
|
></app-folder-filter>
|
||||||
|
</div>
|
||||||
|
<div class="filter">
|
||||||
|
<app-collection-filter
|
||||||
|
[hide]="hideCollections"
|
||||||
|
[activeFilter]="activeFilter"
|
||||||
|
[collapsedFilterNodes]="collapsedFilterNodes"
|
||||||
|
[collectionNodes]="collections"
|
||||||
|
(onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)"
|
||||||
|
(onFilterChange)="applyFilter($event)"
|
||||||
|
></app-collection-filter>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
34
src/app/modules/vault-filter/vault-filter.component.ts
Normal file
34
src/app/modules/vault-filter/vault-filter.component.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||||
|
|
||||||
|
import { VaultFilterComponent as BaseVaultFilterComponent } from "jslib-angular/modules/vault-filter/vault-filter.component";
|
||||||
|
import { VaultFilterService } from "jslib-angular/modules/vault-filter/vault-filter.service";
|
||||||
|
import { Organization } from "jslib-common/models/domain/organization";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-vault-filter",
|
||||||
|
templateUrl: "vault-filter.component.html",
|
||||||
|
})
|
||||||
|
export class VaultFilterComponent extends BaseVaultFilterComponent {
|
||||||
|
@Input() showOrgFilter = true;
|
||||||
|
@Input() showFolders = true;
|
||||||
|
@Input() showFavorites = true;
|
||||||
|
|
||||||
|
@Output() onSearchTextChanged = new EventEmitter<string>();
|
||||||
|
|
||||||
|
searchPlaceholder: string;
|
||||||
|
searchText = "";
|
||||||
|
|
||||||
|
organization: Organization;
|
||||||
|
|
||||||
|
constructor(vaultFilterService: VaultFilterService) {
|
||||||
|
super(vaultFilterService);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchTextChanged() {
|
||||||
|
this.onSearchTextChanged.emit(this.searchText.normalize("NFD").replace(/[\u0300-\u036f]/g, ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
async initCollections() {
|
||||||
|
return await this.vaultFilterService.buildCollections(this.organization?.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/app/modules/vault-filter/vault-filter.module.ts
Normal file
48
src/app/modules/vault-filter/vault-filter.module.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { VaultFilterService } from "jslib-angular/modules/vault-filter/vault-filter.service";
|
||||||
|
import { CipherService } from "jslib-common/abstractions/cipher.service";
|
||||||
|
import { CollectionService } from "jslib-common/abstractions/collection.service";
|
||||||
|
import { FolderService } from "jslib-common/abstractions/folder.service";
|
||||||
|
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||||
|
import { PolicyService } from "jslib-common/abstractions/policy.service";
|
||||||
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
|
||||||
|
import { SharedModule } from "../shared.module";
|
||||||
|
|
||||||
|
import { CollectionFilterComponent } from "./components/collection-filter.component";
|
||||||
|
import { FolderFilterComponent } from "./components/folder-filter.component";
|
||||||
|
import { OrganizationFilterComponent } from "./components/organization-filter.component";
|
||||||
|
import { OrganizationOptionsComponent } from "./components/organization-options.component";
|
||||||
|
import { StatusFilterComponent } from "./components/status-filter.component";
|
||||||
|
import { TypeFilterComponent } from "./components/type-filter.component";
|
||||||
|
import { VaultFilterComponent } from "./vault-filter.component";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [SharedModule],
|
||||||
|
declarations: [
|
||||||
|
VaultFilterComponent,
|
||||||
|
CollectionFilterComponent,
|
||||||
|
FolderFilterComponent,
|
||||||
|
OrganizationFilterComponent,
|
||||||
|
OrganizationOptionsComponent,
|
||||||
|
StatusFilterComponent,
|
||||||
|
TypeFilterComponent,
|
||||||
|
],
|
||||||
|
exports: [VaultFilterComponent],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: VaultFilterService,
|
||||||
|
useClass: VaultFilterService,
|
||||||
|
deps: [
|
||||||
|
StateService,
|
||||||
|
OrganizationService,
|
||||||
|
FolderService,
|
||||||
|
CipherService,
|
||||||
|
CollectionService,
|
||||||
|
PolicyService,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class VaultFilterModule {}
|
||||||
3
src/app/modules/vault-filter/vault-filter.service.ts
Normal file
3
src/app/modules/vault-filter/vault-filter.service.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { VaultFilterService as BaseVaultFilterService } from "jslib-angular/modules/vault-filter/vault-filter.service";
|
||||||
|
|
||||||
|
export class VaultFilterService extends BaseVaultFilterService {}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
import { RouterModule, Routes } from "@angular/router";
|
||||||
|
|
||||||
|
import { IndividualVaultComponent } from "./individual-vault.component";
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
component: IndividualVaultComponent,
|
||||||
|
data: { titleId: "vaults" },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class IndividualVaultRoutingModule {}
|
||||||
@@ -1,23 +1,25 @@
|
|||||||
<div class="container page-content">
|
<div class="container page-content">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
<app-vault-groupings
|
<div class="groupings">
|
||||||
(onAllClicked)="clearGroupingFilters()"
|
<div class="content">
|
||||||
(onFavoritesClicked)="filterFavorites()"
|
<div class="inner-content">
|
||||||
(onCipherTypeClicked)="filterCipherType($event)"
|
<app-vault-filter
|
||||||
(onFolderClicked)="filterFolder($event.id)"
|
#vaultFilter
|
||||||
(onAddFolder)="addFolder()"
|
[activeFilter]="activeFilter"
|
||||||
(onEditFolder)="editFolder($event.id)"
|
(onFilterChange)="applyVaultFilter($event)"
|
||||||
(onCollectionClicked)="filterCollection($event.id)"
|
(onAddFolder)="addFolder()"
|
||||||
(onSearchTextChanged)="filterSearchText($event)"
|
(onEditFolder)="editFolder($event.id)"
|
||||||
(onTrashClicked)="filterDeleted()"
|
(onSearchTextChanged)="filterSearchText($event)"
|
||||||
>
|
></app-vault-filter>
|
||||||
</app-vault-groupings>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div [ngClass]="{ 'col-6': isShowingCards, 'col-9': !isShowingCards }">
|
||||||
<div class="page-header d-flex">
|
<div class="page-header d-flex">
|
||||||
<h1>
|
<h1>
|
||||||
{{ "myVault" | i18n }}
|
{{ "vaultItems" | i18n }}
|
||||||
<small #actionSpinner [appApiAction]="ciphersComponent.actionPromise">
|
<small #actionSpinner [appApiAction]="ciphersComponent.actionPromise">
|
||||||
<ng-container *ngIf="actionSpinner.loading">
|
<ng-container *ngIf="actionSpinner.loading">
|
||||||
<i
|
<i
|
||||||
@@ -30,19 +32,26 @@
|
|||||||
</small>
|
</small>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="ml-auto d-flex">
|
<div class="ml-auto d-flex">
|
||||||
<app-vault-bulk-actions [ciphersComponent]="ciphersComponent" [deleted]="deleted">
|
<app-vault-bulk-actions
|
||||||
|
[ciphersComponent]="ciphersComponent"
|
||||||
|
[deleted]="activeFilter.status === 'trash'"
|
||||||
|
>
|
||||||
</app-vault-bulk-actions>
|
</app-vault-bulk-actions>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-outline-primary btn-sm"
|
class="btn btn-outline-primary btn-sm"
|
||||||
(click)="addCipher()"
|
(click)="addCipher()"
|
||||||
*ngIf="!deleted"
|
*ngIf="activeFilter.status !== 'trash'"
|
||||||
>
|
>
|
||||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "addItem" | i18n }}
|
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "addItem" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<app-callout type="warning" *ngIf="deleted" icon="bwi-exclamation-triangle">
|
<app-callout
|
||||||
|
type="warning"
|
||||||
|
*ngIf="activeFilter.status === 'trash'"
|
||||||
|
icon="bwi-exclamation-triangle"
|
||||||
|
>
|
||||||
{{ trashCleanupWarning }}
|
{{ trashCleanupWarning }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
<app-vault-ciphers
|
<app-vault-ciphers
|
||||||
@@ -52,6 +61,7 @@
|
|||||||
(onShareClicked)="shareCipher($event)"
|
(onShareClicked)="shareCipher($event)"
|
||||||
(onCollectionsClicked)="editCipherCollections($event)"
|
(onCollectionsClicked)="editCipherCollections($event)"
|
||||||
(onCloneClicked)="cloneCipher($event)"
|
(onCloneClicked)="cloneCipher($event)"
|
||||||
|
(onOrganzationBadgeClicked)="applyOrganizationFilter($event)"
|
||||||
>
|
>
|
||||||
</app-vault-ciphers>
|
</app-vault-ciphers>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,45 +102,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
|
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
|
||||||
<a class="btn btn-block btn-outline-secondary" routerLink="/settings/premium">
|
<a
|
||||||
|
class="btn btn-block btn-outline-secondary"
|
||||||
|
routerLink="/settings/subscription/premium"
|
||||||
|
>
|
||||||
{{ "goPremium" | i18n }}
|
{{ "goPremium" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header d-flex">
|
|
||||||
{{ "organizations" | i18n }}
|
|
||||||
<a
|
|
||||||
class="ml-auto"
|
|
||||||
href="https://bitwarden.com/help/about-organizations/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<app-organizations [vault]="true"></app-organizations>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card mt-4" *ngIf="showProviders">
|
|
||||||
<div class="card-header d-flex">
|
|
||||||
{{ "providers" | i18n }}
|
|
||||||
<a
|
|
||||||
class="ml-auto"
|
|
||||||
href="https://bitwarden.com/help/providers/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<app-providers vault="true"></app-providers>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -10,42 +10,41 @@ import {
|
|||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { first } from "rxjs/operators";
|
import { first } from "rxjs/operators";
|
||||||
|
|
||||||
|
import { VaultFilter } from "jslib-angular/modules/vault-filter/models/vault-filter.model";
|
||||||
import { ModalService } from "jslib-angular/services/modal.service";
|
import { ModalService } from "jslib-angular/services/modal.service";
|
||||||
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
|
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
|
||||||
|
import { CipherService } from "jslib-common/abstractions/cipher.service";
|
||||||
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||||
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||||
|
import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service";
|
||||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
|
||||||
import { StateService } from "jslib-common/abstractions/state.service";
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
import { SyncService } from "jslib-common/abstractions/sync.service";
|
import { SyncService } from "jslib-common/abstractions/sync.service";
|
||||||
import { TokenService } from "jslib-common/abstractions/token.service";
|
import { TokenService } from "jslib-common/abstractions/token.service";
|
||||||
import { CipherType } from "jslib-common/enums/cipherType";
|
import { CipherType } from "jslib-common/enums/cipherType";
|
||||||
import { CipherView } from "jslib-common/models/view/cipherView";
|
import { CipherView } from "jslib-common/models/view/cipherView";
|
||||||
|
|
||||||
import { OrganizationsComponent } from "../settings/organizations.component";
|
import { UpdateKeyComponent } from "../../../../settings/update-key.component";
|
||||||
import { UpdateKeyComponent } from "../settings/update-key.component";
|
import { AddEditComponent } from "../../../../vault/add-edit.component";
|
||||||
|
import { AttachmentsComponent } from "../../../../vault/attachments.component";
|
||||||
import { AddEditComponent } from "./add-edit.component";
|
import { CiphersComponent } from "../../../../vault/ciphers.component";
|
||||||
import { AttachmentsComponent } from "./attachments.component";
|
import { CollectionsComponent } from "../../../../vault/collections.component";
|
||||||
import { CiphersComponent } from "./ciphers.component";
|
import { FolderAddEditComponent } from "../../../../vault/folder-add-edit.component";
|
||||||
import { CollectionsComponent } from "./collections.component";
|
import { ShareComponent } from "../../../../vault/share.component";
|
||||||
import { FolderAddEditComponent } from "./folder-add-edit.component";
|
import { VaultFilterComponent } from "../../../vault-filter/vault-filter.component";
|
||||||
import { GroupingsComponent } from "./groupings.component";
|
import { VaultService } from "../../vault.service";
|
||||||
import { ShareComponent } from "./share.component";
|
|
||||||
|
|
||||||
const BroadcasterSubscriptionId = "VaultComponent";
|
const BroadcasterSubscriptionId = "VaultComponent";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-vault",
|
selector: "app-vault",
|
||||||
templateUrl: "vault.component.html",
|
templateUrl: "individual-vault.component.html",
|
||||||
})
|
})
|
||||||
export class VaultComponent implements OnInit, OnDestroy {
|
export class IndividualVaultComponent implements OnInit, OnDestroy {
|
||||||
@ViewChild(GroupingsComponent, { static: true }) groupingsComponent: GroupingsComponent;
|
@ViewChild("vaultFilter", { static: true }) filterComponent: VaultFilterComponent;
|
||||||
@ViewChild(CiphersComponent, { static: true }) ciphersComponent: CiphersComponent;
|
@ViewChild(CiphersComponent, { static: true }) ciphersComponent: CiphersComponent;
|
||||||
@ViewChild(OrganizationsComponent, { static: true })
|
|
||||||
organizationsComponent: OrganizationsComponent;
|
|
||||||
@ViewChild("attachments", { read: ViewContainerRef, static: true })
|
@ViewChild("attachments", { read: ViewContainerRef, static: true })
|
||||||
attachmentsModalRef: ViewContainerRef;
|
attachmentsModalRef: ViewContainerRef;
|
||||||
@ViewChild("folderAddEdit", { read: ViewContainerRef, static: true })
|
@ViewChild("folderAddEdit", { read: ViewContainerRef, static: true })
|
||||||
@@ -62,13 +61,15 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
type: CipherType = null;
|
type: CipherType = null;
|
||||||
folderId: string = null;
|
folderId: string = null;
|
||||||
collectionId: string = null;
|
collectionId: string = null;
|
||||||
|
organizationId: string = null;
|
||||||
|
myVaultOnly = false;
|
||||||
showVerifyEmail = false;
|
showVerifyEmail = false;
|
||||||
showBrowserOutdated = false;
|
showBrowserOutdated = false;
|
||||||
showUpdateKey = false;
|
showUpdateKey = false;
|
||||||
showPremiumCallout = false;
|
showPremiumCallout = false;
|
||||||
showProviders = false;
|
|
||||||
deleted = false;
|
deleted = false;
|
||||||
trashCleanupWarning: string = null;
|
trashCleanupWarning: string = null;
|
||||||
|
activeFilter: VaultFilter = new VaultFilter();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private syncService: SyncService,
|
private syncService: SyncService,
|
||||||
@@ -85,7 +86,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
private ngZone: NgZone,
|
private ngZone: NgZone,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private providerService: ProviderService
|
private vaultService: VaultService,
|
||||||
|
private cipherService: CipherService,
|
||||||
|
private passwordRepromptService: PasswordRepromptService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@@ -99,42 +102,42 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||||
await this.syncService.fullSync(false);
|
await this.syncService.fullSync(false);
|
||||||
|
|
||||||
const canAccessPremium = await this.stateService.getCanAccessPremium();
|
const canAccessPremium = await this.stateService.getCanAccessPremium();
|
||||||
this.showPremiumCallout =
|
this.showPremiumCallout =
|
||||||
!this.showVerifyEmail && !canAccessPremium && !this.platformUtilsService.isSelfHost();
|
!this.showVerifyEmail && !canAccessPremium && !this.platformUtilsService.isSelfHost();
|
||||||
|
|
||||||
this.showProviders = (await this.providerService.getAll()).length > 0;
|
this.filterComponent.reloadCollectionsAndFolders(this.activeFilter);
|
||||||
|
this.filterComponent.reloadOrganizations();
|
||||||
await Promise.all([this.groupingsComponent.load(), this.organizationsComponent.load()]);
|
|
||||||
this.showUpdateKey = !(await this.cryptoService.hasEncKey());
|
this.showUpdateKey = !(await this.cryptoService.hasEncKey());
|
||||||
|
|
||||||
if (params == null) {
|
if (params.cipherId) {
|
||||||
this.groupingsComponent.selectedAll = true;
|
const cipherView = new CipherView();
|
||||||
await this.ciphersComponent.reload();
|
cipherView.id = params.cipherId;
|
||||||
} else {
|
if (params.action === "clone") {
|
||||||
if (params.deleted) {
|
await this.cloneCipher(cipherView);
|
||||||
this.groupingsComponent.selectedTrash = true;
|
} else if (params.action === "edit") {
|
||||||
await this.filterDeleted();
|
await this.editCipher(cipherView);
|
||||||
} else if (params.favorites) {
|
|
||||||
this.groupingsComponent.selectedFavorites = true;
|
|
||||||
await this.filterFavorites();
|
|
||||||
} else if (params.type) {
|
|
||||||
const t = parseInt(params.type, null);
|
|
||||||
this.groupingsComponent.selectedType = t;
|
|
||||||
await this.filterCipherType(t);
|
|
||||||
} else if (params.folderId) {
|
|
||||||
this.groupingsComponent.selectedFolder = true;
|
|
||||||
this.groupingsComponent.selectedFolderId = params.folderId;
|
|
||||||
await this.filterFolder(params.folderId);
|
|
||||||
} else if (params.collectionId) {
|
|
||||||
this.groupingsComponent.selectedCollectionId = params.collectionId;
|
|
||||||
await this.filterCollection(params.collectionId);
|
|
||||||
} else {
|
|
||||||
this.groupingsComponent.selectedAll = true;
|
|
||||||
await this.ciphersComponent.reload();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await this.ciphersComponent.reload();
|
||||||
|
|
||||||
|
this.route.queryParams.subscribe(async (params) => {
|
||||||
|
if (params.cipherId) {
|
||||||
|
if ((await this.cipherService.get(params.cipherId)) != null) {
|
||||||
|
this.editCipherId(params.cipherId);
|
||||||
|
} else {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("unknownCipher")
|
||||||
|
);
|
||||||
|
this.router.navigate([], {
|
||||||
|
queryParams: { cipherId: null },
|
||||||
|
queryParamsHandling: "merge",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||||
this.ngZone.run(async () => {
|
this.ngZone.run(async () => {
|
||||||
@@ -142,8 +145,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
case "syncCompleted":
|
case "syncCompleted":
|
||||||
if (message.successfully) {
|
if (message.successfully) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.groupingsComponent.load(),
|
this.filterComponent.reloadCollectionsAndFolders(this.activeFilter),
|
||||||
this.organizationsComponent.load(),
|
this.filterComponent.reloadOrganizations(),
|
||||||
this.ciphersComponent.load(this.ciphersComponent.filter),
|
this.ciphersComponent.load(this.ciphersComponent.filter),
|
||||||
]);
|
]);
|
||||||
this.changeDetectorRef.detectChanges();
|
this.changeDetectorRef.detectChanges();
|
||||||
@@ -155,72 +158,78 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isShowingCards() {
|
||||||
|
return (
|
||||||
|
this.showBrowserOutdated ||
|
||||||
|
this.showPremiumCallout ||
|
||||||
|
this.showUpdateKey ||
|
||||||
|
this.showVerifyEmail
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearGroupingFilters() {
|
async applyVaultFilter(vaultFilter: VaultFilter) {
|
||||||
this.ciphersComponent.showAddNew = true;
|
this.ciphersComponent.showAddNew = vaultFilter.status !== "trash";
|
||||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t("searchVault");
|
this.activeFilter = vaultFilter;
|
||||||
await this.ciphersComponent.reload();
|
await this.ciphersComponent.reload(this.buildFilter(), vaultFilter.status === "trash");
|
||||||
this.clearFilters();
|
this.filterComponent.searchPlaceholder = this.vaultService.calculateSearchBarLocalizationString(
|
||||||
this.go();
|
this.activeFilter
|
||||||
}
|
|
||||||
|
|
||||||
async filterFavorites() {
|
|
||||||
this.ciphersComponent.showAddNew = true;
|
|
||||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t("searchFavorites");
|
|
||||||
await this.ciphersComponent.reload((c) => c.favorite);
|
|
||||||
this.clearFilters();
|
|
||||||
this.favorites = true;
|
|
||||||
this.go();
|
|
||||||
}
|
|
||||||
|
|
||||||
async filterDeleted() {
|
|
||||||
this.ciphersComponent.showAddNew = false;
|
|
||||||
this.ciphersComponent.deleted = true;
|
|
||||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t("searchTrash");
|
|
||||||
await this.ciphersComponent.reload(null, true);
|
|
||||||
this.clearFilters();
|
|
||||||
this.deleted = true;
|
|
||||||
this.go();
|
|
||||||
}
|
|
||||||
|
|
||||||
async filterCipherType(type: CipherType) {
|
|
||||||
this.ciphersComponent.showAddNew = true;
|
|
||||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t("searchType");
|
|
||||||
await this.ciphersComponent.reload((c) => c.type === type);
|
|
||||||
this.clearFilters();
|
|
||||||
this.type = type;
|
|
||||||
this.go();
|
|
||||||
}
|
|
||||||
|
|
||||||
async filterFolder(folderId: string) {
|
|
||||||
this.ciphersComponent.showAddNew = true;
|
|
||||||
folderId = folderId === "none" ? null : folderId;
|
|
||||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t("searchFolder");
|
|
||||||
await this.ciphersComponent.reload((c) => c.folderId === folderId);
|
|
||||||
this.clearFilters();
|
|
||||||
this.folderId = folderId == null ? "none" : folderId;
|
|
||||||
this.go();
|
|
||||||
}
|
|
||||||
|
|
||||||
async filterCollection(collectionId: string) {
|
|
||||||
this.ciphersComponent.showAddNew = true;
|
|
||||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t("searchCollection");
|
|
||||||
await this.ciphersComponent.reload(
|
|
||||||
(c) => c.collectionIds != null && c.collectionIds.indexOf(collectionId) > -1
|
|
||||||
);
|
);
|
||||||
this.clearFilters();
|
|
||||||
this.collectionId = collectionId;
|
|
||||||
this.go();
|
this.go();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async applyOrganizationFilter(orgId: string) {
|
||||||
|
if (orgId == null) {
|
||||||
|
this.activeFilter.resetOrganization();
|
||||||
|
this.activeFilter.myVaultOnly = true;
|
||||||
|
} else {
|
||||||
|
this.activeFilter.selectedOrganizationId = orgId;
|
||||||
|
}
|
||||||
|
await this.applyVaultFilter(this.activeFilter);
|
||||||
|
}
|
||||||
|
|
||||||
filterSearchText(searchText: string) {
|
filterSearchText(searchText: string) {
|
||||||
this.ciphersComponent.searchText = searchText;
|
this.ciphersComponent.searchText = searchText;
|
||||||
this.ciphersComponent.search(200);
|
this.ciphersComponent.search(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildFilter(): (cipher: CipherView) => boolean {
|
||||||
|
return (cipher) => {
|
||||||
|
let cipherPassesFilter = true;
|
||||||
|
if (this.activeFilter.status === "favorites" && cipherPassesFilter) {
|
||||||
|
cipherPassesFilter = cipher.favorite;
|
||||||
|
}
|
||||||
|
if (this.activeFilter.status === "trash" && cipherPassesFilter) {
|
||||||
|
cipherPassesFilter = cipher.isDeleted;
|
||||||
|
}
|
||||||
|
if (this.activeFilter.cipherType != null && cipherPassesFilter) {
|
||||||
|
cipherPassesFilter = cipher.type === this.activeFilter.cipherType;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.activeFilter.selectedFolder &&
|
||||||
|
this.activeFilter.selectedFolderId != "none" &&
|
||||||
|
cipherPassesFilter
|
||||||
|
) {
|
||||||
|
cipherPassesFilter = cipher.folderId === this.activeFilter.selectedFolderId;
|
||||||
|
}
|
||||||
|
if (this.activeFilter.selectedCollectionId != null && cipherPassesFilter) {
|
||||||
|
cipherPassesFilter =
|
||||||
|
cipher.collectionIds != null &&
|
||||||
|
cipher.collectionIds.indexOf(this.activeFilter.selectedCollectionId) > -1;
|
||||||
|
}
|
||||||
|
if (this.activeFilter.selectedOrganizationId != null && cipherPassesFilter) {
|
||||||
|
cipherPassesFilter = cipher.organizationId === this.activeFilter.selectedOrganizationId;
|
||||||
|
}
|
||||||
|
if (this.activeFilter.myVaultOnly && cipherPassesFilter) {
|
||||||
|
cipherPassesFilter = cipher.organizationId === null;
|
||||||
|
}
|
||||||
|
return cipherPassesFilter;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async editCipherAttachments(cipher: CipherView) {
|
async editCipherAttachments(cipher: CipherView) {
|
||||||
const canAccessPremium = await this.stateService.getCanAccessPremium();
|
const canAccessPremium = await this.stateService.getCanAccessPremium();
|
||||||
if (cipher.organizationId == null && !canAccessPremium) {
|
if (cipher.organizationId == null && !canAccessPremium) {
|
||||||
@@ -292,7 +301,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
comp.folderId = null;
|
comp.folderId = null;
|
||||||
comp.onSavedFolder.subscribe(async () => {
|
comp.onSavedFolder.subscribe(async () => {
|
||||||
modal.close();
|
modal.close();
|
||||||
await this.groupingsComponent.loadFolders();
|
await this.filterComponent.reloadCollectionsAndFolders(this.activeFilter);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -306,13 +315,11 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
comp.folderId = folderId;
|
comp.folderId = folderId;
|
||||||
comp.onSavedFolder.subscribe(async () => {
|
comp.onSavedFolder.subscribe(async () => {
|
||||||
modal.close();
|
modal.close();
|
||||||
await this.groupingsComponent.loadFolders();
|
await this.filterComponent.reloadCollectionsAndFolders(this.activeFilter);
|
||||||
});
|
});
|
||||||
comp.onDeletedFolder.subscribe(async () => {
|
comp.onDeletedFolder.subscribe(async () => {
|
||||||
modal.close();
|
modal.close();
|
||||||
await this.groupingsComponent.loadFolders();
|
await this.filterComponent.reloadCollectionsAndFolders(this.activeFilter);
|
||||||
await this.filterFolder("none");
|
|
||||||
this.groupingsComponent.selectedFolderId = null;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -322,23 +329,41 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
const component = await this.editCipher(null);
|
const component = await this.editCipher(null);
|
||||||
component.type = this.type;
|
component.type = this.type;
|
||||||
component.folderId = this.folderId === "none" ? null : this.folderId;
|
component.folderId = this.folderId === "none" ? null : this.folderId;
|
||||||
if (this.collectionId != null) {
|
if (this.activeFilter.selectedCollectionId != null) {
|
||||||
const collection = this.groupingsComponent.collections.filter(
|
const collection = this.filterComponent.collections.fullList.filter(
|
||||||
(c) => c.id === this.collectionId
|
(c) => c.id === this.activeFilter.selectedCollectionId
|
||||||
);
|
);
|
||||||
if (collection.length > 0) {
|
if (collection.length > 0) {
|
||||||
component.organizationId = collection[0].organizationId;
|
component.organizationId = collection[0].organizationId;
|
||||||
component.collectionIds = [this.collectionId];
|
component.collectionIds = [this.activeFilter.selectedCollectionId];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this.activeFilter.selectedFolderId && this.activeFilter.selectedFolder) {
|
||||||
|
component.folderId = this.activeFilter.selectedFolderId;
|
||||||
|
}
|
||||||
|
if (this.activeFilter.selectedOrganizationId) {
|
||||||
|
component.organizationId = this.activeFilter.selectedOrganizationId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async editCipher(cipher: CipherView) {
|
async editCipher(cipher: CipherView) {
|
||||||
|
return this.editCipherId(cipher?.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async editCipherId(id: string) {
|
||||||
|
const cipher = await this.cipherService.get(id);
|
||||||
|
if (cipher != null && cipher.reprompt != 0) {
|
||||||
|
if (!(await this.passwordRepromptService.showPasswordPrompt())) {
|
||||||
|
this.go({ cipherId: null });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [modal, childComponent] = await this.modalService.openViewRef(
|
const [modal, childComponent] = await this.modalService.openViewRef(
|
||||||
AddEditComponent,
|
AddEditComponent,
|
||||||
this.cipherAddEditModalRef,
|
this.cipherAddEditModalRef,
|
||||||
(comp) => {
|
(comp) => {
|
||||||
comp.cipherId = cipher == null ? null : cipher.id;
|
comp.cipherId = id;
|
||||||
comp.onSavedCipher.subscribe(async () => {
|
comp.onSavedCipher.subscribe(async () => {
|
||||||
modal.close();
|
modal.close();
|
||||||
await this.ciphersComponent.refresh();
|
await this.ciphersComponent.refresh();
|
||||||
@@ -354,6 +379,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
modal.onClosedPromise().then(() => {
|
||||||
|
this.go({ cipherId: null });
|
||||||
|
});
|
||||||
|
|
||||||
return childComponent;
|
return childComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,14 +395,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
await this.modalService.openViewRef(UpdateKeyComponent, this.updateKeyModalRef);
|
await this.modalService.openViewRef(UpdateKeyComponent, this.updateKeyModalRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearFilters() {
|
|
||||||
this.folderId = null;
|
|
||||||
this.collectionId = null;
|
|
||||||
this.favorites = false;
|
|
||||||
this.type = null;
|
|
||||||
this.deleted = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private go(queryParams: any = null) {
|
private go(queryParams: any = null) {
|
||||||
if (queryParams == null) {
|
if (queryParams == null) {
|
||||||
queryParams = {
|
queryParams = {
|
||||||
@@ -388,6 +409,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
relativeTo: this.route,
|
relativeTo: this.route,
|
||||||
queryParams: queryParams,
|
queryParams: queryParams,
|
||||||
|
queryParamsHandling: "merge",
|
||||||
replaceUrl: true,
|
replaceUrl: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { VaultModule } from "../../vault.module";
|
||||||
|
|
||||||
|
import { IndividualVaultRoutingModule } from "./individual-vault-routing.module";
|
||||||
|
import { IndividualVaultComponent } from "./individual-vault.component";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [VaultModule, IndividualVaultRoutingModule],
|
||||||
|
declarations: [IndividualVaultComponent],
|
||||||
|
exports: [IndividualVaultComponent],
|
||||||
|
})
|
||||||
|
export class IndividualVaultModule {}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { SharedModule } from "../../../shared.module";
|
||||||
|
|
||||||
|
import { OrganizationNameBadgeComponent } from "./organization-name-badge.component";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [SharedModule],
|
||||||
|
declarations: [OrganizationNameBadgeComponent],
|
||||||
|
exports: [OrganizationNameBadgeComponent],
|
||||||
|
})
|
||||||
|
export class OrganizationBadgeModule {}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<button
|
||||||
|
bit-badge
|
||||||
|
[style.color]="textColor"
|
||||||
|
[style.background-color]="color"
|
||||||
|
appA11yTitle="{{ organizationName }}"
|
||||||
|
(click)="emitOnOrganizationClicked()"
|
||||||
|
>
|
||||||
|
{{ organizationName | ellipsis: 13 }}
|
||||||
|
</button>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||||
|
|
||||||
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-org-badge",
|
||||||
|
templateUrl: "organization-name-badge.component.html",
|
||||||
|
})
|
||||||
|
export class OrganizationNameBadgeComponent implements OnInit {
|
||||||
|
@Input() organizationName: string;
|
||||||
|
@Input() profileName: string;
|
||||||
|
|
||||||
|
@Output() onOrganizationClicked = new EventEmitter<string>();
|
||||||
|
|
||||||
|
color: string;
|
||||||
|
textColor: string;
|
||||||
|
|
||||||
|
constructor(private i18nService: I18nService) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (this.organizationName == null || this.organizationName === "") {
|
||||||
|
this.organizationName = this.i18nService.t("me");
|
||||||
|
this.color = this.stringToColor(this.profileName.toUpperCase());
|
||||||
|
}
|
||||||
|
if (this.color == null) {
|
||||||
|
this.color = this.stringToColor(this.organizationName.toUpperCase());
|
||||||
|
}
|
||||||
|
this.textColor = this.pickTextColorBasedOnBgColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This value currently isn't stored anywhere, only calculated in the app-avatar component
|
||||||
|
// Once we are allowing org colors to be changed and saved, change this out
|
||||||
|
private stringToColor(str: string): string {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
}
|
||||||
|
let color = "#";
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const value = (hash >> (i * 8)) & 0xff;
|
||||||
|
color += ("00" + value.toString(16)).substr(-2);
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// There are a few ways to calculate text color for contrast, this one seems to fit accessibility guidelines best.
|
||||||
|
// https://stackoverflow.com/a/3943023/6869691
|
||||||
|
private pickTextColorBasedOnBgColor() {
|
||||||
|
const color = this.color.charAt(0) === "#" ? this.color.substring(1, 7) : this.color;
|
||||||
|
const r = parseInt(color.substring(0, 2), 16); // hexToR
|
||||||
|
const g = parseInt(color.substring(2, 4), 16); // hexToG
|
||||||
|
const b = parseInt(color.substring(4, 6), 16); // hexToB
|
||||||
|
return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? "black !important" : "white !important";
|
||||||
|
}
|
||||||
|
|
||||||
|
emitOnOrganizationClicked() {
|
||||||
|
this.onOrganizationClicked.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
import { RouterModule, Routes } from "@angular/router";
|
||||||
|
|
||||||
|
import { OrganizationVaultComponent } from "./organization-vault.component";
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
component: OrganizationVaultComponent,
|
||||||
|
data: { titleId: "vaults" },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class OrganizationVaultRoutingModule {}
|
||||||
@@ -1,22 +1,26 @@
|
|||||||
<div class="container page-content">
|
<div class="container page-content">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
<app-org-vault-groupings
|
<div class="groupings">
|
||||||
[showFolders]="false"
|
<div class="content">
|
||||||
[showFavorites]="false"
|
<div class="inner-content">
|
||||||
[showTrash]="true"
|
<app-vault-filter
|
||||||
(onAllClicked)="clearGroupingFilters()"
|
#vaultFilter
|
||||||
(onCipherTypeClicked)="filterCipherType($event)"
|
[showFolders]="false"
|
||||||
(onCollectionClicked)="filterCollection($event.id)"
|
[showFavorites]="false"
|
||||||
(onSearchTextChanged)="filterSearchText($event)"
|
[activeFilter]="activeFilter"
|
||||||
(onTrashClicked)="filterDeleted()"
|
[showOrgFilter]="false"
|
||||||
>
|
(onFilterChange)="applyVaultFilter($event)"
|
||||||
</app-org-vault-groupings>
|
(onSearchTextChanged)="filterSearchText($event)"
|
||||||
|
></app-vault-filter>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-9">
|
<div class="col-9">
|
||||||
<div class="page-header d-flex">
|
<div class="page-header d-flex">
|
||||||
<h1>
|
<h1>
|
||||||
{{ "vault" | i18n }}
|
{{ "vaultItems" | i18n }}
|
||||||
<small #actionSpinner [appApiAction]="ciphersComponent.actionPromise">
|
<small #actionSpinner [appApiAction]="ciphersComponent.actionPromise">
|
||||||
<ng-container *ngIf="actionSpinner.loading">
|
<ng-container *ngIf="actionSpinner.loading">
|
||||||
<i
|
<i
|
||||||
@@ -10,33 +10,36 @@ import {
|
|||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { first } from "rxjs/operators";
|
import { first } from "rxjs/operators";
|
||||||
|
|
||||||
|
import { VaultFilter } from "jslib-angular/modules/vault-filter/models/vault-filter.model";
|
||||||
import { ModalService } from "jslib-angular/services/modal.service";
|
import { ModalService } from "jslib-angular/services/modal.service";
|
||||||
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
|
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
|
||||||
|
import { CipherService } from "jslib-common/abstractions/cipher.service";
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||||
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||||
|
import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service";
|
||||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
import { SyncService } from "jslib-common/abstractions/sync.service";
|
import { SyncService } from "jslib-common/abstractions/sync.service";
|
||||||
import { CipherType } from "jslib-common/enums/cipherType";
|
import { CipherType } from "jslib-common/enums/cipherType";
|
||||||
import { Organization } from "jslib-common/models/domain/organization";
|
import { Organization } from "jslib-common/models/domain/organization";
|
||||||
import { CipherView } from "jslib-common/models/view/cipherView";
|
import { CipherView } from "jslib-common/models/view/cipherView";
|
||||||
|
|
||||||
import { EntityEventsComponent } from "../manage/entity-events.component";
|
import { EntityEventsComponent } from "../../../../organizations/manage/entity-events.component";
|
||||||
|
import { AddEditComponent } from "../../../../organizations/vault/add-edit.component";
|
||||||
import { AddEditComponent } from "./add-edit.component";
|
import { AttachmentsComponent } from "../../../../organizations/vault/attachments.component";
|
||||||
import { AttachmentsComponent } from "./attachments.component";
|
import { CiphersComponent } from "../../../../organizations/vault/ciphers.component";
|
||||||
import { CiphersComponent } from "./ciphers.component";
|
import { CollectionsComponent } from "../../../../organizations/vault/collections.component";
|
||||||
import { CollectionsComponent } from "./collections.component";
|
import { VaultFilterComponent } from "../../../vault-filter/vault-filter.component";
|
||||||
import { GroupingsComponent } from "./groupings.component";
|
import { VaultService } from "../../vault.service";
|
||||||
|
|
||||||
const BroadcasterSubscriptionId = "OrgVaultComponent";
|
const BroadcasterSubscriptionId = "OrgVaultComponent";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-org-vault",
|
selector: "app-org-vault",
|
||||||
templateUrl: "vault.component.html",
|
templateUrl: "organization-vault.component.html",
|
||||||
})
|
})
|
||||||
export class VaultComponent implements OnInit, OnDestroy {
|
export class OrganizationVaultComponent implements OnInit, OnDestroy {
|
||||||
@ViewChild(GroupingsComponent, { static: true }) groupingsComponent: GroupingsComponent;
|
@ViewChild("vaultFilter", { static: true }) vaultFilterComponent: VaultFilterComponent;
|
||||||
@ViewChild(CiphersComponent, { static: true }) ciphersComponent: CiphersComponent;
|
@ViewChild(CiphersComponent, { static: true }) ciphersComponent: CiphersComponent;
|
||||||
@ViewChild("attachments", { read: ViewContainerRef, static: true })
|
@ViewChild("attachments", { read: ViewContainerRef, static: true })
|
||||||
attachmentsModalRef: ViewContainerRef;
|
attachmentsModalRef: ViewContainerRef;
|
||||||
@@ -52,6 +55,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
type: CipherType = null;
|
type: CipherType = null;
|
||||||
deleted = false;
|
deleted = false;
|
||||||
trashCleanupWarning: string = null;
|
trashCleanupWarning: string = null;
|
||||||
|
activeFilter: VaultFilter = new VaultFilter();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@@ -64,7 +68,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private broadcasterService: BroadcasterService,
|
private broadcasterService: BroadcasterService,
|
||||||
private ngZone: NgZone,
|
private ngZone: NgZone,
|
||||||
private platformUtilsService: PlatformUtilsService
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private vaultService: VaultService,
|
||||||
|
private cipherService: CipherService,
|
||||||
|
private passwordRepromptService: PasswordRepromptService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@@ -73,13 +80,13 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
? "trashCleanupWarningSelfHosted"
|
? "trashCleanupWarningSelfHosted"
|
||||||
: "trashCleanupWarning"
|
: "trashCleanupWarning"
|
||||||
);
|
);
|
||||||
this.route.parent.params.pipe(first()).subscribe(async (params) => {
|
this.route.parent.params.subscribe(async (params: any) => {
|
||||||
this.organization = await this.organizationService.get(params.organizationId);
|
this.organization = await this.organizationService.get(params.organizationId);
|
||||||
this.groupingsComponent.organization = this.organization;
|
this.vaultFilterComponent.organization = this.organization;
|
||||||
this.ciphersComponent.organization = this.organization;
|
this.ciphersComponent.organization = this.organization;
|
||||||
|
|
||||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||||
this.ciphersComponent.searchText = this.groupingsComponent.searchText = qParams.search;
|
this.ciphersComponent.searchText = this.vaultFilterComponent.searchText = qParams.search;
|
||||||
if (!this.organization.canViewAllCollections) {
|
if (!this.organization.canViewAllCollections) {
|
||||||
await this.syncService.fullSync(false);
|
await this.syncService.fullSync(false);
|
||||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||||
@@ -88,7 +95,11 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
case "syncCompleted":
|
case "syncCompleted":
|
||||||
if (message.successfully) {
|
if (message.successfully) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.groupingsComponent.load(),
|
this.vaultFilterComponent.reloadCollectionsAndFolders(
|
||||||
|
new VaultFilter({
|
||||||
|
selectedOrganizationId: this.organization.id,
|
||||||
|
} as Partial<VaultFilter>)
|
||||||
|
),
|
||||||
this.ciphersComponent.refresh(),
|
this.ciphersComponent.refresh(),
|
||||||
]);
|
]);
|
||||||
this.changeDetectorRef.detectChanges();
|
this.changeDetectorRef.detectChanges();
|
||||||
@@ -98,27 +109,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await this.groupingsComponent.load();
|
await this.vaultFilterComponent.reloadCollectionsAndFolders(
|
||||||
|
new VaultFilter({ selectedOrganizationId: this.organization.id } as Partial<VaultFilter>)
|
||||||
if (qParams == null) {
|
);
|
||||||
this.groupingsComponent.selectedAll = true;
|
await this.ciphersComponent.reload();
|
||||||
await this.ciphersComponent.reload();
|
|
||||||
} else {
|
|
||||||
if (qParams.deleted) {
|
|
||||||
this.groupingsComponent.selectedTrash = true;
|
|
||||||
await this.filterDeleted(true);
|
|
||||||
} else if (qParams.type) {
|
|
||||||
const t = parseInt(qParams.type, null);
|
|
||||||
this.groupingsComponent.selectedType = t;
|
|
||||||
await this.filterCipherType(t, true);
|
|
||||||
} else if (qParams.collectionId) {
|
|
||||||
this.groupingsComponent.selectedCollectionId = qParams.collectionId;
|
|
||||||
await this.filterCollection(qParams.collectionId, true);
|
|
||||||
} else {
|
|
||||||
this.groupingsComponent.selectedAll = true;
|
|
||||||
await this.ciphersComponent.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (qParams.viewEvents != null) {
|
if (qParams.viewEvents != null) {
|
||||||
const cipher = this.ciphersComponent.ciphers.filter((c) => c.id === qParams.viewEvents);
|
const cipher = this.ciphersComponent.ciphers.filter((c) => c.id === qParams.viewEvents);
|
||||||
@@ -126,6 +120,28 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
this.viewEvents(cipher[0]);
|
this.viewEvents(cipher[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.route.queryParams.subscribe(async (params) => {
|
||||||
|
if (params.cipherId) {
|
||||||
|
if (
|
||||||
|
// Handle users with implicit collection access since they use the admin endpoint
|
||||||
|
this.organization.canEditAnyCollection ||
|
||||||
|
(await this.cipherService.get(params.cipherId)) != null
|
||||||
|
) {
|
||||||
|
this.editCipherId(params.cipherId);
|
||||||
|
} else {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("unknownCipher")
|
||||||
|
);
|
||||||
|
this.router.navigate([], {
|
||||||
|
queryParams: { cipherId: null },
|
||||||
|
queryParamsHandling: "merge",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -134,63 +150,47 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearGroupingFilters() {
|
async applyVaultFilter(vaultFilter: VaultFilter) {
|
||||||
this.ciphersComponent.showAddNew = true;
|
this.ciphersComponent.showAddNew = vaultFilter.status !== "trash";
|
||||||
this.ciphersComponent.deleted = false;
|
this.activeFilter = vaultFilter;
|
||||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t("searchVault");
|
await this.ciphersComponent.reload(this.buildFilter(), vaultFilter.status === "trash");
|
||||||
await this.ciphersComponent.applyFilter();
|
this.vaultFilterComponent.searchPlaceholder =
|
||||||
this.clearFilters();
|
this.vaultService.calculateSearchBarLocalizationString(this.activeFilter);
|
||||||
this.go();
|
this.go();
|
||||||
}
|
}
|
||||||
|
|
||||||
async filterCipherType(type: CipherType, load = false) {
|
private buildFilter(): (cipher: CipherView) => boolean {
|
||||||
this.ciphersComponent.showAddNew = true;
|
return (cipher) => {
|
||||||
this.ciphersComponent.deleted = false;
|
let cipherPassesFilter = true;
|
||||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t("searchType");
|
if (this.activeFilter.status === "favorites" && cipherPassesFilter) {
|
||||||
const filter = (c: CipherView) => c.type === type;
|
cipherPassesFilter = cipher.favorite;
|
||||||
if (load) {
|
|
||||||
await this.ciphersComponent.reload(filter);
|
|
||||||
} else {
|
|
||||||
await this.ciphersComponent.applyFilter(filter);
|
|
||||||
}
|
|
||||||
this.clearFilters();
|
|
||||||
this.type = type;
|
|
||||||
this.go();
|
|
||||||
}
|
|
||||||
|
|
||||||
async filterCollection(collectionId: string, load = false) {
|
|
||||||
this.ciphersComponent.showAddNew = true;
|
|
||||||
this.ciphersComponent.deleted = false;
|
|
||||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t("searchCollection");
|
|
||||||
const filter = (c: CipherView) => {
|
|
||||||
if (collectionId === "unassigned") {
|
|
||||||
return c.collectionIds == null || c.collectionIds.length === 0;
|
|
||||||
} else {
|
|
||||||
return c.collectionIds != null && c.collectionIds.indexOf(collectionId) > -1;
|
|
||||||
}
|
}
|
||||||
|
if (this.activeFilter.status === "trash" && cipherPassesFilter) {
|
||||||
|
cipherPassesFilter = cipher.isDeleted;
|
||||||
|
}
|
||||||
|
if (this.activeFilter.cipherType != null && cipherPassesFilter) {
|
||||||
|
cipherPassesFilter = cipher.type === this.activeFilter.cipherType;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.activeFilter.selectedFolder != null &&
|
||||||
|
this.activeFilter.selectedFolderId != "none" &&
|
||||||
|
cipherPassesFilter
|
||||||
|
) {
|
||||||
|
cipherPassesFilter = cipher.folderId === this.activeFilter.selectedFolderId;
|
||||||
|
}
|
||||||
|
if (this.activeFilter.selectedCollectionId != null && cipherPassesFilter) {
|
||||||
|
cipherPassesFilter =
|
||||||
|
cipher.collectionIds != null &&
|
||||||
|
cipher.collectionIds.indexOf(this.activeFilter.selectedCollectionId) > -1;
|
||||||
|
}
|
||||||
|
if (this.activeFilter.selectedOrganizationId != null && cipherPassesFilter) {
|
||||||
|
cipherPassesFilter = cipher.organizationId === this.activeFilter.selectedOrganizationId;
|
||||||
|
}
|
||||||
|
if (this.activeFilter.myVaultOnly && cipherPassesFilter) {
|
||||||
|
cipherPassesFilter = cipher.organizationId === null;
|
||||||
|
}
|
||||||
|
return cipherPassesFilter;
|
||||||
};
|
};
|
||||||
if (load) {
|
|
||||||
await this.ciphersComponent.reload(filter);
|
|
||||||
} else {
|
|
||||||
await this.ciphersComponent.applyFilter(filter);
|
|
||||||
}
|
|
||||||
this.clearFilters();
|
|
||||||
this.collectionId = collectionId;
|
|
||||||
this.go();
|
|
||||||
}
|
|
||||||
|
|
||||||
async filterDeleted(load = false) {
|
|
||||||
this.ciphersComponent.showAddNew = false;
|
|
||||||
this.ciphersComponent.deleted = true;
|
|
||||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t("searchTrash");
|
|
||||||
if (load) {
|
|
||||||
await this.ciphersComponent.reload(null, true);
|
|
||||||
} else {
|
|
||||||
await this.ciphersComponent.applyFilter(null);
|
|
||||||
}
|
|
||||||
this.clearFilters();
|
|
||||||
this.deleted = true;
|
|
||||||
this.go();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
filterSearchText(searchText: string) {
|
filterSearchText(searchText: string) {
|
||||||
@@ -232,7 +232,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
(comp) => {
|
(comp) => {
|
||||||
if (this.organization.canEditAnyCollection) {
|
if (this.organization.canEditAnyCollection) {
|
||||||
comp.collectionIds = cipher.collectionIds;
|
comp.collectionIds = cipher.collectionIds;
|
||||||
comp.collections = this.groupingsComponent.collections.filter((c) => !c.readOnly);
|
comp.collections = this.vaultFilterComponent.collections.fullList.filter(
|
||||||
|
(c) => !c.readOnly
|
||||||
|
);
|
||||||
}
|
}
|
||||||
comp.organization = this.organization;
|
comp.organization = this.organization;
|
||||||
comp.cipherId = cipher.id;
|
comp.cipherId = cipher.id;
|
||||||
@@ -249,7 +251,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
component.organizationId = this.organization.id;
|
component.organizationId = this.organization.id;
|
||||||
component.type = this.type;
|
component.type = this.type;
|
||||||
if (this.organization.canEditAnyCollection) {
|
if (this.organization.canEditAnyCollection) {
|
||||||
component.collections = this.groupingsComponent.collections.filter((c) => !c.readOnly);
|
component.collections = this.vaultFilterComponent.collections.fullList.filter(
|
||||||
|
(c) => !c.readOnly
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (this.collectionId != null) {
|
if (this.collectionId != null) {
|
||||||
component.collectionIds = [this.collectionId];
|
component.collectionIds = [this.collectionId];
|
||||||
@@ -257,12 +261,24 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async editCipher(cipher: CipherView) {
|
async editCipher(cipher: CipherView) {
|
||||||
|
return this.editCipherId(cipher?.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async editCipherId(cipherId: string) {
|
||||||
|
const cipher = await this.cipherService.get(cipherId);
|
||||||
|
if (cipher != null && cipher.reprompt != 0) {
|
||||||
|
if (!(await this.passwordRepromptService.showPasswordPrompt())) {
|
||||||
|
this.go({ cipherId: null });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [modal, childComponent] = await this.modalService.openViewRef(
|
const [modal, childComponent] = await this.modalService.openViewRef(
|
||||||
AddEditComponent,
|
AddEditComponent,
|
||||||
this.cipherAddEditModalRef,
|
this.cipherAddEditModalRef,
|
||||||
(comp) => {
|
(comp) => {
|
||||||
comp.organization = this.organization;
|
comp.organization = this.organization;
|
||||||
comp.cipherId = cipher == null ? null : cipher.id;
|
comp.cipherId = cipherId;
|
||||||
comp.onSavedCipher.subscribe(async () => {
|
comp.onSavedCipher.subscribe(async () => {
|
||||||
modal.close();
|
modal.close();
|
||||||
await this.ciphersComponent.refresh();
|
await this.ciphersComponent.refresh();
|
||||||
@@ -278,6 +294,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
modal.onClosedPromise().then(() => {
|
||||||
|
this.go({ cipherId: null });
|
||||||
|
});
|
||||||
|
|
||||||
return childComponent;
|
return childComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,7 +306,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
component.cloneMode = true;
|
component.cloneMode = true;
|
||||||
component.organizationId = this.organization.id;
|
component.organizationId = this.organization.id;
|
||||||
if (this.organization.canEditAnyCollection) {
|
if (this.organization.canEditAnyCollection) {
|
||||||
component.collections = this.groupingsComponent.collections.filter((c) => !c.readOnly);
|
component.collections = this.vaultFilterComponent.collections.fullList.filter(
|
||||||
|
(c) => !c.readOnly
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Regardless of Admin state, the collection Ids need to passed manually as they are not assigned value
|
// Regardless of Admin state, the collection Ids need to passed manually as they are not assigned value
|
||||||
// in the add-edit componenet
|
// in the add-edit componenet
|
||||||
@@ -321,6 +343,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
relativeTo: this.route,
|
relativeTo: this.route,
|
||||||
queryParams: queryParams,
|
queryParams: queryParams,
|
||||||
|
queryParamsHandling: "merge",
|
||||||
replaceUrl: true,
|
replaceUrl: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { VaultModule } from "../../vault.module";
|
||||||
|
|
||||||
|
import { OrganizationVaultRoutingModule } from "./organization-vault-routing.module";
|
||||||
|
import { OrganizationVaultComponent } from "./organization-vault.component";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [VaultModule, OrganizationVaultRoutingModule],
|
||||||
|
declarations: [OrganizationVaultComponent],
|
||||||
|
exports: [OrganizationVaultComponent],
|
||||||
|
})
|
||||||
|
export class OrganizationVaultModule {}
|
||||||
19
src/app/modules/vault/vault.module.ts
Normal file
19
src/app/modules/vault/vault.module.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { LooseComponentsModule } from "../loose-components.module";
|
||||||
|
import { SharedModule } from "../shared.module";
|
||||||
|
import { VaultFilterModule } from "../vault-filter/vault-filter.module";
|
||||||
|
|
||||||
|
import { VaultService } from "./vault.service";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [SharedModule, VaultFilterModule, LooseComponentsModule],
|
||||||
|
exports: [SharedModule, VaultFilterModule, LooseComponentsModule],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: VaultService,
|
||||||
|
useClass: VaultService,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class VaultModule {}
|
||||||
29
src/app/modules/vault/vault.service.ts
Normal file
29
src/app/modules/vault/vault.service.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { VaultFilter } from "jslib-angular/modules/vault-filter/models/vault-filter.model";
|
||||||
|
|
||||||
|
export class VaultService {
|
||||||
|
calculateSearchBarLocalizationString(vaultFilter: VaultFilter): string {
|
||||||
|
if (vaultFilter.status === "favorites") {
|
||||||
|
return "searchFavorites";
|
||||||
|
}
|
||||||
|
if (vaultFilter.status === "trash") {
|
||||||
|
return "searchTrash";
|
||||||
|
}
|
||||||
|
if (vaultFilter.cipherType != null) {
|
||||||
|
return "searchType";
|
||||||
|
}
|
||||||
|
if (vaultFilter.selectedFolderId != null && vaultFilter.selectedFolderId != "none") {
|
||||||
|
return "searchFolder";
|
||||||
|
}
|
||||||
|
if (vaultFilter.selectedCollectionId != null) {
|
||||||
|
return "searchCollection";
|
||||||
|
}
|
||||||
|
if (vaultFilter.selectedOrganizationId != null) {
|
||||||
|
return "searchOrganization";
|
||||||
|
}
|
||||||
|
if (vaultFilter.myVaultOnly) {
|
||||||
|
return "searchMyVault";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "searchVault";
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/app/organizations/guards/permissions.guard.ts
Normal file
58
src/app/organizations/guards/permissions.guard.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router";
|
||||||
|
|
||||||
|
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 { SyncService } from "jslib-common/abstractions/sync.service";
|
||||||
|
import { Permissions } from "jslib-common/enums/permissions";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PermissionsGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private router: Router,
|
||||||
|
private organizationService: OrganizationService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private syncService: SyncService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||||
|
// TODO: We need to fix this issue once and for all.
|
||||||
|
if ((await this.syncService.getLastSync()) == null) {
|
||||||
|
await this.syncService.fullSync(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const org = await this.organizationService.get(route.params.organizationId);
|
||||||
|
if (org == null) {
|
||||||
|
return this.router.createUrlTree(["/"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!org.isOwner && !org.enabled) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("organizationIsDisabled")
|
||||||
|
);
|
||||||
|
return this.router.createUrlTree(["/"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissions = route.data == null ? [] : (route.data.permissions as Permissions[]);
|
||||||
|
if (permissions != null && !org.hasAnyPermission(permissions)) {
|
||||||
|
// Handle linkable ciphers for organizations the user only has view access to
|
||||||
|
// https://bitwarden.atlassian.net/browse/EC-203
|
||||||
|
if (state.root.queryParamMap.has("cipherId")) {
|
||||||
|
return this.router.createUrlTree(["/vault"], {
|
||||||
|
queryParams: {
|
||||||
|
cipherId: state.root.queryParamMap.get("cipherId"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("accessDenied"));
|
||||||
|
return this.router.createUrlTree(["/"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,32 +2,11 @@
|
|||||||
<div class="org-nav" *ngIf="organization">
|
<div class="org-nav" *ngIf="organization">
|
||||||
<div class="container d-flex">
|
<div class="container d-flex">
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<div class="my-auto d-flex align-items-center pl-1">
|
<app-organization-switcher
|
||||||
<app-avatar [data]="organization.name" size="45" [circle]="true"></app-avatar>
|
class="my-auto pl-1"
|
||||||
<div class="org-name ml-3">
|
[activeOrganization]="organization"
|
||||||
<span>{{ organization.name }}</span>
|
></app-organization-switcher>
|
||||||
<small class="text-muted">{{ "organization" | i18n }}</small>
|
<ul class="nav nav-tabs">
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="ml-3 card border-danger text-danger bg-transparent"
|
|
||||||
*ngIf="!organization.enabled"
|
|
||||||
>
|
|
||||||
<div class="card-body py-2">
|
|
||||||
<i class="bwi bwi-exclamation-triangle" aria-hidden="true"></i>
|
|
||||||
{{ "organizationIsDisabled" | i18n }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="ml-3 card border-info text-info bg-transparent"
|
|
||||||
*ngIf="organization.isProviderUser"
|
|
||||||
>
|
|
||||||
<div class="card-body py-2">
|
|
||||||
<i class="bwi bwi-exclamation-triangle" aria-hidden="true"></i>
|
|
||||||
{{ "accessingUsingProvider" | i18n: organization.providerName }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ul class="nav nav-tabs" *ngIf="showMenuBar">
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="vault" routerLinkActive="active">
|
<a class="nav-link" routerLink="vault" routerLinkActive="active">
|
||||||
<i class="bwi bwi-lock" aria-hidden="true"></i>
|
<i class="bwi bwi-lock" aria-hidden="true"></i>
|
||||||
@@ -46,7 +25,7 @@
|
|||||||
{{ "tools" | i18n }}
|
{{ "tools" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" *ngIf="organization.isOwner">
|
<li class="nav-item" *ngIf="showSettingsTab">
|
||||||
<a class="nav-link" routerLink="settings" routerLinkActive="active">
|
<a class="nav-link" routerLink="settings" routerLinkActive="active">
|
||||||
<i class="bwi bwi-cogs" aria-hidden="true"></i>
|
<i class="bwi bwi-cogs" aria-hidden="true"></i>
|
||||||
{{ "settings" | i18n }}
|
{{ "settings" | i18n }}
|
||||||
@@ -5,6 +5,8 @@ import { BroadcasterService } from "jslib-common/abstractions/broadcaster.servic
|
|||||||
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||||
import { Organization } from "jslib-common/models/domain/organization";
|
import { Organization } from "jslib-common/models/domain/organization";
|
||||||
|
|
||||||
|
import { NavigationPermissionsService } from "../services/navigation-permissions.service";
|
||||||
|
|
||||||
const BroadcasterSubscriptionId = "OrganizationLayoutComponent";
|
const BroadcasterSubscriptionId = "OrganizationLayoutComponent";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -25,7 +27,7 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
document.body.classList.remove("layout_frontend");
|
document.body.classList.remove("layout_frontend");
|
||||||
this.route.params.subscribe(async (params) => {
|
this.route.params.subscribe(async (params: any) => {
|
||||||
this.organizationId = params.organizationId;
|
this.organizationId = params.organizationId;
|
||||||
await this.load();
|
await this.load();
|
||||||
});
|
});
|
||||||
@@ -48,23 +50,16 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
|||||||
this.organization = await this.organizationService.get(this.organizationId);
|
this.organization = await this.organizationService.get(this.organizationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
get showMenuBar() {
|
|
||||||
return this.showManageTab || this.showToolsTab || this.organization.isOwner;
|
|
||||||
}
|
|
||||||
|
|
||||||
get showManageTab(): boolean {
|
get showManageTab(): boolean {
|
||||||
return (
|
return NavigationPermissionsService.canAccessManage(this.organization);
|
||||||
this.organization.canManageUsers ||
|
|
||||||
this.organization.canViewAllCollections ||
|
|
||||||
this.organization.canViewAssignedCollections ||
|
|
||||||
this.organization.canManageGroups ||
|
|
||||||
this.organization.canManagePolicies ||
|
|
||||||
this.organization.canAccessEventLogs
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get showToolsTab(): boolean {
|
get showToolsTab(): boolean {
|
||||||
return this.organization.canAccessImportExport || this.organization.canAccessReports;
|
return NavigationPermissionsService.canAccessTools(this.organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
get showSettingsTab(): boolean {
|
||||||
|
return NavigationPermissionsService.canAccessSettings(this.organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
get toolsRoute(): string {
|
get toolsRoute(): string {
|
||||||
@@ -20,8 +20,9 @@ import {
|
|||||||
import { ListResponse } from "jslib-common/models/response/listResponse";
|
import { ListResponse } from "jslib-common/models/response/listResponse";
|
||||||
import { CollectionView } from "jslib-common/models/view/collectionView";
|
import { CollectionView } from "jslib-common/models/view/collectionView";
|
||||||
|
|
||||||
|
import { EntityUsersComponent } from "../../modules/organizations/manage/entity-users.component";
|
||||||
|
|
||||||
import { CollectionAddEditComponent } from "./collection-add-edit.component";
|
import { CollectionAddEditComponent } from "./collection-add-edit.component";
|
||||||
import { EntityUsersComponent } from "./entity-users.component";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-org-manage-collections",
|
selector: "app-org-manage-collections",
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import { SearchService } from "jslib-common/abstractions/search.service";
|
|||||||
import { Utils } from "jslib-common/misc/utils";
|
import { Utils } from "jslib-common/misc/utils";
|
||||||
import { GroupResponse } from "jslib-common/models/response/groupResponse";
|
import { GroupResponse } from "jslib-common/models/response/groupResponse";
|
||||||
|
|
||||||
import { EntityUsersComponent } from "./entity-users.component";
|
import { EntityUsersComponent } from "../../modules/organizations/manage/entity-users.component";
|
||||||
|
|
||||||
import { GroupAddEditComponent } from "./group-add-edit.component";
|
import { GroupAddEditComponent } from "./group-add-edit.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user