mirror of
https://github.com/bitwarden/web
synced 2025-12-06 00:03:28 +00:00
Compare commits
176 Commits
v2.23.0
...
feature/ex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5d3caba65 | ||
|
|
5a2294fb4f | ||
|
|
61ab2fbda3 | ||
|
|
d79f074825 | ||
|
|
e3b962a779 | ||
|
|
cc657eb853 | ||
|
|
e14a266ee0 | ||
|
|
e1732cfa10 | ||
|
|
ce1ae208d1 | ||
|
|
6996b06fa2 | ||
|
|
dc503d3461 | ||
|
|
d95db8fb74 | ||
|
|
1a219daa12 | ||
|
|
2ae98887b7 | ||
|
|
f0c47252e4 | ||
|
|
2ffe3bd6ad | ||
|
|
f387a4d469 | ||
|
|
a0f1b4dd0d | ||
|
|
84a65edc08 | ||
|
|
caad11c571 | ||
|
|
b73449159d | ||
|
|
bf48434d0f | ||
|
|
b6d2d5bf71 | ||
|
|
dfd62c7c3a | ||
|
|
41d3bd8cf2 | ||
|
|
3292d119fe | ||
|
|
b8de92435b | ||
|
|
fd1d512a0f | ||
|
|
14b8903d9a | ||
|
|
45284eefb3 | ||
|
|
49f6cfab7f | ||
|
|
2d271460e3 | ||
|
|
241004f13b | ||
|
|
2f5d0201fe | ||
|
|
7ffb5db310 | ||
|
|
6603521d88 | ||
|
|
d066e0586a | ||
|
|
d0e661b84b | ||
|
|
6fa77cef88 | ||
|
|
6f408b871f | ||
|
|
8a9b992757 | ||
|
|
55ecc4b804 | ||
|
|
a71ce448f4 | ||
|
|
bc82ae961e | ||
|
|
ebcfdcd8a4 | ||
|
|
8991dcbf32 | ||
|
|
cc9b9c91d7 | ||
|
|
3880d60101 | ||
|
|
f5fdb34f7d | ||
|
|
5b8f2034c3 | ||
|
|
56477eb39c | ||
|
|
2b0a9d995e | ||
|
|
595722dfa1 | ||
|
|
6a1e683a93 | ||
|
|
97ca771a00 | ||
|
|
214f82e142 | ||
|
|
17ae5ee57c | ||
|
|
71075cf878 | ||
|
|
56e2c86a7f | ||
|
|
8fba2a693e | ||
|
|
f582d3e7a6 | ||
|
|
75984a2e37 | ||
|
|
1cba6dc3b9 | ||
|
|
a803d58c52 | ||
|
|
d5c0783619 | ||
|
|
35a7d6434a | ||
|
|
78942cabf2 | ||
|
|
d9231ae3f3 | ||
|
|
bca7c14319 | ||
|
|
221931ecaa | ||
|
|
4b856d9016 | ||
|
|
4029554658 | ||
|
|
6ec22a9408 | ||
|
|
9cc7dfb884 | ||
|
|
dca12def8d | ||
|
|
cbf65c5f42 | ||
|
|
f8c943c042 | ||
|
|
346052922e | ||
|
|
2973d06c9f | ||
|
|
0490314cff | ||
|
|
a6abb74810 | ||
|
|
0ce00a15e7 | ||
|
|
cd90949d27 | ||
|
|
0d0eb609d3 | ||
|
|
7c902e61d6 | ||
|
|
1e5c2c35e5 | ||
|
|
977fdef787 | ||
|
|
d6c419bad8 | ||
|
|
f740d8b057 | ||
|
|
8889722388 | ||
|
|
01503f137d | ||
|
|
6171aa89a8 | ||
|
|
40c37143e0 | ||
|
|
57031e7752 | ||
|
|
db5a8df64e | ||
|
|
e5eb5d61fe | ||
|
|
9061af54bf | ||
|
|
83fed7d66f | ||
|
|
f8aea1e861 | ||
|
|
5b6fb16591 | ||
|
|
278cf2ca40 | ||
|
|
fe15de02e5 | ||
|
|
b164a39abc | ||
|
|
e5f77e2c4e | ||
|
|
cf460096af | ||
|
|
1403ecfa6f | ||
|
|
8b60d50050 | ||
|
|
cf5823fe71 | ||
|
|
bb0b5f2d87 | ||
|
|
2700caf2a8 | ||
|
|
523b18156c | ||
|
|
7219b394a0 | ||
|
|
383c29c761 | ||
|
|
b5231425fb | ||
|
|
7cb48e3a81 | ||
|
|
664d10cd06 | ||
|
|
a6a34788a8 | ||
|
|
381ec7af67 | ||
|
|
8be377c7f8 | ||
|
|
c46ca2f9e2 | ||
|
|
6d4f163824 | ||
|
|
6c581b3ebc | ||
|
|
618f950cae | ||
|
|
9dd859af7a | ||
|
|
044ac513ae | ||
|
|
4447b89b05 | ||
|
|
1de569e64d | ||
|
|
3ee61fef96 | ||
|
|
f63b395736 | ||
|
|
ee3c3294f3 | ||
|
|
a7a3381124 | ||
|
|
98bd41d4b1 | ||
|
|
356262975c | ||
|
|
a35024e61d | ||
|
|
df9733081b | ||
|
|
db9ab9f51e | ||
|
|
1b8f316066 | ||
|
|
c3a910e785 | ||
|
|
4b4b5910e3 | ||
|
|
471490f14f | ||
|
|
009e125afd | ||
|
|
c682f460b2 | ||
|
|
fa6f33cbc5 | ||
|
|
ae7493efcf | ||
|
|
fc7a7281fe | ||
|
|
7b21e380cb | ||
|
|
2e4c6b7828 | ||
|
|
d4b13c461d | ||
|
|
37752b566b | ||
|
|
3eda0aa2cd | ||
|
|
4ff38c7148 | ||
|
|
998d36a5d1 | ||
|
|
7a43510cf5 | ||
|
|
0c02cfea2f | ||
|
|
aa58749b34 | ||
|
|
c98a189430 | ||
|
|
1df2225a52 | ||
|
|
f8b0c2ffe4 | ||
|
|
ce3311a0dc | ||
|
|
15ea87d6b6 | ||
|
|
0481bf07e2 | ||
|
|
7d01ad4e20 | ||
|
|
9db6f0bfc2 | ||
|
|
ab0ce71db8 | ||
|
|
582ddc041b | ||
|
|
f1e0f70375 | ||
|
|
eaba23d4ba | ||
|
|
ebb945a0c4 | ||
|
|
7daba63c56 | ||
|
|
30d2aeb6a3 | ||
|
|
c82d1b3c50 | ||
|
|
8180aaa4cc | ||
|
|
a1c1fea976 | ||
|
|
17166dad4d | ||
|
|
7f76084109 | ||
|
|
fb89421b09 |
@@ -12,7 +12,7 @@ insert_final_newline = true
|
||||
[*.{js,ts,scss,html}]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
indent_size = 2
|
||||
|
||||
[*.{ts}]
|
||||
quote_type = single
|
||||
|
||||
2
.git-blame-ignore-revs
Normal file
2
.git-blame-ignore-revs
Normal file
@@ -0,0 +1,2 @@
|
||||
# Apply Prettier https://github.com/bitwarden/web/pull/1347
|
||||
56477eb39cfd8a73c9920577d24d75fed36e2cf5
|
||||
4
.gitattributes
vendored
4
.gitattributes
vendored
@@ -1,3 +1 @@
|
||||
*.sh eol=lf
|
||||
.dockerignore eol=lf
|
||||
dockerfile eol=lf
|
||||
* text=auto eol=lf
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -6,7 +6,7 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
|
||||
|
||||
Please do not submit feature requests. The [Community Forums](https://community.bitwarden.com) has a section for submitting, voting for, and discussing product feature requests.
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
|
||||
32
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
32
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
## Type of change
|
||||
|
||||
- [ ] Bug fix
|
||||
- [ ] New feature development
|
||||
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
|
||||
- [ ] Build/deploy pipeline (DevOps)
|
||||
- [ ] Other
|
||||
|
||||
## Objective
|
||||
|
||||
<!--Describe what the purpose of this PR is. For example: what bug you're fixing or what new feature you're adding-->
|
||||
|
||||
## Code changes
|
||||
|
||||
<!--Explain the changes you've made to each file or major component. This should help the reviewer understand your changes-->
|
||||
<!--Also refer to any related changes or PRs in other repositories-->
|
||||
|
||||
- **file.ext:** Description of what was changed and why
|
||||
|
||||
## Screenshots
|
||||
|
||||
<!--Required for any UI changes. Delete if not applicable-->
|
||||
|
||||
## Testing requirements
|
||||
|
||||
<!--What functionality requires testing by QA? This includes testing new behavior and regression testing-->
|
||||
|
||||
## Before you submit
|
||||
|
||||
- [ ] I have checked for **linting** errors (`npm run lint`) (required)
|
||||
- [ ] This change requires a **documentation update** (notify the documentation team)
|
||||
- [ ] This change has particular **deployment requirements** (notify the DevOps team)
|
||||
359
.github/workflows/build.yml
vendored
359
.github/workflows/build.yml
vendored
@@ -9,13 +9,13 @@ on:
|
||||
required: false
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'l10n_master'
|
||||
- 'gh-pages'
|
||||
- "l10n_master"
|
||||
- "gh-pages"
|
||||
|
||||
jobs:
|
||||
cloc:
|
||||
name: CLOC
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
@@ -28,26 +28,37 @@ jobs:
|
||||
- name: Print lines of code
|
||||
run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
|
||||
|
||||
setup:
|
||||
name: Setup
|
||||
runs-on: ubuntu-20.04
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.value }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
|
||||
build-selfhost:
|
||||
name: Build SelfHost Docker image
|
||||
runs-on: ubuntu-latest
|
||||
- name: Get GitHub sha as version
|
||||
id: version
|
||||
run: echo "::set-output name=value::${GITHUB_SHA:0:7}"
|
||||
|
||||
build-oss-selfhost:
|
||||
name: Build OSS zip
|
||||
runs-on: ubuntu-20.04
|
||||
needs: setup
|
||||
env:
|
||||
_VERSION: ${{ needs.setup.outputs.version }}
|
||||
steps:
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||
with:
|
||||
node-version: '14'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
npm install -g npm@7
|
||||
node-version: "16"
|
||||
|
||||
- name: Cache npm
|
||||
id: npm-cache
|
||||
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
||||
with:
|
||||
path: '~/.npm'
|
||||
key: ${{ runner.os }}-${{ github.run_id }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||
path: "~/.npm"
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
@@ -59,39 +70,107 @@ jobs:
|
||||
echo "GitHub ref: $GITHUB_REF"
|
||||
echo "GitHub event: $GITHUB_EVENT"
|
||||
|
||||
- name: Login to Azure
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
|
||||
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
|
||||
- name: Retrieve secrets
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
|
||||
id: retrieve-secrets
|
||||
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
|
||||
with:
|
||||
keyvault: "bitwarden-prod-kv"
|
||||
secrets: "docker-password,
|
||||
docker-username,
|
||||
dct-delegate-2-repo-passphrase,
|
||||
dct-delegate-2-key"
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Log into Docker
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
|
||||
run: echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ steps.retrieve-secrets.outputs.docker-username }}
|
||||
DOCKER_PASSWORD: ${{ steps.retrieve-secrets.outputs.docker-password }}
|
||||
|
||||
- name: Setup Docker Trust
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
|
||||
- name: Build OSS selfhost
|
||||
run: |
|
||||
mkdir -p ~/.docker/trust/private
|
||||
npm run dist:oss:selfhost
|
||||
zip -r web-$_VERSION-selfhosted-open-source.zip build
|
||||
|
||||
echo "$DCT_DELEGATE_KEY" > ~/.docker/trust/private/$DCT_DELEGATION_KEY_ID.key
|
||||
env:
|
||||
DCT_DELEGATION_KEY_ID: "c9bde8ec820701516491e5e03d3a6354e7bd66d05fa3df2b0062f68b116dc59c"
|
||||
DCT_DELEGATE_KEY: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-key }}
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
||||
with:
|
||||
name: web-${{ env._VERSION }}-selfhosted-open-source.zip
|
||||
path: ./web-${{ env._VERSION }}-selfhosted-open-source.zip
|
||||
if-no-files-found: error
|
||||
|
||||
build-cloud:
|
||||
name: Build Cloud zip
|
||||
runs-on: ubuntu-20.04
|
||||
needs: setup
|
||||
env:
|
||||
_VERSION: ${{ needs.setup.outputs.version }}
|
||||
steps:
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||
with:
|
||||
node-version: "16"
|
||||
|
||||
- name: Cache npm
|
||||
id: npm-cache
|
||||
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
||||
with:
|
||||
path: "~/.npm"
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
whoami
|
||||
node --version
|
||||
npm --version
|
||||
gulp --version
|
||||
docker --version
|
||||
echo "GitHub ref: $GITHUB_REF"
|
||||
echo "GitHub event: $GITHUB_EVENT"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build Cloud
|
||||
run: |
|
||||
npm run dist:bit:cloud
|
||||
zip -r web-$_VERSION-cloud-COMMERCIAL.zip build
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
||||
with:
|
||||
name: web-${{ env._VERSION }}-cloud-COMMERCIAL.zip
|
||||
path: ./web-${{ env._VERSION }}-cloud-COMMERCIAL.zip
|
||||
if-no-files-found: error
|
||||
|
||||
build-commercial-selfhost:
|
||||
name: Build SelfHost Docker image
|
||||
runs-on: ubuntu-20.04
|
||||
needs: setup
|
||||
env:
|
||||
_VERSION: ${{ needs.setup.outputs.version }}
|
||||
steps:
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||
with:
|
||||
node-version: "16"
|
||||
|
||||
- name: Cache npm
|
||||
id: npm-cache
|
||||
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
||||
with:
|
||||
path: "~/.npm"
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
whoami
|
||||
node --version
|
||||
npm --version
|
||||
gulp --version
|
||||
docker --version
|
||||
echo "GitHub ref: $GITHUB_REF"
|
||||
echo "GitHub event: $GITHUB_EVENT"
|
||||
|
||||
- name: Setup DCT
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix'
|
||||
id: setup-dct
|
||||
uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff
|
||||
with:
|
||||
azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
azure-keyvault-name: "bitwarden-prod-kv"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
@@ -100,15 +179,26 @@ jobs:
|
||||
run: dotnet tool restore
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
echo -e "# Building Web\n"
|
||||
echo "Building app"
|
||||
echo "npm version $(npm --version)"
|
||||
npm run dist:selfhost
|
||||
|
||||
npm run dist:bit:selfhost
|
||||
zip -r web-$_VERSION-selfhosted-COMMERCIAL.zip build
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
|
||||
with:
|
||||
name: web-${{ env._VERSION }}-selfhosted-COMMERCIAL.zip
|
||||
path: ./web-${{ env._VERSION }}-selfhosted-COMMERCIAL.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Build Docker image
|
||||
run: |
|
||||
echo -e "\nBuilding Docker image"
|
||||
docker --version
|
||||
docker build -t bitwarden/web .
|
||||
@@ -121,48 +211,54 @@ jobs:
|
||||
if: github.ref == 'refs/heads/master'
|
||||
run: docker tag bitwarden/web bitwarden/web:dev
|
||||
|
||||
- name: Tag hotfix branch
|
||||
if: github.ref == 'refs/heads/hotfix'
|
||||
run: docker tag bitwarden/web bitwarden/web:hotfix
|
||||
|
||||
- name: List Docker images
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix'
|
||||
run: docker images
|
||||
|
||||
- name: Push rc images
|
||||
- name: Push rc image
|
||||
if: github.ref == 'refs/heads/rc'
|
||||
run: docker push bitwarden/web:rc
|
||||
env:
|
||||
DOCKER_CONTENT_TRUST: 1
|
||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
|
||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
|
||||
|
||||
- name: Push dev images
|
||||
- name: Push dev image
|
||||
if: github.ref == 'refs/heads/master'
|
||||
run: docker push bitwarden/web:dev
|
||||
env:
|
||||
DOCKER_CONTENT_TRUST: 1
|
||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
|
||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
|
||||
|
||||
- name: Push hotfix image
|
||||
if: github.ref == 'refs/heads/hotfix'
|
||||
run: docker push bitwarden/web:hotfix
|
||||
env:
|
||||
DOCKER_CONTENT_TRUST: 1
|
||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
|
||||
|
||||
- name: Log out of Docker
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix'
|
||||
run: docker logout
|
||||
|
||||
|
||||
build-qa:
|
||||
name: Build QA Docker image
|
||||
runs-on: ubuntu-latest
|
||||
name: Build Docker images for QA environment
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||
with:
|
||||
node-version: '14'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
npm install -g npm@7
|
||||
node-version: "16"
|
||||
|
||||
- name: Cache npm
|
||||
id: npm-cache
|
||||
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
||||
with:
|
||||
path: '~/.npm'
|
||||
key: ${{ runner.os }}-${{ github.run_id }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||
path: "~/.npm"
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
@@ -200,7 +296,7 @@ jobs:
|
||||
jq --arg version "$VERSION - ${GITHUB_SHA:0:7}" '.version = $version' package.json > package.json.tmp
|
||||
mv package.json.tmp package.json
|
||||
|
||||
npm run build:qa
|
||||
npm run build:bit:qa
|
||||
|
||||
echo "{\"commit_hash\": \"$GITHUB_SHA\", \"ref\": \"$GITHUB_REF\"}" | jq . > build/info.json
|
||||
|
||||
@@ -209,10 +305,10 @@ jobs:
|
||||
docker build -t bitwardenqa.azurecr.io/web .
|
||||
|
||||
- name: Get image tag
|
||||
id: image_tag
|
||||
id: image-tag
|
||||
run: |
|
||||
IMAGE_TAG=$(echo "$GITHUB_REF" | awk '{split($0, a, "/"); print a[3];}')
|
||||
TAG_EXTENSION=${{ github.events.inputs.custom_tag_extension }}
|
||||
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g")
|
||||
TAG_EXTENSION=${{ github.event.inputs.custom_tag_extension }}
|
||||
|
||||
if [[ $TAG_EXTENSION ]]; then
|
||||
IMAGE_TAG=$IMAGE_TAG-$TAG_EXTENSION
|
||||
@@ -221,7 +317,7 @@ jobs:
|
||||
|
||||
- name: Tag image
|
||||
env:
|
||||
IMAGE_TAG: ${{ steps.image_tag.outputs.value }}
|
||||
IMAGE_TAG: ${{ steps.image-tag.outputs.value }}
|
||||
run: docker tag bitwardenqa.azurecr.io/web "bitwardenqa.azurecr.io/web:$IMAGE_TAG"
|
||||
|
||||
- name: Tag dev
|
||||
@@ -233,7 +329,7 @@ jobs:
|
||||
|
||||
- name: Push image
|
||||
env:
|
||||
IMAGE_TAG: ${{ steps.image_tag.outputs.value }}
|
||||
IMAGE_TAG: ${{ steps.image-tag.outputs.value }}
|
||||
run: docker push "bitwardenqa.azurecr.io/web:$IMAGE_TAG"
|
||||
|
||||
- name: Push dev images
|
||||
@@ -243,27 +339,29 @@ jobs:
|
||||
- name: Log out of Docker
|
||||
run: docker logout
|
||||
|
||||
|
||||
windows:
|
||||
name: Test code on Windows
|
||||
runs-on: windows-latest
|
||||
runs-on: windows-2019
|
||||
steps:
|
||||
- name: Set up NuGet
|
||||
uses: nuget/setup-nuget@04b0c2b8d1b97922f67eca497d7cf0bf17b8ffe1
|
||||
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
|
||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||
with:
|
||||
node-version: '14'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
npm install -g npm@7
|
||||
node-version: "16"
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
@@ -281,8 +379,115 @@ jobs:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
|
||||
- name: NPM install
|
||||
run: npm install
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run linter
|
||||
run: npm run lint
|
||||
|
||||
- name: NPM build
|
||||
run: npm run build:cloud
|
||||
run: npm run build:bit:cloud
|
||||
|
||||
crowdin-push:
|
||||
name: Crowdin Push
|
||||
if: github.ref == 'refs/heads/master'
|
||||
needs:
|
||||
- build-oss-selfhost
|
||||
- build-cloud
|
||||
- build-commercial-selfhost
|
||||
- build-qa
|
||||
runs-on: ubuntu-20.04
|
||||
env:
|
||||
_CROWDIN_PROJECT_ID: "308189"
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
||||
|
||||
- name: Login to Azure
|
||||
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
|
||||
with:
|
||||
keyvault: "bitwarden-prod-kv"
|
||||
secrets: "crowdin-api-token"
|
||||
|
||||
- name: Upload Sources
|
||||
uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea # v1.3.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
with:
|
||||
config: crowdin.yml
|
||||
crowdin_branch_name: master
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
|
||||
check-failures:
|
||||
name: Check for failures
|
||||
if: always()
|
||||
runs-on: ubuntu-20.04
|
||||
needs:
|
||||
- cloc
|
||||
- setup
|
||||
- build-oss-selfhost
|
||||
- build-cloud
|
||||
- build-commercial-selfhost
|
||||
- build-qa
|
||||
- crowdin-push
|
||||
- windows
|
||||
steps:
|
||||
- name: Check if any job failed
|
||||
if: ${{ (github.ref == 'refs/heads/master') || (github.ref == 'refs/heads/rc') }}
|
||||
env:
|
||||
CLOC_STATUS: ${{ needs.cloc.result }}
|
||||
SETUP_STATUS: ${{ needs.setup.result }}
|
||||
BUILD_OSS_SELFHOST_STATUS: ${{ needs.build-oss-selfhost.result }}
|
||||
BUILD_CLOUD_STATUS: ${{ needs.build-cloud.result }}
|
||||
BUILD_COMMERCIAL_SELFHOST_STATUS: ${{ needs.build-commercial-selfhost.result }}
|
||||
BUILD_QA_STATUS: ${{ needs.build-qa.result }}
|
||||
CROWDIN_PUSH_STATUS: ${{ needs.crowdin-push.result }}
|
||||
WINDOWS_STATUS: ${{ needs.windows.result }}
|
||||
run: |
|
||||
if [ "$CLOC_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$SETUP_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$BUILD_OSS_SELFHOST_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$BUILD_CLOUD_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$BUILD_COMMERCIAL_SELFHOST_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$BUILD_QA_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$CROWDIN_PUSH_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$WINDOWS_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Login to Azure - Prod Subscription
|
||||
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
||||
if: failure()
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
|
||||
if: failure()
|
||||
with:
|
||||
keyvault: "bitwarden-prod-kv"
|
||||
secrets: "devops-alerts-slack-webhook-url"
|
||||
|
||||
- name: Notify Slack on failure
|
||||
uses: act10ns/slack@e4e71685b9b239384b0f676a63c32367f59c2522 # v1.2.2
|
||||
if: failure()
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
---
|
||||
name: Crowdin Sync
|
||||
name: Crowdin Pull
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs: {}
|
||||
# schedule:
|
||||
# - cron: '0 0 * * *'
|
||||
schedule:
|
||||
- cron: "0 0 * * 5"
|
||||
|
||||
jobs:
|
||||
crowdin-sync:
|
||||
name: Autosync
|
||||
crowdin-pull:
|
||||
name: Pull
|
||||
runs-on: ubuntu-20.04
|
||||
env:
|
||||
_CROWDIN_PROJECT_ID: "308189"
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
secrets: "crowdin-api-token"
|
||||
|
||||
- name: Download translations
|
||||
uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea
|
||||
uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea # v1.3.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
73
.github/workflows/deploy.yml
vendored
73
.github/workflows/deploy.yml
vendored
@@ -1,73 +0,0 @@
|
||||
---
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_version:
|
||||
description: "Release Tag Version <vX.X.X>"
|
||||
required: true
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy Web Vault
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
||||
with:
|
||||
ref: gh-pages
|
||||
|
||||
- name: Get release version
|
||||
id: release-version
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "release" ]]; then
|
||||
echo "::set-output name=version::${{ github.event.release.tag_name }}"
|
||||
else
|
||||
echo "::set-output name=version::${{ github.event.inputs.release_version }}"
|
||||
fi
|
||||
|
||||
- name: Create deploy branch
|
||||
run: |
|
||||
git switch -c deploy-${{ steps.release-version.outputs.version }}
|
||||
git push -u origin deploy-${{ steps.release-version.outputs.version }}
|
||||
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
||||
with:
|
||||
ref: rc
|
||||
|
||||
- name: Setup git config
|
||||
run: |
|
||||
git config user.name = "GitHub Action Bot"
|
||||
git config user.email = "<>"
|
||||
git config --global url."https://github.com/".insteadOf ssh://git@github.com/
|
||||
git config --global url."https://".insteadOf ssh://
|
||||
|
||||
- name: Install and Build
|
||||
run: |
|
||||
npm run sub:init
|
||||
npm ci
|
||||
npm run dist
|
||||
|
||||
- name: Deploy GitHub Pages
|
||||
uses: crazy-max/ghaction-github-pages@db4476a01402e1a7ce05f41832040eef16d14925 # v2.5.0
|
||||
with:
|
||||
target_branch: deploy-${{ steps.release-version.outputs.version }}
|
||||
build_dir: build
|
||||
keep_history: true
|
||||
commit_message: "Staging deploy ${{ steps.release-version.outputs.version }}"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create Deploy PR
|
||||
run: |
|
||||
gh pr create --title "Deploy $VERSION" --body "Deploying $VERSION" --base gh-pages --head "$PR_BRANCH"
|
||||
env:
|
||||
VERSION: ${{ steps.release-version.outputs.version }}
|
||||
PR_BRANCH: deploy-${{ steps.release-version.outputs.version }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
23
.github/workflows/qa-deploy.yml
vendored
23
.github/workflows/qa-deploy.yml
vendored
@@ -9,22 +9,21 @@ on:
|
||||
required: false
|
||||
|
||||
env:
|
||||
_QA_CLUSTER_RESOURCE_GROUP: "bitwarden-devops"
|
||||
_QA_CLUSTER_NAME: "dev-aks"
|
||||
_QA_CLUSTER_RESOURCE_GROUP: "bw-env-qa"
|
||||
_QA_CLUSTER_NAME: "bw-aks-qa"
|
||||
_QA_K8S_NAMESPACE: "bw-qa"
|
||||
_QA_K8S_APP_NAME: "bw-web"
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy QA Web
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
||||
|
||||
- name: Setup
|
||||
run:
|
||||
export PATH=$PATH:~/work/web/web
|
||||
run: export PATH=$PATH:~/work/web/web
|
||||
|
||||
- name: Login to Azure
|
||||
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
||||
@@ -36,16 +35,16 @@ jobs:
|
||||
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
|
||||
with:
|
||||
keyvault: "bitwarden-qa-kv"
|
||||
secrets: "dev-aks-kubectl-credentials"
|
||||
secrets: "qa-aks-kubectl-credentials"
|
||||
|
||||
- name: Login to dev-aks-kubectl SP
|
||||
- name: Login with qa-aks-kubectl-credentials SP
|
||||
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
||||
with:
|
||||
creds: ${{ env.dev-aks-kubectl-credentials }}
|
||||
creds: ${{ env.qa-aks-kubectl-credentials }}
|
||||
|
||||
- name: Setup AKS access
|
||||
env:
|
||||
USER_ID: ${{ env.qa-kubectl-managed-identity-clientId }}
|
||||
#env:
|
||||
# USER_ID: ${{ env.qa-kubectl-managed-identity-clientId }}
|
||||
run: |
|
||||
echo "---az install---"
|
||||
az aks install-cli --install-location ./kubectl --kubelogin-install-location ./kubelogin
|
||||
@@ -55,8 +54,8 @@ jobs:
|
||||
- name: Get image tag
|
||||
id: image_tag
|
||||
run: |
|
||||
IMAGE_TAG=$(echo "$GITHUB_REF" | awk '{split($0, a, "/"); print a[3];}')
|
||||
TAG_EXTENSION=${{ github.events.inputs.image_extension }}
|
||||
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g")
|
||||
TAG_EXTENSION=${{ github.event.inputs.image_extension }}
|
||||
|
||||
if [[ $TAG_EXTENSION ]]; then
|
||||
IMAGE_TAG=$IMAGE_TAG-$TAG_EXTENSION
|
||||
|
||||
255
.github/workflows/release.yml
vendored
255
.github/workflows/release.yml
vendored
@@ -4,24 +4,29 @@ name: Release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag_name_input:
|
||||
description: "Release Tag Name <X.X.X>"
|
||||
release_type:
|
||||
description: 'Release Options'
|
||||
required: true
|
||||
default: 'Initial Release'
|
||||
type: choice
|
||||
options:
|
||||
- Initial Release
|
||||
- Redeploy
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Setup
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
outputs:
|
||||
release_upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
release_version: ${{ steps.create_tags.outputs.package_version }}
|
||||
tag_version: ${{ steps.create_tags.outputs.tag_version }}
|
||||
release_version: ${{ steps.version.outputs.package }}
|
||||
tag_version: ${{ steps.version.outputs.tag }}
|
||||
branch-name: ${{ steps.branch.outputs.branch-name }}
|
||||
steps:
|
||||
- name: Branch check
|
||||
run: |
|
||||
if [[ "$GITHUB_REF" != "refs/heads/rc" ]]; then
|
||||
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix" ]]; then
|
||||
echo "==================================="
|
||||
echo "[!] Can only release from rc branch"
|
||||
echo "[!] Can only release from the 'rc' or 'hotfix' branches"
|
||||
echo "==================================="
|
||||
exit 1
|
||||
fi
|
||||
@@ -29,132 +34,168 @@ jobs:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # 2.3.4
|
||||
|
||||
- name: Create Release Vars
|
||||
id: create_tags
|
||||
- name: Check Release Version
|
||||
id: version
|
||||
run: |
|
||||
case "${RELEASE_TAG_NAME_INPUT:0:1}" in
|
||||
v)
|
||||
echo "RELEASE_NAME=${RELEASE_TAG_NAME_INPUT:1}" >> $GITHUB_ENV
|
||||
echo "RELEASE_TAG_NAME=$RELEASE_TAG_NAME_INPUT" >> $GITHUB_ENV
|
||||
echo "::set-output name=package_version::${RELEASE_TAG_NAME_INPUT:1}"
|
||||
echo "::set-output name=tag_version::$RELEASE_TAG_NAME_INPUT"
|
||||
;;
|
||||
[0-9])
|
||||
echo "RELEASE_NAME=$RELEASE_TAG_NAME_INPUT" >> $GITHUB_ENV
|
||||
echo "RELEASE_TAG_NAME=v$RELEASE_TAG_NAME_INPUT" >> $GITHUB_ENV
|
||||
echo "::set-output name=package_version::$RELEASE_TAG_NAME_INPUT"
|
||||
echo "::set-output name=tag_version::v$RELEASE_TAG_NAME_INPUT"
|
||||
;;
|
||||
*)
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
env:
|
||||
RELEASE_TAG_NAME_INPUT: ${{ github.event.inputs.release_tag_name_input }}
|
||||
version=$( jq -r ".version" package.json)
|
||||
previous_release_tag_version=$(
|
||||
curl -sL https://api.github.com/repos/$GITHUB_REPOSITORY/releases/latest | jq -r ".tag_name"
|
||||
)
|
||||
|
||||
- name: Create Draft Release
|
||||
id: create_release
|
||||
uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e # 1.1.4 - Repo Archived
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ env.RELEASE_TAG_NAME }}
|
||||
release_name: Version ${{ env.RELEASE_NAME }}
|
||||
draft: true
|
||||
prerelease: false
|
||||
if [ "v$version" == "$previous_release_tag_version" ] && \
|
||||
[ "${{ github.event.inputs.release_type }}" == "Initial Release" ]; then
|
||||
echo "[!] Already released v$version. Please bump version to continue"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ubuntu:
|
||||
name: Ubuntu
|
||||
runs-on: ubuntu-latest
|
||||
echo "::set-output name=package::$version"
|
||||
echo "::set-output name=tag::v$version"
|
||||
|
||||
- name: Get branch name
|
||||
id: branch
|
||||
run: |
|
||||
BRANCH_NAME=$(basename ${{ github.ref }})
|
||||
echo "::set-output name=branch-name::$BRANCH_NAME"
|
||||
|
||||
self-host:
|
||||
name: Release self-host docker
|
||||
runs-on: ubuntu-20.04
|
||||
needs: setup
|
||||
env:
|
||||
_BRANCH_NAME: ${{ needs.setup.outputs.branch-name }}
|
||||
_RELEASE_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||
steps:
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||
with:
|
||||
node-version: '14'
|
||||
|
||||
- name: Update NPM
|
||||
run: |
|
||||
npm install -g npm@7
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
whoami
|
||||
node --version
|
||||
npm --version
|
||||
gulp --version
|
||||
docker --version
|
||||
echo "GitHub ref: $GITHUB_REF"
|
||||
echo "GitHub event: $GITHUB_EVENT"
|
||||
|
||||
- name: Login to Azure
|
||||
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
||||
- name: Setup DCT
|
||||
id: setup-dct
|
||||
uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
|
||||
with:
|
||||
keyvault: "bitwarden-prod-kv"
|
||||
secrets: "docker-password,
|
||||
docker-username,
|
||||
dct-delegate-2-repo-passphrase,
|
||||
dct-delegate-2-key"
|
||||
|
||||
- name: Log into Docker
|
||||
run: echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ steps.retrieve-secrets.outputs.docker-username }}
|
||||
DOCKER_PASSWORD: ${{ steps.retrieve-secrets.outputs.docker-password }}
|
||||
|
||||
- name: Setup Docker Trust
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release' || github.ref == 'refs/heads/rc'
|
||||
run: |
|
||||
mkdir -p ~/.docker/trust/private
|
||||
|
||||
echo "$DCT_DELEGATE_KEY" > ~/.docker/trust/private/$DCT_DELEGATION_KEY_ID.key
|
||||
env:
|
||||
DCT_DELEGATION_KEY_ID: "c9bde8ec820701516491e5e03d3a6354e7bd66d05fa3df2b0062f68b116dc59c"
|
||||
DCT_DELEGATE_KEY: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-key }}
|
||||
azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
azure-keyvault-name: "bitwarden-prod-kv"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
|
||||
- name: Restore
|
||||
run: dotnet tool restore
|
||||
- name: Pull latest selfhost image
|
||||
run: docker pull bitwarden/web:$_BRANCH_NAME
|
||||
|
||||
- name: Build
|
||||
- name: Tag version and latest
|
||||
run: |
|
||||
echo -e "# Building Web\n"
|
||||
echo "Building app"
|
||||
echo "npm version $(npm --version)"
|
||||
npm install
|
||||
npm run dist:selfhost
|
||||
|
||||
echo -e "\nBuilding Docker image"
|
||||
docker --version
|
||||
docker build -t bitwarden/web .
|
||||
|
||||
- name: Tag version
|
||||
run: docker tag bitwarden/web bitwarden/web:$_RELEASE_VERSION
|
||||
docker tag bitwarden/web:$_BRANCH_NAME bitwarden/web:$_RELEASE_VERSION
|
||||
docker tag bitwarden/web:$_BRANCH_NAME bitwarden/web:latest
|
||||
|
||||
- name: List Docker images
|
||||
run: docker images
|
||||
|
||||
- name: Push latest images
|
||||
run: docker push bitwarden/web:latest
|
||||
- name: Push version and latest image
|
||||
env:
|
||||
DOCKER_CONTENT_TRUST: 1
|
||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
|
||||
|
||||
- name: Push version images
|
||||
run: docker push bitwarden/web:$_RELEASE_VERSION
|
||||
env:
|
||||
DOCKER_CONTENT_TRUST: 1
|
||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
|
||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
|
||||
run: |
|
||||
docker push bitwarden/web:$_RELEASE_VERSION
|
||||
docker push bitwarden/web:latest
|
||||
|
||||
- name: Log out of Docker
|
||||
run: docker logout
|
||||
|
||||
ghpages-deploy:
|
||||
name: Deploy Web Vault
|
||||
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@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
||||
with:
|
||||
ref: gh-pages
|
||||
|
||||
- name: Create deploy branch
|
||||
run: |
|
||||
git switch -c deploy-$_TAG_VERSION
|
||||
git push -u origin deploy-$_TAG_VERSION
|
||||
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
||||
|
||||
- name: Setup git config
|
||||
run: |
|
||||
git config user.name = "GitHub Action Bot"
|
||||
git config user.email = "<>"
|
||||
git config --global url."https://github.com/".insteadOf ssh://git@github.com/
|
||||
git config --global url."https://".insteadOf ssh://
|
||||
|
||||
- name: Download latest cloud asset
|
||||
uses: bitwarden/gh-actions/download-artifacts@23433be15ed6fd046ce12b6889c5184a8d9c8783
|
||||
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: Deploy GitHub Pages
|
||||
uses: crazy-max/ghaction-github-pages@db4476a01402e1a7ce05f41832040eef16d14925 # v2.5.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
target_branch: deploy-${{ needs.setup.outputs.tag_version }}
|
||||
build_dir: build
|
||||
keep_history: true
|
||||
commit_message: "Staging deploy ${{ needs.setup.outputs.release_version }}"
|
||||
|
||||
- name: Create Deploy PR
|
||||
env:
|
||||
PR_BRANCH: deploy-${{ env._TAG_VERSION }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh pr create --title "Deploy $_RELEASE_VERSION" \
|
||||
--body "Deploying $_RELEASE_VERSION" \
|
||||
--base gh-pages \
|
||||
--head "$PR_BRANCH"
|
||||
|
||||
release:
|
||||
name: Create GitHub Release
|
||||
runs-on: ubuntu-20.04
|
||||
needs:
|
||||
- setup
|
||||
- self-host
|
||||
- ghpages-deploy
|
||||
steps:
|
||||
- name: Download latest build artifacts
|
||||
uses: bitwarden/gh-actions/download-artifacts@23433be15ed6fd046ce12b6889c5184a8d9c8783
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
branch: ${{ needs.setup.outputs.branch-name }}
|
||||
artifacts: "web-*-selfhosted-COMMERCIAL.zip,
|
||||
web-*-selfhosted-open-source.zip"
|
||||
|
||||
- name: Rename assets
|
||||
run: |
|
||||
mv web-*-selfhosted-COMMERCIAL.zip web-${{ needs.setup.outputs.release_version }}-selfhosted-COMMERCIAL.zip
|
||||
mv web-*-selfhosted-open-source.zip web-${{ needs.setup.outputs.release_version }}-selfhosted-open-source.zip
|
||||
|
||||
- name: Create release
|
||||
uses: ncipollo/release-action@95215a3cb6e6a1908b3c44e00b4fdb15548b1e09
|
||||
with:
|
||||
name: "Version ${{ needs.setup.outputs.release_version }}"
|
||||
commit: ${{ github.sha }}
|
||||
tag: "${{ needs.setup.outputs.tag_version }}"
|
||||
body: "<insert release notes here>"
|
||||
artifacts: "web-${{ needs.setup.outputs.release_version }}-selfhosted-COMMERCIAL.zip,
|
||||
web-${{ needs.setup.outputs.release_version }}-selfhosted-open-source.zip"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
draft: true
|
||||
|
||||
71
.github/workflows/version-bump.yml
vendored
Normal file
71
.github/workflows/version-bump.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
name: Version Bump
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_number:
|
||||
description: "New Version"
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
bump_props_version:
|
||||
name: "Create version_bump_${{ github.event.inputs.version_number }} branch"
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||
|
||||
- name: Create Version Branch
|
||||
run: |
|
||||
git switch -c version_bump_${{ github.event.inputs.version_number }}
|
||||
git push -u origin version_bump_${{ github.event.inputs.version_number }}
|
||||
|
||||
- name: Checkout Version Branch
|
||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||
with:
|
||||
ref: version_bump_${{ github.event.inputs.version_number }}
|
||||
|
||||
- name: Bump Version - package.json
|
||||
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
|
||||
with:
|
||||
version: ${{ github.event.inputs.version_number }}
|
||||
file_path: "./package.json"
|
||||
|
||||
- name: Bump Version - package-lock.json
|
||||
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
|
||||
with:
|
||||
version: ${{ github.event.inputs.version_number }}
|
||||
file_path: "./package-lock.json"
|
||||
|
||||
- name: Commit files
|
||||
run: |
|
||||
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
|
||||
|
||||
- name: Push changes
|
||||
run: git push -u origin version_bump_${{ github.event.inputs.version_number }}
|
||||
|
||||
- name: Create Version PR
|
||||
env:
|
||||
PR_BRANCH: "version_bump_${{ github.event.inputs.version_number }}"
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
BASE_BRANCH: master
|
||||
TITLE: "Bump version to ${{ github.event.inputs.version_number }}"
|
||||
run: |
|
||||
gh pr create --title "$TITLE" \
|
||||
--base "$BASE" \
|
||||
--head "$PR_BRANCH" \
|
||||
--label "version update" \
|
||||
--label "automated pr" \
|
||||
--body "
|
||||
## Type of change
|
||||
- [ ] Bug fix
|
||||
- [ ] New feature development
|
||||
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
|
||||
- [ ] Build/deploy pipeline (DevOps)
|
||||
- [X] Other
|
||||
|
||||
## Objective
|
||||
Automated version bump to ${{ github.event.inputs.version_number }}"
|
||||
4
.husky/pre-commit
Normal file
4
.husky/pre-commit
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
12
.prettierignore
Normal file
12
.prettierignore
Normal file
@@ -0,0 +1,12 @@
|
||||
# Build directories
|
||||
build
|
||||
dist
|
||||
|
||||
jslib
|
||||
|
||||
# External libraries / auto synced locales
|
||||
src/locales
|
||||
src/404/*.min.css
|
||||
|
||||
# Github Workflows
|
||||
.github/workflows
|
||||
3
.prettierrc.json
Normal file
3
.prettierrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"printWidth": 100
|
||||
}
|
||||
@@ -6,17 +6,12 @@ Please visit our [Community Forums](https://community.bitwarden.com/) for genera
|
||||
|
||||
Here is how you can get involved:
|
||||
|
||||
* **Request a new feature:** Go to the [Feature Requests category](https://community.bitwarden.com/c/feature-requests/) of the Community Forums. Please search existing feature requests before making a new one
|
||||
|
||||
* **Write code for a new feature:** Make a new post in the [Github Contributions category](https://community.bitwarden.com/c/github-contributions/) of the Community Forums. Include a description of your proposed contribution, screeshots, and links to any relevant feature requests. This helps get feedback from the community and Bitwarden team members before you start writing code
|
||||
|
||||
* **Report a bug or submit a bugfix:** Use Github issues and pull requests
|
||||
|
||||
* **Write documentation:** Submit a pull request to the [Bitwarden help repository](https://github.com/bitwarden/help)
|
||||
|
||||
* **Help other users:** Go to the [User-to-User Support category](https://community.bitwarden.com/c/support/) on the Community Forums
|
||||
|
||||
* **Translate:** See the localization (l10n) section below
|
||||
- **Request a new feature:** Go to the [Feature Requests category](https://community.bitwarden.com/c/feature-requests/) of the Community Forums. Please search existing feature requests before making a new one
|
||||
- **Write code for a new feature:** Make a new post in the [Github Contributions category](https://community.bitwarden.com/c/github-contributions/) of the Community Forums. Include a description of your proposed contribution, screeshots, and links to any relevant feature requests. This helps get feedback from the community and Bitwarden team members before you start writing code
|
||||
- **Report a bug or submit a bugfix:** Use Github issues and pull requests
|
||||
- **Write documentation:** Submit a pull request to the [Bitwarden help repository](https://github.com/bitwarden/help)
|
||||
- **Help other users:** Go to the [User-to-User Support category](https://community.bitwarden.com/c/support/) on the Community Forums
|
||||
- **Translate:** See the localization (l10n) section below
|
||||
|
||||
## Contributor Agreement
|
||||
|
||||
@@ -24,9 +19,9 @@ Please sign the [Contributor Agreement](https://cla-assistant.io/bitwarden/web)
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
* use `npm run lint` and fix any linting suggestions before submitting a pull request
|
||||
* commit any pull requests against the `master` branch
|
||||
* include a link to your Community Forums post
|
||||
- use `npm run lint` and fix any linting suggestions before submitting a pull request
|
||||
- commit any pull requests against the `master` branch
|
||||
- include a link to your Community Forums post
|
||||
|
||||
# Localization (l10n)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM bitwarden/server
|
||||
FROM bitwarden/server:dev
|
||||
|
||||
LABEL com.bitwarden.product="bitwarden"
|
||||
|
||||
|
||||
43
README.md
43
README.md
@@ -23,8 +23,8 @@
|
||||
|
||||
### Requirements
|
||||
|
||||
- [Node.js](https://nodejs.org) v14.17 or greater
|
||||
- NPM v7
|
||||
- [Node.js](https://nodejs.org) v16.13.1 or greater
|
||||
- NPM v8
|
||||
|
||||
### Run the app
|
||||
|
||||
@@ -32,7 +32,7 @@ For local development, run the app with:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run build:watch
|
||||
npm run build:oss:watch
|
||||
```
|
||||
|
||||
You can now access the web vault in your browser at `https://localhost:8080`.
|
||||
@@ -41,33 +41,48 @@ If you want to point the development web vault to the production APIs, you can r
|
||||
|
||||
```
|
||||
npm install
|
||||
ENV=production npm run build:watch
|
||||
ENV=cloud npm run build:oss:watch
|
||||
```
|
||||
|
||||
You can also manually adjusting your API endpoint settings by adding `config/local.json` overriding any of the following values:
|
||||
|
||||
```json
|
||||
{
|
||||
"dev": {
|
||||
"proxyApi": "http://your-api-url",
|
||||
"proxyIdentity": "http://your-identity-url",
|
||||
"proxyEvents": "http://your-events-url",
|
||||
"proxyNotifications": "http://your-notifications-url",
|
||||
"proxyPortal": "http://your-portal-url",
|
||||
"allowedHosts": ["hostnames-to-allow-in-webpack"],
|
||||
"urls": {
|
||||
|
||||
}
|
||||
"allowedHosts": ["hostnames-to-allow-in-webpack"]
|
||||
},
|
||||
"urls": {}
|
||||
}
|
||||
```
|
||||
|
||||
Where the `urls` object is defined by the [Urls type in jslib](https://github.com/bitwarden/jslib/blob/master/common/src/abstractions/environment.service.ts). To pick up the overrides in the newly created `config/local.json` file, run the app with:
|
||||
|
||||
```
|
||||
npm run build:dev:watch
|
||||
```
|
||||
Where the `urls` object is defined by the [Urls type in jslib](https://github.com/bitwarden/jslib/blob/master/common/src/abstractions/environment.service.ts).
|
||||
|
||||
## Contribute
|
||||
|
||||
Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file.
|
||||
|
||||
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file.
|
||||
|
||||
## Prettier
|
||||
|
||||
We recently migrated to using Prettier as code formatter. All previous branches will need to updated to avoid large merge conflicts using the following steps:
|
||||
|
||||
1. Check out your local Branch
|
||||
2. Run `git merge 2b0a9d995e0147601ca8ae4778434a19354a60c2`
|
||||
3. Resolve any merge conflicts, commit.
|
||||
4. Run `npm run prettier`
|
||||
5. Commit
|
||||
6. Run `git merge -Xours 56477eb39cfd8a73c9920577d24d75fed36e2cf5`
|
||||
7. Push
|
||||
|
||||
### Git blame
|
||||
|
||||
We also recommend that you configure git to ignore the prettier revision using:
|
||||
|
||||
```bash
|
||||
git config blame.ignoreRevsFile .git-blame-ignore-revs
|
||||
```
|
||||
|
||||
@@ -7,7 +7,7 @@ notify us. We welcome working with you to resolve the issue promptly. Thanks in
|
||||
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every
|
||||
effort to quickly resolve the issue.
|
||||
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a
|
||||
third-party. We may publicly disclose the issue before resolving it, if appropriate.
|
||||
third-party. We may publicly disclose the issue before resolving it, if appropriate.
|
||||
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or
|
||||
degradation of our service. Only interact with accounts you own or with explicit permission of the
|
||||
account holder.
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'providers',
|
||||
loadChildren: async () => (await import('./providers/providers.module')).ProvidersModule,
|
||||
},
|
||||
{
|
||||
path: "providers",
|
||||
loadChildren: async () => (await import("./providers/providers.module")).ProvidersModule,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
export class AppRoutingModule {}
|
||||
|
||||
20
bitwarden_license/src/app/app.component.ts
Normal file
20
bitwarden_license/src/app/app.component.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { AppComponent as BaseAppComponent } from "src/app/app.component";
|
||||
import { DisablePersonalVaultExportPolicy } from "./policies/disable-personal-vault-export.component";
|
||||
import { MaximumVaultTimeoutPolicy } from "./policies/maximum-vault-timeout.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-root",
|
||||
templateUrl: "../../../src/app/app.component.html",
|
||||
})
|
||||
export class AppComponent extends BaseAppComponent {
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
|
||||
this.policyListService.addPolicies([
|
||||
new MaximumVaultTimeoutPolicy(),
|
||||
new DisablePersonalVaultExportPolicy(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,48 @@
|
||||
import { ToasterModule } from 'angular2-toaster';
|
||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||
import { DragDropModule } from "@angular/cdk/drag-drop";
|
||||
import { NgModule } from "@angular/core";
|
||||
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { InfiniteScrollModule } from "ngx-infinite-scroll";
|
||||
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { BitwardenToastModule } from "jslib-angular/components/toastr.component";
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppRoutingModule } from "./app-routing.module";
|
||||
import { AppComponent } from "./app.component";
|
||||
import { OrganizationsModule } from "./organizations/organizations.module";
|
||||
import { DisablePersonalVaultExportPolicyComponent } from "./policies/disable-personal-vault-export.component";
|
||||
import { MaximumVaultTimeoutPolicyComponent } from "./policies/maximum-vault-timeout.component";
|
||||
|
||||
import { AppComponent } from 'src/app/app.component';
|
||||
import { OssRoutingModule } from 'src/app/oss-routing.module';
|
||||
import { OssModule } from 'src/app/oss.module';
|
||||
import { ServicesModule } from 'src/app/services/services.module';
|
||||
import { OssRoutingModule } from "src/app/oss-routing.module";
|
||||
import { OssModule } from "src/app/oss.module";
|
||||
import { ServicesModule } from "src/app/services/services.module";
|
||||
import { WildcardRoutingModule } from "src/app/wildcard-routing.module";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
OssModule,
|
||||
BrowserAnimationsModule,
|
||||
FormsModule,
|
||||
ServicesModule,
|
||||
ToasterModule.forRoot(),
|
||||
InfiniteScrollModule,
|
||||
DragDropModule,
|
||||
AppRoutingModule,
|
||||
OssRoutingModule,
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [
|
||||
OssModule,
|
||||
BrowserAnimationsModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
ServicesModule,
|
||||
BitwardenToastModule.forRoot({
|
||||
maxOpened: 5,
|
||||
autoDismiss: true,
|
||||
closeButton: true,
|
||||
}),
|
||||
InfiniteScrollModule,
|
||||
DragDropModule,
|
||||
AppRoutingModule,
|
||||
OssRoutingModule,
|
||||
OrganizationsModule,
|
||||
RouterModule,
|
||||
WildcardRoutingModule, // Needs to be last to catch all non-existing routes
|
||||
],
|
||||
declarations: [
|
||||
AppComponent,
|
||||
MaximumVaultTimeoutPolicyComponent,
|
||||
DisablePersonalVaultExportPolicyComponent,
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule { }
|
||||
export class AppModule {}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
import { enableProdMode } from "@angular/core";
|
||||
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
|
||||
|
||||
import 'bootstrap';
|
||||
import 'jquery';
|
||||
import 'popper.js';
|
||||
import "bootstrap";
|
||||
import "jquery";
|
||||
import "popper.js";
|
||||
|
||||
// tslint:disable-next-line
|
||||
require('src/scss/styles.scss');
|
||||
require("src/scss/styles.scss");
|
||||
|
||||
import { AppModule } from './app.module';
|
||||
import { AppModule } from "./app.module";
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
enableProdMode();
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });
|
||||
|
||||
@@ -0,0 +1,493 @@
|
||||
<div class="page-header d-flex">
|
||||
<h1>{{ "singleSignOn" | i18n }}</h1>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
|
||||
<form
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[formGroup]="data"
|
||||
[appApiAction]="formPromise"
|
||||
*ngIf="!loading"
|
||||
ngNativeValidate
|
||||
>
|
||||
<p>
|
||||
{{ "ssoPolicyHelpStart" | i18n }}
|
||||
<a routerLink="../policies">{{ "ssoPolicyHelpLink" | i18n }}</a>
|
||||
{{ "ssoPolicyHelpEnd" | i18n }}
|
||||
<br />
|
||||
{{ "ssoPolicyHelpKeyConnector" | i18n }}
|
||||
</p>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="enabled"
|
||||
[formControl]="enabled"
|
||||
name="Enabled"
|
||||
/>
|
||||
<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">
|
||||
<label for="keyConnectorUrl">{{ "keyConnectorUrl" | i18n }}</label>
|
||||
<div class="input-group">
|
||||
<input
|
||||
class="form-control"
|
||||
formControlName="keyConnectorUrl"
|
||||
id="keyConnectorUrl"
|
||||
required
|
||||
/>
|
||||
<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>
|
||||
<ng-container *ngIf="keyConnectorUrl.pristine && !keyConnectorUrl.pending">
|
||||
<div class="text-danger" *ngIf="keyConnectorUrl.hasError('invalidUrl')" role="alert">
|
||||
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
|
||||
{{ "keyConnectorTestFail" | i18n }}
|
||||
</div>
|
||||
<div class="text-success" *ngIf="!keyConnectorUrl.hasError('invalidUrl')" role="alert">
|
||||
<i class="bwi bwi-check-circle" aria-hidden="true"></i>
|
||||
{{ "keyConnectorTestSuccess" | i18n }}
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</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 -->
|
||||
<div *ngIf="data.value.configType == 1">
|
||||
<div class="config-section">
|
||||
<h2>{{ "openIdConnectConfig" | i18n }}</h2>
|
||||
<div class="form-group">
|
||||
<label>{{ "callbackPath" | i18n }}</label>
|
||||
<div class="input-group">
|
||||
<input class="form-control" readonly [value]="callbackPath" />
|
||||
<div class="input-group-append">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{ 'copyValue' | i18n }}"
|
||||
(click)="copy(callbackPath)"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ "signedOutCallbackPath" | i18n }}</label>
|
||||
<div class="input-group">
|
||||
<input class="form-control" readonly [value]="signedOutCallbackPath" />
|
||||
<div class="input-group-append">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{ 'copyValue' | i18n }}"
|
||||
(click)="copy(signedOutCallbackPath)"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</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"
|
||||
id="additionalUserIdClaimTypes"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="additionalEmailClaimTypes">{{ "additionalEmailClaimTypes" | i18n }}</label>
|
||||
<input
|
||||
class="form-control"
|
||||
formControlName="additionalEmailClaimTypes"
|
||||
id="additionalEmailClaimTypes"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="additionalNameClaimTypes">{{ "additionalNameClaimTypes" | i18n }}</label>
|
||||
<input
|
||||
class="form-control"
|
||||
formControlName="additionalNameClaimTypes"
|
||||
id="additionalNameClaimTypes"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="acrValues">{{ "acrValues" | i18n }}</label>
|
||||
<input class="form-control" formControlName="acrValues" id="acrValues" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="expectedReturnAcrValue">{{ "expectedReturnAcrValue" | i18n }}</label>
|
||||
<input
|
||||
class="form-control"
|
||||
formControlName="expectedReturnAcrValue"
|
||||
id="expectedReturnAcrValue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="data.value.configType == 2">
|
||||
<!-- SAML2 SP -->
|
||||
<div class="config-section">
|
||||
<h2>{{ "samlSpConfig" | i18n }}</h2>
|
||||
<div class="form-group">
|
||||
<label>{{ "spEntityId" | i18n }}</label>
|
||||
<div class="input-group">
|
||||
<input class="form-control" readonly [value]="spEntityId" />
|
||||
<div class="input-group-append">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{ 'copyValue' | i18n }}"
|
||||
(click)="copy(spEntityId)"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ "spMetadataUrl" | i18n }}</label>
|
||||
<div class="input-group">
|
||||
<input class="form-control" readonly [value]="spMetadataUrl" />
|
||||
<div class="input-group-append">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{ 'launch' | i18n }}"
|
||||
(click)="launchUri(spMetadataUrl)"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{ 'copyValue' | i18n }}"
|
||||
(click)="copy(spMetadataUrl)"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ "spAcsUrl" | i18n }}</label>
|
||||
<div class="input-group">
|
||||
<input class="form-control" readonly [value]="spAcsUrl" />
|
||||
<div class="input-group-append">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{ 'copyValue' | i18n }}"
|
||||
(click)="copy(spAcsUrl)"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="spNameIdFormat">{{ "spNameIdFormat" | i18n }}</label>
|
||||
<select class="form-control" formControlName="spNameIdFormat" id="spNameIdFormat">
|
||||
<option value="0">Not Configured</option>
|
||||
<option value="1">Unspecified</option>
|
||||
<option value="2">Email Address</option>
|
||||
<option value="3">X.509 Subject Name</option>
|
||||
<option value="4">Windows Domain Qualified Name</option>
|
||||
<option value="5">Kerberos Principal Name</option>
|
||||
<option value="6">Entity Identifier</option>
|
||||
<option value="7">Persistent</option>
|
||||
<option value="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 value="0">If IdP Wants Authn Requests Signed</option>
|
||||
<option value="1">Always</option>
|
||||
<option value="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>
|
||||
|
||||
<!-- SAML2 IDP -->
|
||||
<div class="config-section">
|
||||
<h2>{{ "samlIdpConfig" | i18n }}</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="idpEntityId">{{ "idpEntityId" | i18n }}</label>
|
||||
<input class="form-control" formControlName="idpEntityId" id="idpEntityId" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="idpBindingType">{{ "idpBindingType" | i18n }}</label>
|
||||
<select class="form-control" formControlName="idpBindingType" id="idpBindingType">
|
||||
<option value="1">Redirect</option>
|
||||
<option value="2">HTTP POST</option>
|
||||
<option value="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
|
||||
formControlName="idpX509PublicCert"
|
||||
class="form-control form-control-sm text-monospace"
|
||||
rows="6"
|
||||
id="idpX509PublicCert"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="idpOutboundSigningAlgorithm">{{ "idpOutboundSigningAlgorithm" | i18n }}</label>
|
||||
<select
|
||||
class="form-control"
|
||||
formControlName="idpOutboundSigningAlgorithm"
|
||||
id="idpOutboundSigningAlgorithm"
|
||||
>
|
||||
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{ o }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" [hidden]="true">
|
||||
<!--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>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "save" | i18n }}</span>
|
||||
</button>
|
||||
</form>
|
||||
183
bitwarden_license/src/app/organizations/manage/sso.component.ts
Normal file
183
bitwarden_license/src/app/organizations/manage/sso.component.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
|
||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
|
||||
import { Organization } from "jslib-common/models/domain/organization";
|
||||
|
||||
import { OrganizationSsoRequest } from "jslib-common/models/request/organization/organizationSsoRequest";
|
||||
|
||||
@Component({
|
||||
selector: "app-org-manage-sso",
|
||||
templateUrl: "sso.component.html",
|
||||
})
|
||||
export class SsoComponent implements OnInit {
|
||||
samlSigningAlgorithms = [
|
||||
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
|
||||
"http://www.w3.org/2000/09/xmldsig#rsa-sha384",
|
||||
"http://www.w3.org/2000/09/xmldsig#rsa-sha512",
|
||||
"http://www.w3.org/2000/09/xmldsig#rsa-sha1",
|
||||
];
|
||||
|
||||
loading = true;
|
||||
organizationId: string;
|
||||
organization: Organization;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
callbackPath: string;
|
||||
signedOutCallbackPath: string;
|
||||
spEntityId: string;
|
||||
spMetadataUrl: string;
|
||||
spAcsUrl: string;
|
||||
|
||||
enabled = this.formBuilder.control(false);
|
||||
data = this.formBuilder.group({
|
||||
configType: [],
|
||||
|
||||
keyConnectorEnabled: [],
|
||||
keyConnectorUrl: [],
|
||||
|
||||
// OpenId
|
||||
authority: [],
|
||||
clientId: [],
|
||||
clientSecret: [],
|
||||
metadataAddress: [],
|
||||
redirectBehavior: [],
|
||||
getClaimsFromUserInfoEndpoint: [],
|
||||
additionalScopes: [],
|
||||
additionalUserIdClaimTypes: [],
|
||||
additionalEmailClaimTypes: [],
|
||||
additionalNameClaimTypes: [],
|
||||
acrValues: [],
|
||||
expectedReturnAcrValue: [],
|
||||
|
||||
// SAML
|
||||
spNameIdFormat: [],
|
||||
spOutboundSigningAlgorithm: [],
|
||||
spSigningBehavior: [],
|
||||
spMinIncomingSigningAlgorithm: [],
|
||||
spWantAssertionsSigned: [],
|
||||
spValidateCertificates: [],
|
||||
|
||||
idpEntityId: [],
|
||||
idpBindingType: [],
|
||||
idpSingleSignOnServiceUrl: [],
|
||||
idpSingleLogoutServiceUrl: [],
|
||||
idpArtifactResolutionServiceUrl: [],
|
||||
idpX509PublicCert: [],
|
||||
idpOutboundSigningAlgorithm: [],
|
||||
idpAllowUnsolicitedAuthnResponse: [],
|
||||
idpDisableOutboundLogoutRequests: [],
|
||||
idpWantAuthnRequestsSigned: [],
|
||||
});
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private route: ActivatedRoute,
|
||||
private apiService: ApiService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private organizationService: OrganizationService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organizationId = params.organizationId;
|
||||
await this.load();
|
||||
});
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.organization = await this.organizationService.get(this.organizationId);
|
||||
const ssoSettings = await this.apiService.getOrganizationSso(this.organizationId);
|
||||
|
||||
this.data.patchValue(ssoSettings.data);
|
||||
this.enabled.setValue(ssoSettings.enabled);
|
||||
|
||||
this.callbackPath = ssoSettings.urls.callbackPath;
|
||||
this.signedOutCallbackPath = ssoSettings.urls.signedOutCallbackPath;
|
||||
this.spEntityId = ssoSettings.urls.spEntityId;
|
||||
this.spMetadataUrl = ssoSettings.urls.spMetadataUrl;
|
||||
this.spAcsUrl = ssoSettings.urls.spAcsUrl;
|
||||
|
||||
this.keyConnectorUrl.markAsDirty();
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
copy(value: string) {
|
||||
this.platformUtilsService.copyToClipboard(value);
|
||||
}
|
||||
|
||||
launchUri(url: string) {
|
||||
this.platformUtilsService.launchUri(url);
|
||||
}
|
||||
|
||||
async submit() {
|
||||
this.formPromise = this.postData();
|
||||
|
||||
try {
|
||||
const response = await this.formPromise;
|
||||
|
||||
this.data.patchValue(response.data);
|
||||
this.enabled.setValue(response.enabled);
|
||||
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("ssoSettingsSaved"));
|
||||
} catch {
|
||||
// Logged by appApiAction, do nothing
|
||||
}
|
||||
|
||||
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() {
|
||||
if (this.keyConnectorUrl.pristine) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.keyConnectorUrl.markAsPending();
|
||||
|
||||
try {
|
||||
await this.apiService.getKeyConnectorAlive(this.keyConnectorUrl.value);
|
||||
this.keyConnectorUrl.updateValueAndValidity();
|
||||
} catch {
|
||||
this.keyConnectorUrl.setErrors({
|
||||
invalidUrl: true,
|
||||
});
|
||||
}
|
||||
|
||||
this.keyConnectorUrl.markAsPristine();
|
||||
}
|
||||
|
||||
get enableTestKeyConnector() {
|
||||
return (
|
||||
this.data.get("keyConnectorEnabled").value &&
|
||||
this.keyConnectorUrl != null &&
|
||||
this.keyConnectorUrl.value !== ""
|
||||
);
|
||||
}
|
||||
|
||||
get keyConnectorUrl() {
|
||||
return this.data.get("keyConnectorUrl");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { AuthGuardService } from "jslib-angular/services/auth-guard.service";
|
||||
|
||||
import { Permissions } from "jslib-common/enums/permissions";
|
||||
|
||||
import { OrganizationLayoutComponent } from "src/app/layouts/organization-layout.component";
|
||||
import { ManageComponent } from "src/app/organizations/manage/manage.component";
|
||||
import { OrganizationGuardService } from "src/app/services/organization-guard.service";
|
||||
import { OrganizationTypeGuardService } from "src/app/services/organization-type-guard.service";
|
||||
|
||||
import { SsoComponent } from "./manage/sso.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: "organizations/:organizationId",
|
||||
component: OrganizationLayoutComponent,
|
||||
canActivate: [AuthGuardService, OrganizationGuardService],
|
||||
children: [
|
||||
{
|
||||
path: "manage",
|
||||
component: ManageComponent,
|
||||
canActivate: [OrganizationTypeGuardService],
|
||||
data: {
|
||||
permissions: [
|
||||
Permissions.CreateNewCollections,
|
||||
Permissions.EditAnyCollection,
|
||||
Permissions.DeleteAnyCollection,
|
||||
Permissions.EditAssignedCollections,
|
||||
Permissions.DeleteAssignedCollections,
|
||||
Permissions.AccessEventLogs,
|
||||
Permissions.ManageGroups,
|
||||
Permissions.ManageUsers,
|
||||
Permissions.ManagePolicies,
|
||||
Permissions.ManageSso,
|
||||
],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "sso",
|
||||
component: SsoComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class OrganizationsRoutingModule {}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
|
||||
import { OssModule } from "src/app/oss.module";
|
||||
|
||||
import { SsoComponent } from "./manage/sso.component";
|
||||
import { OrganizationsRoutingModule } from "./organizations-routing.module";
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, FormsModule, ReactiveFormsModule, OssModule, OrganizationsRoutingModule],
|
||||
declarations: [SsoComponent],
|
||||
})
|
||||
export class OrganizationsModule {}
|
||||
@@ -0,0 +1,12 @@
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="enabled"
|
||||
[formControl]="enabled"
|
||||
name="Enabled"
|
||||
/>
|
||||
<label class="form-check-label" for="enabled">{{ "enabled" | i18n }}</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
|
||||
import { PolicyType } from "jslib-common/enums/policyType";
|
||||
|
||||
import { PolicyRequest } from "jslib-common/models/request/policyRequest";
|
||||
|
||||
import {
|
||||
BasePolicy,
|
||||
BasePolicyComponent,
|
||||
} from "src/app/organizations/policies/base-policy.component";
|
||||
|
||||
export class DisablePersonalVaultExportPolicy extends BasePolicy {
|
||||
name = "disablePersonalVaultExport";
|
||||
description = "disablePersonalVaultExportDesc";
|
||||
type = PolicyType.DisablePersonalVaultExport;
|
||||
component = DisablePersonalVaultExportPolicyComponent;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "policy-disable-personal-vault-export",
|
||||
templateUrl: "disable-personal-vault-export.component.html",
|
||||
})
|
||||
export class DisablePersonalVaultExportPolicyComponent extends BasePolicyComponent {}
|
||||
@@ -0,0 +1,47 @@
|
||||
<app-callout type="tip" title="{{ 'prerequisite' | i18n }}">
|
||||
{{ "requireSsoPolicyReq" | i18n }}
|
||||
</app-callout>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="enabled"
|
||||
[formControl]="enabled"
|
||||
name="Enabled"
|
||||
/>
|
||||
<label class="form-check-label" for="enabled">{{ "enabled" | i18n }}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div [formGroup]="data">
|
||||
<div class="form-group">
|
||||
<label for="hours">{{ "maximumVaultTimeoutLabel" | i18n }}</label>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<input
|
||||
id="hours"
|
||||
class="form-control"
|
||||
type="number"
|
||||
min="0"
|
||||
name="hours"
|
||||
formControlName="hours"
|
||||
/>
|
||||
<small>{{ "hours" | i18n }}</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<input
|
||||
id="minutes"
|
||||
class="form-control"
|
||||
type="number"
|
||||
min="0"
|
||||
max="59"
|
||||
name="minutes"
|
||||
formControlName="minutes"
|
||||
/>
|
||||
<small>{{ "minutes" | i18n }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
|
||||
import { PolicyType } from "jslib-common/enums/policyType";
|
||||
|
||||
import { PolicyRequest } from "jslib-common/models/request/policyRequest";
|
||||
|
||||
import {
|
||||
BasePolicy,
|
||||
BasePolicyComponent,
|
||||
} from "src/app/organizations/policies/base-policy.component";
|
||||
|
||||
export class MaximumVaultTimeoutPolicy extends BasePolicy {
|
||||
name = "maximumVaultTimeout";
|
||||
description = "maximumVaultTimeoutDesc";
|
||||
type = PolicyType.MaximumVaultTimeout;
|
||||
component = MaximumVaultTimeoutPolicyComponent;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "policy-maximum-timeout",
|
||||
templateUrl: "maximum-vault-timeout.component.html",
|
||||
})
|
||||
export class MaximumVaultTimeoutPolicyComponent extends BasePolicyComponent {
|
||||
data = this.formBuilder.group({
|
||||
hours: [null],
|
||||
minutes: [null],
|
||||
});
|
||||
|
||||
constructor(private formBuilder: FormBuilder, private i18nService: I18nService) {
|
||||
super();
|
||||
}
|
||||
|
||||
loadData() {
|
||||
const minutes = this.policyResponse.data?.minutes;
|
||||
|
||||
if (minutes == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.data.patchValue({
|
||||
hours: Math.floor(minutes / 60),
|
||||
minutes: minutes % 60,
|
||||
});
|
||||
}
|
||||
|
||||
buildRequestData() {
|
||||
if (this.data.value.hours == null && this.data.value.minutes == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
minutes: this.data.value.hours * 60 + this.data.value.minutes,
|
||||
};
|
||||
}
|
||||
|
||||
buildRequest(policiesEnabledMap: Map<PolicyType, boolean>): Promise<PolicyRequest> {
|
||||
const singleOrgEnabled = policiesEnabledMap.get(PolicyType.SingleOrg) ?? false;
|
||||
if (this.enabled.value && !singleOrgEnabled) {
|
||||
throw new Error(this.i18nService.t("requireSsoPolicyReqError"));
|
||||
}
|
||||
|
||||
const data = this.buildRequestData();
|
||||
if (data?.minutes == null || data?.minutes <= 0) {
|
||||
throw new Error(this.i18nService.t("invalidMaximumVaultTimeout"));
|
||||
}
|
||||
|
||||
return super.buildRequest(policiesEnabledMap);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,46 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="addTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="addTitle">
|
||||
{{'addExistingOrganization' | i18n}}
|
||||
</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="card-body text-center" *ngIf="loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
{{'loading' | i18n}}
|
||||
</div>
|
||||
<ng-container *ngIf="!loading">
|
||||
<table class="table table-hover table-list">
|
||||
<tr *ngFor="let o of organizations">
|
||||
<td width="30">
|
||||
<app-avatar [data]="o.name" size="25" [circle]="true" [fontSize]="14"></app-avatar>
|
||||
</td>
|
||||
<td>
|
||||
{{o.name}}
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-outline-secondary pull-right" (click)="add(o)" [disabled]="formPromise">Add</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="addTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="addTitle">
|
||||
{{ "addExistingOrganization" | i18n }}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="card-body text-center" *ngIf="loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
{{ "loading" | i18n }}
|
||||
</div>
|
||||
<ng-container *ngIf="!loading">
|
||||
<table class="table table-hover table-list">
|
||||
<tr *ngFor="let o of organizations">
|
||||
<td width="30">
|
||||
<app-avatar [data]="o.name" size="25" [circle]="true" [fontSize]="14"></app-avatar>
|
||||
</td>
|
||||
<td>
|
||||
{{ o.name }}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
class="btn btn-outline-secondary pull-right"
|
||||
(click)="add(o)"
|
||||
[disabled]="formPromise"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,83 +1,86 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
|
||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||
import { UserService } from 'jslib-common/abstractions/user.service';
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
||||
|
||||
import { ValidationService } from 'jslib-angular/services/validation.service';
|
||||
import { ValidationService } from "jslib-angular/services/validation.service";
|
||||
|
||||
import { ProviderService } from '../services/provider.service';
|
||||
import { WebProviderService } from "../services/webProvider.service";
|
||||
|
||||
import { Organization } from 'jslib-common/models/domain/organization';
|
||||
import { Provider } from 'jslib-common/models/domain/provider';
|
||||
|
||||
import { PlanType } from 'jslib-common/enums/planType';
|
||||
import { Organization } from "jslib-common/models/domain/organization";
|
||||
import { Provider } from "jslib-common/models/domain/provider";
|
||||
|
||||
@Component({
|
||||
selector: 'provider-add-organization',
|
||||
templateUrl: 'add-organization.component.html',
|
||||
selector: "provider-add-organization",
|
||||
templateUrl: "add-organization.component.html",
|
||||
})
|
||||
export class AddOrganizationComponent implements OnInit {
|
||||
@Input() providerId: string;
|
||||
@Input() organizations: Organization[];
|
||||
@Output() onAddedOrganization = new EventEmitter();
|
||||
|
||||
@Input() providerId: string;
|
||||
@Input() organizations: Organization[];
|
||||
@Output() onAddedOrganization = new EventEmitter();
|
||||
provider: Provider;
|
||||
formPromise: Promise<any>;
|
||||
loading = true;
|
||||
|
||||
provider: Provider;
|
||||
formPromise: Promise<any>;
|
||||
loading = true;
|
||||
constructor(
|
||||
private providerService: ProviderService,
|
||||
private webProviderService: WebProviderService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private validationService: ValidationService
|
||||
) {}
|
||||
|
||||
constructor(private userService: UserService, private providerService: ProviderService,
|
||||
private toasterService: ToasterService, private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService, private validationService: ValidationService,
|
||||
private apiService: ApiService) { }
|
||||
async ngOnInit() {
|
||||
await this.load();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.load();
|
||||
async load() {
|
||||
if (this.providerId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (this.providerId == null) {
|
||||
return;
|
||||
}
|
||||
this.provider = await this.providerService.get(this.providerId);
|
||||
|
||||
this.provider = await this.userService.getProvider(this.providerId);
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
async add(organization: Organization) {
|
||||
if (this.formPromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
async add(organization: Organization) {
|
||||
if (this.formPromise) {
|
||||
return;
|
||||
}
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t("addOrganizationConfirmation", organization.name, this.provider.name),
|
||||
organization.name,
|
||||
this.i18nService.t("yes"),
|
||||
this.i18nService.t("no"),
|
||||
"warning"
|
||||
);
|
||||
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('addOrganizationConfirmation', organization.name, this.provider.name), organization.name,
|
||||
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this.formPromise = this.providerService.addOrganizationToProvider(this.providerId, organization.id);
|
||||
await this.formPromise;
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
return;
|
||||
} finally {
|
||||
this.formPromise = null;
|
||||
}
|
||||
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('organizationJoinedProvider'));
|
||||
this.onAddedOrganization.emit();
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this.formPromise = this.webProviderService.addOrganizationToProvider(
|
||||
this.providerId,
|
||||
organization.id
|
||||
);
|
||||
await this.formPromise;
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
return;
|
||||
} finally {
|
||||
this.formPromise = null;
|
||||
}
|
||||
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("organizationJoinedProvider")
|
||||
);
|
||||
this.onAddedOrganization.emit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +1,90 @@
|
||||
<div class="page-header d-flex">
|
||||
<h1>{{'clients' | i18n}}</h1>
|
||||
<h1>{{ "clients" | i18n }}</h1>
|
||||
|
||||
<div class="ml-auto d-flex">
|
||||
<div>
|
||||
<label class="sr-only" for="search">{{'search' | i18n}}</label>
|
||||
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
|
||||
[(ngModel)]="searchText">
|
||||
</div>
|
||||
<a class="btn btn-sm btn-outline-primary ml-3" routerLink="create" *ngIf="manageOrganizations">
|
||||
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
|
||||
{{'newClientOrganization' | i18n}}
|
||||
</a>
|
||||
<button class="btn btn-sm btn-outline-primary ml-3" (click)="addExistingOrganization()"
|
||||
*ngIf="manageOrganizations && showAddExisting">
|
||||
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
|
||||
{{'addExistingOrganization' | i18n}}
|
||||
</button>
|
||||
<div class="ml-auto d-flex">
|
||||
<div>
|
||||
<label class="sr-only" for="search">{{ "search" | i18n }}</label>
|
||||
<input
|
||||
type="search"
|
||||
class="form-control form-control-sm"
|
||||
id="search"
|
||||
placeholder="{{ 'search' | i18n }}"
|
||||
[(ngModel)]="searchText"
|
||||
/>
|
||||
</div>
|
||||
<a class="btn btn-sm btn-outline-primary ml-3" routerLink="create" *ngIf="manageOrganizations">
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "newClientOrganization" | i18n }}
|
||||
</a>
|
||||
<button
|
||||
class="btn btn-sm btn-outline-primary ml-3"
|
||||
(click)="addExistingOrganization()"
|
||||
*ngIf="manageOrganizations && showAddExisting"
|
||||
>
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "addExistingOrganization" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
|
||||
<ng-container
|
||||
*ngIf="!loading && (clients | search:searchText:'organizationName':'id') as searchedClients">
|
||||
<p *ngIf="!searchedClients.length">{{'noClientsInList' | i18n}}</p>
|
||||
<ng-container *ngIf="searchedClients.length">
|
||||
<table class="table table-hover table-list" infiniteScroll [infiniteScrollDistance]="1"
|
||||
[infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
|
||||
<tbody>
|
||||
<tr *ngFor="let o of searchedClients">
|
||||
<td width="30">
|
||||
<app-avatar [data]="o.organizationName" size="25" [circle]="true" [fontSize]="14"></app-avatar>
|
||||
</td>
|
||||
<td>
|
||||
<a [routerLink]="['/organizations', o.organizationId]">{{o.organizationName}}</a>
|
||||
</td>
|
||||
<td class="table-list-options" *ngIf="manageOrganizations">
|
||||
<div class="dropdown" appListDropdown>
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
|
||||
appA11yTitle="{{'options' | i18n}}">
|
||||
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(o)">
|
||||
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
|
||||
{{'remove' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
*ngIf="!loading && (clients | search: searchText:'organizationName':'id') as searchedClients"
|
||||
>
|
||||
<p *ngIf="!searchedClients.length">{{ "noClientsInList" | i18n }}</p>
|
||||
<ng-container *ngIf="searchedClients.length">
|
||||
<table
|
||||
class="table table-hover table-list"
|
||||
infiniteScroll
|
||||
[infiniteScrollDistance]="1"
|
||||
[infiniteScrollDisabled]="!isPaging()"
|
||||
(scrolled)="loadMore()"
|
||||
>
|
||||
<tbody>
|
||||
<tr *ngFor="let o of searchedClients">
|
||||
<td width="30">
|
||||
<app-avatar
|
||||
[data]="o.organizationName"
|
||||
size="25"
|
||||
[circle]="true"
|
||||
[fontSize]="14"
|
||||
></app-avatar>
|
||||
</td>
|
||||
<td>
|
||||
<a [routerLink]="['/organizations', o.organizationId]">{{ o.organizationName }}</a>
|
||||
</td>
|
||||
<td class="table-list-options" *ngIf="manageOrganizations">
|
||||
<div class="dropdown" appListDropdown>
|
||||
<button
|
||||
class="btn btn-outline-secondary dropdown-toggle"
|
||||
type="button"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(o)">
|
||||
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
||||
{{ "remove" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #add></ng-template>
|
||||
|
||||
@@ -1,160 +1,183 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ViewContainerRef
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
|
||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||
import { SearchService } from 'jslib-common/abstractions/search.service';
|
||||
import { UserService } from 'jslib-common/abstractions/user.service';
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { ModalService } from 'jslib-angular/services/modal.service';
|
||||
import { ValidationService } from 'jslib-angular/services/validation.service';
|
||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { LogService } from "jslib-common/abstractions/log.service";
|
||||
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
||||
import { SearchService } from "jslib-common/abstractions/search.service";
|
||||
|
||||
import { PlanType } from 'jslib-common/enums/planType';
|
||||
import { ProviderUserType } from 'jslib-common/enums/providerUserType';
|
||||
import { ModalService } from "jslib-angular/services/modal.service";
|
||||
import { ValidationService } from "jslib-angular/services/validation.service";
|
||||
|
||||
import { Organization } from 'jslib-common/models/domain/organization';
|
||||
import {
|
||||
ProviderOrganizationOrganizationDetailsResponse
|
||||
} from 'jslib-common/models/response/provider/providerOrganizationResponse';
|
||||
import { PlanType } from "jslib-common/enums/planType";
|
||||
import { ProviderUserType } from "jslib-common/enums/providerUserType";
|
||||
|
||||
import { ProviderService } from '../services/provider.service';
|
||||
import { Organization } from "jslib-common/models/domain/organization";
|
||||
import { ProviderOrganizationOrganizationDetailsResponse } from "jslib-common/models/response/provider/providerOrganizationResponse";
|
||||
|
||||
import { AddOrganizationComponent } from './add-organization.component';
|
||||
import { WebProviderService } from "../services/webProvider.service";
|
||||
|
||||
const DisallowedPlanTypes = [PlanType.Free, PlanType.FamiliesAnnually2019, PlanType.FamiliesAnnually];
|
||||
import { AddOrganizationComponent } from "./add-organization.component";
|
||||
|
||||
const DisallowedPlanTypes = [
|
||||
PlanType.Free,
|
||||
PlanType.FamiliesAnnually2019,
|
||||
PlanType.FamiliesAnnually,
|
||||
];
|
||||
|
||||
@Component({
|
||||
templateUrl: 'clients.component.html',
|
||||
templateUrl: "clients.component.html",
|
||||
})
|
||||
export class ClientsComponent implements OnInit {
|
||||
@ViewChild("add", { read: ViewContainerRef, static: true }) addModalRef: ViewContainerRef;
|
||||
|
||||
@ViewChild('add', { read: ViewContainerRef, static: true }) addModalRef: ViewContainerRef;
|
||||
providerId: any;
|
||||
searchText: string;
|
||||
addableOrganizations: Organization[];
|
||||
loading = true;
|
||||
manageOrganizations = false;
|
||||
showAddExisting = false;
|
||||
|
||||
providerId: any;
|
||||
searchText: string;
|
||||
addableOrganizations: Organization[];
|
||||
loading = true;
|
||||
manageOrganizations = false;
|
||||
showAddExisting = false;
|
||||
clients: ProviderOrganizationOrganizationDetailsResponse[];
|
||||
pagedClients: ProviderOrganizationOrganizationDetailsResponse[];
|
||||
|
||||
clients: ProviderOrganizationOrganizationDetailsResponse[];
|
||||
pagedClients: ProviderOrganizationOrganizationDetailsResponse[];
|
||||
protected didScroll = false;
|
||||
protected pageSize = 100;
|
||||
protected actionPromise: Promise<any>;
|
||||
private pagedClientsCount = 0;
|
||||
|
||||
protected didScroll = false;
|
||||
protected pageSize = 100;
|
||||
protected actionPromise: Promise<any>;
|
||||
private pagedClientsCount = 0;
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private providerService: ProviderService,
|
||||
private apiService: ApiService,
|
||||
private searchService: SearchService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private validationService: ValidationService,
|
||||
private webProviderService: WebProviderService,
|
||||
private logService: LogService,
|
||||
private modalService: ModalService,
|
||||
private organizationService: OrganizationService
|
||||
) {}
|
||||
|
||||
constructor(private route: ActivatedRoute, private userService: UserService,
|
||||
private apiService: ApiService, private searchService: SearchService,
|
||||
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
|
||||
private toasterService: ToasterService, private validationService: ValidationService,
|
||||
private providerService: ProviderService, private logService: LogService,
|
||||
private modalService: ModalService) { }
|
||||
async ngOnInit() {
|
||||
this.route.parent.params.subscribe(async (params) => {
|
||||
this.providerId = params.providerId;
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.params.subscribe(async params => {
|
||||
this.providerId = params.providerId;
|
||||
await this.load();
|
||||
|
||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||
this.searchText = qParams.search;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async load() {
|
||||
const response = await this.apiService.getProviderClients(this.providerId);
|
||||
this.clients = response.data != null && response.data.length > 0 ? response.data : [];
|
||||
this.manageOrganizations =
|
||||
(await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin;
|
||||
const candidateOrgs = (await this.organizationService.getAll()).filter(
|
||||
(o) => o.isOwner && o.providerId == null
|
||||
);
|
||||
const allowedOrgsIds = await Promise.all(
|
||||
candidateOrgs.map((o) => this.apiService.getOrganization(o.id))
|
||||
).then((orgs) =>
|
||||
orgs.filter((o) => !DisallowedPlanTypes.includes(o.planType)).map((o) => o.id)
|
||||
);
|
||||
this.addableOrganizations = candidateOrgs.filter((o) => allowedOrgsIds.includes(o.id));
|
||||
|
||||
this.showAddExisting = this.addableOrganizations.length !== 0;
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
isPaging() {
|
||||
const searching = this.isSearching();
|
||||
if (searching && this.didScroll) {
|
||||
this.resetPaging();
|
||||
}
|
||||
return !searching && this.clients && this.clients.length > this.pageSize;
|
||||
}
|
||||
|
||||
isSearching() {
|
||||
return this.searchService.isSearchable(this.searchText);
|
||||
}
|
||||
|
||||
async resetPaging() {
|
||||
this.pagedClients = [];
|
||||
this.loadMore();
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
if (!this.clients || this.clients.length <= this.pageSize) {
|
||||
return;
|
||||
}
|
||||
const pagedLength = this.pagedClients.length;
|
||||
let pagedSize = this.pageSize;
|
||||
if (pagedLength === 0 && this.pagedClientsCount > this.pageSize) {
|
||||
pagedSize = this.pagedClientsCount;
|
||||
}
|
||||
if (this.clients.length > pagedLength) {
|
||||
this.pagedClients = this.pagedClients.concat(
|
||||
this.clients.slice(pagedLength, pagedLength + pagedSize)
|
||||
);
|
||||
}
|
||||
this.pagedClientsCount = this.pagedClients.length;
|
||||
this.didScroll = this.pagedClients.length > this.pageSize;
|
||||
}
|
||||
|
||||
async addExistingOrganization() {
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
AddOrganizationComponent,
|
||||
this.addModalRef,
|
||||
(comp) => {
|
||||
comp.providerId = this.providerId;
|
||||
comp.organizations = this.addableOrganizations;
|
||||
comp.onAddedOrganization.subscribe(async () => {
|
||||
try {
|
||||
await this.load();
|
||||
|
||||
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
|
||||
this.searchText = qParams.search;
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
modal.close();
|
||||
} catch (e) {
|
||||
this.logService.error(`Handled exception: ${e}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async remove(organization: ProviderOrganizationOrganizationDetailsResponse) {
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t("detachOrganizationConfirmation"),
|
||||
organization.organizationName,
|
||||
this.i18nService.t("yes"),
|
||||
this.i18nService.t("no"),
|
||||
"warning"
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
async load() {
|
||||
const response = await this.apiService.getProviderClients(this.providerId);
|
||||
this.clients = response.data != null && response.data.length > 0 ? response.data : [];
|
||||
this.manageOrganizations = (await this.userService.getProvider(this.providerId)).type === ProviderUserType.ProviderAdmin;
|
||||
const candidateOrgs = (await this.userService.getAllOrganizations()).filter(o => o.isOwner && o.providerId == null);
|
||||
const allowedOrgsIds = await Promise.all(candidateOrgs.map(o => this.apiService.getOrganization(o.id))).then(orgs =>
|
||||
orgs.filter(o => !DisallowedPlanTypes.includes(o.planType))
|
||||
.map(o => o.id));
|
||||
this.addableOrganizations = candidateOrgs.filter(o => allowedOrgsIds.includes(o.id));
|
||||
|
||||
this.showAddExisting = this.addableOrganizations.length !== 0;
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
isPaging() {
|
||||
const searching = this.isSearching();
|
||||
if (searching && this.didScroll) {
|
||||
this.resetPaging();
|
||||
}
|
||||
return !searching && this.clients && this.clients.length > this.pageSize;
|
||||
}
|
||||
|
||||
isSearching() {
|
||||
return this.searchService.isSearchable(this.searchText);
|
||||
}
|
||||
|
||||
async resetPaging() {
|
||||
this.pagedClients = [];
|
||||
this.loadMore();
|
||||
}
|
||||
|
||||
|
||||
loadMore() {
|
||||
if (!this.clients || this.clients.length <= this.pageSize) {
|
||||
return;
|
||||
}
|
||||
const pagedLength = this.pagedClients.length;
|
||||
let pagedSize = this.pageSize;
|
||||
if (pagedLength === 0 && this.pagedClientsCount > this.pageSize) {
|
||||
pagedSize = this.pagedClientsCount;
|
||||
}
|
||||
if (this.clients.length > pagedLength) {
|
||||
this.pagedClients = this.pagedClients.concat(this.clients.slice(pagedLength, pagedLength + pagedSize));
|
||||
}
|
||||
this.pagedClientsCount = this.pagedClients.length;
|
||||
this.didScroll = this.pagedClients.length > this.pageSize;
|
||||
}
|
||||
|
||||
async addExistingOrganization() {
|
||||
const [modal] = await this.modalService.openViewRef(AddOrganizationComponent, this.addModalRef, comp => {
|
||||
comp.providerId = this.providerId;
|
||||
comp.organizations = this.addableOrganizations;
|
||||
comp.onAddedOrganization.subscribe(async () => {
|
||||
try {
|
||||
await this.load();
|
||||
modal.close();
|
||||
} catch (e) {
|
||||
this.logService.error(`Handled exception: ${e}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async remove(organization: ProviderOrganizationOrganizationDetailsResponse) {
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('detachOrganizationConfirmation'), organization.organizationName,
|
||||
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.actionPromise = this.providerService.detachOrganizastion(this.providerId, organization.id);
|
||||
try {
|
||||
await this.actionPromise;
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('detachedOrganization', organization.organizationName));
|
||||
await this.load();
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = null;
|
||||
this.actionPromise = this.webProviderService.detachOrganizastion(
|
||||
this.providerId,
|
||||
organization.id
|
||||
);
|
||||
try {
|
||||
await this.actionPromise;
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("detachedOrganization", organization.organizationName)
|
||||
);
|
||||
await this.load();
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="page-header">
|
||||
<h1>{{'newClientOrganization' | i18n}}</h1>
|
||||
<h1>{{ "newClientOrganization" | i18n }}</h1>
|
||||
</div>
|
||||
<p>{{'newClientOrganizationDesc' | i18n}}</p>
|
||||
<p>{{ "newClientOrganizationDesc" | i18n }}</p>
|
||||
<app-organization-plans [providerId]="providerId"></app-organization-plans>
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Component, OnInit, ViewChild } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
|
||||
import { OrganizationPlansComponent } from 'src/app/settings/organization-plans.component';
|
||||
import { OrganizationPlansComponent } from "src/app/settings/organization-plans.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-organization',
|
||||
templateUrl: 'create-organization.component.html',
|
||||
selector: "app-create-organization",
|
||||
templateUrl: "create-organization.component.html",
|
||||
})
|
||||
export class CreateOrganizationComponent implements OnInit {
|
||||
@ViewChild(OrganizationPlansComponent, { static: true }) orgPlansComponent: OrganizationPlansComponent;
|
||||
@ViewChild(OrganizationPlansComponent, { static: true })
|
||||
orgPlansComponent: OrganizationPlansComponent;
|
||||
|
||||
providerId: string;
|
||||
providerId: string;
|
||||
|
||||
constructor(private route: ActivatedRoute) { }
|
||||
constructor(private route: ActivatedRoute) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.parent.params.subscribe(async params => {
|
||||
this.providerId = params.providerId;
|
||||
});
|
||||
}
|
||||
ngOnInit() {
|
||||
this.route.parent.params.subscribe(async (params) => {
|
||||
this.providerId = params.providerId;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,42 @@
|
||||
<div class="mt-5 d-flex justify-content-center" *ngIf="loading">
|
||||
<div>
|
||||
<img src="/src/images/logo-dark@2x.png" class="mb-4 logo" alt="Bitwarden">
|
||||
<p class="text-center">
|
||||
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<img class="mb-4 logo logo-themed" alt="Bitwarden" />
|
||||
<p class="text-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container" *ngIf="!loading && !authed">
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="lead text-center mb-4">{{'joinProvider' | i18n}}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body">
|
||||
<p class="text-center">
|
||||
{{providerName}}
|
||||
<strong class="d-block mt-2">{{email}}</strong>
|
||||
</p>
|
||||
<p>{{'joinProviderDesc' | i18n}}</p>
|
||||
<hr>
|
||||
<div class="d-flex">
|
||||
<a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
|
||||
{{'logIn' | i18n}}
|
||||
</a>
|
||||
<a routerLink="/register" [queryParams]="{email: email}"
|
||||
class="btn btn-primary btn-block ml-2 mt-0">
|
||||
{{'createAccount' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="lead text-center mb-4">{{ "joinProvider" | i18n }}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body">
|
||||
<p class="text-center">
|
||||
{{ providerName }}
|
||||
<strong class="d-block mt-2">{{ email }}</strong>
|
||||
</p>
|
||||
<p>{{ "joinProviderDesc" | i18n }}</p>
|
||||
<hr />
|
||||
<div class="d-flex">
|
||||
<a routerLink="/" [queryParams]="{ email: email }" class="btn btn-primary btn-block">
|
||||
{{ "logIn" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
routerLink="/register"
|
||||
[queryParams]="{ email: email }"
|
||||
class="btn btn-primary btn-block ml-2 mt-0"
|
||||
>
|
||||
{{ "createAccount" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,48 +1,56 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Toast, ToasterService } from 'angular2-toaster';
|
||||
import { Component } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
import { BaseAcceptComponent } from 'src/app/common/base.accept.component';
|
||||
import { BaseAcceptComponent } from "src/app/common/base.accept.component";
|
||||
|
||||
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 { StateService } from 'jslib-common/abstractions/state.service';
|
||||
import { UserService } from 'jslib-common/abstractions/user.service';
|
||||
import { ProviderUserAcceptRequest } from 'jslib-common/models/request/provider/providerUserAcceptRequest';
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { StateService } from "jslib-common/abstractions/state.service";
|
||||
import { ProviderUserAcceptRequest } from "jslib-common/models/request/provider/providerUserAcceptRequest";
|
||||
|
||||
@Component({
|
||||
selector: 'app-accept-provider',
|
||||
templateUrl: 'accept-provider.component.html',
|
||||
selector: "app-accept-provider",
|
||||
templateUrl: "accept-provider.component.html",
|
||||
})
|
||||
export class AcceptProviderComponent extends BaseAcceptComponent {
|
||||
providerName: string;
|
||||
providerName: string;
|
||||
|
||||
failedMessage = 'providerInviteAcceptFailed';
|
||||
failedMessage = "providerInviteAcceptFailed";
|
||||
|
||||
requiredParameters = ['providerId', 'providerUserId', 'token'];
|
||||
requiredParameters = ["providerId", "providerUserId", "token"];
|
||||
|
||||
constructor(router: Router, toasterService: ToasterService, i18nService: I18nService, route: ActivatedRoute,
|
||||
userService: UserService, stateService: StateService, private apiService: ApiService) {
|
||||
super(router, toasterService, i18nService, route, userService, stateService);
|
||||
}
|
||||
constructor(
|
||||
router: Router,
|
||||
i18nService: I18nService,
|
||||
route: ActivatedRoute,
|
||||
stateService: StateService,
|
||||
private apiService: ApiService,
|
||||
platformUtilService: PlatformUtilsService
|
||||
) {
|
||||
super(router, platformUtilService, i18nService, route, stateService);
|
||||
}
|
||||
|
||||
async authedHandler(qParams: any) {
|
||||
const request = new ProviderUserAcceptRequest();
|
||||
request.token = qParams.token;
|
||||
async authedHandler(qParams: any) {
|
||||
const request = new ProviderUserAcceptRequest();
|
||||
request.token = qParams.token;
|
||||
|
||||
await this.apiService.postProviderUserAccept(qParams.providerId, qParams.providerUserId, request);
|
||||
const toast: Toast = {
|
||||
type: 'success',
|
||||
title: this.i18nService.t('inviteAccepted'),
|
||||
body: this.i18nService.t('providerInviteAcceptedDesc'),
|
||||
timeout: 10000,
|
||||
};
|
||||
this.toasterService.popAsync(toast);
|
||||
this.router.navigate(['/vault']);
|
||||
}
|
||||
await this.apiService.postProviderUserAccept(
|
||||
qParams.providerId,
|
||||
qParams.providerUserId,
|
||||
request
|
||||
);
|
||||
this.platformUtilService.showToast(
|
||||
"success",
|
||||
this.i18nService.t("inviteAccepted"),
|
||||
this.i18nService.t("providerInviteAcceptedDesc"),
|
||||
{ timeout: 10000 }
|
||||
);
|
||||
this.router.navigate(["/vault"]);
|
||||
}
|
||||
|
||||
async unauthedHandler(qParams: any) {
|
||||
this.providerName = qParams.providerName;
|
||||
}
|
||||
async unauthedHandler(qParams: any) {
|
||||
this.providerName = qParams.providerName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,34 @@
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
} from '@angular/core';
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { ProviderUserBulkConfirmRequest } from 'jslib-common/models/request/provider/providerUserBulkConfirmRequest';
|
||||
import { ProviderUserBulkRequest } from 'jslib-common/models/request/provider/providerUserBulkRequest';
|
||||
import { ProviderUserBulkConfirmRequest } from "jslib-common/models/request/provider/providerUserBulkConfirmRequest";
|
||||
import { ProviderUserBulkRequest } from "jslib-common/models/request/provider/providerUserBulkRequest";
|
||||
|
||||
import { ProviderUserStatusType } from 'jslib-common/enums/providerUserStatusType';
|
||||
import { ProviderUserStatusType } from "jslib-common/enums/providerUserStatusType";
|
||||
|
||||
import { BulkConfirmComponent as OrganizationBulkConfirmComponent } from 'src/app/organizations/manage/bulk/bulk-confirm.component';
|
||||
import { BulkUserDetails } from 'src/app/organizations/manage/bulk/bulk-status.component';
|
||||
import { BulkConfirmComponent as OrganizationBulkConfirmComponent } from "src/app/organizations/manage/bulk/bulk-confirm.component";
|
||||
import { BulkUserDetails } from "src/app/organizations/manage/bulk/bulk-status.component";
|
||||
|
||||
@Component({
|
||||
templateUrl: '/src/app/organizations/manage/bulk/bulk-confirm.component.html',
|
||||
templateUrl: "../../../../../../src/app/organizations/manage/bulk/bulk-confirm.component.html",
|
||||
})
|
||||
export class BulkConfirmComponent extends OrganizationBulkConfirmComponent {
|
||||
@Input() providerId: string;
|
||||
|
||||
@Input() providerId: string;
|
||||
protected isAccepted(user: BulkUserDetails) {
|
||||
return user.status === ProviderUserStatusType.Accepted;
|
||||
}
|
||||
|
||||
protected isAccepted(user: BulkUserDetails) {
|
||||
return user.status === ProviderUserStatusType.Accepted;
|
||||
}
|
||||
protected async getPublicKeys() {
|
||||
const request = new ProviderUserBulkRequest(this.filteredUsers.map((user) => user.id));
|
||||
return await this.apiService.postProviderUsersPublicKey(this.providerId, request);
|
||||
}
|
||||
|
||||
protected async getPublicKeys() {
|
||||
const request = new ProviderUserBulkRequest(this.filteredUsers.map(user => user.id));
|
||||
return await this.apiService.postProviderUsersPublicKey(this.providerId, request);
|
||||
}
|
||||
protected getCryptoKey() {
|
||||
return this.cryptoService.getProviderKey(this.providerId);
|
||||
}
|
||||
|
||||
protected getCryptoKey() {
|
||||
return this.cryptoService.getProviderKey(this.providerId);
|
||||
}
|
||||
|
||||
protected async postConfirmRequest(userIdsWithKeys: any[]) {
|
||||
const request = new ProviderUserBulkConfirmRequest(userIdsWithKeys);
|
||||
return await this.apiService.postProviderUserBulkConfirm(this.providerId, request);
|
||||
}
|
||||
protected async postConfirmRequest(userIdsWithKeys: any[]) {
|
||||
const request = new ProviderUserBulkConfirmRequest(userIdsWithKeys);
|
||||
return await this.apiService.postProviderUserBulkConfirm(this.providerId, request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
} from '@angular/core';
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { ProviderUserBulkRequest } from 'jslib-common/models/request/provider/providerUserBulkRequest';
|
||||
import { ProviderUserBulkRequest } from "jslib-common/models/request/provider/providerUserBulkRequest";
|
||||
|
||||
import { BulkRemoveComponent as OrganizationBulkRemoveComponent } from 'src/app/organizations/manage/bulk/bulk-remove.component';
|
||||
import { BulkRemoveComponent as OrganizationBulkRemoveComponent } from "src/app/organizations/manage/bulk/bulk-remove.component";
|
||||
|
||||
@Component({
|
||||
templateUrl: '/src/app/organizations/manage/bulk/bulk-remove.component.html',
|
||||
templateUrl: "../../../../../../src/app/organizations/manage/bulk/bulk-remove.component.html",
|
||||
})
|
||||
export class BulkRemoveComponent extends OrganizationBulkRemoveComponent {
|
||||
@Input() providerId: string;
|
||||
|
||||
@Input() providerId: string;
|
||||
|
||||
async deleteUsers() {
|
||||
const request = new ProviderUserBulkRequest(this.users.map(user => user.id));
|
||||
return await this.apiService.deleteManyProviderUsers(this.providerId, request);
|
||||
}
|
||||
async deleteUsers() {
|
||||
const request = new ProviderUserBulkRequest(this.users.map((user) => user.id));
|
||||
return await this.apiService.deleteManyProviderUsers(this.providerId, request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +1,107 @@
|
||||
<div class="page-header d-flex">
|
||||
<h1>{{'eventLogs' | i18n}}</h1>
|
||||
<div class="ml-auto d-flex">
|
||||
<div class="form-inline">
|
||||
<label class="sr-only" for="start">{{'startDate' | i18n}}</label>
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="start"
|
||||
placeholder="{{'startDate' | i18n}}" [(ngModel)]="start" placeholder="YYYY-MM-DDTHH:MM"
|
||||
(change)="dirtyDates = true">
|
||||
<span class="mx-2">-</span>
|
||||
<label class="sr-only" for="end">{{'endDate' | i18n}}</label>
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="end"
|
||||
placeholder="{{'endDate' | i18n}}" [(ngModel)]="end" placeholder="YYYY-MM-DDTHH:MM"
|
||||
(change)="dirtyDates = true">
|
||||
</div>
|
||||
<form #refreshForm [appApiAction]="refreshPromise" class="d-inline">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="loadEvents(true)"
|
||||
[disabled]="loaded && refreshForm.loading">
|
||||
<i class="fa fa-refresh fa-fw" aria-hidden="true" [ngClass]="{'fa-spin': loaded && refreshForm.loading}"></i>
|
||||
{{'refresh' | i18n}}
|
||||
</button>
|
||||
</form>
|
||||
<form #exportForm [appApiAction]="exportPromise" class="d-inline">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary btn-submit manual ml-3"
|
||||
[ngClass]="{loading:exportForm.loading}" (click)="exportEvents()"
|
||||
[disabled]="loaded && exportForm.loading || dirtyDates">
|
||||
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
|
||||
<span>{{'export' | i18n}}</span>
|
||||
</button>
|
||||
</form>
|
||||
<h1>{{ "eventLogs" | i18n }}</h1>
|
||||
<div class="ml-auto d-flex">
|
||||
<div class="form-inline">
|
||||
<label class="sr-only" for="start">{{ "startDate" | i18n }}</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
class="form-control form-control-sm"
|
||||
id="start"
|
||||
placeholder="{{ 'startDate' | i18n }}"
|
||||
[(ngModel)]="start"
|
||||
placeholder="YYYY-MM-DDTHH:MM"
|
||||
(change)="dirtyDates = true"
|
||||
/>
|
||||
<span class="mx-2">-</span>
|
||||
<label class="sr-only" for="end">{{ "endDate" | i18n }}</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
class="form-control form-control-sm"
|
||||
id="end"
|
||||
placeholder="{{ 'endDate' | i18n }}"
|
||||
[(ngModel)]="end"
|
||||
placeholder="YYYY-MM-DDTHH:MM"
|
||||
(change)="dirtyDates = true"
|
||||
/>
|
||||
</div>
|
||||
<form #refreshForm [appApiAction]="refreshPromise" class="d-inline">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-primary ml-3"
|
||||
(click)="loadEvents(true)"
|
||||
[disabled]="loaded && refreshForm.loading"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-refresh bwi-fw"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-spin': loaded && refreshForm.loading }"
|
||||
></i>
|
||||
{{ "refresh" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
<form #exportForm [appApiAction]="exportPromise" class="d-inline">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-primary btn-submit manual ml-3"
|
||||
[ngClass]="{ loading: exportForm.loading }"
|
||||
(click)="exportEvents()"
|
||||
[disabled]="(loaded && exportForm.loading) || dirtyDates"
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" aria-hidden="true"></i>
|
||||
<span>{{ "export" | i18n }}</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="!loaded">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="loaded">
|
||||
<p *ngIf="!events || !events.length">{{'noEventsInList' | i18n}}</p>
|
||||
<table class="table table-hover" *ngIf="events && events.length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border-top-0" width="210">{{'timestamp' | i18n}}</th>
|
||||
<th class="border-top-0" width="40">
|
||||
<span class="sr-only">{{'device' | i18n}}</span>
|
||||
</th>
|
||||
<th class="border-top-0" width="150">{{'user' | i18n}}</th>
|
||||
<th class="border-top-0">{{'event' | i18n}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let e of events">
|
||||
<td>{{e.date | date:'medium'}}</td>
|
||||
<td>
|
||||
<i class="text-muted fa fa-lg {{e.appIcon}}" title="{{e.appName}}, {{e.ip}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{e.appName}}, {{e.ip}}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span title="{{e.userEmail}}">{{e.userName}}</span>
|
||||
</td>
|
||||
<td [innerHTML]="e.message"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button #moreBtn [appApiAction]="morePromise" type="button" class="btn btn-block btn-link btn-submit"
|
||||
(click)="loadEvents(false)" [disabled]="loaded && moreBtn.loading" *ngIf="continuationToken">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'loadMore' | i18n}}</span>
|
||||
</button>
|
||||
<p *ngIf="!events || !events.length">{{ "noEventsInList" | i18n }}</p>
|
||||
<table class="table table-hover" *ngIf="events && events.length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border-top-0" width="210">{{ "timestamp" | i18n }}</th>
|
||||
<th class="border-top-0" width="40">
|
||||
<span class="sr-only">{{ "device" | i18n }}</span>
|
||||
</th>
|
||||
<th class="border-top-0" width="150">{{ "user" | i18n }}</th>
|
||||
<th class="border-top-0">{{ "event" | i18n }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let e of events">
|
||||
<td>{{ e.date | date: "medium" }}</td>
|
||||
<td>
|
||||
<i
|
||||
class="text-muted bwi bwi-lg {{ e.appIcon }}"
|
||||
title="{{ e.appName }}, {{ e.ip }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ e.appName }}, {{ e.ip }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span title="{{ e.userEmail }}">{{ e.userName }}</span>
|
||||
</td>
|
||||
<td [innerHTML]="e.message"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button
|
||||
#moreBtn
|
||||
[appApiAction]="morePromise"
|
||||
type="button"
|
||||
class="btn btn-block btn-link btn-submit"
|
||||
(click)="loadEvents(false)"
|
||||
[disabled]="loaded && moreBtn.loading"
|
||||
*ngIf="continuationToken"
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "loadMore" | i18n }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
@@ -1,71 +1,82 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||
import { ExportService } from 'jslib-common/abstractions/export.service';
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||
import { UserService } from 'jslib-common/abstractions/user.service';
|
||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||
import { ExportService } from "jslib-common/abstractions/export.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { LogService } from "jslib-common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
||||
|
||||
import { UserNamePipe } from 'jslib-angular/pipes/user-name.pipe';
|
||||
import { UserNamePipe } from "jslib-angular/pipes/user-name.pipe";
|
||||
|
||||
import { EventResponse } from 'jslib-common/models/response/eventResponse';
|
||||
import { EventResponse } from "jslib-common/models/response/eventResponse";
|
||||
|
||||
import { EventService } from 'src/app/services/event.service';
|
||||
import { EventService } from "src/app/services/event.service";
|
||||
|
||||
import { BaseEventsComponent } from 'src/app/common/base.events.component';
|
||||
import { BaseEventsComponent } from "src/app/common/base.events.component";
|
||||
|
||||
@Component({
|
||||
selector: 'provider-events',
|
||||
templateUrl: 'events.component.html',
|
||||
selector: "provider-events",
|
||||
templateUrl: "events.component.html",
|
||||
})
|
||||
export class EventsComponent extends BaseEventsComponent implements OnInit {
|
||||
exportFileName: string = 'provider-events';
|
||||
providerId: string;
|
||||
exportFileName: string = "provider-events";
|
||||
providerId: string;
|
||||
|
||||
private providerUsersUserIdMap = new Map<string, any>();
|
||||
private providerUsersIdMap = new Map<string, any>();
|
||||
private providerUsersUserIdMap = new Map<string, any>();
|
||||
private providerUsersIdMap = new Map<string, any>();
|
||||
|
||||
constructor(private apiService: ApiService, private route: ActivatedRoute, eventService: EventService,
|
||||
i18nService: I18nService, toasterService: ToasterService, private userService: UserService,
|
||||
exportService: ExportService, platformUtilsService: PlatformUtilsService, private router: Router,
|
||||
logService: LogService, private userNamePipe: UserNamePipe) {
|
||||
super(eventService, i18nService, toasterService, exportService, platformUtilsService, logService);
|
||||
}
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private route: ActivatedRoute,
|
||||
eventService: EventService,
|
||||
i18nService: I18nService,
|
||||
private providerService: ProviderService,
|
||||
exportService: ExportService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
private router: Router,
|
||||
logService: LogService,
|
||||
private userNamePipe: UserNamePipe
|
||||
) {
|
||||
super(eventService, i18nService, exportService, platformUtilsService, logService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async params => {
|
||||
this.providerId = params.providerId;
|
||||
const provider = await this.userService.getProvider(this.providerId);
|
||||
if (provider == null || !provider.useEvents) {
|
||||
this.router.navigate(['/providers', this.providerId]);
|
||||
return;
|
||||
}
|
||||
await this.load();
|
||||
});
|
||||
}
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.providerId = params.providerId;
|
||||
const provider = await this.providerService.get(this.providerId);
|
||||
if (provider == null || !provider.useEvents) {
|
||||
this.router.navigate(["/providers", this.providerId]);
|
||||
return;
|
||||
}
|
||||
await this.load();
|
||||
});
|
||||
}
|
||||
|
||||
async load() {
|
||||
const response = await this.apiService.getProviderUsers(this.providerId);
|
||||
response.data.forEach(u => {
|
||||
const name = this.userNamePipe.transform(u);
|
||||
this.providerUsersIdMap.set(u.id, { name: name, email: u.email });
|
||||
this.providerUsersUserIdMap.set(u.userId, { name: name, email: u.email });
|
||||
});
|
||||
await this.loadEvents(true);
|
||||
this.loaded = true;
|
||||
}
|
||||
async load() {
|
||||
const response = await this.apiService.getProviderUsers(this.providerId);
|
||||
response.data.forEach((u) => {
|
||||
const name = this.userNamePipe.transform(u);
|
||||
this.providerUsersIdMap.set(u.id, { name: name, email: u.email });
|
||||
this.providerUsersUserIdMap.set(u.userId, { name: name, email: u.email });
|
||||
});
|
||||
await this.loadEvents(true);
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
protected requestEvents(startDate: string, endDate: string, continuationToken: string) {
|
||||
return this.apiService.getEventsProvider(this.providerId, startDate, endDate, continuationToken);
|
||||
}
|
||||
protected requestEvents(startDate: string, endDate: string, continuationToken: string) {
|
||||
return this.apiService.getEventsProvider(
|
||||
this.providerId,
|
||||
startDate,
|
||||
endDate,
|
||||
continuationToken
|
||||
);
|
||||
}
|
||||
|
||||
protected getUserName(r: EventResponse, userId: string) {
|
||||
return userId != null && this.providerUsersUserIdMap.has(userId) ? this.providerUsersUserIdMap.get(userId) : null;
|
||||
}
|
||||
protected getUserName(r: EventResponse, userId: string) {
|
||||
return userId != null && this.providerUsersUserIdMap.has(userId)
|
||||
? this.providerUsersUserIdMap.get(userId)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
<div class="container page-content">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<div class="card" *ngIf="provider">
|
||||
<div class="card-header">{{'manage' | i18n}}</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a routerLink="people" class="list-group-item" routerLinkActive="active"
|
||||
*ngIf="provider.canManageUsers">
|
||||
{{'people' | i18n}}
|
||||
</a>
|
||||
<a routerLink="events" class="list-group-item" routerLinkActive="active"
|
||||
*ngIf="provider.canAccessEventLogs && accessEvents">
|
||||
{{'eventLogs' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<router-outlet></router-outlet>
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<div class="card" *ngIf="provider">
|
||||
<div class="card-header">{{ "manage" | i18n }}</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a
|
||||
routerLink="people"
|
||||
class="list-group-item"
|
||||
routerLinkActive="active"
|
||||
*ngIf="provider.canManageUsers"
|
||||
>
|
||||
{{ "people" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
routerLink="events"
|
||||
class="list-group-item"
|
||||
routerLinkActive="active"
|
||||
*ngIf="provider.canAccessEventLogs && accessEvents"
|
||||
>
|
||||
{{ "eventLogs" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
|
||||
import { UserService } from 'jslib-common/abstractions/user.service';
|
||||
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
||||
|
||||
import { Provider } from 'jslib-common/models/domain/provider';
|
||||
import { Provider } from "jslib-common/models/domain/provider";
|
||||
|
||||
@Component({
|
||||
selector: 'provider-manage',
|
||||
templateUrl: 'manage.component.html',
|
||||
selector: "provider-manage",
|
||||
templateUrl: "manage.component.html",
|
||||
})
|
||||
export class ManageComponent implements OnInit {
|
||||
provider: Provider;
|
||||
accessEvents = false;
|
||||
provider: Provider;
|
||||
accessEvents = false;
|
||||
|
||||
constructor(private route: ActivatedRoute, private userService: UserService) { }
|
||||
constructor(private route: ActivatedRoute, private providerService: ProviderService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.parent.params.subscribe(async params => {
|
||||
this.provider = await this.userService.getProvider(params.providerId);
|
||||
this.accessEvents = this.provider.useEvents;
|
||||
});
|
||||
}
|
||||
ngOnInit() {
|
||||
this.route.parent.params.subscribe(async (params) => {
|
||||
this.provider = await this.providerService.get(params.providerId);
|
||||
this.accessEvents = this.provider.useEvents;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,145 +1,229 @@
|
||||
<div class="page-header d-flex">
|
||||
<h1>{{'people' | i18n}}</h1>
|
||||
<div class="ml-auto d-flex">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary" [ngClass]="{active: status == null}"
|
||||
(click)="filter(null)">
|
||||
{{'all' | i18n}}
|
||||
<span class="badge badge-pill badge-info" *ngIf="allCount">{{allCount}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
[ngClass]="{active: status == userStatusType.Invited}"
|
||||
(click)="filter(userStatusType.Invited)">
|
||||
{{'invited' | i18n}}
|
||||
<span class="badge badge-pill badge-info" *ngIf="invitedCount">{{invitedCount}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
[ngClass]="{active: status == userStatusType.Accepted}"
|
||||
(click)="filter(userStatusType.Accepted)">
|
||||
{{'accepted' | i18n}}
|
||||
<span class="badge badge-pill badge-warning" *ngIf="acceptedCount">{{acceptedCount}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<label class="sr-only" for="search">{{'search' | i18n}}</label>
|
||||
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
|
||||
[(ngModel)]="searchText">
|
||||
</div>
|
||||
<div class="dropdown ml-3" appListDropdown>
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="bulkActionsButton"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" appA11yTitle="{{'options' | i18n}}">
|
||||
<i class="fa fa-cog" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="bulkActionsButton">
|
||||
<button class="dropdown-item" appStopClick (click)="bulkReinvite()">
|
||||
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
|
||||
{{'reinviteSelected' | i18n}}
|
||||
</button>
|
||||
<button class="dropdown-item text-success" appStopClick (click)="bulkConfirm()"
|
||||
*ngIf="showBulkConfirmUsers">
|
||||
<i class="fa fa-fw fa-check" aria-hidden="true"></i>
|
||||
{{'confirmSelected' | i18n}}
|
||||
</button>
|
||||
<button class="dropdown-item text-danger" appStopClick (click)="bulkRemove()">
|
||||
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
|
||||
{{'remove' | i18n}}
|
||||
</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="dropdown-item" appStopClick (click)="selectAll(true)">
|
||||
<i class="fa fa-fw fa-check-square-o" aria-hidden="true"></i>
|
||||
{{'selectAll' | i18n}}
|
||||
</button>
|
||||
<button class="dropdown-item" appStopClick (click)="selectAll(false)">
|
||||
<i class="fa fa-fw fa-minus-square-o" aria-hidden="true"></i>
|
||||
{{'unselectAll' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="invite()">
|
||||
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
|
||||
{{'inviteUser' | i18n}}
|
||||
</button>
|
||||
<h1>{{ "people" | i18n }}</h1>
|
||||
<div class="ml-auto d-flex">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
[ngClass]="{ active: status == null }"
|
||||
(click)="filter(null)"
|
||||
>
|
||||
{{ "all" | i18n }}
|
||||
<span class="badge badge-pill badge-info" *ngIf="allCount">{{ allCount }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
[ngClass]="{ active: status == userStatusType.Invited }"
|
||||
(click)="filter(userStatusType.Invited)"
|
||||
>
|
||||
{{ "invited" | i18n }}
|
||||
<span class="badge badge-pill badge-info" *ngIf="invitedCount">{{ invitedCount }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
[ngClass]="{ active: status == userStatusType.Accepted }"
|
||||
(click)="filter(userStatusType.Accepted)"
|
||||
>
|
||||
{{ "accepted" | i18n }}
|
||||
<span class="badge badge-pill badge-warning" *ngIf="acceptedCount">{{
|
||||
acceptedCount
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<label class="sr-only" for="search">{{ "search" | i18n }}</label>
|
||||
<input
|
||||
type="search"
|
||||
class="form-control form-control-sm"
|
||||
id="search"
|
||||
placeholder="{{ 'search' | i18n }}"
|
||||
[(ngModel)]="searchText"
|
||||
/>
|
||||
</div>
|
||||
<div class="dropdown ml-3" appListDropdown>
|
||||
<button
|
||||
class="btn btn-sm btn-outline-secondary dropdown-toggle"
|
||||
type="button"
|
||||
id="bulkActionsButton"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-cog" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="bulkActionsButton">
|
||||
<button class="dropdown-item" appStopClick (click)="bulkReinvite()">
|
||||
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
|
||||
{{ "reinviteSelected" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
class="dropdown-item text-success"
|
||||
appStopClick
|
||||
(click)="bulkConfirm()"
|
||||
*ngIf="showBulkConfirmUsers"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
||||
{{ "confirmSelected" | i18n }}
|
||||
</button>
|
||||
<button class="dropdown-item text-danger" appStopClick (click)="bulkRemove()">
|
||||
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
||||
{{ "remove" | i18n }}
|
||||
</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="dropdown-item" appStopClick (click)="selectAll(true)">
|
||||
<i class="bwi bwi-fw bwi-check-square" aria-hidden="true"></i>
|
||||
{{ "selectAll" | i18n }}
|
||||
</button>
|
||||
<button class="dropdown-item" appStopClick (click)="selectAll(false)">
|
||||
<i class="bwi bwi-fw bwi-minus-square" aria-hidden="true"></i>
|
||||
{{ "unselectAll" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="invite()">
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "inviteUser" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container
|
||||
*ngIf="!loading && (isPaging() ? pagedUsers : users | search:searchText:'name':'email':'id') as searchedUsers">
|
||||
<p *ngIf="!searchedUsers.length">{{'noUsersInList' | i18n}}</p>
|
||||
<ng-container *ngIf="searchedUsers.length">
|
||||
<app-callout type="info" title="{{'confirmUsers' | i18n}}" icon="fa-check-circle" *ngIf="showConfirmUsers">
|
||||
{{'providerUsersNeedConfirmed' | i18n}}
|
||||
</app-callout>
|
||||
<table class="table table-hover table-list" infiniteScroll [infiniteScrollDistance]="1"
|
||||
[infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
|
||||
<tbody>
|
||||
<tr *ngFor="let u of searchedUsers">
|
||||
<td (click)="checkUser(u)" class="table-list-checkbox">
|
||||
<input type="checkbox" [(ngModel)]="u.checked" appStopProp>
|
||||
</td>
|
||||
<td width="30">
|
||||
<app-avatar [data]="u | userName" [email]="u.email" size="25" [circle]="true"
|
||||
[fontSize]="14"></app-avatar>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" appStopClick (click)="edit(u)">{{u.email}}</a>
|
||||
<span class="badge badge-secondary"
|
||||
*ngIf="u.status === userStatusType.Invited">{{'invited' | i18n}}</span>
|
||||
<span class="badge badge-warning"
|
||||
*ngIf="u.status === userStatusType.Accepted">{{'accepted' | i18n}}</span>
|
||||
<small class="text-muted d-block" *ngIf="u.name">{{u.name}}</small>
|
||||
</td>
|
||||
<td>
|
||||
<ng-container *ngIf="u.twoFactorEnabled">
|
||||
<i class="fa fa-lock" title="{{'userUsingTwoStep' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'userUsingTwoStep' | i18n}}</span>
|
||||
</ng-container>
|
||||
</td>
|
||||
<td>
|
||||
<span *ngIf="u.type === userType.ProviderAdmin">{{'providerAdmin' | i18n}}</span>
|
||||
<span *ngIf="u.type === userType.ServiceUser">{{'serviceUser' | i18n}}</span>
|
||||
<span *ngIf="u.type === userType.Custom">{{'custom' | i18n}}</span>
|
||||
</td>
|
||||
<td class="table-list-options">
|
||||
<div class="dropdown" appListDropdown>
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
|
||||
appA11yTitle="{{'options' | i18n}}">
|
||||
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="reinvite(u)"
|
||||
*ngIf="u.status === userStatusType.Invited">
|
||||
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
|
||||
{{'resendInvitation' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item text-success" href="#" appStopClick (click)="confirm(u)"
|
||||
*ngIf="u.status === userStatusType.Accepted">
|
||||
<i class="fa fa-fw fa-check" aria-hidden="true"></i>
|
||||
{{'confirm' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="groups(u)" *ngIf="accessGroups">
|
||||
<i class="fa fa-fw fa-sitemap" aria-hidden="true"></i>
|
||||
{{'groups' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="events(u)"
|
||||
*ngIf="accessEvents && u.status === userStatusType.Confirmed">
|
||||
<i class="fa fa-fw fa-file-text-o" aria-hidden="true"></i>
|
||||
{{'eventLogs' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(u)">
|
||||
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
|
||||
{{'remove' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
*ngIf="
|
||||
!loading &&
|
||||
(isPaging() ? pagedUsers : (users | search: searchText:'name':'email':'id')) as searchedUsers
|
||||
"
|
||||
>
|
||||
<p *ngIf="!searchedUsers.length">{{ "noUsersInList" | i18n }}</p>
|
||||
<ng-container *ngIf="searchedUsers.length">
|
||||
<app-callout
|
||||
type="info"
|
||||
title="{{ 'confirmUsers' | i18n }}"
|
||||
icon="bwi bwi-check-circle"
|
||||
*ngIf="showConfirmUsers"
|
||||
>
|
||||
{{ "providerUsersNeedConfirmed" | i18n }}
|
||||
</app-callout>
|
||||
<table
|
||||
class="table table-hover table-list"
|
||||
infiniteScroll
|
||||
[infiniteScrollDistance]="1"
|
||||
[infiniteScrollDisabled]="!isPaging()"
|
||||
(scrolled)="loadMore()"
|
||||
>
|
||||
<tbody>
|
||||
<tr *ngFor="let u of searchedUsers">
|
||||
<td (click)="checkUser(u)" class="table-list-checkbox">
|
||||
<input type="checkbox" [(ngModel)]="u.checked" appStopProp />
|
||||
</td>
|
||||
<td width="30">
|
||||
<app-avatar
|
||||
[data]="u | userName"
|
||||
[email]="u.email"
|
||||
size="25"
|
||||
[circle]="true"
|
||||
[fontSize]="14"
|
||||
>
|
||||
</app-avatar>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" appStopClick (click)="edit(u)">{{ u.email }}</a>
|
||||
<span class="badge badge-secondary" *ngIf="u.status === userStatusType.Invited">{{
|
||||
"invited" | i18n
|
||||
}}</span>
|
||||
<span class="badge badge-warning" *ngIf="u.status === userStatusType.Accepted">{{
|
||||
"accepted" | i18n
|
||||
}}</span>
|
||||
<small class="text-muted d-block" *ngIf="u.name">{{ u.name }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<ng-container *ngIf="u.twoFactorEnabled">
|
||||
<i
|
||||
class="bwi bwi-lock"
|
||||
title="{{ 'userUsingTwoStep' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "userUsingTwoStep" | i18n }}</span>
|
||||
</ng-container>
|
||||
</td>
|
||||
<td>
|
||||
<span *ngIf="u.type === userType.ProviderAdmin">{{ "providerAdmin" | i18n }}</span>
|
||||
<span *ngIf="u.type === userType.ServiceUser">{{ "serviceUser" | i18n }}</span>
|
||||
<span *ngIf="u.type === userType.Custom">{{ "custom" | i18n }}</span>
|
||||
</td>
|
||||
<td class="table-list-options">
|
||||
<div class="dropdown" appListDropdown>
|
||||
<button
|
||||
class="btn btn-outline-secondary dropdown-toggle"
|
||||
type="button"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="reinvite(u)"
|
||||
*ngIf="u.status === userStatusType.Invited"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
|
||||
{{ "resendInvitation" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item text-success"
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="confirm(u)"
|
||||
*ngIf="u.status === userStatusType.Accepted"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
||||
{{ "confirm" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="groups(u)"
|
||||
*ngIf="accessGroups"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-sitemap" aria-hidden="true"></i>
|
||||
{{ "groups" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="events(u)"
|
||||
*ngIf="accessEvents && u.status === userStatusType.Confirmed"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>
|
||||
{{ "eventLogs" | i18n }}
|
||||
</a>
|
||||
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(u)">
|
||||
<i class="bwi bwi-fw bwi-remove" aria-hidden="true"></i>
|
||||
{{ "remove" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-template #addEdit></ng-template>
|
||||
<ng-template #eventsTemplate></ng-template>
|
||||
|
||||
@@ -1,238 +1,292 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ViewContainerRef
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||
import { SearchService } from 'jslib-common/abstractions/search.service';
|
||||
import { StorageService } from 'jslib-common/abstractions/storage.service';
|
||||
import { UserService } from 'jslib-common/abstractions/user.service';
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { ModalService } from 'jslib-angular/services/modal.service';
|
||||
import { ValidationService } from 'jslib-angular/services/validation.service';
|
||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { LogService } from "jslib-common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
||||
import { SearchService } from "jslib-common/abstractions/search.service";
|
||||
import { StateService } from "jslib-common/abstractions/state.service";
|
||||
|
||||
import { ProviderUserStatusType } from 'jslib-common/enums/providerUserStatusType';
|
||||
import { ProviderUserType } from 'jslib-common/enums/providerUserType';
|
||||
import { ModalService } from "jslib-angular/services/modal.service";
|
||||
import { ValidationService } from "jslib-angular/services/validation.service";
|
||||
|
||||
import { SearchPipe } from 'jslib-angular/pipes/search.pipe';
|
||||
import { UserNamePipe } from 'jslib-angular/pipes/user-name.pipe';
|
||||
import { ProviderUserStatusType } from "jslib-common/enums/providerUserStatusType";
|
||||
import { ProviderUserType } from "jslib-common/enums/providerUserType";
|
||||
|
||||
import { ListResponse } from 'jslib-common/models/response/listResponse';
|
||||
import { ProviderUserUserDetailsResponse } from 'jslib-common/models/response/provider/providerUserResponse';
|
||||
import { SearchPipe } from "jslib-angular/pipes/search.pipe";
|
||||
import { UserNamePipe } from "jslib-angular/pipes/user-name.pipe";
|
||||
|
||||
import { ProviderUserBulkRequest } from 'jslib-common/models/request/provider/providerUserBulkRequest';
|
||||
import { ProviderUserConfirmRequest } from 'jslib-common/models/request/provider/providerUserConfirmRequest';
|
||||
import { ProviderUserBulkResponse } from 'jslib-common/models/response/provider/providerUserBulkResponse';
|
||||
import { ListResponse } from "jslib-common/models/response/listResponse";
|
||||
import { ProviderUserUserDetailsResponse } from "jslib-common/models/response/provider/providerUserResponse";
|
||||
|
||||
import { BasePeopleComponent } from 'src/app/common/base.people.component';
|
||||
import { BulkStatusComponent } from 'src/app/organizations/manage/bulk/bulk-status.component';
|
||||
import { EntityEventsComponent } from 'src/app/organizations/manage/entity-events.component';
|
||||
import { BulkConfirmComponent } from './bulk/bulk-confirm.component';
|
||||
import { BulkRemoveComponent } from './bulk/bulk-remove.component';
|
||||
import { UserAddEditComponent } from './user-add-edit.component';
|
||||
import { ProviderUserBulkRequest } from "jslib-common/models/request/provider/providerUserBulkRequest";
|
||||
import { ProviderUserConfirmRequest } from "jslib-common/models/request/provider/providerUserConfirmRequest";
|
||||
import { ProviderUserBulkResponse } from "jslib-common/models/response/provider/providerUserBulkResponse";
|
||||
|
||||
import { BasePeopleComponent } from "src/app/common/base.people.component";
|
||||
import { BulkStatusComponent } from "src/app/organizations/manage/bulk/bulk-status.component";
|
||||
import { EntityEventsComponent } from "src/app/organizations/manage/entity-events.component";
|
||||
import { BulkConfirmComponent } from "./bulk/bulk-confirm.component";
|
||||
import { BulkRemoveComponent } from "./bulk/bulk-remove.component";
|
||||
import { UserAddEditComponent } from "./user-add-edit.component";
|
||||
|
||||
@Component({
|
||||
selector: 'provider-people',
|
||||
templateUrl: 'people.component.html',
|
||||
selector: "provider-people",
|
||||
templateUrl: "people.component.html",
|
||||
})
|
||||
export class PeopleComponent extends BasePeopleComponent<ProviderUserUserDetailsResponse> implements OnInit {
|
||||
export class PeopleComponent
|
||||
extends BasePeopleComponent<ProviderUserUserDetailsResponse>
|
||||
implements OnInit
|
||||
{
|
||||
@ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
||||
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
|
||||
groupsModalRef: ViewContainerRef;
|
||||
@ViewChild("eventsTemplate", { read: ViewContainerRef, static: true })
|
||||
eventsModalRef: ViewContainerRef;
|
||||
@ViewChild("bulkStatusTemplate", { read: ViewContainerRef, static: true })
|
||||
bulkStatusModalRef: ViewContainerRef;
|
||||
@ViewChild("bulkConfirmTemplate", { read: ViewContainerRef, static: true })
|
||||
bulkConfirmModalRef: ViewContainerRef;
|
||||
@ViewChild("bulkRemoveTemplate", { read: ViewContainerRef, static: true })
|
||||
bulkRemoveModalRef: ViewContainerRef;
|
||||
|
||||
@ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
||||
@ViewChild('groupsTemplate', { read: ViewContainerRef, static: true }) groupsModalRef: ViewContainerRef;
|
||||
@ViewChild('eventsTemplate', { read: ViewContainerRef, static: true }) eventsModalRef: ViewContainerRef;
|
||||
@ViewChild('bulkStatusTemplate', { read: ViewContainerRef, static: true }) bulkStatusModalRef: ViewContainerRef;
|
||||
@ViewChild('bulkConfirmTemplate', { read: ViewContainerRef, static: true }) bulkConfirmModalRef: ViewContainerRef;
|
||||
@ViewChild('bulkRemoveTemplate', { read: ViewContainerRef, static: true }) bulkRemoveModalRef: ViewContainerRef;
|
||||
userType = ProviderUserType;
|
||||
userStatusType = ProviderUserStatusType;
|
||||
providerId: string;
|
||||
accessEvents = false;
|
||||
|
||||
userType = ProviderUserType;
|
||||
userStatusType = ProviderUserStatusType;
|
||||
providerId: string;
|
||||
accessEvents = false;
|
||||
constructor(
|
||||
apiService: ApiService,
|
||||
private route: ActivatedRoute,
|
||||
i18nService: I18nService,
|
||||
modalService: ModalService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
cryptoService: CryptoService,
|
||||
private router: Router,
|
||||
searchService: SearchService,
|
||||
validationService: ValidationService,
|
||||
logService: LogService,
|
||||
searchPipe: SearchPipe,
|
||||
userNamePipe: UserNamePipe,
|
||||
stateService: StateService,
|
||||
private providerService: ProviderService
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
searchService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
cryptoService,
|
||||
validationService,
|
||||
modalService,
|
||||
logService,
|
||||
searchPipe,
|
||||
userNamePipe,
|
||||
stateService
|
||||
);
|
||||
}
|
||||
|
||||
constructor(apiService: ApiService, private route: ActivatedRoute,
|
||||
i18nService: I18nService, modalService: ModalService,
|
||||
platformUtilsService: PlatformUtilsService, toasterService: ToasterService,
|
||||
cryptoService: CryptoService, private userService: UserService, private router: Router,
|
||||
storageService: StorageService, searchService: SearchService, validationService: ValidationService,
|
||||
logService: LogService, searchPipe: SearchPipe, userNamePipe: UserNamePipe) {
|
||||
super(apiService, searchService, i18nService, platformUtilsService, toasterService, cryptoService,
|
||||
storageService, validationService, modalService, logService, searchPipe, userNamePipe);
|
||||
}
|
||||
ngOnInit() {
|
||||
this.route.parent.params.subscribe(async (params) => {
|
||||
this.providerId = params.providerId;
|
||||
const provider = await this.providerService.get(this.providerId);
|
||||
|
||||
ngOnInit() {
|
||||
this.route.parent.params.subscribe(async params => {
|
||||
this.providerId = params.providerId;
|
||||
const provider = await this.userService.getProvider(this.providerId);
|
||||
if (!provider.canManageUsers) {
|
||||
this.router.navigate(["../"], { relativeTo: this.route });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!provider.canManageUsers) {
|
||||
this.router.navigate(['../'], { relativeTo: this.route });
|
||||
return;
|
||||
}
|
||||
this.accessEvents = provider.useEvents;
|
||||
|
||||
this.accessEvents = provider.useEvents;
|
||||
await this.load();
|
||||
|
||||
await this.load();
|
||||
|
||||
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
|
||||
this.searchText = qParams.search;
|
||||
if (qParams.viewEvents != null) {
|
||||
const user = this.users.filter(u => u.id === qParams.viewEvents);
|
||||
if (user.length > 0 && user[0].status === ProviderUserStatusType.Confirmed) {
|
||||
this.events(user[0]);
|
||||
}
|
||||
}
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getUsers(): Promise<ListResponse<ProviderUserUserDetailsResponse>> {
|
||||
return this.apiService.getProviderUsers(this.providerId);
|
||||
}
|
||||
|
||||
deleteUser(id: string): Promise<any> {
|
||||
return this.apiService.deleteProviderUser(this.providerId, id);
|
||||
}
|
||||
|
||||
reinviteUser(id: string): Promise<any> {
|
||||
return this.apiService.postProviderUserReinvite(this.providerId, id);
|
||||
}
|
||||
|
||||
async confirmUser(user: ProviderUserUserDetailsResponse, publicKey: Uint8Array): Promise<any> {
|
||||
const providerKey = await this.cryptoService.getProviderKey(this.providerId);
|
||||
const key = await this.cryptoService.rsaEncrypt(providerKey.key, publicKey.buffer);
|
||||
const request = new ProviderUserConfirmRequest();
|
||||
request.key = key.encryptedString;
|
||||
await this.apiService.postProviderUserConfirm(this.providerId, user.id, request);
|
||||
}
|
||||
|
||||
async edit(user: ProviderUserUserDetailsResponse) {
|
||||
const [modal] = await this.modalService.openViewRef(UserAddEditComponent, this.addEditModalRef, comp => {
|
||||
comp.name = this.userNamePipe.transform(user);
|
||||
comp.providerId = this.providerId;
|
||||
comp.providerUserId = user != null ? user.id : null;
|
||||
comp.onSavedUser.subscribe(() => {
|
||||
modal.close();
|
||||
this.load();
|
||||
});
|
||||
comp.onDeletedUser.subscribe(() => {
|
||||
modal.close();
|
||||
this.removeUser(user);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async events(user: ProviderUserUserDetailsResponse) {
|
||||
const [modal] = await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, comp => {
|
||||
comp.name = this.userNamePipe.transform(user);
|
||||
comp.providerId = this.providerId;
|
||||
comp.entityId = user.id;
|
||||
comp.showUser = false;
|
||||
comp.entity = 'user';
|
||||
});
|
||||
}
|
||||
|
||||
async bulkRemove() {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||
this.searchText = qParams.search;
|
||||
if (qParams.viewEvents != null) {
|
||||
const user = this.users.filter((u) => u.id === qParams.viewEvents);
|
||||
if (user.length > 0 && user[0].status === ProviderUserStatusType.Confirmed) {
|
||||
this.events(user[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const [modal] = await this.modalService.openViewRef(BulkRemoveComponent, this.bulkRemoveModalRef, comp => {
|
||||
comp.providerId = this.providerId;
|
||||
comp.users = this.getCheckedUsers();
|
||||
getUsers(): Promise<ListResponse<ProviderUserUserDetailsResponse>> {
|
||||
return this.apiService.getProviderUsers(this.providerId);
|
||||
}
|
||||
|
||||
deleteUser(id: string): Promise<any> {
|
||||
return this.apiService.deleteProviderUser(this.providerId, id);
|
||||
}
|
||||
|
||||
reinviteUser(id: string): Promise<any> {
|
||||
return this.apiService.postProviderUserReinvite(this.providerId, id);
|
||||
}
|
||||
|
||||
async confirmUser(user: ProviderUserUserDetailsResponse, publicKey: Uint8Array): Promise<any> {
|
||||
const providerKey = await this.cryptoService.getProviderKey(this.providerId);
|
||||
const key = await this.cryptoService.rsaEncrypt(providerKey.key, publicKey.buffer);
|
||||
const request = new ProviderUserConfirmRequest();
|
||||
request.key = key.encryptedString;
|
||||
await this.apiService.postProviderUserConfirm(this.providerId, user.id, request);
|
||||
}
|
||||
|
||||
async edit(user: ProviderUserUserDetailsResponse) {
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
UserAddEditComponent,
|
||||
this.addEditModalRef,
|
||||
(comp) => {
|
||||
comp.name = this.userNamePipe.transform(user);
|
||||
comp.providerId = this.providerId;
|
||||
comp.providerUserId = user != null ? user.id : null;
|
||||
comp.onSavedUser.subscribe(() => {
|
||||
modal.close();
|
||||
this.load();
|
||||
});
|
||||
comp.onDeletedUser.subscribe(() => {
|
||||
modal.close();
|
||||
this.removeUser(user);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
await modal.onClosedPromise();
|
||||
await this.load();
|
||||
async events(user: ProviderUserUserDetailsResponse) {
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
EntityEventsComponent,
|
||||
this.eventsModalRef,
|
||||
(comp) => {
|
||||
comp.name = this.userNamePipe.transform(user);
|
||||
comp.providerId = this.providerId;
|
||||
comp.entityId = user.id;
|
||||
comp.showUser = false;
|
||||
comp.entity = "user";
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async bulkRemove() {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
async bulkReinvite() {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
BulkRemoveComponent,
|
||||
this.bulkRemoveModalRef,
|
||||
(comp) => {
|
||||
comp.providerId = this.providerId;
|
||||
comp.users = this.getCheckedUsers();
|
||||
}
|
||||
);
|
||||
|
||||
const users = this.getCheckedUsers();
|
||||
const filteredUsers = users.filter(u => u.status === ProviderUserStatusType.Invited);
|
||||
await modal.onClosedPromise();
|
||||
await this.load();
|
||||
}
|
||||
|
||||
if (filteredUsers.length <= 0) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('noSelectedUsersApplicable'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const request = new ProviderUserBulkRequest(filteredUsers.map(user => user.id));
|
||||
const response = this.apiService.postManyProviderUserReinvite(this.providerId, request);
|
||||
this.showBulkStatus(users, filteredUsers, response, this.i18nService.t('bulkReinviteMessage'));
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = null;
|
||||
async bulkReinvite() {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
async bulkConfirm() {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
const users = this.getCheckedUsers();
|
||||
const filteredUsers = users.filter((u) => u.status === ProviderUserStatusType.Invited);
|
||||
|
||||
const [modal] = await this.modalService.openViewRef(BulkConfirmComponent, this.bulkConfirmModalRef, comp => {
|
||||
comp.providerId = this.providerId;
|
||||
comp.users = this.getCheckedUsers();
|
||||
});
|
||||
|
||||
await modal.onClosedPromise();
|
||||
await this.load();
|
||||
if (filteredUsers.length <= 0) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("noSelectedUsersApplicable")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
private async showBulkStatus(users: ProviderUserUserDetailsResponse[], filteredUsers: ProviderUserUserDetailsResponse[],
|
||||
request: Promise<ListResponse<ProviderUserBulkResponse>>, successfullMessage: string) {
|
||||
|
||||
const [modal, childComponent] = await this.modalService.openViewRef(BulkStatusComponent, this.bulkStatusModalRef, comp => {
|
||||
comp.loading = true;
|
||||
});
|
||||
|
||||
// Workaround to handle closing the modal shortly after it has been opened
|
||||
let close = false;
|
||||
modal.onShown.subscribe(() => {
|
||||
if (close) {
|
||||
modal.close();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await request;
|
||||
|
||||
if (modal) {
|
||||
const keyedErrors: any = response.data.filter(r => r.error !== '').reduce((a, x) => ({ ...a, [x.id]: x.error }), {});
|
||||
const keyedFilteredUsers: any = filteredUsers.reduce((a, x) => ({ ...a, [x.id]: x }), {});
|
||||
|
||||
childComponent.users = users.map(user => {
|
||||
let message = keyedErrors[user.id] ?? successfullMessage;
|
||||
if (!keyedFilteredUsers.hasOwnProperty(user.id)) {
|
||||
message = this.i18nService.t('bulkFilteredMessage');
|
||||
}
|
||||
|
||||
return {
|
||||
user: user,
|
||||
error: keyedErrors.hasOwnProperty(user.id),
|
||||
message: message,
|
||||
};
|
||||
});
|
||||
childComponent.loading = false;
|
||||
}
|
||||
} catch {
|
||||
close = true;
|
||||
modal.close();
|
||||
}
|
||||
try {
|
||||
const request = new ProviderUserBulkRequest(filteredUsers.map((user) => user.id));
|
||||
const response = this.apiService.postManyProviderUserReinvite(this.providerId, request);
|
||||
this.showBulkStatus(
|
||||
users,
|
||||
filteredUsers,
|
||||
response,
|
||||
this.i18nService.t("bulkReinviteMessage")
|
||||
);
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = null;
|
||||
}
|
||||
|
||||
async bulkConfirm() {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
BulkConfirmComponent,
|
||||
this.bulkConfirmModalRef,
|
||||
(comp) => {
|
||||
comp.providerId = this.providerId;
|
||||
comp.users = this.getCheckedUsers();
|
||||
}
|
||||
);
|
||||
|
||||
await modal.onClosedPromise();
|
||||
await this.load();
|
||||
}
|
||||
|
||||
private async showBulkStatus(
|
||||
users: ProviderUserUserDetailsResponse[],
|
||||
filteredUsers: ProviderUserUserDetailsResponse[],
|
||||
request: Promise<ListResponse<ProviderUserBulkResponse>>,
|
||||
successfullMessage: string
|
||||
) {
|
||||
const [modal, childComponent] = await this.modalService.openViewRef(
|
||||
BulkStatusComponent,
|
||||
this.bulkStatusModalRef,
|
||||
(comp) => {
|
||||
comp.loading = true;
|
||||
}
|
||||
);
|
||||
|
||||
// Workaround to handle closing the modal shortly after it has been opened
|
||||
let close = false;
|
||||
modal.onShown.subscribe(() => {
|
||||
if (close) {
|
||||
modal.close();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await request;
|
||||
|
||||
if (modal) {
|
||||
const keyedErrors: any = response.data
|
||||
.filter((r) => r.error !== "")
|
||||
.reduce((a, x) => ({ ...a, [x.id]: x.error }), {});
|
||||
const keyedFilteredUsers: any = filteredUsers.reduce((a, x) => ({ ...a, [x.id]: x }), {});
|
||||
|
||||
childComponent.users = users.map((user) => {
|
||||
let message = keyedErrors[user.id] ?? successfullMessage;
|
||||
if (!keyedFilteredUsers.hasOwnProperty(user.id)) {
|
||||
message = this.i18nService.t("bulkFilteredMessage");
|
||||
}
|
||||
|
||||
return {
|
||||
user: user,
|
||||
error: keyedErrors.hasOwnProperty(user.id),
|
||||
message: message,
|
||||
};
|
||||
});
|
||||
childComponent.loading = false;
|
||||
}
|
||||
} catch {
|
||||
close = true;
|
||||
modal.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +1,124 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="userAddEditTitle">
|
||||
{{title}}
|
||||
<small class="text-muted" *ngIf="name">{{name}}</small>
|
||||
</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="!loading">
|
||||
<ng-container *ngIf="!editMode">
|
||||
<p>{{'providerInviteUserDesc' | i18n}}</p>
|
||||
<div class="form-group mb-4">
|
||||
<label for="emails">{{'email' | i18n}}</label>
|
||||
<input id="emails" class="form-control" type="text" name="Emails" [(ngModel)]="emails" required
|
||||
appAutoFocus>
|
||||
<small class="text-muted">{{'inviteMultipleEmailDesc' | i18n : '20'}}</small>
|
||||
</div>
|
||||
</ng-container>
|
||||
<h3>
|
||||
{{'userType' | i18n}}
|
||||
<a target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}"
|
||||
href="https://bitwarden.com/help/article/user-types-access-control/#user-types">
|
||||
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
|
||||
</a>
|
||||
</h3>
|
||||
<div class="form-check mt-2 form-check-block">
|
||||
<input class="form-check-input" type="radio" name="userType" id="userTypeServiceUser"
|
||||
[value]="userType.ServiceUser" [(ngModel)]="type">
|
||||
<label class="form-check-label" for="userTypeServiceUser">
|
||||
{{'serviceUser' | i18n}}
|
||||
<small>{{'serviceUserDesc' | i18n}}</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mt-2 form-check-block">
|
||||
<input class="form-check-input" type="radio" name="userType" id="userTypeProviderAdmin"
|
||||
[value]="userType.ProviderAdmin" [(ngModel)]="type">
|
||||
<label class="form-check-label" for="userTypeProviderAdmin">
|
||||
{{'providerAdmin' | i18n}}
|
||||
<small>{{'providerAdminDesc' | i18n}}</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'save' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{'cancel' | i18n}}
|
||||
</button>
|
||||
<div class="ml-auto">
|
||||
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
|
||||
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"
|
||||
[appApiAction]="deletePromise">
|
||||
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading" aria-hidden="true"></i>
|
||||
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"
|
||||
title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
||||
<form
|
||||
class="modal-content"
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="userAddEditTitle">
|
||||
{{ title }}
|
||||
<small class="text-muted" *ngIf="name">{{ name }}</small>
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="!loading">
|
||||
<ng-container *ngIf="!editMode">
|
||||
<p>{{ "providerInviteUserDesc" | i18n }}</p>
|
||||
<div class="form-group mb-4">
|
||||
<label for="emails">{{ "email" | i18n }}</label>
|
||||
<input
|
||||
id="emails"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Emails"
|
||||
[(ngModel)]="emails"
|
||||
required
|
||||
appAutoFocus
|
||||
/>
|
||||
<small class="text-muted">{{ "inviteMultipleEmailDesc" | i18n: "20" }}</small>
|
||||
</div>
|
||||
</ng-container>
|
||||
<h3>
|
||||
{{ "userType" | i18n }}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
href="https://bitwarden.com/help/provider-users/"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</h3>
|
||||
<div class="form-check mt-2 form-check-block">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="userType"
|
||||
id="userTypeServiceUser"
|
||||
[value]="userType.ServiceUser"
|
||||
[(ngModel)]="type"
|
||||
/>
|
||||
<label class="form-check-label" for="userTypeServiceUser">
|
||||
{{ "serviceUser" | i18n }}
|
||||
<small>{{ "serviceUserDesc" | i18n }}</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mt-2 form-check-block">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="userType"
|
||||
id="userTypeProviderAdmin"
|
||||
[value]="userType.ProviderAdmin"
|
||||
[(ngModel)]="type"
|
||||
/>
|
||||
<label class="form-check-label" for="userTypeProviderAdmin">
|
||||
{{ "providerAdmin" | i18n }}
|
||||
<small>{{ "providerAdminDesc" | i18n }}</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "save" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
<div class="ml-auto">
|
||||
<button
|
||||
#deleteBtn
|
||||
type="button"
|
||||
(click)="delete()"
|
||||
class="btn btn-outline-danger"
|
||||
appA11yTitle="{{ 'delete' | i18n }}"
|
||||
*ngIf="editMode"
|
||||
[disabled]="deleteBtn.loading"
|
||||
[appApiAction]="deletePromise"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-trash bwi-lg bwi-fw"
|
||||
[hidden]="deleteBtn.loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!deleteBtn.loading"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,104 +1,121 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { LogService } from "jslib-common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
|
||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||
import { ProviderUserInviteRequest } from "jslib-common/models/request/provider/providerUserInviteRequest";
|
||||
|
||||
import { ProviderUserInviteRequest } from 'jslib-common/models/request/provider/providerUserInviteRequest';
|
||||
import { PermissionsApi } from "jslib-common/models/api/permissionsApi";
|
||||
|
||||
import { PermissionsApi } from 'jslib-common/models/api/permissionsApi';
|
||||
|
||||
import { ProviderUserType } from 'jslib-common/enums/providerUserType';
|
||||
import { ProviderUserUpdateRequest } from 'jslib-common/models/request/provider/providerUserUpdateRequest';
|
||||
import { ProviderUserType } from "jslib-common/enums/providerUserType";
|
||||
import { ProviderUserUpdateRequest } from "jslib-common/models/request/provider/providerUserUpdateRequest";
|
||||
|
||||
@Component({
|
||||
selector: 'provider-user-add-edit',
|
||||
templateUrl: 'user-add-edit.component.html',
|
||||
selector: "provider-user-add-edit",
|
||||
templateUrl: "user-add-edit.component.html",
|
||||
})
|
||||
export class UserAddEditComponent implements OnInit {
|
||||
@Input() name: string;
|
||||
@Input() providerUserId: string;
|
||||
@Input() providerId: string;
|
||||
@Output() onSavedUser = new EventEmitter();
|
||||
@Output() onDeletedUser = new EventEmitter();
|
||||
@Input() name: string;
|
||||
@Input() providerUserId: string;
|
||||
@Input() providerId: string;
|
||||
@Output() onSavedUser = new EventEmitter();
|
||||
@Output() onDeletedUser = new EventEmitter();
|
||||
|
||||
loading = true;
|
||||
editMode: boolean = false;
|
||||
title: string;
|
||||
emails: string;
|
||||
type: ProviderUserType = ProviderUserType.ServiceUser;
|
||||
permissions = new PermissionsApi();
|
||||
showCustom = false;
|
||||
access: 'all' | 'selected' = 'selected';
|
||||
formPromise: Promise<any>;
|
||||
deletePromise: Promise<any>;
|
||||
userType = ProviderUserType;
|
||||
loading = true;
|
||||
editMode: boolean = false;
|
||||
title: string;
|
||||
emails: string;
|
||||
type: ProviderUserType = ProviderUserType.ServiceUser;
|
||||
permissions = new PermissionsApi();
|
||||
showCustom = false;
|
||||
access: "all" | "selected" = "selected";
|
||||
formPromise: Promise<any>;
|
||||
deletePromise: Promise<any>;
|
||||
userType = ProviderUserType;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private toasterService: ToasterService, private platformUtilsService: PlatformUtilsService) { }
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.editMode = this.loading = this.providerUserId != null;
|
||||
async ngOnInit() {
|
||||
this.editMode = this.loading = this.providerUserId != null;
|
||||
|
||||
if (this.editMode) {
|
||||
this.editMode = true;
|
||||
this.title = this.i18nService.t('editUser');
|
||||
try {
|
||||
const user = await this.apiService.getProviderUser(this.providerId, this.providerUserId);
|
||||
this.type = user.type;
|
||||
} catch { }
|
||||
} else {
|
||||
this.title = this.i18nService.t('inviteUser');
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
if (this.editMode) {
|
||||
this.editMode = true;
|
||||
this.title = this.i18nService.t("editUser");
|
||||
try {
|
||||
const user = await this.apiService.getProviderUser(this.providerId, this.providerUserId);
|
||||
this.type = user.type;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
} else {
|
||||
this.title = this.i18nService.t("inviteUser");
|
||||
}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
if (this.editMode) {
|
||||
const request = new ProviderUserUpdateRequest();
|
||||
request.type = this.type;
|
||||
this.formPromise = this.apiService.putProviderUser(this.providerId, this.providerUserId, request);
|
||||
} else {
|
||||
const request = new ProviderUserInviteRequest();
|
||||
request.emails = this.emails.trim().split(/\s*,\s*/);
|
||||
request.type = this.type;
|
||||
this.formPromise = this.apiService.postProviderUserInvite(this.providerId, request);
|
||||
}
|
||||
await this.formPromise;
|
||||
this.toasterService.popAsync('success', null,
|
||||
this.i18nService.t(this.editMode ? 'editedUserId' : 'invitedUsers', this.name));
|
||||
this.onSavedUser.emit();
|
||||
} catch { }
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
if (this.editMode) {
|
||||
const request = new ProviderUserUpdateRequest();
|
||||
request.type = this.type;
|
||||
this.formPromise = this.apiService.putProviderUser(
|
||||
this.providerId,
|
||||
this.providerUserId,
|
||||
request
|
||||
);
|
||||
} else {
|
||||
const request = new ProviderUserInviteRequest();
|
||||
request.emails = this.emails.trim().split(/\s*,\s*/);
|
||||
request.type = this.type;
|
||||
this.formPromise = this.apiService.postProviderUserInvite(this.providerId, request);
|
||||
}
|
||||
await this.formPromise;
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.name)
|
||||
);
|
||||
this.onSavedUser.emit();
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async delete() {
|
||||
if (!this.editMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
async delete() {
|
||||
if (!this.editMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('removeUserConfirmation'), this.name,
|
||||
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this.deletePromise = this.apiService.deleteProviderUser(this.providerId, this.providerUserId);
|
||||
await this.deletePromise;
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('removedUserId', this.name));
|
||||
this.onDeletedUser.emit();
|
||||
} catch { }
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t("removeUserConfirmation"),
|
||||
this.name,
|
||||
this.i18nService.t("yes"),
|
||||
this.i18nService.t("no"),
|
||||
"warning"
|
||||
);
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this.deletePromise = this.apiService.deleteProviderUser(this.providerId, this.providerUserId);
|
||||
await this.deletePromise;
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("removedUserId", this.name)
|
||||
);
|
||||
this.onDeletedUser.emit();
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
<app-navbar></app-navbar>
|
||||
<div class="org-nav" *ngIf="provider">
|
||||
<div class="container d-flex">
|
||||
<div class="d-flex flex-column">
|
||||
<div class="my-auto d-flex align-items-center pl-1">
|
||||
<app-avatar [data]="provider.name" size="45" [circle]="true"></app-avatar>
|
||||
<div class="org-name ml-3">
|
||||
<span>{{provider.name}}</span>
|
||||
<small class="text-muted">{{'provider' | i18n}}</small>
|
||||
</div>
|
||||
<div class="ml-3 card border-danger text-danger bg-transparent" *ngIf="!provider.enabled">
|
||||
<div class="card-body py-2">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
{{'providerIsDisabled' | i18n}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="nav nav-tabs" *ngIf="showMenuBar">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="clients" routerLinkActive="active">
|
||||
<i class="fa fa-university" aria-hidden="true"></i>
|
||||
{{'clients' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="showManageTab">
|
||||
<a class="nav-link" [routerLink]="manageRoute" routerLinkActive="active">
|
||||
<i class="fa fa-sliders" aria-hidden="true"></i>
|
||||
{{'manage' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="showSettingsTab">
|
||||
<a class="nav-link" routerLink="settings" routerLinkActive="active">
|
||||
<i class="fa fa-cogs" aria-hidden="true"></i>
|
||||
{{'settings' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="container d-flex">
|
||||
<div class="d-flex flex-column">
|
||||
<div class="my-auto d-flex align-items-center pl-1">
|
||||
<app-avatar [data]="provider.name" size="45" [circle]="true"></app-avatar>
|
||||
<div class="org-name ml-3">
|
||||
<span>{{ provider.name }}</span>
|
||||
<small class="text-muted">{{ "provider" | i18n }}</small>
|
||||
</div>
|
||||
<div class="ml-3 card border-danger text-danger bg-transparent" *ngIf="!provider.enabled">
|
||||
<div class="card-body py-2">
|
||||
<i class="bwi bwi-exclamation-triangle" aria-hidden="true"></i>
|
||||
{{ "providerIsDisabled" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="nav nav-tabs" *ngIf="showMenuBar">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="clients" routerLinkActive="active">
|
||||
<i class="bwi bwi-bank" aria-hidden="true"></i>
|
||||
{{ "clients" | i18n }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="showManageTab">
|
||||
<a class="nav-link" [routerLink]="manageRoute" routerLinkActive="active">
|
||||
<i class="bwi bwi-sliders" aria-hidden="true"></i>
|
||||
{{ "manage" | i18n }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="showSettingsTab">
|
||||
<a class="nav-link" routerLink="settings" routerLinkActive="active">
|
||||
<i class="bwi bwi-cogs" aria-hidden="true"></i>
|
||||
{{ "settings" | i18n }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container page-content">
|
||||
<router-outlet></router-outlet>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
<app-footer></app-footer>
|
||||
|
||||
@@ -1,51 +1,50 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Component } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
|
||||
import { UserService } from 'jslib-common/abstractions/user.service';
|
||||
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
||||
|
||||
import { Provider } from 'jslib-common/models/domain/provider';
|
||||
import { Provider } from "jslib-common/models/domain/provider";
|
||||
|
||||
@Component({
|
||||
selector: 'providers-layout',
|
||||
templateUrl: 'providers-layout.component.html',
|
||||
selector: "providers-layout",
|
||||
templateUrl: "providers-layout.component.html",
|
||||
})
|
||||
export class ProvidersLayoutComponent {
|
||||
provider: Provider;
|
||||
private providerId: string;
|
||||
|
||||
provider: Provider;
|
||||
private providerId: string;
|
||||
constructor(private route: ActivatedRoute, private providerService: ProviderService) {}
|
||||
|
||||
constructor(private route: ActivatedRoute, private userService: UserService) { }
|
||||
ngOnInit() {
|
||||
document.body.classList.remove("layout_frontend");
|
||||
this.route.params.subscribe(async (params) => {
|
||||
this.providerId = params.providerId;
|
||||
await this.load();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
document.body.classList.remove('layout_frontend');
|
||||
this.route.params.subscribe(async params => {
|
||||
this.providerId = params.providerId;
|
||||
await this.load();
|
||||
});
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.provider = await this.userService.getProvider(this.providerId);
|
||||
}
|
||||
|
||||
get showMenuBar() {
|
||||
return this.showManageTab || this.showSettingsTab;
|
||||
}
|
||||
|
||||
get showManageTab() {
|
||||
return this.provider.canManageUsers || this.provider.canAccessEventLogs;
|
||||
}
|
||||
|
||||
get showSettingsTab() {
|
||||
return this.provider.isProviderAdmin;
|
||||
}
|
||||
|
||||
get manageRoute(): string {
|
||||
switch (true) {
|
||||
case this.provider.canManageUsers:
|
||||
return 'manage/people';
|
||||
case this.provider.canAccessEventLogs:
|
||||
return 'manage/events';
|
||||
}
|
||||
async load() {
|
||||
this.provider = await this.providerService.get(this.providerId);
|
||||
}
|
||||
|
||||
get showMenuBar() {
|
||||
return this.showManageTab || this.showSettingsTab;
|
||||
}
|
||||
|
||||
get showManageTab() {
|
||||
return this.provider.canManageUsers || this.provider.canAccessEventLogs;
|
||||
}
|
||||
|
||||
get showSettingsTab() {
|
||||
return this.provider.isProviderAdmin;
|
||||
}
|
||||
|
||||
get manageRoute(): string {
|
||||
switch (true) {
|
||||
case this.provider.canManageUsers:
|
||||
return "manage/people";
|
||||
case this.provider.canAccessEventLogs:
|
||||
return "manage/events";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,123 +1,123 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { AuthGuardService } from 'jslib-angular/services/auth-guard.service';
|
||||
import { Permissions } from 'jslib-common/enums/permissions';
|
||||
import { AuthGuardService } from "jslib-angular/services/auth-guard.service";
|
||||
import { Permissions } from "jslib-common/enums/permissions";
|
||||
|
||||
import { AddOrganizationComponent } from './clients/add-organization.component';
|
||||
import { ClientsComponent } from './clients/clients.component';
|
||||
import { CreateOrganizationComponent } from './clients/create-organization.component';
|
||||
import { AcceptProviderComponent } from './manage/accept-provider.component';
|
||||
import { EventsComponent } from './manage/events.component';
|
||||
import { ManageComponent } from './manage/manage.component';
|
||||
import { PeopleComponent } from './manage/people.component';
|
||||
import { ProvidersLayoutComponent } from './providers-layout.component';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
import { SetupProviderComponent } from './setup/setup-provider.component';
|
||||
import { SetupComponent } from './setup/setup.component';
|
||||
import { AddOrganizationComponent } from "./clients/add-organization.component";
|
||||
import { ClientsComponent } from "./clients/clients.component";
|
||||
import { CreateOrganizationComponent } from "./clients/create-organization.component";
|
||||
import { AcceptProviderComponent } from "./manage/accept-provider.component";
|
||||
import { EventsComponent } from "./manage/events.component";
|
||||
import { ManageComponent } from "./manage/manage.component";
|
||||
import { PeopleComponent } from "./manage/people.component";
|
||||
import { ProvidersLayoutComponent } from "./providers-layout.component";
|
||||
import { SettingsComponent } from "./settings/settings.component";
|
||||
import { SetupProviderComponent } from "./setup/setup-provider.component";
|
||||
import { SetupComponent } from "./setup/setup.component";
|
||||
|
||||
import { FrontendLayoutComponent } from 'src/app/layouts/frontend-layout.component';
|
||||
import { FrontendLayoutComponent } from "src/app/layouts/frontend-layout.component";
|
||||
|
||||
import { ProvidersComponent } from 'src/app/providers/providers.component';
|
||||
import { ProviderGuardService } from './services/provider-guard.service';
|
||||
import { ProviderTypeGuardService } from './services/provider-type-guard.service';
|
||||
import { AccountComponent } from './settings/account.component';
|
||||
import { ProvidersComponent } from "src/app/providers/providers.component";
|
||||
import { ProviderGuardService } from "./services/provider-guard.service";
|
||||
import { ProviderTypeGuardService } from "./services/provider-type-guard.service";
|
||||
import { AccountComponent } from "./settings/account.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
canActivate: [AuthGuardService],
|
||||
component: ProvidersComponent,
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: FrontendLayoutComponent,
|
||||
{
|
||||
path: "",
|
||||
canActivate: [AuthGuardService],
|
||||
component: ProvidersComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: FrontendLayoutComponent,
|
||||
children: [
|
||||
{
|
||||
path: "setup-provider",
|
||||
component: SetupProviderComponent,
|
||||
data: { titleId: "setupProvider" },
|
||||
},
|
||||
{
|
||||
path: "accept-provider",
|
||||
component: AcceptProviderComponent,
|
||||
data: { titleId: "acceptProvider" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
canActivate: [AuthGuardService],
|
||||
children: [
|
||||
{
|
||||
path: "setup",
|
||||
component: SetupComponent,
|
||||
},
|
||||
{
|
||||
path: ":providerId",
|
||||
component: ProvidersLayoutComponent,
|
||||
canActivate: [ProviderGuardService],
|
||||
children: [
|
||||
{
|
||||
path: 'setup-provider',
|
||||
component: SetupProviderComponent,
|
||||
data: { titleId: 'setupProvider' },
|
||||
},
|
||||
{
|
||||
path: 'accept-provider',
|
||||
component: AcceptProviderComponent,
|
||||
data: { titleId: 'acceptProvider' },
|
||||
},
|
||||
{ path: "", pathMatch: "full", redirectTo: "clients" },
|
||||
{ path: "clients/create", component: CreateOrganizationComponent },
|
||||
{ path: "clients", component: ClientsComponent, data: { titleId: "clients" } },
|
||||
{
|
||||
path: "manage",
|
||||
component: ManageComponent,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
pathMatch: "full",
|
||||
redirectTo: "people",
|
||||
},
|
||||
{
|
||||
path: "people",
|
||||
component: PeopleComponent,
|
||||
canActivate: [ProviderTypeGuardService],
|
||||
data: {
|
||||
titleId: "people",
|
||||
permissions: [Permissions.ManageUsers],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "events",
|
||||
component: EventsComponent,
|
||||
canActivate: [ProviderTypeGuardService],
|
||||
data: {
|
||||
titleId: "eventLogs",
|
||||
permissions: [Permissions.AccessEventLogs],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "settings",
|
||||
component: SettingsComponent,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
pathMatch: "full",
|
||||
redirectTo: "account",
|
||||
},
|
||||
{
|
||||
path: "account",
|
||||
component: AccountComponent,
|
||||
canActivate: [ProviderTypeGuardService],
|
||||
data: {
|
||||
titleId: "myProvider",
|
||||
permissions: [Permissions.ManageProvider],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
canActivate: [AuthGuardService],
|
||||
children: [
|
||||
{
|
||||
path: 'setup',
|
||||
component: SetupComponent,
|
||||
},
|
||||
{
|
||||
path: ':providerId',
|
||||
component: ProvidersLayoutComponent,
|
||||
canActivate: [ProviderGuardService],
|
||||
children: [
|
||||
{ path: '', pathMatch: 'full', redirectTo: 'clients' },
|
||||
{ path: 'clients/create', component: CreateOrganizationComponent },
|
||||
{ path: 'clients', component: ClientsComponent, data: { titleId: 'clients' } },
|
||||
{
|
||||
path: 'manage',
|
||||
component: ManageComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
redirectTo: 'people',
|
||||
},
|
||||
{
|
||||
path: 'people',
|
||||
component: PeopleComponent,
|
||||
canActivate: [ProviderTypeGuardService],
|
||||
data: {
|
||||
titleId: 'people',
|
||||
permissions: [Permissions.ManageUsers],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'events',
|
||||
component: EventsComponent,
|
||||
canActivate: [ProviderTypeGuardService],
|
||||
data: {
|
||||
titleId: 'eventLogs',
|
||||
permissions: [Permissions.AccessEventLogs],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
component: SettingsComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
redirectTo: 'account',
|
||||
},
|
||||
{
|
||||
path: 'account',
|
||||
component: AccountComponent,
|
||||
canActivate: [ProviderTypeGuardService],
|
||||
data: {
|
||||
titleId: 'myProvider',
|
||||
permissions: [Permissions.ManageProvider],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class ProvidersRoutingModule { }
|
||||
export class ProvidersRoutingModule {}
|
||||
|
||||
@@ -1,69 +1,63 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ComponentFactoryResolver } from '@angular/core';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ComponentFactoryResolver } from "@angular/core";
|
||||
import { NgModule } from "@angular/core";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
|
||||
import { ModalService } from 'jslib-angular/services/modal.service';
|
||||
import { ModalService } from "jslib-angular/services/modal.service";
|
||||
|
||||
import { ProviderGuardService } from './services/provider-guard.service';
|
||||
import { ProviderTypeGuardService } from './services/provider-type-guard.service';
|
||||
import { ProviderService } from './services/provider.service';
|
||||
import { ProviderGuardService } from "./services/provider-guard.service";
|
||||
import { ProviderTypeGuardService } from "./services/provider-type-guard.service";
|
||||
import { WebProviderService } from "./services/webProvider.service";
|
||||
|
||||
import { ProvidersLayoutComponent } from './providers-layout.component';
|
||||
import { ProvidersRoutingModule } from './providers-routing.module';
|
||||
import { ProvidersLayoutComponent } from "./providers-layout.component";
|
||||
import { ProvidersRoutingModule } from "./providers-routing.module";
|
||||
|
||||
import { AddOrganizationComponent } from './clients/add-organization.component';
|
||||
import { ClientsComponent } from './clients/clients.component';
|
||||
import { CreateOrganizationComponent } from './clients/create-organization.component';
|
||||
import { AddOrganizationComponent } from "./clients/add-organization.component";
|
||||
import { ClientsComponent } from "./clients/clients.component";
|
||||
import { CreateOrganizationComponent } from "./clients/create-organization.component";
|
||||
|
||||
import { AcceptProviderComponent } from './manage/accept-provider.component';
|
||||
import { BulkConfirmComponent } from './manage/bulk/bulk-confirm.component';
|
||||
import { BulkRemoveComponent } from './manage/bulk/bulk-remove.component';
|
||||
import { EventsComponent } from './manage/events.component';
|
||||
import { ManageComponent } from './manage/manage.component';
|
||||
import { PeopleComponent } from './manage/people.component';
|
||||
import { UserAddEditComponent } from './manage/user-add-edit.component';
|
||||
import { AcceptProviderComponent } from "./manage/accept-provider.component";
|
||||
import { BulkConfirmComponent } from "./manage/bulk/bulk-confirm.component";
|
||||
import { BulkRemoveComponent } from "./manage/bulk/bulk-remove.component";
|
||||
import { EventsComponent } from "./manage/events.component";
|
||||
import { ManageComponent } from "./manage/manage.component";
|
||||
import { PeopleComponent } from "./manage/people.component";
|
||||
import { UserAddEditComponent } from "./manage/user-add-edit.component";
|
||||
|
||||
import { AccountComponent } from './settings/account.component';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
import { AccountComponent } from "./settings/account.component";
|
||||
import { SettingsComponent } from "./settings/settings.component";
|
||||
|
||||
import { SetupProviderComponent } from './setup/setup-provider.component';
|
||||
import { SetupComponent } from './setup/setup.component';
|
||||
import { SetupProviderComponent } from "./setup/setup-provider.component";
|
||||
import { SetupComponent } from "./setup/setup.component";
|
||||
|
||||
import { OssModule } from 'src/app/oss.module';
|
||||
import { OssModule } from "src/app/oss.module";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
OssModule,
|
||||
ProvidersRoutingModule,
|
||||
],
|
||||
declarations: [
|
||||
AcceptProviderComponent,
|
||||
AccountComponent,
|
||||
AddOrganizationComponent,
|
||||
BulkConfirmComponent,
|
||||
BulkRemoveComponent,
|
||||
ClientsComponent,
|
||||
CreateOrganizationComponent,
|
||||
EventsComponent,
|
||||
ManageComponent,
|
||||
PeopleComponent,
|
||||
ProvidersLayoutComponent,
|
||||
SettingsComponent,
|
||||
SetupComponent,
|
||||
SetupProviderComponent,
|
||||
UserAddEditComponent,
|
||||
],
|
||||
providers: [
|
||||
ProviderService,
|
||||
ProviderGuardService,
|
||||
ProviderTypeGuardService,
|
||||
],
|
||||
imports: [CommonModule, FormsModule, OssModule, ProvidersRoutingModule],
|
||||
declarations: [
|
||||
AcceptProviderComponent,
|
||||
AccountComponent,
|
||||
AddOrganizationComponent,
|
||||
BulkConfirmComponent,
|
||||
BulkRemoveComponent,
|
||||
ClientsComponent,
|
||||
CreateOrganizationComponent,
|
||||
EventsComponent,
|
||||
ManageComponent,
|
||||
PeopleComponent,
|
||||
ProvidersLayoutComponent,
|
||||
SettingsComponent,
|
||||
SetupComponent,
|
||||
SetupProviderComponent,
|
||||
UserAddEditComponent,
|
||||
],
|
||||
providers: [WebProviderService, ProviderGuardService, ProviderTypeGuardService],
|
||||
})
|
||||
export class ProvidersModule {
|
||||
constructor(modalService: ModalService, componentFactoryResolver: ComponentFactoryResolver) {
|
||||
modalService.registerComponentFactoryResolver(AddOrganizationComponent, componentFactoryResolver);
|
||||
}
|
||||
constructor(modalService: ModalService, componentFactoryResolver: ComponentFactoryResolver) {
|
||||
modalService.registerComponentFactoryResolver(
|
||||
AddOrganizationComponent,
|
||||
componentFactoryResolver
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
CanActivate,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
import { Injectable } from "@angular/core";
|
||||
import { ActivatedRouteSnapshot, CanActivate, Router } from "@angular/router";
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { UserService } from 'jslib-common/abstractions/user.service';
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
||||
|
||||
@Injectable()
|
||||
export class ProviderGuardService implements CanActivate {
|
||||
constructor(private userService: UserService, private router: Router,
|
||||
private toasterService: ToasterService, private i18nService: I18nService) { }
|
||||
constructor(
|
||||
private router: Router,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private providerService: ProviderService
|
||||
) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot) {
|
||||
const provider = await this.userService.getProvider(route.params.providerId);
|
||||
if (provider == null) {
|
||||
this.router.navigate(['/']);
|
||||
return false;
|
||||
}
|
||||
if (!provider.isProviderAdmin && !provider.enabled) {
|
||||
this.toasterService.popAsync('error', null, this.i18nService.t('providerIsDisabled'));
|
||||
this.router.navigate(['/']);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
async canActivate(route: ActivatedRouteSnapshot) {
|
||||
const provider = await this.providerService.get(route.params.providerId);
|
||||
if (provider == null) {
|
||||
this.router.navigate(["/"]);
|
||||
return false;
|
||||
}
|
||||
if (!provider.isProviderAdmin && !provider.enabled) {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("providerIsDisabled"));
|
||||
this.router.navigate(["/"]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,27 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
CanActivate,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
import { Injectable } from "@angular/core";
|
||||
import { ActivatedRouteSnapshot, CanActivate, Router } from "@angular/router";
|
||||
|
||||
import { UserService } from 'jslib-common/abstractions/user.service';
|
||||
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
||||
|
||||
import { Permissions } from 'jslib-common/enums/permissions';
|
||||
import { Permissions } from "jslib-common/enums/permissions";
|
||||
|
||||
@Injectable()
|
||||
export class ProviderTypeGuardService implements CanActivate {
|
||||
constructor(private userService: UserService, private router: Router) { }
|
||||
constructor(private providerService: ProviderService, private router: Router) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot) {
|
||||
const provider = await this.userService.getProvider(route.params.providerId);
|
||||
const permissions = route.data == null ? null : route.data.permissions as Permissions[];
|
||||
async canActivate(route: ActivatedRouteSnapshot) {
|
||||
const provider = await this.providerService.get(route.params.providerId);
|
||||
const permissions = route.data == null ? null : (route.data.permissions as Permissions[]);
|
||||
|
||||
if (
|
||||
(permissions.indexOf(Permissions.AccessEventLogs) !== -1 && provider.canAccessEventLogs) ||
|
||||
(permissions.indexOf(Permissions.ManageProvider) !== -1 && provider.isProviderAdmin) ||
|
||||
(permissions.indexOf(Permissions.ManageUsers) !== -1 && provider.canManageUsers)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.router.navigate(['/providers', provider.id]);
|
||||
return false;
|
||||
if (
|
||||
(permissions.indexOf(Permissions.AccessEventLogs) !== -1 && provider.canAccessEventLogs) ||
|
||||
(permissions.indexOf(Permissions.ManageProvider) !== -1 && provider.isProviderAdmin) ||
|
||||
(permissions.indexOf(Permissions.ManageUsers) !== -1 && provider.canManageUsers)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.router.navigate(["/providers", provider.id]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
|
||||
import { SyncService } from 'jslib-common/abstractions/sync.service';
|
||||
|
||||
import { ProviderAddOrganizationRequest } from 'jslib-common/models/request/provider/providerAddOrganizationRequest';
|
||||
|
||||
@Injectable()
|
||||
export class ProviderService {
|
||||
constructor(private cryptoService: CryptoService, private syncService: SyncService, private apiService: ApiService) {}
|
||||
|
||||
async addOrganizationToProvider(providerId: string, organizationId: string) {
|
||||
const orgKey = await this.cryptoService.getOrgKey(organizationId);
|
||||
const providerKey = await this.cryptoService.getProviderKey(providerId);
|
||||
|
||||
const encryptedOrgKey = await this.cryptoService.encrypt(orgKey.key, providerKey);
|
||||
|
||||
const request = new ProviderAddOrganizationRequest();
|
||||
request.organizationId = organizationId;
|
||||
request.key = encryptedOrgKey.encryptedString;
|
||||
|
||||
const response = await this.apiService.postProviderAddOrganization(providerId, request);
|
||||
await this.syncService.fullSync(true);
|
||||
return response;
|
||||
}
|
||||
|
||||
async detachOrganizastion(providerId: string, organizationId: string): Promise<any> {
|
||||
await this.apiService.deleteProviderOrganization(providerId, organizationId);
|
||||
await this.syncService.fullSync(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||
import { SyncService } from "jslib-common/abstractions/sync.service";
|
||||
|
||||
import { ProviderAddOrganizationRequest } from "jslib-common/models/request/provider/providerAddOrganizationRequest";
|
||||
|
||||
@Injectable()
|
||||
export class WebProviderService {
|
||||
constructor(
|
||||
private cryptoService: CryptoService,
|
||||
private syncService: SyncService,
|
||||
private apiService: ApiService
|
||||
) {}
|
||||
|
||||
async addOrganizationToProvider(providerId: string, organizationId: string) {
|
||||
const orgKey = await this.cryptoService.getOrgKey(organizationId);
|
||||
const providerKey = await this.cryptoService.getProviderKey(providerId);
|
||||
|
||||
const encryptedOrgKey = await this.cryptoService.encrypt(orgKey.key, providerKey);
|
||||
|
||||
const request = new ProviderAddOrganizationRequest();
|
||||
request.organizationId = organizationId;
|
||||
request.key = encryptedOrgKey.encryptedString;
|
||||
|
||||
const response = await this.apiService.postProviderAddOrganization(providerId, request);
|
||||
await this.syncService.fullSync(true);
|
||||
return response;
|
||||
}
|
||||
|
||||
async detachOrganizastion(providerId: string, organizationId: string): Promise<any> {
|
||||
await this.apiService.deleteProviderOrganization(providerId, organizationId);
|
||||
await this.syncService.fullSync(true);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,52 @@
|
||||
<div class="page-header">
|
||||
<h1>{{'myProvider' | i18n}}</h1>
|
||||
<h1>{{ "myProvider" | i18n }}</h1>
|
||||
</div>
|
||||
<div *ngIf="loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
<form *ngIf="provider && !loading" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="name">{{'providerName' | i18n}}</label>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="provider.name"
|
||||
[disabled]="selfHosted">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="billingEmail">{{'billingEmail' | i18n}}</label>
|
||||
<input id="billingEmail" class="form-control" type="text" name="BillingEmail"
|
||||
[(ngModel)]="provider.billingEmail" [disabled]="selfHosted">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<app-avatar data="{{provider.name}}" dynamic="true" size="75" fontSize="35"></app-avatar>
|
||||
</div>
|
||||
<form
|
||||
*ngIf="provider && !loading"
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="name">{{ "providerName" | i18n }}</label>
|
||||
<input
|
||||
id="name"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Name"
|
||||
[(ngModel)]="provider.name"
|
||||
[disabled]="selfHosted"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="billingEmail">{{ "billingEmail" | i18n }}</label>
|
||||
<input
|
||||
id="billingEmail"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="BillingEmail"
|
||||
[(ngModel)]="provider.billingEmail"
|
||||
[disabled]="selfHosted"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'save' | i18n}}</span>
|
||||
</button>
|
||||
<div class="col-6">
|
||||
<app-avatar data="{{ provider.name }}" dynamic="true" size="75" fontSize="35"></app-avatar>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "save" | i18n }}</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -1,62 +1,65 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Component } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
|
||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||
import { SyncService } from 'jslib-common/abstractions/sync.service';
|
||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { LogService } from "jslib-common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { SyncService } from "jslib-common/abstractions/sync.service";
|
||||
|
||||
import { ProviderUpdateRequest } from 'jslib-common/models/request/provider/providerUpdateRequest';
|
||||
import { ProviderUpdateRequest } from "jslib-common/models/request/provider/providerUpdateRequest";
|
||||
|
||||
import { ProviderResponse } from 'jslib-common/models/response/provider/providerResponse';
|
||||
import { ProviderResponse } from "jslib-common/models/response/provider/providerResponse";
|
||||
|
||||
@Component({
|
||||
selector: 'provider-account',
|
||||
templateUrl: 'account.component.html',
|
||||
selector: "provider-account",
|
||||
templateUrl: "account.component.html",
|
||||
})
|
||||
export class AccountComponent {
|
||||
selfHosted = false;
|
||||
loading = true;
|
||||
provider: ProviderResponse;
|
||||
formPromise: Promise<any>;
|
||||
taxFormPromise: Promise<any>;
|
||||
selfHosted = false;
|
||||
loading = true;
|
||||
provider: ProviderResponse;
|
||||
formPromise: Promise<any>;
|
||||
taxFormPromise: Promise<any>;
|
||||
|
||||
private providerId: string;
|
||||
private providerId: string;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private toasterService: ToasterService, private route: ActivatedRoute,
|
||||
private syncService: SyncService, private platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService) { }
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private route: ActivatedRoute,
|
||||
private syncService: SyncService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.selfHosted = this.platformUtilsService.isSelfHost();
|
||||
this.route.parent.parent.params.subscribe(async params => {
|
||||
this.providerId = params.providerId;
|
||||
try {
|
||||
this.provider = await this.apiService.getProvider(this.providerId);
|
||||
} catch (e) {
|
||||
this.logService.error(`Handled exception: ${e}`);
|
||||
}
|
||||
});
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
const request = new ProviderUpdateRequest();
|
||||
request.name = this.provider.name;
|
||||
request.businessName = this.provider.businessName;
|
||||
request.billingEmail = this.provider.billingEmail;
|
||||
|
||||
this.formPromise = this.apiService.putProvider(this.providerId, request).then(() => {
|
||||
return this.syncService.fullSync(true);
|
||||
});
|
||||
await this.formPromise;
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('providerUpdated'));
|
||||
} catch (e) {
|
||||
this.logService.error(`Handled exception: ${e}`);
|
||||
}
|
||||
async ngOnInit() {
|
||||
this.selfHosted = this.platformUtilsService.isSelfHost();
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.providerId = params.providerId;
|
||||
try {
|
||||
this.provider = await this.apiService.getProvider(this.providerId);
|
||||
} catch (e) {
|
||||
this.logService.error(`Handled exception: ${e}`);
|
||||
}
|
||||
});
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
const request = new ProviderUpdateRequest();
|
||||
request.name = this.provider.name;
|
||||
request.businessName = this.provider.businessName;
|
||||
request.billingEmail = this.provider.billingEmail;
|
||||
|
||||
this.formPromise = this.apiService.putProvider(this.providerId, request).then(() => {
|
||||
return this.syncService.fullSync(true);
|
||||
});
|
||||
await this.formPromise;
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("providerUpdated"));
|
||||
} catch (e) {
|
||||
this.logService.error(`Handled exception: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<div class="container page-content">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<div class="card">
|
||||
<div class="card-header">{{'settings' | i18n}}</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a routerLink="account" class="list-group-item" routerLinkActive="active">
|
||||
{{'myProvider' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<router-outlet></router-outlet>
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<div class="card">
|
||||
<div class="card-header">{{ "settings" | i18n }}</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a routerLink="account" class="list-group-item" routerLinkActive="active">
|
||||
{{ "myProvider" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Component } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
|
||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||
import { UserService } from 'jslib-common/abstractions/user.service';
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
||||
|
||||
@Component({
|
||||
selector: 'provider-settings',
|
||||
templateUrl: 'settings.component.html',
|
||||
selector: "provider-settings",
|
||||
templateUrl: "settings.component.html",
|
||||
})
|
||||
export class SettingsComponent {
|
||||
constructor(private route: ActivatedRoute, private userService: UserService,
|
||||
private platformUtilsService: PlatformUtilsService) { }
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private providerService: ProviderService,
|
||||
private platformUtilsService: PlatformUtilsService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.parent.params.subscribe(async params => {
|
||||
const provider = await this.userService.getProvider(params.providerId);
|
||||
});
|
||||
}
|
||||
ngOnInit() {
|
||||
this.route.parent.params.subscribe(async (params) => {
|
||||
const provider = await this.providerService.get(params.providerId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
<div class="mt-5 d-flex justify-content-center" *ngIf="loading">
|
||||
<div>
|
||||
<img src="/src/images/logo-dark@2x.png" class="mb-4 logo" alt="Bitwarden">
|
||||
<p class="text-center">
|
||||
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<img class="mb-4 logo logo-themed" alt="Bitwarden" />
|
||||
<p class="text-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container" *ngIf="!loading && !authed">
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="lead text-center mb-4">{{'setupProvider' | i18n}}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body">
|
||||
<p>{{'setupProviderLoginDesc' | i18n}}</p>
|
||||
<hr>
|
||||
<div class="d-flex">
|
||||
<a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
|
||||
{{'logIn' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="lead text-center mb-4">{{ "setupProvider" | i18n }}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body">
|
||||
<p>{{ "setupProviderLoginDesc" | i18n }}</p>
|
||||
<hr />
|
||||
<div class="d-flex">
|
||||
<a routerLink="/" [queryParams]="{ email: email }" class="btn btn-primary btn-block">
|
||||
{{ "logIn" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { BaseAcceptComponent } from 'src/app/common/base.accept.component';
|
||||
import { BaseAcceptComponent } from "src/app/common/base.accept.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-setup-provider',
|
||||
templateUrl: 'setup-provider.component.html',
|
||||
selector: "app-setup-provider",
|
||||
templateUrl: "setup-provider.component.html",
|
||||
})
|
||||
export class SetupProviderComponent extends BaseAcceptComponent {
|
||||
failedShortMessage = "inviteAcceptFailedShort";
|
||||
failedMessage = "inviteAcceptFailed";
|
||||
|
||||
failedShortMessage = 'inviteAcceptFailedShort';
|
||||
failedMessage = 'inviteAcceptFailed';
|
||||
requiredParameters = ["providerId", "email", "token"];
|
||||
|
||||
requiredParameters = ['providerId', 'email', 'token'];
|
||||
async authedHandler(qParams: any) {
|
||||
this.router.navigate(["/providers/setup"], { queryParams: qParams });
|
||||
}
|
||||
|
||||
async authedHandler(qParams: any) {
|
||||
this.router.navigate(['/providers/setup'], {queryParams: qParams});
|
||||
}
|
||||
|
||||
// tslint:disable-next-line
|
||||
async unauthedHandler(qParams: any) {}
|
||||
// tslint:disable-next-line
|
||||
async unauthedHandler(qParams: any) {}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,39 @@
|
||||
<app-navbar></app-navbar>
|
||||
<div class="container page-content">
|
||||
<div class="page-header">
|
||||
<h1>{{'setupProvider' | i18n}}</h1>
|
||||
<div class="page-header">
|
||||
<h1>{{ "setupProvider" | i18n }}</h1>
|
||||
</div>
|
||||
<p>{{ "setupProviderDesc" | i18n }}</p>
|
||||
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="loading">
|
||||
<h2 class="mt-5">{{ "generalInformation" | i18n }}</h2>
|
||||
<div class="row">
|
||||
<div class="form-group col-6">
|
||||
<label for="name">{{ "providerName" | i18n }}</label>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required />
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label for="billingEmail">{{ "billingEmail" | i18n }}</label>
|
||||
<input
|
||||
id="billingEmail"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="BillingEmail"
|
||||
[(ngModel)]="billingEmail"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p>{{'setupProviderDesc' | i18n}}</p>
|
||||
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="loading">
|
||||
<h2 class="mt-5">{{'generalInformation' | i18n}}</h2>
|
||||
<div class="row">
|
||||
<div class="form-group col-6">
|
||||
<label for="name">{{'providerName' | i18n}}</label>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required>
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label for="billingEmail">{{'billingEmail' | i18n}}</label>
|
||||
<input id="billingEmail" class="form-control" type="text" name="BillingEmail" [(ngModel)]="billingEmail" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'submit' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" *ngIf="showCancel">
|
||||
{{'cancel' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "submit" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" *ngIf="showCancel">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<app-footer></app-footer>
|
||||
|
||||
@@ -1,106 +1,99 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
import {
|
||||
Toast,
|
||||
ToasterService,
|
||||
} from 'angular2-toaster';
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { ValidationService } from 'jslib-angular/services/validation.service';
|
||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||
import { SyncService } from 'jslib-common/abstractions/sync.service';
|
||||
import { ProviderSetupRequest } from 'jslib-common/models/request/provider/providerSetupRequest';
|
||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||
|
||||
import { ValidationService } from "jslib-angular/services/validation.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { SyncService } from "jslib-common/abstractions/sync.service";
|
||||
import { ProviderSetupRequest } from "jslib-common/models/request/provider/providerSetupRequest";
|
||||
|
||||
@Component({
|
||||
selector: 'provider-setup',
|
||||
templateUrl: 'setup.component.html',
|
||||
selector: "provider-setup",
|
||||
templateUrl: "setup.component.html",
|
||||
})
|
||||
export class SetupComponent implements OnInit {
|
||||
loading = true;
|
||||
authed = false;
|
||||
email: string;
|
||||
formPromise: Promise<any>;
|
||||
loading = true;
|
||||
authed = false;
|
||||
email: string;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
providerId: string;
|
||||
token: string;
|
||||
name: string;
|
||||
billingEmail: string;
|
||||
providerId: string;
|
||||
token: string;
|
||||
name: string;
|
||||
billingEmail: string;
|
||||
|
||||
constructor(private router: Router, private toasterService: ToasterService,
|
||||
private i18nService: I18nService, private route: ActivatedRoute,
|
||||
private cryptoService: CryptoService, private apiService: ApiService,
|
||||
private syncService: SyncService, private validationService: ValidationService) { }
|
||||
constructor(
|
||||
private router: Router,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private route: ActivatedRoute,
|
||||
private cryptoService: CryptoService,
|
||||
private apiService: ApiService,
|
||||
private syncService: SyncService,
|
||||
private validationService: ValidationService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
document.body.classList.remove('layout_frontend');
|
||||
let fired = false;
|
||||
this.route.queryParams.subscribe(async qParams => {
|
||||
if (fired) {
|
||||
return;
|
||||
}
|
||||
fired = true;
|
||||
const error = qParams.providerId == null || qParams.email == null || qParams.token == null;
|
||||
ngOnInit() {
|
||||
document.body.classList.remove("layout_frontend");
|
||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||
const error = qParams.providerId == null || qParams.email == null || qParams.token == null;
|
||||
|
||||
if (error) {
|
||||
const toast: Toast = {
|
||||
type: 'error',
|
||||
title: null,
|
||||
body: this.i18nService.t('emergencyInviteAcceptFailed'),
|
||||
timeout: 10000,
|
||||
};
|
||||
this.toasterService.popAsync(toast);
|
||||
this.router.navigate(['/']);
|
||||
return;
|
||||
}
|
||||
if (error) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
null,
|
||||
this.i18nService.t("emergencyInviteAcceptFailed"),
|
||||
{ timeout: 10000 }
|
||||
);
|
||||
this.router.navigate(["/"]);
|
||||
return;
|
||||
}
|
||||
|
||||
this.providerId = qParams.providerId;
|
||||
this.token = qParams.token;
|
||||
this.providerId = qParams.providerId;
|
||||
this.token = qParams.token;
|
||||
|
||||
// Check if provider exists, redirect if it does
|
||||
try {
|
||||
const provider = await this.apiService.getProvider(this.providerId);
|
||||
if (provider.name != null) {
|
||||
this.router.navigate(['/providers', provider.id], { replaceUrl: true });
|
||||
}
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async submit() {
|
||||
this.formPromise = this.doSubmit();
|
||||
await this.formPromise;
|
||||
this.formPromise = null;
|
||||
}
|
||||
|
||||
async doSubmit() {
|
||||
try {
|
||||
const shareKey = await this.cryptoService.makeShareKey();
|
||||
const key = shareKey[0].encryptedString;
|
||||
|
||||
const request = new ProviderSetupRequest();
|
||||
request.name = this.name;
|
||||
request.billingEmail = this.billingEmail;
|
||||
request.token = this.token;
|
||||
request.key = key;
|
||||
|
||||
const provider = await this.apiService.postProviderSetup(this.providerId, request);
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('providerSetup'));
|
||||
await this.syncService.fullSync(true);
|
||||
|
||||
this.router.navigate(['/providers', provider.id]);
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
// Check if provider exists, redirect if it does
|
||||
try {
|
||||
const provider = await this.apiService.getProvider(this.providerId);
|
||||
if (provider.name != null) {
|
||||
this.router.navigate(["/providers", provider.id], { replaceUrl: true });
|
||||
}
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
this.router.navigate(["/"]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async submit() {
|
||||
this.formPromise = this.doSubmit();
|
||||
await this.formPromise;
|
||||
this.formPromise = null;
|
||||
}
|
||||
|
||||
async doSubmit() {
|
||||
try {
|
||||
const shareKey = await this.cryptoService.makeShareKey();
|
||||
const key = shareKey[0].encryptedString;
|
||||
|
||||
const request = new ProviderSetupRequest();
|
||||
request.name = this.name;
|
||||
request.billingEmail = this.billingEmail;
|
||||
request.token = this.token;
|
||||
request.key = key;
|
||||
|
||||
const provider = await this.apiService.postProviderSetup(this.providerId, request);
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("providerSetup"));
|
||||
await this.syncService.fullSync(true);
|
||||
|
||||
this.router.navigate(["/providers", provider.id]);
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin;
|
||||
const { AngularWebpackPlugin } = require("@ngtools/webpack");
|
||||
|
||||
const webpackConfig = require('../webpack.config');
|
||||
const webpackConfig = require("../webpack.config");
|
||||
|
||||
webpackConfig.entry['app/main'] = './bitwarden_license/src/app/main.ts';
|
||||
webpackConfig.plugins[webpackConfig.plugins.length -1] = new AngularCompilerPlugin({
|
||||
tsConfigPath: 'tsconfig.json',
|
||||
entryModule: 'bitwarden_license/src/app/app.module#AppModule',
|
||||
sourceMap: true,
|
||||
webpackConfig.entry["app/main"] = "./bitwarden_license/src/app/main.ts";
|
||||
webpackConfig.plugins[webpackConfig.plugins.length - 1] = new AngularWebpackPlugin({
|
||||
tsConfigPath: "tsconfig.json",
|
||||
entryModule: "bitwarden_license/src/app/app.module#AppModule",
|
||||
sourceMap: true,
|
||||
});
|
||||
|
||||
module.exports = webpackConfig;
|
||||
|
||||
44
config.js
44
config.js
@@ -1,32 +1,36 @@
|
||||
function load(envName) {
|
||||
return {
|
||||
...require('./config/base.json'),
|
||||
...loadConfig(envName),
|
||||
...loadConfig('local'),
|
||||
};
|
||||
return {
|
||||
...require("./config/base.json"),
|
||||
...loadConfig(envName),
|
||||
...loadConfig("local"),
|
||||
dev: {
|
||||
...require("./config/base.json").dev,
|
||||
...loadConfig(envName).dev,
|
||||
...loadConfig("local").dev,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function log(configObj) {
|
||||
const repeatNum = 50;
|
||||
console.log(`${"=".repeat(repeatNum)}\nenvConfig`);
|
||||
console.log(JSON.stringify(configObj, null, 2));
|
||||
console.log(`${"=".repeat(repeatNum)}`);
|
||||
const repeatNum = 50;
|
||||
console.log(`${"=".repeat(repeatNum)}\nenvConfig`);
|
||||
console.log(JSON.stringify(configObj, null, 2));
|
||||
console.log(`${"=".repeat(repeatNum)}`);
|
||||
}
|
||||
|
||||
function loadConfig(configName) {
|
||||
try {
|
||||
return require(`./config/${configName}.json`);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.code === "MODULE_NOT_FOUND") {
|
||||
return {};
|
||||
}
|
||||
else {
|
||||
throw e;
|
||||
}
|
||||
try {
|
||||
return require(`./config/${configName}.json`);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.code === "MODULE_NOT_FOUND") {
|
||||
return {};
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
load,
|
||||
log
|
||||
load,
|
||||
log,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
{
|
||||
"urls": {}
|
||||
"urls": {},
|
||||
"stripeKey": "pk_test_KPoCfZXu7mznb9uSCPZ2JpTD",
|
||||
"braintreeKey": "sandbox_r72q8jq6_9pnxkwm75f87sdc2",
|
||||
"paypal": {
|
||||
"businessId": "AD3LAUZSNVPJY",
|
||||
"buttonAction": "https://www.sandbox.paypal.com/cgi-bin/webscr"
|
||||
},
|
||||
"dev": {
|
||||
"allowedHosts": "auto"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
{
|
||||
"urls": {
|
||||
"icons": "https://icons.bitwarden.net",
|
||||
"notifications": "https://notifications.bitwarden.com",
|
||||
"enterprise": "https://portal.bitwarden.com"
|
||||
}
|
||||
"urls": {
|
||||
"icons": "https://icons.bitwarden.net",
|
||||
"notifications": "https://notifications.bitwarden.com"
|
||||
},
|
||||
"stripeKey": "pk_live_bpN0P37nMxrMQkcaHXtAybJk",
|
||||
"braintreeKey": "production_qfbsv8kc_njj2zjtyngtjmbjd",
|
||||
"paypal": {
|
||||
"businessId": "4ZDA7DLUUJGMN",
|
||||
"buttonAction": "https://www.paypal.com/cgi-bin/webscr"
|
||||
},
|
||||
"dev": {
|
||||
"proxyApi": "https://api.bitwarden.com",
|
||||
"proxyIdentity": "https://identity.bitwarden.com",
|
||||
"proxyEvents": "https://events.bitwarden.com"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
{
|
||||
"urls": {
|
||||
"notifications": "http://localhost:61840"
|
||||
},
|
||||
"dev": {
|
||||
"proxyApi": "http://localhost:4000",
|
||||
"proxyIdentity": "http://localhost:33656",
|
||||
"proxyEvents": "http://localhost:46273",
|
||||
"proxyNotifications": "http://localhost:61840",
|
||||
"proxyEnterprise": "http://localhost:52313",
|
||||
"allowedHosts": [],
|
||||
"urls": {
|
||||
"notifications": "http://localhost:61840",
|
||||
"enterprise": "http://localhost:52313"
|
||||
}
|
||||
"proxyNotifications": "http://localhost:61840"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
{
|
||||
"urls": {
|
||||
"icons": "https://icons.qa.bitwarden.pw",
|
||||
"notifications": "https://notifications.qa.bitwarden.pw",
|
||||
"enterprise": "https://portal.qa.bitwarden.pw"
|
||||
}
|
||||
|
||||
"urls": {
|
||||
"icons": "https://icons.qa.bitwarden.pw",
|
||||
"notifications": "https://notifications.qa.bitwarden.pw"
|
||||
},
|
||||
"dev": {
|
||||
"proxyApi": "https://api.qa.bitwarden.pw",
|
||||
"proxyIdentity": "https://identity.qa.bitwarden.pw",
|
||||
"proxyEvents": "https://events.qa.bitwarden.pw"
|
||||
}
|
||||
}
|
||||
|
||||
23
connectors/src/captcha-mobile.html
Normal file
23
connectors/src/captcha-mobile.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"
|
||||
/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="HandheldFriendly" content="true" />
|
||||
<title>Bitwarden Captcha Connector</title>
|
||||
</head>
|
||||
|
||||
<body class="layout_frontend">
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div>
|
||||
<img src="../../src/images/logo-dark@2x.png" class="logo mb-2" alt="Bitwarden" />
|
||||
<p id="captchaRequired" class="lead text-center mx-4 mb-4">Captcha Required</p>
|
||||
<div id="captcha"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
17
connectors/src/captcha.html
Normal file
17
connectors/src/captcha.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"
|
||||
/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="HandheldFriendly" content="true" />
|
||||
<title>Bitwarden Captcha Connector</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="captcha"></div>
|
||||
</body>
|
||||
</html>
|
||||
46
connectors/src/captcha/captcha-mobile.scss
Normal file
46
connectors/src/captcha/captcha-mobile.scss
Normal file
@@ -0,0 +1,46 @@
|
||||
@import "../common/styles.scss";
|
||||
|
||||
.justify-content-md-center {
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-right: -10px;
|
||||
margin-left: -10px;
|
||||
}
|
||||
|
||||
.mt-5,
|
||||
.my-5 {
|
||||
margin-top: 3rem !important;
|
||||
}
|
||||
|
||||
.mb-2,
|
||||
.my-2 {
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
.ml-4,
|
||||
.mx-4 {
|
||||
margin-left: 1.5rem !important;
|
||||
}
|
||||
|
||||
.mb-4,
|
||||
.my-4 {
|
||||
margin-bottom: 1.5rem !important;
|
||||
}
|
||||
|
||||
.mr-4,
|
||||
.mx-4 {
|
||||
margin-right: 1.5rem !important;
|
||||
}
|
||||
|
||||
.lead {
|
||||
font-size: 1.25rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center !important;
|
||||
}
|
||||
6
connectors/src/captcha/captcha.scss
Normal file
6
connectors/src/captcha/captcha.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
body {
|
||||
min-width: 0px !important;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
}
|
||||
145
connectors/src/captcha/captcha.ts
Normal file
145
connectors/src/captcha/captcha.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { b64Decode, getQsParam } from "../common";
|
||||
|
||||
declare var hcaptcha: any;
|
||||
|
||||
if (window.location.pathname.includes("mobile")) {
|
||||
// tslint:disable-next-line
|
||||
require("./captcha-mobile.scss");
|
||||
} else {
|
||||
// tslint:disable-next-line
|
||||
require("./captcha.scss");
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
init();
|
||||
});
|
||||
|
||||
(window as any).captchaSuccess = captchaSuccess;
|
||||
(window as any).captchaError = captchaError;
|
||||
|
||||
let parentUrl: string = null;
|
||||
let parentOrigin: string = null;
|
||||
let mobileResponse: boolean = null;
|
||||
let sentSuccess = false;
|
||||
|
||||
async function init() {
|
||||
await start();
|
||||
onMessage();
|
||||
}
|
||||
|
||||
async function start() {
|
||||
sentSuccess = false;
|
||||
|
||||
const data = getQsParam("data");
|
||||
if (!data) {
|
||||
error("No data.");
|
||||
return;
|
||||
}
|
||||
|
||||
parentUrl = getQsParam("parent");
|
||||
if (!parentUrl) {
|
||||
error("No parent.");
|
||||
return;
|
||||
} else {
|
||||
parentUrl = decodeURIComponent(parentUrl);
|
||||
parentOrigin = new URL(parentUrl).origin;
|
||||
}
|
||||
|
||||
let decodedData: any;
|
||||
try {
|
||||
decodedData = JSON.parse(b64Decode(data));
|
||||
} catch (e) {
|
||||
error("Cannot parse data.");
|
||||
return;
|
||||
}
|
||||
mobileResponse = decodedData.callbackUri != null || decodedData.mobile === true;
|
||||
|
||||
let src = "https://hcaptcha.com/1/api.js?render=explicit";
|
||||
|
||||
// Set language code
|
||||
if (decodedData.locale) {
|
||||
src += `&hl=${encodeURIComponent(decodedData.locale) ?? "en"}`;
|
||||
}
|
||||
|
||||
// Set captchaRequired subtitle for mobile
|
||||
const subtitleEl = document.getElementById("captchaRequired");
|
||||
if (decodedData.captchaRequiredText && subtitleEl) {
|
||||
subtitleEl.textContent = decodedData.captchaRequiredText;
|
||||
}
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.src = src;
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
script.addEventListener("load", (e) => {
|
||||
hcaptcha.render("captcha", {
|
||||
sitekey: encodeURIComponent(decodedData.siteKey),
|
||||
callback: "captchaSuccess",
|
||||
"error-callback": "captchaError",
|
||||
});
|
||||
watchHeight();
|
||||
});
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
function captchaSuccess(response: string) {
|
||||
if (mobileResponse) {
|
||||
document.location.replace("bitwarden://captcha-callback?token=" + encodeURIComponent(response));
|
||||
} else {
|
||||
success(response);
|
||||
}
|
||||
}
|
||||
|
||||
function captchaError() {
|
||||
error("An error occurred with the captcha. Try again.");
|
||||
}
|
||||
|
||||
function onMessage() {
|
||||
window.addEventListener(
|
||||
"message",
|
||||
(event) => {
|
||||
if (!event.origin || event.origin === "" || event.origin !== parentOrigin) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data === "start") {
|
||||
start();
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
function error(message: string) {
|
||||
parent.postMessage("error|" + message, parentUrl);
|
||||
}
|
||||
|
||||
function success(data: string) {
|
||||
if (sentSuccess) {
|
||||
return;
|
||||
}
|
||||
parent.postMessage("success|" + data, parentUrl);
|
||||
sentSuccess = true;
|
||||
}
|
||||
|
||||
function info(message: string | object) {
|
||||
parent.postMessage("info|" + JSON.stringify(message), parentUrl);
|
||||
}
|
||||
|
||||
async function watchHeight() {
|
||||
const imagesDiv = document.body.lastChild as HTMLElement;
|
||||
while (true) {
|
||||
info({
|
||||
height:
|
||||
imagesDiv.style.visibility === "hidden"
|
||||
? document.documentElement.offsetHeight
|
||||
: document.documentElement.scrollHeight,
|
||||
width: document.documentElement.scrollWidth,
|
||||
});
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
async function sleep(ms: number) {
|
||||
await new Promise((r) => setTimeout(r, ms));
|
||||
}
|
||||
25
connectors/src/common/index.ts
Normal file
25
connectors/src/common/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export function getQsParam(name: string) {
|
||||
const url = window.location.href;
|
||||
name = name.replace(/[\[\]]/g, "\\$&");
|
||||
const regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)");
|
||||
const results = regex.exec(url);
|
||||
|
||||
if (!results) {
|
||||
return null;
|
||||
}
|
||||
if (!results[2]) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return decodeURIComponent(results[2].replace(/\+/g, " "));
|
||||
}
|
||||
|
||||
export function b64Decode(str: string) {
|
||||
return decodeURIComponent(
|
||||
Array.prototype.map
|
||||
.call(atob(str), (c: string) => {
|
||||
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
})
|
||||
.join("")
|
||||
);
|
||||
}
|
||||
22
connectors/src/common/styles.scss
Normal file
22
connectors/src/common/styles.scss
Normal file
@@ -0,0 +1,22 @@
|
||||
@import "~bootstrap/scss/_functions";
|
||||
@import "~bootstrap/scss/_variables";
|
||||
@import "~bootstrap/scss/_mixins";
|
||||
@import "~bootstrap/scss/_root";
|
||||
@import "~bootstrap/scss/_reboot";
|
||||
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
html.theme_light body.layout_frontend {
|
||||
background-color: #ecf0f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
img.logo {
|
||||
display: block;
|
||||
height: 43px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 0px;
|
||||
width: 284px;
|
||||
}
|
||||
13
connectors/src/duo.html
Normal file
13
connectors/src/duo.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width"
|
||||
/>
|
||||
<title>Bitwarden Duo Connector</title>
|
||||
</head>
|
||||
|
||||
<body></body>
|
||||
</html>
|
||||
18
connectors/src/duo/duo.scss
Normal file
18
connectors/src/duo/duo.scss
Normal file
@@ -0,0 +1,18 @@
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #efeff4 url("../../../src/images/loading.svg") 0 0 no-repeat;
|
||||
}
|
||||
|
||||
iframe {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
47
connectors/src/duo/duo.ts
Normal file
47
connectors/src/duo/duo.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import * as DuoWebSDK from "duo_web_sdk";
|
||||
import { getQsParam } from "../common";
|
||||
|
||||
// tslint:disable-next-line
|
||||
require("./duo.scss");
|
||||
|
||||
document.addEventListener("DOMContentLoaded", (event) => {
|
||||
const frameElement = document.createElement("iframe");
|
||||
frameElement.setAttribute("id", "duo_iframe");
|
||||
setFrameHeight();
|
||||
document.body.appendChild(frameElement);
|
||||
|
||||
const hostParam = getQsParam("host");
|
||||
const requestParam = getQsParam("request");
|
||||
|
||||
const hostUrl = new URL("https://" + hostParam);
|
||||
if (
|
||||
!hostUrl.hostname.endsWith(".duosecurity.com") &&
|
||||
!hostUrl.hostname.endsWith(".duofederal.com")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
DuoWebSDK.init({
|
||||
iframe: "duo_iframe",
|
||||
host: hostParam,
|
||||
sig_request: requestParam,
|
||||
submit_callback: (form: any) => {
|
||||
invokeCSCode(form.elements.sig_response.value);
|
||||
},
|
||||
});
|
||||
|
||||
window.onresize = setFrameHeight;
|
||||
|
||||
function setFrameHeight() {
|
||||
frameElement.style.height = window.innerHeight + "px";
|
||||
}
|
||||
});
|
||||
|
||||
function invokeCSCode(data: string) {
|
||||
try {
|
||||
(window as any).invokeCSharpAction(data);
|
||||
} catch (err) {
|
||||
// tslint:disable-next-line
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
47
connectors/src/sso.html
Normal file
47
connectors/src/sso.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="theme_light">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=1010" />
|
||||
<meta name="theme-color" content="#175DDC" />
|
||||
|
||||
<title>Bitwarden</title>
|
||||
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="../../src/images/icons/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="../../src/images/icons/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="../../src/images/icons/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="mask-icon" href="../../src/images/icons/safari-pinned-tab.svg" color="#175DDC" />
|
||||
<link rel="manifest" href="../../src/manifest.json" />
|
||||
</head>
|
||||
|
||||
<body class="layout_frontend">
|
||||
<div class="mt-5 d-flex justify-content-center">
|
||||
<div>
|
||||
<img src="../../src/images/logo-dark@2x.png" class="mb-4 logo" alt="Bitwarden" />
|
||||
<div id="content">
|
||||
<p class="text-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
|
||||
title="Loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
82
connectors/src/sso/sso.scss
Normal file
82
connectors/src/sso/sso.scss
Normal file
@@ -0,0 +1,82 @@
|
||||
@import "../common/styles.scss";
|
||||
|
||||
.mt-5,
|
||||
.my-5 {
|
||||
margin-top: 3rem !important;
|
||||
}
|
||||
|
||||
.d-flex {
|
||||
display: -ms-flexbox !important;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.justify-content-center {
|
||||
-ms-flex-pack: center !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
.mb-4,
|
||||
.my-4 {
|
||||
margin-bottom: 1.5rem !important;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
$icomoon-font-family: "bwi-font" !default;
|
||||
$icomoon-font-path: "~@bitwarden/jslib-angular/src/scss/bwicons/fonts/" !default;
|
||||
|
||||
@font-face {
|
||||
font-family: "#{$icomoon-font-family}";
|
||||
src: url($icomoon-font-path + "bwi-font.svg") format("svg"),
|
||||
url($icomoon-font-path + "bwi-font.ttf") format("truetype"),
|
||||
url($icomoon-font-path + "bwi-font.woff") format("woff"),
|
||||
url($icomoon-font-path + "bwi-font.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
// Base Class
|
||||
.bwi {
|
||||
/* use !important to prevent issues with browser extensions that change fonts */
|
||||
font-family: "#{$icomoon-font-family}" !important;
|
||||
speak: never;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
/* Better Font Rendering */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.bwi-2x {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
// Spin Animations
|
||||
.bwi-spin {
|
||||
animation: bwi-spin 2s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes bwi-spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
|
||||
// Rotation
|
||||
.bwi-rotate-270 {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
|
||||
.bwi-spinner:before {
|
||||
content: "\e937";
|
||||
}
|
||||
48
connectors/src/sso/sso.ts
Normal file
48
connectors/src/sso/sso.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { getQsParam } from "../common";
|
||||
|
||||
// tslint:disable-next-line
|
||||
require("./sso.scss");
|
||||
|
||||
document.addEventListener("DOMContentLoaded", (event) => {
|
||||
const code = getQsParam("code");
|
||||
const state = getQsParam("state");
|
||||
|
||||
if (state != null && state.includes(":clientId=browser")) {
|
||||
initiateBrowserSso(code, state);
|
||||
} else {
|
||||
window.location.href = window.location.origin + "/#/sso?code=" + code + "&state=" + state;
|
||||
// Match any characters between "_returnUri='" and the next "'"
|
||||
const returnUri = extractFromRegex(state, "(?<=_returnUri=')(.*)(?=')");
|
||||
if (returnUri) {
|
||||
window.location.href = window.location.origin + `/#${returnUri}`;
|
||||
} else {
|
||||
window.location.href = window.location.origin + "/#/sso?code=" + code + "&state=" + state;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function initiateBrowserSso(code: string, state: string) {
|
||||
window.postMessage({ command: "authResult", code: code, state: state }, "*");
|
||||
const handOffMessage = ("; " + document.cookie)
|
||||
.split("; ssoHandOffMessage=")
|
||||
.pop()
|
||||
.split(";")
|
||||
.shift();
|
||||
document.cookie = "ssoHandOffMessage=;SameSite=strict;max-age=0";
|
||||
const content = document.getElementById("content");
|
||||
content.innerHTML = "";
|
||||
const p = document.createElement("p");
|
||||
p.innerText = handOffMessage;
|
||||
content.appendChild(p);
|
||||
}
|
||||
|
||||
function extractFromRegex(s: string, regexString: string) {
|
||||
const regex = new RegExp(regexString);
|
||||
const results = regex.exec(s);
|
||||
|
||||
if (!results) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return results[0];
|
||||
}
|
||||
39
connectors/src/webauthn-fallback.html
Normal file
39
connectors/src/webauthn-fallback.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Bitwarden WebAuthn Connector</title>
|
||||
</head>
|
||||
|
||||
<body class="layout_frontend">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center mt-5">
|
||||
<div class="col-5">
|
||||
<img src="../../src/images/logo-dark@2x.png" class="mb-4 logo" alt="Bitwarden" />
|
||||
<div id="spinner">
|
||||
<p class="text-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
|
||||
title="Loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</p>
|
||||
</div>
|
||||
<div id="content" class="card mt-4 d-none">
|
||||
<div class="card-body ng-star-inserted">
|
||||
<p id="msg" class="text-center"></p>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="remember" name="remember" />
|
||||
<label class="form-check-label" for="remember" id="remember-label"></label>
|
||||
</div>
|
||||
<hr />
|
||||
<p class="text-center mb-0">
|
||||
<button id="webauthn-button" class="btn btn-primary btn-lg"></button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
30
connectors/src/webauthn-mobile.html
Normal file
30
connectors/src/webauthn-mobile.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="theme_light">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"
|
||||
/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="HandheldFriendly" content="true" />
|
||||
<title>Bitwarden WebAuthn Connector</title>
|
||||
</head>
|
||||
|
||||
<body style="background: transparent">
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div>
|
||||
<img src="../../src/images/logo-dark@2x.png" class="logo mb-2" alt="Bitwarden" />
|
||||
<p id="webauthn-header" class="lead text-center mx-4 mb-4"></p>
|
||||
<picture>
|
||||
<source srcset="../../src/images/u2fkey-mobile.avif" type="image/avif" />
|
||||
<source srcset="../../src/images/u2fkey-mobile.webp" type="image/webp" />
|
||||
<img src="../../src/images/u2fkey-mobile.jpg" class="rounded img-fluid" />
|
||||
</picture>
|
||||
<div class="text-center mt-4">
|
||||
<button id="webauthn-button" class="btn btn-primary btn-lg"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
18
connectors/src/webauthn.html
Normal file
18
connectors/src/webauthn.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Bitwarden WebAuthn Connector</title>
|
||||
</head>
|
||||
|
||||
<body style="background: transparent">
|
||||
<picture>
|
||||
<source srcset="../../src/images/u2fkey.avif" type="image/avif" />
|
||||
<source srcset="../../src/images/u2fkey.webp" type="image/webp" />
|
||||
<img src="../../src/images/u2fkey.jpg" class="rounded img-fluid mb-3" />
|
||||
</picture>
|
||||
<div class="text-center">
|
||||
<button id="webauthn-button" class="btn btn-primary"></button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
70
connectors/src/webauthn/common-webauthn.ts
Normal file
70
connectors/src/webauthn/common-webauthn.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
export function buildDataString(assertedCredential: PublicKeyCredential) {
|
||||
const response = assertedCredential.response as AuthenticatorAssertionResponse;
|
||||
|
||||
const authData = new Uint8Array(response.authenticatorData);
|
||||
const clientDataJSON = new Uint8Array(response.clientDataJSON);
|
||||
const rawId = new Uint8Array(assertedCredential.rawId);
|
||||
const sig = new Uint8Array(response.signature);
|
||||
|
||||
const data = {
|
||||
id: assertedCredential.id,
|
||||
rawId: coerceToBase64Url(rawId),
|
||||
type: assertedCredential.type,
|
||||
extensions: assertedCredential.getClientExtensionResults(),
|
||||
response: {
|
||||
authenticatorData: coerceToBase64Url(authData),
|
||||
clientDataJson: coerceToBase64Url(clientDataJSON),
|
||||
signature: coerceToBase64Url(sig),
|
||||
},
|
||||
};
|
||||
|
||||
return JSON.stringify(data);
|
||||
}
|
||||
|
||||
export function parseWebauthnJson(jsonString: string) {
|
||||
const json = JSON.parse(jsonString);
|
||||
|
||||
const challenge = json.challenge.replace(/-/g, "+").replace(/_/g, "/");
|
||||
json.challenge = Uint8Array.from(atob(challenge), (c) => c.charCodeAt(0));
|
||||
|
||||
json.allowCredentials.forEach((listItem: any) => {
|
||||
const fixedId = listItem.id.replace(/\_/g, "/").replace(/\-/g, "+");
|
||||
listItem.id = Uint8Array.from(atob(fixedId), (c) => c.charCodeAt(0));
|
||||
});
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
// From https://github.com/abergs/fido2-net-lib/blob/b487a1d47373ea18cd752b4988f7262035b7b54e/Demo/wwwroot/js/helpers.js#L34
|
||||
// License: https://github.com/abergs/fido2-net-lib/blob/master/LICENSE.txt
|
||||
function coerceToBase64Url(thing: any) {
|
||||
// Array or ArrayBuffer to Uint8Array
|
||||
if (Array.isArray(thing)) {
|
||||
thing = Uint8Array.from(thing);
|
||||
}
|
||||
|
||||
if (thing instanceof ArrayBuffer) {
|
||||
thing = new Uint8Array(thing);
|
||||
}
|
||||
|
||||
// Uint8Array to base64
|
||||
if (thing instanceof Uint8Array) {
|
||||
let str = "";
|
||||
const len = thing.byteLength;
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
str += String.fromCharCode(thing[i]);
|
||||
}
|
||||
thing = window.btoa(str);
|
||||
}
|
||||
|
||||
if (typeof thing !== "string") {
|
||||
throw new Error("could not coerce to string");
|
||||
}
|
||||
|
||||
// base64 to base64url
|
||||
// NOTE: "=" at the end of challenge is optional, strip it off here
|
||||
thing = thing.replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, "");
|
||||
|
||||
return thing;
|
||||
}
|
||||
167
connectors/src/webauthn/webauthn-fallback.ts
Normal file
167
connectors/src/webauthn/webauthn-fallback.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { b64Decode, getQsParam } from "../common";
|
||||
import { buildDataString, parseWebauthnJson } from "./common-webauthn";
|
||||
|
||||
// tslint:disable-next-line
|
||||
require("./webauthn.scss");
|
||||
|
||||
let parsed = false;
|
||||
let webauthnJson: any;
|
||||
let parentUrl: string = null;
|
||||
let parentOrigin: string = null;
|
||||
let sentSuccess = false;
|
||||
let locale: string = "en";
|
||||
|
||||
let locales: any = {};
|
||||
|
||||
function parseParameters() {
|
||||
if (parsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
parentUrl = getQsParam("parent");
|
||||
if (!parentUrl) {
|
||||
error("No parent.");
|
||||
return;
|
||||
} else {
|
||||
parentUrl = decodeURIComponent(parentUrl);
|
||||
parentOrigin = new URL(parentUrl).origin;
|
||||
}
|
||||
|
||||
locale = getQsParam("locale").replace("-", "_");
|
||||
|
||||
const version = getQsParam("v");
|
||||
|
||||
if (version === "1") {
|
||||
parseParametersV1();
|
||||
} else {
|
||||
parseParametersV2();
|
||||
}
|
||||
parsed = true;
|
||||
}
|
||||
|
||||
function parseParametersV1() {
|
||||
const data = getQsParam("data");
|
||||
if (!data) {
|
||||
error("No data.");
|
||||
return;
|
||||
}
|
||||
|
||||
webauthnJson = b64Decode(data);
|
||||
}
|
||||
|
||||
function parseParametersV2() {
|
||||
let dataObj: { data: any; btnText: string } = null;
|
||||
try {
|
||||
dataObj = JSON.parse(b64Decode(getQsParam("data")));
|
||||
} catch (e) {
|
||||
error("Cannot parse data.");
|
||||
return;
|
||||
}
|
||||
|
||||
webauthnJson = dataObj.data;
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
parseParameters();
|
||||
try {
|
||||
locales = await loadLocales(locale);
|
||||
} catch {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error("Failed to load the locale", locale);
|
||||
locales = await loadLocales("en");
|
||||
}
|
||||
|
||||
document.getElementById("msg").innerText = translate("webAuthnFallbackMsg");
|
||||
document.getElementById("remember-label").innerText = translate("rememberMe");
|
||||
|
||||
const button = document.getElementById("webauthn-button");
|
||||
button.innerText = translate("webAuthnAuthenticate");
|
||||
button.onclick = start;
|
||||
|
||||
document.getElementById("spinner").classList.add("d-none");
|
||||
const content = document.getElementById("content");
|
||||
content.classList.add("d-block");
|
||||
content.classList.remove("d-none");
|
||||
});
|
||||
|
||||
async function loadLocales(newLocale: string) {
|
||||
const filePath = `/locales/${newLocale}/messages.json?cache=${process.env.CACHE_TAG}`;
|
||||
const localesResult = await fetch(filePath);
|
||||
return await localesResult.json();
|
||||
}
|
||||
|
||||
function translate(id: string) {
|
||||
return locales[id]?.message || "";
|
||||
}
|
||||
|
||||
function start() {
|
||||
if (sentSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!("credentials" in navigator)) {
|
||||
error(translate("webAuthnNotSupported"));
|
||||
return;
|
||||
}
|
||||
|
||||
parseParameters();
|
||||
if (!webauthnJson) {
|
||||
error("No data.");
|
||||
return;
|
||||
}
|
||||
|
||||
let json: any;
|
||||
try {
|
||||
json = parseWebauthnJson(webauthnJson);
|
||||
} catch (e) {
|
||||
error("Cannot parse data.");
|
||||
return;
|
||||
}
|
||||
|
||||
initWebAuthn(json);
|
||||
}
|
||||
|
||||
async function initWebAuthn(obj: any) {
|
||||
try {
|
||||
const assertedCredential = (await navigator.credentials.get({
|
||||
publicKey: obj,
|
||||
})) as PublicKeyCredential;
|
||||
|
||||
if (sentSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataString = buildDataString(assertedCredential);
|
||||
const remember = (document.getElementById("remember") as HTMLInputElement).checked;
|
||||
window.postMessage({ command: "webAuthnResult", data: dataString, remember: remember }, "*");
|
||||
|
||||
sentSuccess = true;
|
||||
success(translate("webAuthnSuccess"));
|
||||
} catch (err) {
|
||||
error(err);
|
||||
}
|
||||
}
|
||||
|
||||
function error(message: string) {
|
||||
const el = document.getElementById("msg");
|
||||
resetMsgBox(el);
|
||||
el.textContent = message;
|
||||
el.classList.add("alert");
|
||||
el.classList.add("alert-danger");
|
||||
}
|
||||
|
||||
function success(message: string) {
|
||||
(document.getElementById("webauthn-button") as HTMLButtonElement).disabled = true;
|
||||
|
||||
const el = document.getElementById("msg");
|
||||
resetMsgBox(el);
|
||||
el.textContent = message;
|
||||
el.classList.add("alert");
|
||||
el.classList.add("alert-success");
|
||||
}
|
||||
|
||||
function resetMsgBox(el: HTMLElement) {
|
||||
el.classList.remove("alert");
|
||||
el.classList.remove("alert-danger");
|
||||
el.classList.remove("alert-success");
|
||||
}
|
||||
197
connectors/src/webauthn/webauthn.scss
Normal file
197
connectors/src/webauthn/webauthn.scss
Normal file
@@ -0,0 +1,197 @@
|
||||
@import "../common/styles.scss";
|
||||
|
||||
body {
|
||||
min-width: 0px !important;
|
||||
}
|
||||
|
||||
.mb-3,
|
||||
.my-3 {
|
||||
margin-bottom: 1rem !important;
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: 0.25rem !important;
|
||||
}
|
||||
|
||||
.img-fluid {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
user-select: none;
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
border-left-color: transparent;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
border-radius: 0.25rem;
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out,
|
||||
border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #175ddc;
|
||||
border-color: #175ddc;
|
||||
}
|
||||
|
||||
.btn:not(:disabled):not(.disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn:hover,
|
||||
.swal2-popup .swal2-actions button:hover {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
color: #fff;
|
||||
background-color: #134eb9;
|
||||
border-color: #1249ae;
|
||||
}
|
||||
|
||||
/** Mobile **/
|
||||
|
||||
.mt-5,
|
||||
.my-5 {
|
||||
margin-top: 3rem !important;
|
||||
}
|
||||
|
||||
.justify-content-center,
|
||||
.justify-content-md-center {
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-right: -10px;
|
||||
margin-left: -10px;
|
||||
}
|
||||
|
||||
.mb-2,
|
||||
.my-2 {
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
.ml-4,
|
||||
.mx-4 {
|
||||
margin-left: 1.5rem !important;
|
||||
}
|
||||
|
||||
.mb-4,
|
||||
.my-4 {
|
||||
margin-bottom: 1.5rem !important;
|
||||
}
|
||||
|
||||
.mr-4,
|
||||
.mx-4 {
|
||||
margin-right: 1.5rem !important;
|
||||
}
|
||||
|
||||
.lead {
|
||||
font-size: 1.25rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/** Fallback **/
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
max-width: 980px;
|
||||
}
|
||||
|
||||
.col-5 {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-right: 10px;
|
||||
padding-left: 10px;
|
||||
flex: 0 0 41.6666666667%;
|
||||
max-width: 41.6666666667%;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
word-wrap: break-word;
|
||||
background-color: #fff;
|
||||
background-clip: border-box;
|
||||
border: 1px solid rgba(0, 0, 0, 0.125);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.d-block {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 1px;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.alert {
|
||||
position: relative;
|
||||
padding: 0.75rem 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid transparent;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
color: #73271e;
|
||||
background-color: #f8dbd7;
|
||||
border-color: #f5cdc8;
|
||||
}
|
||||
|
||||
.form-check {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.form-check-input {
|
||||
position: absolute;
|
||||
margin-top: 0.3rem;
|
||||
margin-left: -1.25rem;
|
||||
}
|
||||
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-check-label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 0;
|
||||
border-top-color: currentcolor;
|
||||
border-top-style: none;
|
||||
border-top-width: 0px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
201
connectors/src/webauthn/webauthn.ts
Normal file
201
connectors/src/webauthn/webauthn.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { b64Decode, getQsParam } from "../common";
|
||||
import { buildDataString, parseWebauthnJson } from "./common-webauthn";
|
||||
|
||||
// tslint:disable-next-line
|
||||
require("./webauthn.scss");
|
||||
|
||||
const mobileCallbackUri = "bitwarden://webauthn-callback";
|
||||
|
||||
let parsed = false;
|
||||
let webauthnJson: any;
|
||||
let headerText: string = null;
|
||||
let btnText: string = null;
|
||||
let btnReturnText: string = null;
|
||||
let parentUrl: string = null;
|
||||
let parentOrigin: string = null;
|
||||
let mobileResponse = false;
|
||||
let stopWebAuthn = false;
|
||||
let sentSuccess = false;
|
||||
let obj: any = null;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
init();
|
||||
|
||||
parseParameters();
|
||||
if (headerText) {
|
||||
const header = document.getElementById("webauthn-header");
|
||||
header.innerText = decodeURI(headerText);
|
||||
}
|
||||
if (btnText) {
|
||||
const button = document.getElementById("webauthn-button");
|
||||
button.innerText = decodeURI(btnText);
|
||||
button.onclick = executeWebAuthn;
|
||||
}
|
||||
});
|
||||
|
||||
function init() {
|
||||
start();
|
||||
onMessage();
|
||||
info("ready");
|
||||
}
|
||||
|
||||
function parseParameters() {
|
||||
if (parsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
parentUrl = getQsParam("parent");
|
||||
if (!parentUrl) {
|
||||
error("No parent.");
|
||||
return;
|
||||
} else {
|
||||
parentUrl = decodeURIComponent(parentUrl);
|
||||
parentOrigin = new URL(parentUrl).origin;
|
||||
}
|
||||
|
||||
const version = getQsParam("v");
|
||||
|
||||
if (version === "1") {
|
||||
parseParametersV1();
|
||||
} else {
|
||||
parseParametersV2();
|
||||
}
|
||||
parsed = true;
|
||||
}
|
||||
|
||||
function parseParametersV1() {
|
||||
const data = getQsParam("data");
|
||||
if (!data) {
|
||||
error("No data.");
|
||||
return;
|
||||
}
|
||||
|
||||
webauthnJson = b64Decode(data);
|
||||
headerText = getQsParam("headerText");
|
||||
btnText = getQsParam("btnText");
|
||||
btnReturnText = getQsParam("btnReturnText");
|
||||
}
|
||||
|
||||
function parseParametersV2() {
|
||||
let dataObj: {
|
||||
data: any;
|
||||
headerText: string;
|
||||
btnText: string;
|
||||
btnReturnText: string;
|
||||
callbackUri?: string;
|
||||
mobile?: boolean;
|
||||
} = null;
|
||||
try {
|
||||
dataObj = JSON.parse(b64Decode(getQsParam("data")));
|
||||
} catch (e) {
|
||||
error("Cannot parse data.");
|
||||
return;
|
||||
}
|
||||
|
||||
mobileResponse = dataObj.callbackUri != null || dataObj.mobile === true;
|
||||
webauthnJson = dataObj.data;
|
||||
headerText = dataObj.headerText;
|
||||
btnText = dataObj.btnText;
|
||||
btnReturnText = dataObj.btnReturnText;
|
||||
}
|
||||
|
||||
function start() {
|
||||
sentSuccess = false;
|
||||
|
||||
if (!("credentials" in navigator)) {
|
||||
error("WebAuthn is not supported in this browser.");
|
||||
return;
|
||||
}
|
||||
|
||||
parseParameters();
|
||||
if (!webauthnJson) {
|
||||
error("No data.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
obj = parseWebauthnJson(webauthnJson);
|
||||
} catch (e) {
|
||||
error("Cannot parse webauthn data.");
|
||||
return;
|
||||
}
|
||||
|
||||
stopWebAuthn = false;
|
||||
|
||||
if (
|
||||
mobileResponse ||
|
||||
(navigator.userAgent.indexOf(" Safari/") !== -1 && navigator.userAgent.indexOf("Chrome") === -1)
|
||||
) {
|
||||
// Safari and mobile chrome blocks non-user initiated WebAuthn requests.
|
||||
} else {
|
||||
executeWebAuthn();
|
||||
}
|
||||
}
|
||||
|
||||
function executeWebAuthn() {
|
||||
if (stopWebAuthn) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.credentials.get({ publicKey: obj }).then(success).catch(error);
|
||||
}
|
||||
|
||||
function onMessage() {
|
||||
window.addEventListener(
|
||||
"message",
|
||||
(event) => {
|
||||
if (!event.origin || event.origin === "" || event.origin !== parentOrigin) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data === "stop") {
|
||||
stopWebAuthn = true;
|
||||
} else if (event.data === "start" && stopWebAuthn) {
|
||||
start();
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
function error(message: string) {
|
||||
if (mobileResponse) {
|
||||
document.location.replace(mobileCallbackUri + "?error=" + encodeURIComponent(message));
|
||||
returnButton(mobileCallbackUri + "?error=" + encodeURIComponent(message));
|
||||
} else {
|
||||
parent.postMessage("error|" + message, parentUrl);
|
||||
}
|
||||
}
|
||||
|
||||
function success(assertedCredential: PublicKeyCredential) {
|
||||
if (sentSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataString = buildDataString(assertedCredential);
|
||||
|
||||
if (mobileResponse) {
|
||||
document.location.replace(mobileCallbackUri + "?data=" + encodeURIComponent(dataString));
|
||||
returnButton(mobileCallbackUri + "?data=" + encodeURIComponent(dataString));
|
||||
} else {
|
||||
parent.postMessage("success|" + dataString, parentUrl);
|
||||
sentSuccess = true;
|
||||
}
|
||||
}
|
||||
|
||||
function info(message: string) {
|
||||
if (mobileResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
parent.postMessage("info|" + message, parentUrl);
|
||||
}
|
||||
|
||||
function returnButton(uri: string) {
|
||||
// provides 'return' button in case scripted navigation is blocked
|
||||
const button = document.getElementById("webauthn-button");
|
||||
button.innerText = decodeURI(btnReturnText);
|
||||
button.onclick = () => {
|
||||
document.location.replace(uri);
|
||||
};
|
||||
}
|
||||
150
connectors/webpack.config.js
Normal file
150
connectors/webpack.config.js
Normal file
@@ -0,0 +1,150 @@
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const HtmlWebpackInjector = require("html-webpack-injector");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV == null ? "development" : process.env.NODE_ENV;
|
||||
|
||||
const moduleRules = [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
enforce: "pre",
|
||||
loader: "tslint-loader",
|
||||
},
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: [
|
||||
{
|
||||
loader: "ts-loader",
|
||||
options: {
|
||||
transpileOnly: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(html)$/,
|
||||
loader: "html-loader",
|
||||
},
|
||||
{
|
||||
test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
|
||||
exclude: /loading(|-white).svg/,
|
||||
generator: {
|
||||
filename: "fonts/[name].[contenthash][ext]",
|
||||
},
|
||||
type: "asset/resource",
|
||||
},
|
||||
{
|
||||
test: /\.(jpe?g|png|gif|svg|webp|avif)$/i,
|
||||
exclude: /.*(fontawesome-webfont)\.svg/,
|
||||
generator: {
|
||||
filename: "images/[name].[contenthash][ext]",
|
||||
},
|
||||
type: "asset/resource",
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
},
|
||||
"css-loader",
|
||||
"sass-loader",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const plugins = [
|
||||
new HtmlWebpackInjector(),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/duo.html",
|
||||
filename: "duo.html",
|
||||
chunks: ["duo"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/webauthn.html",
|
||||
filename: "webauthn.html",
|
||||
chunks: ["webauthn"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/webauthn-mobile.html",
|
||||
filename: "webauthn-mobile.html",
|
||||
chunks: ["webauthn"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/webauthn-fallback.html",
|
||||
filename: "webauthn-fallback.html",
|
||||
chunks: ["webauthn-fallback"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/sso.html",
|
||||
filename: "sso.html",
|
||||
chunks: ["sso"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/captcha.html",
|
||||
filename: "captcha.html",
|
||||
chunks: ["captcha"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/captcha-mobile.html",
|
||||
filename: "captcha-mobile.html",
|
||||
chunks: ["captcha"],
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "assets/[name].[contenthash].css",
|
||||
chunkFilename: "assets/[id].[contenthash].css",
|
||||
}),
|
||||
new webpack.EnvironmentPlugin({
|
||||
CACHE_TAG: Math.random().toString(36).substring(7),
|
||||
}),
|
||||
new webpack.ProvidePlugin({
|
||||
process: "process/browser",
|
||||
}),
|
||||
];
|
||||
|
||||
const webpackConfig = {
|
||||
mode: NODE_ENV,
|
||||
devtool: "source-map",
|
||||
entry: {
|
||||
webauthn: "./src/webauthn/webauthn.ts",
|
||||
"webauthn-fallback": "./src/webauthn/webauthn-fallback.ts",
|
||||
duo: "./src/duo/duo.ts",
|
||||
sso: "./src/sso/sso.ts",
|
||||
captcha: "./src/captcha/captcha.ts",
|
||||
},
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
cacheGroups: {
|
||||
commons: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name: "app/vendor",
|
||||
chunks: (chunk) => {
|
||||
return chunk.name === "app/main";
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".ts", ".js"],
|
||||
symlinks: false,
|
||||
modules: [path.resolve("../", "node_modules")],
|
||||
fallback: {
|
||||
buffer: false,
|
||||
util: require.resolve("util/"),
|
||||
assert: false,
|
||||
},
|
||||
},
|
||||
output: {
|
||||
filename: "assets/[name].[contenthash].js",
|
||||
path: path.resolve(__dirname, "build"),
|
||||
publicPath: "/connectors/",
|
||||
clean: true,
|
||||
},
|
||||
module: { rules: moduleRules },
|
||||
plugins: plugins,
|
||||
};
|
||||
|
||||
module.exports = webpackConfig;
|
||||
@@ -1,7 +1,9 @@
|
||||
project_id_env: _CROWDIN_PROJECT_ID
|
||||
api_token_env: CROWDIN_API_TOKEN
|
||||
preserve_hierarchy: true
|
||||
files:
|
||||
- source: /src/locales/en/messages.json
|
||||
dest: /src/locales/en/%file_name%.%file_extension%
|
||||
translation: /src/locales/%two_letters_code%/%original_file_name%
|
||||
update_option: update_as_unapproved
|
||||
languages_mapping:
|
||||
|
||||
37
gulpfile.js
37
gulpfile.js
@@ -1,37 +0,0 @@
|
||||
const gulp = require('gulp');
|
||||
const googleWebFonts = require('gulp-google-webfonts');
|
||||
const del = require('del');
|
||||
const package = require('./package.json');
|
||||
const fs = require('fs');
|
||||
|
||||
const paths = {
|
||||
node_modules: './node_modules/',
|
||||
src: './src/',
|
||||
build: './build/',
|
||||
cssDir: './src/css/',
|
||||
};
|
||||
|
||||
function clean() {
|
||||
return del([paths.cssDir]);
|
||||
}
|
||||
|
||||
function webfonts() {
|
||||
return gulp.src('./webfonts.list')
|
||||
.pipe(googleWebFonts({
|
||||
fontsDir: 'webfonts',
|
||||
cssFilename: 'webfonts.css',
|
||||
format: 'woff',
|
||||
}))
|
||||
.pipe(gulp.dest(paths.cssDir));
|
||||
};
|
||||
|
||||
function version(cb) {
|
||||
fs.writeFileSync(paths.build + 'version.json', '{"version":"' + package.version + '"}');
|
||||
cb();
|
||||
}
|
||||
|
||||
exports.clean = clean;
|
||||
exports.webfonts = gulp.series(clean, webfonts);
|
||||
exports.prebuild = gulp.series(clean, webfonts);
|
||||
exports.version = version;
|
||||
exports.postdist = version;
|
||||
2
jslib
2
jslib
Submodule jslib updated: 5784a6d4fc...92a65b7b36
20101
package-lock.json
generated
20101
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
114
package.json
114
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bitwarden-web",
|
||||
"version": "2.22.3",
|
||||
"name": "@bitwarden/web-vault",
|
||||
"version": "2.25.1",
|
||||
"license": "GPL-3.0",
|
||||
"repository": "https://github.com/bitwarden/web",
|
||||
"scripts": {
|
||||
@@ -11,65 +11,75 @@
|
||||
"symlink:win": "rm -rf ./jslib && cmd /c mklink /J .\\jslib ..\\jslib",
|
||||
"symlink:mac": "npm run symlink:lin",
|
||||
"symlink:lin": "rm -rf ./jslib && ln -s ../jslib ./jslib",
|
||||
"build": "gulp prebuild && webpack -c bitwarden_license/webpack.config.js",
|
||||
"build:oss": "gulp prebuild && webpack",
|
||||
"build:watch": "gulp prebuild && webpack serve -c bitwarden_license/webpack.config.js",
|
||||
"build:watch:oss": "gulp prebuild && webpack serve",
|
||||
"build:dev": "cross-env ENV=development npm run build",
|
||||
"build:dev:watch": "cross-env ENV=development npm run build:watch",
|
||||
"build:qa": "cross-env NODE_ENV=production ENV=qa npm run build",
|
||||
"build:cloud": "cross-env NODE_ENV=production ENV=cloud npm run build",
|
||||
"build:cloud:oss": "cross-env NODE_ENV=production ENV=cloud npm run build:oss",
|
||||
"build:selfhost": "cross-env ENV=selfhosted npm run build:watch",
|
||||
"build:selfhost:watch": "cross-env ENV=selfhosted npm run build:watch",
|
||||
"build:selfhost:prod": "cross-env ENV=selfhosted NODE_ENV=production npm run build",
|
||||
"build:selfhost:prod:oss": "cross-env ENV=selfhosted NODE_ENV=production npm run build:oss",
|
||||
"build:oss": "webpack",
|
||||
"build:bit": "webpack -c bitwarden_license/webpack.config.js",
|
||||
"build:oss:watch": "webpack serve",
|
||||
"build:bit:watch": "webpack serve -c bitwarden_license/webpack.config.js",
|
||||
"build:bit:dev": "cross-env ENV=development npm run build:bit",
|
||||
"build:bit:dev:watch": "cross-env ENV=development npm run build:bit:watch",
|
||||
"build:bit:qa": "cross-env NODE_ENV=production ENV=qa npm run build:bit",
|
||||
"build:bit:cloud": "cross-env NODE_ENV=production ENV=cloud npm run build:bit",
|
||||
"build:oss:selfhost:watch": "cross-env ENV=selfhosted npm run build:oss:watch",
|
||||
"build:bit:selfhost:watch": "cross-env ENV=selfhosted npm run build:bit:watch",
|
||||
"build:oss:selfhost:prod": "cross-env ENV=selfhosted NODE_ENV=production npm run build:oss",
|
||||
"build:bit:selfhost:prod": "cross-env ENV=selfhosted NODE_ENV=production npm run build:bit",
|
||||
"clean:l10n": "git push origin --delete l10n_master",
|
||||
"dist": "npm run build:cloud && gulp postdist",
|
||||
"dist:oss": "npm run build:cloud:oss && gulp postdist",
|
||||
"dist:selfhost": "npm run build:selfhost:prod && gulp postdist",
|
||||
"dist:selfhost:oss": "npm run build:selfhost:prod:oss && gulp postdist",
|
||||
"deploy": "npm run dist && gh-pages -d build",
|
||||
"deploy:dev": "npm run dist && gh-pages -d build -r git@github.com:kspearrin/bitwarden-web-dev.git",
|
||||
"lint": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' || true",
|
||||
"lint:fix": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' --fix"
|
||||
"dist:bit:cloud": "npm run build:bit:cloud",
|
||||
"dist:oss:selfhost": "npm run build:oss:selfhost:prod",
|
||||
"dist:bit:selfhost": "npm run build:bit:selfhost:prod",
|
||||
"deploy": "npm run dist:bit && gh-pages -d build",
|
||||
"deploy:dev": "npm run dist:bit && gh-pages -d build -r git@github.com:kspearrin/bitwarden-web-dev.git",
|
||||
"lint": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' && prettier --check .",
|
||||
"lint:fix": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' --fix",
|
||||
"prettier": "prettier --write .",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/compiler-cli": "^11.2.11",
|
||||
"@ngtools/webpack": "^11.2.10",
|
||||
"@angular/compiler-cli": "^12.2.13",
|
||||
"@ngtools/webpack": "^12.2.13",
|
||||
"@types/jquery": "^3.5.5",
|
||||
"@types/node": "^14.17.2",
|
||||
"@types/node": "^16.11.12",
|
||||
"@types/webcrypto": "^0.0.28",
|
||||
"@types/webpack": "^4.4.27",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"copy-webpack-plugin": "^6.4.0",
|
||||
"@types/webpack": "^5.28.0",
|
||||
"buffer": "^6.0.3",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"copy-webpack-plugin": "^10.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^5.2.3",
|
||||
"del": "^6.0.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"css-loader": "^6.5.1",
|
||||
"gh-pages": "^3.1.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-google-webfonts": "^4.0.0",
|
||||
"html-loader": "^1.3.2",
|
||||
"html-webpack-plugin": "^4.5.1",
|
||||
"mini-css-extract-plugin": "^1.5.0",
|
||||
"html-loader": "^3.0.1",
|
||||
"html-webpack-injector": "1.1.4",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"husky": "^7.0.4",
|
||||
"lint-staged": "^12.1.2",
|
||||
"mini-css-extract-plugin": "^2.4.5",
|
||||
"prettier": "2.5.1",
|
||||
"process": "^0.11.10",
|
||||
"sass": "^1.32.10",
|
||||
"sass-loader": "^10.1.1",
|
||||
"style-loader": "^2.0.0",
|
||||
"tapable": "^1.1.3",
|
||||
"terser-webpack-plugin": "^4.2.3",
|
||||
"ts-loader": "^8.1.0",
|
||||
"sass-loader": "^12.4.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"terser-webpack-plugin": "^5.2.5",
|
||||
"ts-loader": "^9.2.5",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint-loader": "^3.5.4",
|
||||
"typescript": "4.1.5",
|
||||
"webpack": "^4.46.0",
|
||||
"webpack-cli": "^4.6.0",
|
||||
"webpack-dev-server": "^3.11.2"
|
||||
"typescript": "4.3.5",
|
||||
"util": "^0.12.4",
|
||||
"webpack": "^5.64.4",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"webpack-dev-server": "^4.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "^12.2.13",
|
||||
"@angular/cdk": "^12.2.13",
|
||||
"@angular/common": "^12.2.13",
|
||||
"@angular/compiler": "^12.2.13",
|
||||
"@angular/core": "^12.2.13",
|
||||
"@angular/forms": "^12.2.13",
|
||||
"@angular/platform-browser": "^12.2.13",
|
||||
"@angular/platform-browser-dynamic": "^12.2.13",
|
||||
"@angular/router": "^12.2.13",
|
||||
"@bitwarden/jslib-angular": "file:jslib/angular",
|
||||
"@bitwarden/jslib-common": "file:jslib/common",
|
||||
"angular2-toaster": "11.0.1",
|
||||
"bootstrap": "4.6.0",
|
||||
"braintree-web-drop-in": "1.30.1",
|
||||
"browser-hrtime": "^1.1.8",
|
||||
@@ -78,14 +88,20 @@
|
||||
"font-awesome": "4.7.0",
|
||||
"jquery": "3.6.0",
|
||||
"ngx-infinite-scroll": "^10.0.1",
|
||||
"ngx-toastr": "14.1.4",
|
||||
"popper.js": "1.16.1",
|
||||
"qrious": "4.0.2",
|
||||
"rxjs": "^7.4.0",
|
||||
"sweetalert2": "^10.16.6",
|
||||
"webcrypto-shim": "0.1.7",
|
||||
"whatwg-fetch": "3.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "~14",
|
||||
"npm": "~7"
|
||||
"node": "~16",
|
||||
"npm": "~8"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "prettier --ignore-unknown --write",
|
||||
"*.png": "node scripts/optimize.js"
|
||||
}
|
||||
}
|
||||
|
||||
21
scripts/optimize.js
Normal file
21
scripts/optimize.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const child_process = require("child_process");
|
||||
const path = require("path");
|
||||
|
||||
const images = process.argv.slice(2);
|
||||
|
||||
images.forEach((img) => {
|
||||
switch (img.split(".").pop()) {
|
||||
case "png":
|
||||
child_process.execSync(
|
||||
`npx @squoosh/cli --oxipng {} --output-dir "${path.dirname(img)}" "${img}"`
|
||||
);
|
||||
break;
|
||||
case "jpg":
|
||||
child_process.execSync(
|
||||
`npx @squoosh/cli --mozjpeg {"quality":85,"baseline":false,"arithmetic":false,"progressive":true,"optimize_coding":true,"smoothing":0,"color_space":3,"quant_table":3,"trellis_multipass":false,"trellis_opt_zero":false,"trellis_opt_table":false,"trellis_loops":1,"auto_subsample":true,"chroma_subsample":2,"separate_chroma_quality":false,"chroma_quality":75} --output-dir "${path.dirname(
|
||||
img
|
||||
)}" "${img}"`
|
||||
);
|
||||
break;
|
||||
}
|
||||
});
|
||||
86
src/404.html
86
src/404.html
@@ -1,50 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<link href="/404/bootstrap.min.css" rel="stylesheet" type="text/css"
|
||||
integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l">
|
||||
<link href="/404/font-awesome.min.css" rel="stylesheet" type="text/css"
|
||||
integrity="sha512-SfTiTlX6kk+qitfevl/7LibUOeJWlt9rbyDn92a1DqWOw9vWG2MFoays0sgObmWazO5BQPiFucnnEAjpAB+/Sw==">
|
||||
<link href="/404/styles.css" rel="stylesheet" type="text/css">
|
||||
<link
|
||||
href="/404/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
integrity="sha384-hA/ESrxp2b05ywLtD9YwM6m+pNyLRY4+ruk6dWK00SM4k6SQs0bfrITJVSf6uZyH"
|
||||
/>
|
||||
<link href="/404/styles.css" rel="stylesheet" type="text/css" />
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/favicon-16x16.png">
|
||||
<link rel="mask-icon" href="/images/icons/safari-pinned-tab.svg" color="#175DDC">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/favicon-16x16.png" />
|
||||
<link rel="mask-icon" href="/images/icons/safari-pinned-tab.svg" color="#175DDC" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<title>Page not found!</title>
|
||||
<meta name="description" content="404 Page Not Found">
|
||||
</head>
|
||||
<title>Page not found!</title>
|
||||
<meta name="description" content="404 Page Not Found" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="banner">
|
||||
<div class="container inner banner">
|
||||
<div class="row align-items-center">
|
||||
<div class="col brand">
|
||||
<i class="fa fa-shield"></i>
|
||||
<strong>bit</strong>warden</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<body>
|
||||
<div class="banner">
|
||||
<div class="container inner banner">
|
||||
<div class="row align-items-center">
|
||||
<div class="col brand">
|
||||
<i class="bwi bwi-shield"></i> <strong>bit</strong>warden
|
||||
</div>
|
||||
</div>
|
||||
<div class="container inner content">
|
||||
<h2>Page not found!</h2>
|
||||
<p>Sorry, but the page you were looking for could not be found.</p>
|
||||
<p>
|
||||
<a href="/">
|
||||
<img src="/images/404.png" class="img-fluid" alt="404 image" width="80%"/>
|
||||
</a>
|
||||
</p>
|
||||
<p>You can <a href="/">return to the web vault</a>, check our <a href="https://status.bitwarden.com/">status page</a>
|
||||
or <a href="https://bitwarden.com/contact/">contact us</a>.</p>
|
||||
</div>
|
||||
<div class="container footer text-muted content">
|
||||
© Copyright 2021 Bitwarden, Inc.
|
||||
</div>
|
||||
</body>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container inner content">
|
||||
<h2>Page not found!</h2>
|
||||
<p>Sorry, but the page you were looking for could not be found.</p>
|
||||
<p>
|
||||
<a href="/">
|
||||
<img src="/images/404.png" class="img-fluid" alt="404 image" width="80%" />
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
You can <a href="/">return to the web vault</a>, check our
|
||||
<a href="https://status.bitwarden.com/">status page</a> or
|
||||
<a href="https://bitwarden.com/contact/">contact us</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="container footer text-muted content">© Copyright 2022 Bitwarden, Inc.</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
2
src/404/bootstrap.min.css
vendored
2
src/404/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user