mirror of
https://github.com/bitwarden/web
synced 2025-12-06 00:03:28 +00:00
Compare commits
123 Commits
feature/re
...
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 |
@@ -12,7 +12,7 @@ insert_final_newline = true
|
|||||||
[*.{js,ts,scss,html}]
|
[*.{js,ts,scss,html}]
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 4
|
indent_size = 2
|
||||||
|
|
||||||
[*.{ts}]
|
[*.{ts}]
|
||||||
quote_type = single
|
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
|
* text=auto eol=lf
|
||||||
.dockerignore eol=lf
|
|
||||||
dockerfile eol=lf
|
|
||||||
|
|||||||
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)
|
||||||
133
.github/workflows/build.yml
vendored
133
.github/workflows/build.yml
vendored
@@ -9,8 +9,8 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
push:
|
push:
|
||||||
branches-ignore:
|
branches-ignore:
|
||||||
- 'l10n_master'
|
- "l10n_master"
|
||||||
- 'gh-pages'
|
- "gh-pages"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cloc:
|
cloc:
|
||||||
@@ -28,7 +28,6 @@ jobs:
|
|||||||
- name: Print lines of code
|
- name: Print lines of code
|
||||||
run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
|
run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
|
||||||
|
|
||||||
|
|
||||||
setup:
|
setup:
|
||||||
name: Setup
|
name: Setup
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
@@ -40,9 +39,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Get GitHub sha as version
|
- name: Get GitHub sha as version
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: echo "::set-output name=value::${GITHUB_SHA:0:7}"
|
||||||
echo "::set-output name=value::${GITHUB_SHA:0:7}"
|
|
||||||
|
|
||||||
|
|
||||||
build-oss-selfhost:
|
build-oss-selfhost:
|
||||||
name: Build OSS zip
|
name: Build OSS zip
|
||||||
@@ -54,17 +51,13 @@ jobs:
|
|||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||||
with:
|
with:
|
||||||
node-version: '14'
|
node-version: "16"
|
||||||
|
|
||||||
- name: Update NPM
|
|
||||||
run: |
|
|
||||||
npm install -g npm@7
|
|
||||||
|
|
||||||
- name: Cache npm
|
- name: Cache npm
|
||||||
id: npm-cache
|
id: npm-cache
|
||||||
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
||||||
with:
|
with:
|
||||||
path: '~/.npm'
|
path: "~/.npm"
|
||||||
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
@@ -95,7 +88,6 @@ jobs:
|
|||||||
path: ./web-${{ env._VERSION }}-selfhosted-open-source.zip
|
path: ./web-${{ env._VERSION }}-selfhosted-open-source.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
|
||||||
build-cloud:
|
build-cloud:
|
||||||
name: Build Cloud zip
|
name: Build Cloud zip
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
@@ -106,17 +98,13 @@ jobs:
|
|||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||||
with:
|
with:
|
||||||
node-version: '14'
|
node-version: "16"
|
||||||
|
|
||||||
- name: Update NPM
|
|
||||||
run: |
|
|
||||||
npm install -g npm@7
|
|
||||||
|
|
||||||
- name: Cache npm
|
- name: Cache npm
|
||||||
id: npm-cache
|
id: npm-cache
|
||||||
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
||||||
with:
|
with:
|
||||||
path: '~/.npm'
|
path: "~/.npm"
|
||||||
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
@@ -147,7 +135,6 @@ jobs:
|
|||||||
path: ./web-${{ env._VERSION }}-cloud-COMMERCIAL.zip
|
path: ./web-${{ env._VERSION }}-cloud-COMMERCIAL.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
|
||||||
build-commercial-selfhost:
|
build-commercial-selfhost:
|
||||||
name: Build SelfHost Docker image
|
name: Build SelfHost Docker image
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
@@ -158,17 +145,13 @@ jobs:
|
|||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||||
with:
|
with:
|
||||||
node-version: '14'
|
node-version: "16"
|
||||||
|
|
||||||
- name: Update NPM
|
|
||||||
run: |
|
|
||||||
npm install -g npm@7
|
|
||||||
|
|
||||||
- name: Cache npm
|
- name: Cache npm
|
||||||
id: npm-cache
|
id: npm-cache
|
||||||
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
||||||
with:
|
with:
|
||||||
path: '~/.npm'
|
path: "~/.npm"
|
||||||
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
@@ -182,7 +165,7 @@ jobs:
|
|||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
- name: Setup DCT
|
- name: Setup DCT
|
||||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
|
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix'
|
||||||
id: setup-dct
|
id: setup-dct
|
||||||
uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff
|
uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff
|
||||||
with:
|
with:
|
||||||
@@ -228,28 +211,38 @@ jobs:
|
|||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/master'
|
||||||
run: docker tag bitwarden/web bitwarden/web:dev
|
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
|
- 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
|
run: docker images
|
||||||
|
|
||||||
- name: Push rc images
|
- name: Push rc image
|
||||||
if: github.ref == 'refs/heads/rc'
|
if: github.ref == 'refs/heads/rc'
|
||||||
run: docker push bitwarden/web:rc
|
run: docker push bitwarden/web:rc
|
||||||
env:
|
env:
|
||||||
DOCKER_CONTENT_TRUST: 1
|
DOCKER_CONTENT_TRUST: 1
|
||||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
|
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
|
||||||
|
|
||||||
- name: Push dev images
|
- name: Push dev image
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/master'
|
||||||
run: docker push bitwarden/web:dev
|
run: docker push bitwarden/web:dev
|
||||||
env:
|
env:
|
||||||
DOCKER_CONTENT_TRUST: 1
|
DOCKER_CONTENT_TRUST: 1
|
||||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
|
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
|
||||||
|
|
||||||
- name: Log out of Docker
|
- name: Push hotfix image
|
||||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
|
if: github.ref == 'refs/heads/hotfix'
|
||||||
run: docker logout
|
run: docker push bitwarden/web:hotfix
|
||||||
|
env:
|
||||||
|
DOCKER_CONTENT_TRUST: 1
|
||||||
|
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
|
||||||
|
|
||||||
|
- name: Log out of Docker
|
||||||
|
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix'
|
||||||
|
run: docker logout
|
||||||
|
|
||||||
build-qa:
|
build-qa:
|
||||||
name: Build Docker images for QA environment
|
name: Build Docker images for QA environment
|
||||||
@@ -258,17 +251,13 @@ jobs:
|
|||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||||
with:
|
with:
|
||||||
node-version: '14'
|
node-version: "16"
|
||||||
|
|
||||||
- name: Update NPM
|
|
||||||
run: |
|
|
||||||
npm install -g npm@7
|
|
||||||
|
|
||||||
- name: Cache npm
|
- name: Cache npm
|
||||||
id: npm-cache
|
id: npm-cache
|
||||||
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
||||||
with:
|
with:
|
||||||
path: '~/.npm'
|
path: "~/.npm"
|
||||||
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
@@ -318,7 +307,7 @@ jobs:
|
|||||||
- name: Get image tag
|
- name: Get image tag
|
||||||
id: image-tag
|
id: image-tag
|
||||||
run: |
|
run: |
|
||||||
IMAGE_TAG=$(echo "$GITHUB_REF" | awk '{split($0, a, "/"); print a[3];}')
|
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g")
|
||||||
TAG_EXTENSION=${{ github.event.inputs.custom_tag_extension }}
|
TAG_EXTENSION=${{ github.event.inputs.custom_tag_extension }}
|
||||||
|
|
||||||
if [[ $TAG_EXTENSION ]]; then
|
if [[ $TAG_EXTENSION ]]; then
|
||||||
@@ -350,7 +339,6 @@ jobs:
|
|||||||
- name: Log out of Docker
|
- name: Log out of Docker
|
||||||
run: docker logout
|
run: docker logout
|
||||||
|
|
||||||
|
|
||||||
windows:
|
windows:
|
||||||
name: Test code on Windows
|
name: Test code on Windows
|
||||||
runs-on: windows-2019
|
runs-on: windows-2019
|
||||||
@@ -358,7 +346,7 @@ jobs:
|
|||||||
- name: Set up NuGet
|
- name: Set up NuGet
|
||||||
uses: nuget/setup-nuget@04b0c2b8d1b97922f67eca497d7cf0bf17b8ffe1
|
uses: nuget/setup-nuget@04b0c2b8d1b97922f67eca497d7cf0bf17b8ffe1
|
||||||
with:
|
with:
|
||||||
nuget-version: 'latest'
|
nuget-version: "latest"
|
||||||
|
|
||||||
- name: Set up MSBuild
|
- name: Set up MSBuild
|
||||||
uses: microsoft/setup-msbuild@c26a08ba26249b81327e26f6ef381897b6a8754d
|
uses: microsoft/setup-msbuild@c26a08ba26249b81327e26f6ef381897b6a8754d
|
||||||
@@ -367,17 +355,13 @@ jobs:
|
|||||||
id: npm-cache
|
id: npm-cache
|
||||||
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
||||||
with:
|
with:
|
||||||
path: '~/.npm'
|
path: "~/.npm"
|
||||||
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||||
with:
|
with:
|
||||||
node-version: '14'
|
node-version: "16"
|
||||||
|
|
||||||
- name: Update NPM
|
|
||||||
run: |
|
|
||||||
npm install -g npm@7
|
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
@@ -398,12 +382,49 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: NPM install
|
- name: Run linter
|
||||||
run: npm ci
|
run: npm run lint
|
||||||
|
|
||||||
- name: NPM build
|
- name: NPM build
|
||||||
run: npm run build:bit: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:
|
check-failures:
|
||||||
name: Check for failures
|
name: Check for failures
|
||||||
@@ -416,6 +437,7 @@ jobs:
|
|||||||
- build-cloud
|
- build-cloud
|
||||||
- build-commercial-selfhost
|
- build-commercial-selfhost
|
||||||
- build-qa
|
- build-qa
|
||||||
|
- crowdin-push
|
||||||
- windows
|
- windows
|
||||||
steps:
|
steps:
|
||||||
- name: Check if any job failed
|
- name: Check if any job failed
|
||||||
@@ -426,8 +448,9 @@ jobs:
|
|||||||
BUILD_OSS_SELFHOST_STATUS: ${{ needs.build-oss-selfhost.result }}
|
BUILD_OSS_SELFHOST_STATUS: ${{ needs.build-oss-selfhost.result }}
|
||||||
BUILD_CLOUD_STATUS: ${{ needs.build-cloud.result }}
|
BUILD_CLOUD_STATUS: ${{ needs.build-cloud.result }}
|
||||||
BUILD_COMMERCIAL_SELFHOST_STATUS: ${{ needs.build-commercial-selfhost.result }}
|
BUILD_COMMERCIAL_SELFHOST_STATUS: ${{ needs.build-commercial-selfhost.result }}
|
||||||
BUILD_QA: ${{ needs.build-qa.result }}
|
BUILD_QA_STATUS: ${{ needs.build-qa.result }}
|
||||||
WINDOWS: ${{ needs.windows.result }}
|
CROWDIN_PUSH_STATUS: ${{ needs.crowdin-push.result }}
|
||||||
|
WINDOWS_STATUS: ${{ needs.windows.result }}
|
||||||
run: |
|
run: |
|
||||||
if [ "$CLOC_STATUS" = "failure" ]; then
|
if [ "$CLOC_STATUS" = "failure" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
@@ -439,9 +462,11 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
elif [ "$BUILD_COMMERCIAL_SELFHOST_STATUS" = "failure" ]; then
|
elif [ "$BUILD_COMMERCIAL_SELFHOST_STATUS" = "failure" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
elif [ "$BUILD_QA" = "failure" ]; then
|
elif [ "$BUILD_QA_STATUS" = "failure" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
elif [ "$WINDOWS" = "failure" ]; then
|
elif [ "$CROWDIN_PUSH_STATUS" = "failure" ]; then
|
||||||
|
exit 1
|
||||||
|
elif [ "$WINDOWS_STATUS" = "failure" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
---
|
---
|
||||||
name: Crowdin Sync
|
name: Crowdin Pull
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs: {}
|
inputs: {}
|
||||||
# schedule:
|
schedule:
|
||||||
# - cron: '0 0 * * *'
|
- cron: "0 0 * * 5"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
crowdin-sync:
|
crowdin-pull:
|
||||||
name: Autosync
|
name: Pull
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
env:
|
env:
|
||||||
_CROWDIN_PROJECT_ID: "308189"
|
_CROWDIN_PROJECT_ID: "308189"
|
||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
secrets: "crowdin-api-token"
|
secrets: "crowdin-api-token"
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea
|
uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea # v1.3.2
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||||
19
.github/workflows/qa-deploy.yml
vendored
19
.github/workflows/qa-deploy.yml
vendored
@@ -9,8 +9,8 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
|
|
||||||
env:
|
env:
|
||||||
_QA_CLUSTER_RESOURCE_GROUP: "bitwarden-devops"
|
_QA_CLUSTER_RESOURCE_GROUP: "bw-env-qa"
|
||||||
_QA_CLUSTER_NAME: "dev-aks"
|
_QA_CLUSTER_NAME: "bw-aks-qa"
|
||||||
_QA_K8S_NAMESPACE: "bw-qa"
|
_QA_K8S_NAMESPACE: "bw-qa"
|
||||||
_QA_K8S_APP_NAME: "bw-web"
|
_QA_K8S_APP_NAME: "bw-web"
|
||||||
|
|
||||||
@@ -23,8 +23,7 @@ jobs:
|
|||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
||||||
|
|
||||||
- name: Setup
|
- name: Setup
|
||||||
run:
|
run: export PATH=$PATH:~/work/web/web
|
||||||
export PATH=$PATH:~/work/web/web
|
|
||||||
|
|
||||||
- name: Login to Azure
|
- name: Login to Azure
|
||||||
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
||||||
@@ -36,16 +35,16 @@ jobs:
|
|||||||
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
|
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
|
||||||
with:
|
with:
|
||||||
keyvault: "bitwarden-qa-kv"
|
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
|
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
||||||
with:
|
with:
|
||||||
creds: ${{ env.dev-aks-kubectl-credentials }}
|
creds: ${{ env.qa-aks-kubectl-credentials }}
|
||||||
|
|
||||||
- name: Setup AKS access
|
- name: Setup AKS access
|
||||||
env:
|
#env:
|
||||||
USER_ID: ${{ env.qa-kubectl-managed-identity-clientId }}
|
# USER_ID: ${{ env.qa-kubectl-managed-identity-clientId }}
|
||||||
run: |
|
run: |
|
||||||
echo "---az install---"
|
echo "---az install---"
|
||||||
az aks install-cli --install-location ./kubectl --kubelogin-install-location ./kubelogin
|
az aks install-cli --install-location ./kubectl --kubelogin-install-location ./kubelogin
|
||||||
@@ -55,7 +54,7 @@ jobs:
|
|||||||
- name: Get image tag
|
- name: Get image tag
|
||||||
id: image_tag
|
id: image_tag
|
||||||
run: |
|
run: |
|
||||||
IMAGE_TAG=$(echo "$GITHUB_REF" | awk '{split($0, a, "/"); print a[3];}')
|
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g")
|
||||||
TAG_EXTENSION=${{ github.event.inputs.image_extension }}
|
TAG_EXTENSION=${{ github.event.inputs.image_extension }}
|
||||||
|
|
||||||
if [[ $TAG_EXTENSION ]]; then
|
if [[ $TAG_EXTENSION ]]; then
|
||||||
|
|||||||
55
.github/workflows/release.yml
vendored
55
.github/workflows/release.yml
vendored
@@ -3,7 +3,15 @@ name: Release
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs: {}
|
inputs:
|
||||||
|
release_type:
|
||||||
|
description: 'Release Options'
|
||||||
|
required: true
|
||||||
|
default: 'Initial Release'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- Initial Release
|
||||||
|
- Redeploy
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
setup:
|
setup:
|
||||||
@@ -12,12 +20,13 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
release_version: ${{ steps.version.outputs.package }}
|
release_version: ${{ steps.version.outputs.package }}
|
||||||
tag_version: ${{ steps.version.outputs.tag }}
|
tag_version: ${{ steps.version.outputs.tag }}
|
||||||
|
branch-name: ${{ steps.branch.outputs.branch-name }}
|
||||||
steps:
|
steps:
|
||||||
- name: Branch check
|
- name: Branch check
|
||||||
run: |
|
run: |
|
||||||
if [[ "$GITHUB_REF" != "refs/heads/rc" ]]; then
|
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix" ]]; then
|
||||||
echo "==================================="
|
echo "==================================="
|
||||||
echo "[!] Can only release from rc branch"
|
echo "[!] Can only release from the 'rc' or 'hotfix' branches"
|
||||||
echo "==================================="
|
echo "==================================="
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -33,7 +42,8 @@ jobs:
|
|||||||
curl -sL https://api.github.com/repos/$GITHUB_REPOSITORY/releases/latest | jq -r ".tag_name"
|
curl -sL https://api.github.com/repos/$GITHUB_REPOSITORY/releases/latest | jq -r ".tag_name"
|
||||||
)
|
)
|
||||||
|
|
||||||
if [ "v$version" == "$previous_release_tag_version" ]; then
|
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"
|
echo "[!] Already released v$version. Please bump version to continue"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -41,12 +51,18 @@ jobs:
|
|||||||
echo "::set-output name=package::$version"
|
echo "::set-output name=package::$version"
|
||||||
echo "::set-output name=tag::v$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:
|
self-host:
|
||||||
name: Build self-host docker
|
name: Release self-host docker
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: setup
|
needs: setup
|
||||||
env:
|
env:
|
||||||
|
_BRANCH_NAME: ${{ needs.setup.outputs.branch-name }}
|
||||||
_RELEASE_VERSION: ${{ needs.setup.outputs.release_version }}
|
_RELEASE_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
@@ -66,29 +82,28 @@ jobs:
|
|||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||||
|
|
||||||
- name: Pull latest selfhost rc image
|
- name: Pull latest selfhost image
|
||||||
run: docker pull bitwarden/web:rc
|
run: docker pull bitwarden/web:$_BRANCH_NAME
|
||||||
|
|
||||||
- name: Tag version
|
- name: Tag version and latest
|
||||||
run: |
|
run: |
|
||||||
docker tag bitwarden/web:rc bitwarden/web:latest
|
docker tag bitwarden/web:$_BRANCH_NAME bitwarden/web:$_RELEASE_VERSION
|
||||||
docker tag bitwarden/web:rc bitwarden/web:$_RELEASE_VERSION
|
docker tag bitwarden/web:$_BRANCH_NAME bitwarden/web:latest
|
||||||
|
|
||||||
- name: List Docker images
|
- name: List Docker images
|
||||||
run: docker images
|
run: docker images
|
||||||
|
|
||||||
- name: Push images
|
- name: Push version and latest image
|
||||||
run: |
|
|
||||||
docker push bitwarden/web:latest
|
|
||||||
docker push bitwarden/web:$_RELEASE_VERSION
|
|
||||||
env:
|
env:
|
||||||
DOCKER_CONTENT_TRUST: 1
|
DOCKER_CONTENT_TRUST: 1
|
||||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
|
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
|
||||||
|
run: |
|
||||||
|
docker push bitwarden/web:$_RELEASE_VERSION
|
||||||
|
docker push bitwarden/web:latest
|
||||||
|
|
||||||
- name: Log out of Docker
|
- name: Log out of Docker
|
||||||
run: docker logout
|
run: docker logout
|
||||||
|
|
||||||
|
|
||||||
ghpages-deploy:
|
ghpages-deploy:
|
||||||
name: Deploy Web Vault
|
name: Deploy Web Vault
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
@@ -108,7 +123,9 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
git switch -c deploy-$_TAG_VERSION
|
git switch -c deploy-$_TAG_VERSION
|
||||||
git push -u origin deploy-$_TAG_VERSION
|
git push -u origin deploy-$_TAG_VERSION
|
||||||
git switch rc
|
|
||||||
|
- name: Checkout Repo
|
||||||
|
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
||||||
|
|
||||||
- name: Setup git config
|
- name: Setup git config
|
||||||
run: |
|
run: |
|
||||||
@@ -122,7 +139,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: rc
|
branch: ${{ needs.setup.outputs.branch-name }}
|
||||||
artifacts: web-*-cloud-COMMERCIAL.zip
|
artifacts: web-*-cloud-COMMERCIAL.zip
|
||||||
|
|
||||||
# This should result in a build directory in the current working directory
|
# This should result in a build directory in the current working directory
|
||||||
@@ -149,7 +166,6 @@ jobs:
|
|||||||
--base gh-pages \
|
--base gh-pages \
|
||||||
--head "$PR_BRANCH"
|
--head "$PR_BRANCH"
|
||||||
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: Create GitHub Release
|
name: Create GitHub Release
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
@@ -163,7 +179,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: rc
|
branch: ${{ needs.setup.outputs.branch-name }}
|
||||||
artifacts: "web-*-selfhosted-COMMERCIAL.zip,
|
artifacts: "web-*-selfhosted-COMMERCIAL.zip,
|
||||||
web-*-selfhosted-open-source.zip"
|
web-*-selfhosted-open-source.zip"
|
||||||
|
|
||||||
@@ -182,3 +198,4 @@ jobs:
|
|||||||
artifacts: "web-${{ needs.setup.outputs.release_version }}-selfhosted-COMMERCIAL.zip,
|
artifacts: "web-${{ needs.setup.outputs.release_version }}-selfhosted-COMMERCIAL.zip,
|
||||||
web-${{ needs.setup.outputs.release_version }}-selfhosted-open-source.zip"
|
web-${{ needs.setup.outputs.release_version }}-selfhosted-open-source.zip"
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
draft: true
|
||||||
|
|||||||
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:
|
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
|
- **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
|
||||||
* **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)
|
||||||
* **Report a bug or submit a bugfix:** Use Github issues and pull requests
|
- **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
|
||||||
* **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
|
## Contributor Agreement
|
||||||
|
|
||||||
@@ -24,9 +19,9 @@ Please sign the [Contributor Agreement](https://cla-assistant.io/bitwarden/web)
|
|||||||
|
|
||||||
## Pull Request Guidelines
|
## Pull Request Guidelines
|
||||||
|
|
||||||
* use `npm run lint` and fix any linting suggestions before submitting a pull request
|
- use `npm run lint` and fix any linting suggestions before submitting a pull request
|
||||||
* commit any pull requests against the `master` branch
|
- commit any pull requests against the `master` branch
|
||||||
* include a link to your Community Forums post
|
- include a link to your Community Forums post
|
||||||
|
|
||||||
# Localization (l10n)
|
# Localization (l10n)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM bitwarden/server
|
FROM bitwarden/server:dev
|
||||||
|
|
||||||
LABEL com.bitwarden.product="bitwarden"
|
LABEL com.bitwarden.product="bitwarden"
|
||||||
|
|
||||||
|
|||||||
34
README.md
34
README.md
@@ -23,8 +23,8 @@
|
|||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org) v14.17 or greater
|
- [Node.js](https://nodejs.org) v16.13.1 or greater
|
||||||
- NPM v7
|
- NPM v8
|
||||||
|
|
||||||
### Run the app
|
### Run the app
|
||||||
|
|
||||||
@@ -41,21 +41,21 @@ If you want to point the development web vault to the production APIs, you can r
|
|||||||
|
|
||||||
```
|
```
|
||||||
npm install
|
npm install
|
||||||
ENV=production npm run build:oss: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:
|
You can also manually adjusting your API endpoint settings by adding `config/local.json` overriding any of the following values:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"dev": {
|
||||||
"proxyApi": "http://your-api-url",
|
"proxyApi": "http://your-api-url",
|
||||||
"proxyIdentity": "http://your-identity-url",
|
"proxyIdentity": "http://your-identity-url",
|
||||||
"proxyEvents": "http://your-events-url",
|
"proxyEvents": "http://your-events-url",
|
||||||
"proxyNotifications": "http://your-notifications-url",
|
"proxyNotifications": "http://your-notifications-url",
|
||||||
"allowedHosts": ["hostnames-to-allow-in-webpack"],
|
"allowedHosts": ["hostnames-to-allow-in-webpack"]
|
||||||
"urls": {
|
},
|
||||||
|
"urls": {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -66,3 +66,23 @@ Where the `urls` object is defined by the [Urls type in jslib](https://github.co
|
|||||||
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.
|
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.
|
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
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from "@angular/core";
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from "@angular/router";
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: 'providers',
|
path: "providers",
|
||||||
loadChildren: async () => (await import('./providers/providers.module')).ProvidersModule,
|
loadChildren: async () => (await import("./providers/providers.module")).ProvidersModule,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -12,4 +12,4 @@ const routes: Routes = [
|
|||||||
imports: [RouterModule.forChild(routes)],
|
imports: [RouterModule.forChild(routes)],
|
||||||
exports: [RouterModule],
|
exports: [RouterModule],
|
||||||
})
|
})
|
||||||
export class AppRoutingModule { }
|
export class AppRoutingModule {}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
import { AppComponent as BaseAppComponent } from 'src/app/app.component';
|
import { AppComponent as BaseAppComponent } from "src/app/app.component";
|
||||||
import { DisablePersonalVaultExportPolicy } from './policies/disable-personal-vault-export.component';
|
import { DisablePersonalVaultExportPolicy } from "./policies/disable-personal-vault-export.component";
|
||||||
import { MaximumVaultTimeoutPolicy } from './policies/maximum-vault-timeout.component';
|
import { MaximumVaultTimeoutPolicy } from "./policies/maximum-vault-timeout.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: "app-root",
|
||||||
templateUrl: '../../../src/app/app.component.html',
|
templateUrl: "../../../src/app/app.component.html",
|
||||||
})
|
})
|
||||||
export class AppComponent extends BaseAppComponent {
|
export class AppComponent extends BaseAppComponent {
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
|
|
||||||
@@ -18,5 +17,4 @@ export class AppComponent extends BaseAppComponent {
|
|||||||
new DisablePersonalVaultExportPolicy(),
|
new DisablePersonalVaultExportPolicy(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import { ToasterModule } from 'angular2-toaster';
|
import { DragDropModule } from "@angular/cdk/drag-drop";
|
||||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
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 { BitwardenToastModule } from "jslib-angular/components/toastr.component";
|
||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
|
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from "./app-routing.module";
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from "./app.component";
|
||||||
import { OrganizationsModule } from './organizations/organizations.module';
|
import { OrganizationsModule } from "./organizations/organizations.module";
|
||||||
import { DisablePersonalVaultExportPolicyComponent } from './policies/disable-personal-vault-export.component';
|
import { DisablePersonalVaultExportPolicyComponent } from "./policies/disable-personal-vault-export.component";
|
||||||
import { MaximumVaultTimeoutPolicyComponent } from './policies/maximum-vault-timeout.component';
|
import { MaximumVaultTimeoutPolicyComponent } from "./policies/maximum-vault-timeout.component";
|
||||||
|
|
||||||
import { OssRoutingModule } from 'src/app/oss-routing.module';
|
import { OssRoutingModule } from "src/app/oss-routing.module";
|
||||||
import { OssModule } from 'src/app/oss.module';
|
import { OssModule } from "src/app/oss.module";
|
||||||
import { ServicesModule } from 'src/app/services/services.module';
|
import { ServicesModule } from "src/app/services/services.module";
|
||||||
import { WildcardRoutingModule } from 'src/app/wildcard-routing.module';
|
import { WildcardRoutingModule } from "src/app/wildcard-routing.module";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -25,7 +25,11 @@ import { WildcardRoutingModule } from 'src/app/wildcard-routing.module';
|
|||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
ServicesModule,
|
ServicesModule,
|
||||||
ToasterModule.forRoot(),
|
BitwardenToastModule.forRoot({
|
||||||
|
maxOpened: 5,
|
||||||
|
autoDismiss: true,
|
||||||
|
closeButton: true,
|
||||||
|
}),
|
||||||
InfiniteScrollModule,
|
InfiniteScrollModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
@@ -41,4 +45,4 @@ import { WildcardRoutingModule } from 'src/app/wildcard-routing.module';
|
|||||||
],
|
],
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
export class AppModule {}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { enableProdMode } from '@angular/core';
|
import { enableProdMode } from "@angular/core";
|
||||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
|
||||||
|
|
||||||
import 'bootstrap';
|
import "bootstrap";
|
||||||
import 'jquery';
|
import "jquery";
|
||||||
import 'popper.js';
|
import "popper.js";
|
||||||
|
|
||||||
// tslint:disable-next-line
|
// 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') {
|
if (process.env.NODE_ENV === "production") {
|
||||||
enableProdMode();
|
enableProdMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,114 +1,251 @@
|
|||||||
<div class="page-header d-flex">
|
<div class="page-header d-flex">
|
||||||
<h1>{{'singleSignOn' | i18n}}</h1>
|
<h1>{{ "singleSignOn" | i18n }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="loading">
|
<ng-container *ngIf="loading">
|
||||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
<i
|
||||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
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>
|
||||||
|
|
||||||
<form #form (ngSubmit)="submit()" [formGroup]="data" [appApiAction]="formPromise" *ngIf="!loading">
|
<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-group">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" id="enabled" [formControl]="enabled" name="Enabled">
|
<input
|
||||||
<label class="form-check-label" for="enabled">{{'enabled' | i18n}}</label>
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id="enabled"
|
||||||
|
[formControl]="enabled"
|
||||||
|
name="Enabled"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="enabled">{{ "allowSso" | i18n }}</label>
|
||||||
</div>
|
</div>
|
||||||
|
<small class="form-text text-muted">{{ "allowSsoDesc" | i18n }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="type">{{'type' | i18n}}</label>
|
<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">
|
<select class="form-control" id="type" formControlName="configType">
|
||||||
<option value="0" disabled>{{'selectType' | i18n}}</option>
|
<option [ngValue]="0" disabled>{{ "selectType" | i18n }}</option>
|
||||||
<option value="1">OpenID Connect</option>
|
<option [ngValue]="1">OpenID Connect</option>
|
||||||
<option value="2">SAML 2.0</option>
|
<option [ngValue]="2">SAML 2.0</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- OIDC -->
|
<!-- OIDC -->
|
||||||
<div *ngIf="data.value.configType == 1">
|
<div *ngIf="data.value.configType == 1">
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<h2>{{'openIdConnectConfig' | i18n}}</h2>
|
<h2>{{ "openIdConnectConfig" | i18n }}</h2>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'callbackPath' | i18n}}</label>
|
<label>{{ "callbackPath" | i18n }}</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input class="form-control" readonly [value]="callbackPath">
|
<input class="form-control" readonly [value]="callbackPath" />
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<button type="button" class="btn btn-outline-secondary"
|
<button
|
||||||
appA11yTitle="{{'copyValue' | i18n}}"
|
type="button"
|
||||||
(click)="copy(callbackPath)">
|
class="btn btn-outline-secondary"
|
||||||
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
appA11yTitle="{{ 'copyValue' | i18n }}"
|
||||||
|
(click)="copy(callbackPath)"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'signedOutCallbackPath' | i18n}}</label>
|
<label>{{ "signedOutCallbackPath" | i18n }}</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input class="form-control" readonly [value]="signedOutCallbackPath">
|
<input class="form-control" readonly [value]="signedOutCallbackPath" />
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<button type="button" class="btn btn-outline-secondary"
|
<button
|
||||||
appA11yTitle="{{'copyValue' | i18n}}"
|
type="button"
|
||||||
(click)="copy(signedOutCallbackPath)">
|
class="btn btn-outline-secondary"
|
||||||
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
appA11yTitle="{{ 'copyValue' | i18n }}"
|
||||||
|
(click)="copy(signedOutCallbackPath)"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'authority' | i18n}}</label>
|
<label for="authority">{{ "authority" | i18n }}</label>
|
||||||
<input class="form-control" formControlName="authority">
|
<input class="form-control" formControlName="authority" id="authority" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'clientId' | i18n}}</label>
|
<label for="clientId">{{ "clientId" | i18n }}</label>
|
||||||
<input class="form-control" formControlName="clientId">
|
<input class="form-control" formControlName="clientId" id="clientId" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'clientSecret' | i18n}}</label>
|
<label for="clientSecret">{{ "clientSecret" | i18n }}</label>
|
||||||
<input class="form-control" formControlName="clientSecret">
|
<input class="form-control" formControlName="clientSecret" id="clientSecret" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'metadataAddress' | i18n}}</label>
|
<label for="metadataAddress">{{ "metadataAddress" | i18n }}</label>
|
||||||
<input class="form-control" formControlName="metadataAddress">
|
<input class="form-control" formControlName="metadataAddress" id="metadataAddress" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'oidcRedirectBehavior' | i18n}}</label>
|
<label for="redirectBehavior">{{ "oidcRedirectBehavior" | i18n }}</label>
|
||||||
<select class="form-control" formControlName="redirectBehavior">
|
<select class="form-control" formControlName="redirectBehavior" id="redirectBehavior">
|
||||||
<option value="0">Redirect GET</option>
|
<option [ngValue]="0">Redirect GET</option>
|
||||||
<option value="1">Form POST</option>
|
<option [ngValue]="1">Form POST</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" id="getClaimsFromUserInfoEndpoint"
|
<input
|
||||||
formControlName="getClaimsFromUserInfoEndpoint">
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id="getClaimsFromUserInfoEndpoint"
|
||||||
|
formControlName="getClaimsFromUserInfoEndpoint"
|
||||||
|
/>
|
||||||
<label class="form-check-label" for="getClaimsFromUserInfoEndpoint">
|
<label class="form-check-label" for="getClaimsFromUserInfoEndpoint">
|
||||||
{{'getClaimsFromUserInfoEndpoint' | i18n}}
|
{{ "getClaimsFromUserInfoEndpoint" | i18n }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'additionalScopes' | i18n}}</label>
|
<label for="additionalScopes">{{ "additionalScopes" | i18n }}</label>
|
||||||
<input class="form-control" formControlName="additionalScopes">
|
<input class="form-control" formControlName="additionalScopes" id="additionalScopes" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'additionalUserIdClaimTypes' | i18n}}</label>
|
<label for="additionalUserIdClaimTypes">{{ "additionalUserIdClaimTypes" | i18n }}</label>
|
||||||
<input class="form-control" formControlName="additionalUserIdClaimTypes">
|
<input
|
||||||
|
class="form-control"
|
||||||
|
formControlName="additionalUserIdClaimTypes"
|
||||||
|
id="additionalUserIdClaimTypes"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'additionalEmailClaimTypes' | i18n}}</label>
|
<label for="additionalEmailClaimTypes">{{ "additionalEmailClaimTypes" | i18n }}</label>
|
||||||
<input class="form-control" formControlName="additionalEmailClaimTypes">
|
<input
|
||||||
|
class="form-control"
|
||||||
|
formControlName="additionalEmailClaimTypes"
|
||||||
|
id="additionalEmailClaimTypes"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'additionalNameClaimTypes' | i18n}}</label>
|
<label for="additionalNameClaimTypes">{{ "additionalNameClaimTypes" | i18n }}</label>
|
||||||
<input class="form-control" formControlName="additionalNameClaimTypes">
|
<input
|
||||||
|
class="form-control"
|
||||||
|
formControlName="additionalNameClaimTypes"
|
||||||
|
id="additionalNameClaimTypes"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'acrValues' | i18n}}</label>
|
<label for="acrValues">{{ "acrValues" | i18n }}</label>
|
||||||
<input class="form-control" formControlName="acrValues">
|
<input class="form-control" formControlName="acrValues" id="acrValues" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'expectedReturnAcrValue' | i18n}}</label>
|
<label for="expectedReturnAcrValue">{{ "expectedReturnAcrValue" | i18n }}</label>
|
||||||
<input class="form-control" formControlName="expectedReturnAcrValue">
|
<input
|
||||||
|
class="form-control"
|
||||||
|
formControlName="expectedReturnAcrValue"
|
||||||
|
id="expectedReturnAcrValue"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,54 +253,66 @@
|
|||||||
<div *ngIf="data.value.configType == 2">
|
<div *ngIf="data.value.configType == 2">
|
||||||
<!-- SAML2 SP -->
|
<!-- SAML2 SP -->
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<h2>{{'samlSpConfig' | i18n}}</h2>
|
<h2>{{ "samlSpConfig" | i18n }}</h2>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'spEntityId' | i18n}}</label>
|
<label>{{ "spEntityId" | i18n }}</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input class="form-control" readonly [value]="spEntityId" >
|
<input class="form-control" readonly [value]="spEntityId" />
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<button type="button" class="btn btn-outline-secondary"
|
<button
|
||||||
appA11yTitle="{{'copyValue' | i18n}}"
|
type="button"
|
||||||
(click)="copy(spEntityId)">
|
class="btn btn-outline-secondary"
|
||||||
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
appA11yTitle="{{ 'copyValue' | i18n }}"
|
||||||
|
(click)="copy(spEntityId)"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'spMetadataUrl' | i18n}}</label>
|
<label>{{ "spMetadataUrl" | i18n }}</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input class="form-control" readonly [value]="spMetadataUrl">
|
<input class="form-control" readonly [value]="spMetadataUrl" />
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<button type="button" class="btn btn-outline-secondary"
|
<button
|
||||||
appA11yTitle="{{'launch' | i18n}}"
|
type="button"
|
||||||
(click)="launchUri(spMetadataUrl)">
|
class="btn btn-outline-secondary"
|
||||||
<i class="fa fa-lg fa-external-link" aria-hidden="true"></i>
|
appA11yTitle="{{ 'launch' | i18n }}"
|
||||||
|
(click)="launchUri(spMetadataUrl)"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-outline-secondary"
|
<button
|
||||||
appA11yTitle="{{'copyValue' | i18n}}"
|
type="button"
|
||||||
(click)="copy(spMetadataUrl)">
|
class="btn btn-outline-secondary"
|
||||||
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
appA11yTitle="{{ 'copyValue' | i18n }}"
|
||||||
|
(click)="copy(spMetadataUrl)"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'spAcsUrl' | i18n}}</label>
|
<label>{{ "spAcsUrl" | i18n }}</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input class="form-control" readonly [value]="spAcsUrl">
|
<input class="form-control" readonly [value]="spAcsUrl" />
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<button type="button" class="btn btn-outline-secondary"
|
<button
|
||||||
appA11yTitle="{{'copyValue' | i18n}}"
|
type="button"
|
||||||
(click)="copy(spAcsUrl)">
|
class="btn btn-outline-secondary"
|
||||||
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
appA11yTitle="{{ 'copyValue' | i18n }}"
|
||||||
|
(click)="copy(spAcsUrl)"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'spNameIdFormat' | i18n}}</label>
|
<label for="spNameIdFormat">{{ "spNameIdFormat" | i18n }}</label>
|
||||||
<select class="form-control" formControlName="spNameIdFormat">
|
<select class="form-control" formControlName="spNameIdFormat" id="spNameIdFormat">
|
||||||
<option value="0">Not Configured</option>
|
<option value="0">Not Configured</option>
|
||||||
<option value="1">Unspecified</option>
|
<option value="1">Unspecified</option>
|
||||||
<option value="2">Email Address</option>
|
<option value="2">Email Address</option>
|
||||||
@@ -176,101 +325,161 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'spOutboundSigningAlgorithm' | i18n}}</label>
|
<label for="spOutboundSigningAlgorithm">{{ "spOutboundSigningAlgorithm" | i18n }}</label>
|
||||||
<select class="form-control" formControlName="spOutboundSigningAlgorithm">
|
<select
|
||||||
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{o}}</option>
|
class="form-control"
|
||||||
|
formControlName="spOutboundSigningAlgorithm"
|
||||||
|
id="spOutboundSigningAlgorithm"
|
||||||
|
>
|
||||||
|
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{ o }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'spSigningBehavior' | i18n}}</label>
|
<label for="spSigningBehavior">{{ "spSigningBehavior" | i18n }}</label>
|
||||||
<select class="form-control" formControlName="spSigningBehavior">
|
<select class="form-control" formControlName="spSigningBehavior" id="spSigningBehavior">
|
||||||
<option value="0">If IdP Wants Authn Requests Signed</option>
|
<option value="0">If IdP Wants Authn Requests Signed</option>
|
||||||
<option value="1">Always</option>
|
<option value="1">Always</option>
|
||||||
<option value="3">Never</option>
|
<option value="3">Never</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'spMinIncomingSigningAlgorithm' | i18n}}</label>
|
<label for="spMinIncomingSigningAlgorithm">{{
|
||||||
<select class="form-control" formControlName="spMinIncomingSigningAlgorithm">
|
"spMinIncomingSigningAlgorithm" | i18n
|
||||||
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{o}}</option>
|
}}</label>
|
||||||
|
<select
|
||||||
|
class="form-control"
|
||||||
|
formControlName="spMinIncomingSigningAlgorithm"
|
||||||
|
id="spMinIncomingSigningAlgorithm"
|
||||||
|
>
|
||||||
|
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{ o }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" id="spWantAssertionsSigned" formControlName="spWantAssertionsSigned">
|
<input
|
||||||
<label class="form-check-label" for="spWantAssertionsSigned">{{'spWantAssertionsSigned' | i18n}}</label>
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id="spWantAssertionsSigned"
|
||||||
|
formControlName="spWantAssertionsSigned"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="spWantAssertionsSigned">
|
||||||
|
{{ "spWantAssertionsSigned" | i18n }}
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" id="spValidateCertificates" formControlName="spValidateCertificates">
|
<input
|
||||||
<label class="form-check-label" for="spValidateCertificates">{{'spValidateCertificates' | i18n}}</label>
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id="spValidateCertificates"
|
||||||
|
formControlName="spValidateCertificates"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="spValidateCertificates">
|
||||||
|
{{ "spValidateCertificates" | i18n }}
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- SAML2 IDP -->
|
<!-- SAML2 IDP -->
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<h2>{{'samlIdpConfig' | i18n}}</h2>
|
<h2>{{ "samlIdpConfig" | i18n }}</h2>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'idpEntityId' | i18n}}</label>
|
<label for="idpEntityId">{{ "idpEntityId" | i18n }}</label>
|
||||||
<input class="form-control" formControlName="idpEntityId">
|
<input class="form-control" formControlName="idpEntityId" id="idpEntityId" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'idpBindingType' | i18n}}</label>
|
<label for="idpBindingType">{{ "idpBindingType" | i18n }}</label>
|
||||||
<select class="form-control" formControlName="idpBindingType">
|
<select class="form-control" formControlName="idpBindingType" id="idpBindingType">
|
||||||
<option value="1">Redirect</option>
|
<option value="1">Redirect</option>
|
||||||
<option value="2">HTTP POST</option>
|
<option value="2">HTTP POST</option>
|
||||||
<option value="4">Artifact</option>
|
<option value="4">Artifact</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'idpSingleSignOnServiceUrl' | i18n}}</label>
|
<label for="idpSingleSignOnServiceUrl">{{ "idpSingleSignOnServiceUrl" | i18n }}</label>
|
||||||
<input class="form-control" formControlName="idpSingleSignOnServiceUrl">
|
<input
|
||||||
|
class="form-control"
|
||||||
|
formControlName="idpSingleSignOnServiceUrl"
|
||||||
|
id="idpSingleSignOnServiceUrl"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'idpSingleLogoutServiceUrl' | i18n}}</label>
|
<label for="idpSingleLogoutServiceUrl">{{ "idpSingleLogoutServiceUrl" | i18n }}</label>
|
||||||
<input class="form-control" formControlName="idpSingleLogoutServiceUrl">
|
<input
|
||||||
|
class="form-control"
|
||||||
|
formControlName="idpSingleLogoutServiceUrl"
|
||||||
|
id="idpSingleLogoutServiceUrl"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'idpArtifactResolutionServiceUrl' | i18n}}</label>
|
<label for="idpArtifactResolutionServiceUrl">{{
|
||||||
<input class="form-control" formControlName="idpArtifactResolutionServiceUrl">
|
"idpArtifactResolutionServiceUrl" | i18n
|
||||||
|
}}</label>
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
formControlName="idpArtifactResolutionServiceUrl"
|
||||||
|
id="idpArtifactResolutionServiceUrl"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'idpX509PublicCert' | i18n}}</label>
|
<label for="idpX509PublicCert">{{ "idpX509PublicCert" | i18n }}</label>
|
||||||
<textarea formControlName="idpX509PublicCert" class="form-control form-control-sm text-monospace" rows="6"></textarea>
|
<textarea
|
||||||
|
formControlName="idpX509PublicCert"
|
||||||
|
class="form-control form-control-sm text-monospace"
|
||||||
|
rows="6"
|
||||||
|
id="idpX509PublicCert"
|
||||||
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'idpOutboundSigningAlgorithm' | i18n}}</label>
|
<label for="idpOutboundSigningAlgorithm">{{ "idpOutboundSigningAlgorithm" | i18n }}</label>
|
||||||
<select class="form-control" formControlName="idpOutboundSigningAlgorithm">
|
<select
|
||||||
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{o}}</option>
|
class="form-control"
|
||||||
|
formControlName="idpOutboundSigningAlgorithm"
|
||||||
|
id="idpOutboundSigningAlgorithm"
|
||||||
|
>
|
||||||
|
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{ o }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group" [hidden]="true">
|
||||||
|
<!--TODO: Unhide once Unsolicited IdP Response is supported-->
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" id="idpAllowUnsolicitedAuthnResponse"
|
<input
|
||||||
formControlName="idpAllowUnsolicitedAuthnResponse">
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id="idpAllowUnsolicitedAuthnResponse"
|
||||||
|
formControlName="idpAllowUnsolicitedAuthnResponse"
|
||||||
|
/>
|
||||||
<label class="form-check-label" for="idpAllowUnsolicitedAuthnResponse">
|
<label class="form-check-label" for="idpAllowUnsolicitedAuthnResponse">
|
||||||
{{'idpAllowUnsolicitedAuthnResponse' | i18n}}
|
{{ "idpAllowUnsolicitedAuthnResponse" | i18n }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" id="idpDisableOutboundLogoutRequests"
|
<input
|
||||||
formControlName="idpDisableOutboundLogoutRequests">
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id="idpDisableOutboundLogoutRequests"
|
||||||
|
formControlName="idpDisableOutboundLogoutRequests"
|
||||||
|
/>
|
||||||
<label class="form-check-label" for="idpDisableOutboundLogoutRequests">
|
<label class="form-check-label" for="idpDisableOutboundLogoutRequests">
|
||||||
{{'idpDisableOutboundLogoutRequests' | i18n}}
|
{{ "idpDisableOutboundLogoutRequests" | i18n }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" id="idpWantAuthnRequestsSigned"
|
<input
|
||||||
formControlName="idpWantAuthnRequestsSigned">
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id="idpWantAuthnRequestsSigned"
|
||||||
|
formControlName="idpWantAuthnRequestsSigned"
|
||||||
|
/>
|
||||||
<label class="form-check-label" for="idpWantAuthnRequestsSigned">
|
<label class="form-check-label" for="idpWantAuthnRequestsSigned">
|
||||||
{{'idpWantAuthnRequestsSigned' | i18n}}
|
{{ "idpWantAuthnRequestsSigned" | i18n }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -278,7 +487,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||||
<span>{{'save' | i18n}}</span>
|
<span>{{ "save" | i18n }}</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,30 +1,31 @@
|
|||||||
import {
|
import { Component, OnInit } from "@angular/core";
|
||||||
Component,
|
import { FormBuilder } from "@angular/forms";
|
||||||
OnInit,
|
import { ActivatedRoute } from "@angular/router";
|
||||||
} from '@angular/core';
|
|
||||||
import { FormBuilder } from '@angular/forms';
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
|
|
||||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||||
import { OrganizationSsoRequest } from 'jslib-common/models/request/organization/organizationSsoRequest';
|
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({
|
@Component({
|
||||||
selector: 'app-org-manage-sso',
|
selector: "app-org-manage-sso",
|
||||||
templateUrl: 'sso.component.html',
|
templateUrl: "sso.component.html",
|
||||||
})
|
})
|
||||||
export class SsoComponent implements OnInit {
|
export class SsoComponent implements OnInit {
|
||||||
|
|
||||||
samlSigningAlgorithms = [
|
samlSigningAlgorithms = [
|
||||||
'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
|
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
|
||||||
'http://www.w3.org/2000/09/xmldsig#rsa-sha384',
|
"http://www.w3.org/2000/09/xmldsig#rsa-sha384",
|
||||||
'http://www.w3.org/2000/09/xmldsig#rsa-sha512',
|
"http://www.w3.org/2000/09/xmldsig#rsa-sha512",
|
||||||
'http://www.w3.org/2000/09/xmldsig#rsa-sha1',
|
"http://www.w3.org/2000/09/xmldsig#rsa-sha1",
|
||||||
];
|
];
|
||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
|
organization: Organization;
|
||||||
formPromise: Promise<any>;
|
formPromise: Promise<any>;
|
||||||
|
|
||||||
callbackPath: string;
|
callbackPath: string;
|
||||||
@@ -33,10 +34,13 @@ export class SsoComponent implements OnInit {
|
|||||||
spMetadataUrl: string;
|
spMetadataUrl: string;
|
||||||
spAcsUrl: string;
|
spAcsUrl: string;
|
||||||
|
|
||||||
enabled = this.fb.control(false);
|
enabled = this.formBuilder.control(false);
|
||||||
data = this.fb.group({
|
data = this.formBuilder.group({
|
||||||
configType: [],
|
configType: [],
|
||||||
|
|
||||||
|
keyConnectorEnabled: [],
|
||||||
|
keyConnectorUrl: [],
|
||||||
|
|
||||||
// OpenId
|
// OpenId
|
||||||
authority: [],
|
authority: [],
|
||||||
clientId: [],
|
clientId: [],
|
||||||
@@ -71,17 +75,24 @@ export class SsoComponent implements OnInit {
|
|||||||
idpWantAuthnRequestsSigned: [],
|
idpWantAuthnRequestsSigned: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(private fb: FormBuilder, private route: ActivatedRoute, private apiService: ApiService,
|
constructor(
|
||||||
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService) { }
|
private formBuilder: FormBuilder,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private apiService: ApiService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private organizationService: OrganizationService
|
||||||
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.route.parent.parent.params.subscribe(async params => {
|
this.route.parent.parent.params.subscribe(async (params) => {
|
||||||
this.organizationId = params.organizationId;
|
this.organizationId = params.organizationId;
|
||||||
await this.load();
|
await this.load();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
|
this.organization = await this.organizationService.get(this.organizationId);
|
||||||
const ssoSettings = await this.apiService.getOrganizationSso(this.organizationId);
|
const ssoSettings = await this.apiService.getOrganizationSso(this.organizationId);
|
||||||
|
|
||||||
this.data.patchValue(ssoSettings.data);
|
this.data.patchValue(ssoSettings.data);
|
||||||
@@ -93,6 +104,8 @@ export class SsoComponent implements OnInit {
|
|||||||
this.spMetadataUrl = ssoSettings.urls.spMetadataUrl;
|
this.spMetadataUrl = ssoSettings.urls.spMetadataUrl;
|
||||||
this.spAcsUrl = ssoSettings.urls.spAcsUrl;
|
this.spAcsUrl = ssoSettings.urls.spAcsUrl;
|
||||||
|
|
||||||
|
this.keyConnectorUrl.markAsDirty();
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,17 +118,66 @@ export class SsoComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async submit() {
|
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();
|
const request = new OrganizationSsoRequest();
|
||||||
request.enabled = this.enabled.value;
|
request.enabled = this.enabled.value;
|
||||||
request.data = this.data.value;
|
request.data = this.data.value;
|
||||||
|
|
||||||
this.formPromise = this.apiService.postOrganizationSso(this.organizationId, request);
|
return this.apiService.postOrganizationSso(this.organizationId, request);
|
||||||
|
}
|
||||||
|
|
||||||
const response = await this.formPromise;
|
async validateKeyConnectorUrl() {
|
||||||
this.data.patchValue(response.data);
|
if (this.keyConnectorUrl.pristine) {
|
||||||
this.enabled.setValue(response.enabled);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.formPromise = null;
|
this.keyConnectorUrl.markAsPending();
|
||||||
this.platformUtilsService.showToast('success', null, this.i18nService.t('ssoSettingsSaved'));
|
|
||||||
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from "@angular/core";
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from "@angular/router";
|
||||||
|
|
||||||
import { AuthGuardService } from 'jslib-angular/services/auth-guard.service';
|
import { AuthGuardService } from "jslib-angular/services/auth-guard.service";
|
||||||
|
|
||||||
import { Permissions } from 'jslib-common/enums/permissions';
|
import { Permissions } from "jslib-common/enums/permissions";
|
||||||
|
|
||||||
import { OrganizationLayoutComponent } from 'src/app/layouts/organization-layout.component';
|
import { OrganizationLayoutComponent } from "src/app/layouts/organization-layout.component";
|
||||||
import { ManageComponent } from 'src/app/organizations/manage/manage.component';
|
import { ManageComponent } from "src/app/organizations/manage/manage.component";
|
||||||
import { OrganizationGuardService } from 'src/app/services/organization-guard.service';
|
import { OrganizationGuardService } from "src/app/services/organization-guard.service";
|
||||||
import { OrganizationTypeGuardService } from 'src/app/services/organization-type-guard.service';
|
import { OrganizationTypeGuardService } from "src/app/services/organization-type-guard.service";
|
||||||
|
|
||||||
import { SsoComponent } from './manage/sso.component';
|
import { SsoComponent } from "./manage/sso.component";
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: 'organizations/:organizationId',
|
path: "organizations/:organizationId",
|
||||||
component: OrganizationLayoutComponent,
|
component: OrganizationLayoutComponent,
|
||||||
canActivate: [AuthGuardService, OrganizationGuardService],
|
canActivate: [AuthGuardService, OrganizationGuardService],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'manage',
|
path: "manage",
|
||||||
component: ManageComponent,
|
component: ManageComponent,
|
||||||
canActivate: [OrganizationTypeGuardService],
|
canActivate: [OrganizationTypeGuardService],
|
||||||
data: {
|
data: {
|
||||||
@@ -38,7 +38,7 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'sso',
|
path: "sso",
|
||||||
component: SsoComponent,
|
component: SsoComponent,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -51,4 +51,4 @@ const routes: Routes = [
|
|||||||
imports: [RouterModule.forChild(routes)],
|
imports: [RouterModule.forChild(routes)],
|
||||||
exports: [RouterModule],
|
exports: [RouterModule],
|
||||||
})
|
})
|
||||||
export class OrganizationsRoutingModule { }
|
export class OrganizationsRoutingModule {}
|
||||||
|
|||||||
@@ -1,22 +1,14 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from "@angular/common";
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from "@angular/core";
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||||
|
|
||||||
import { OssModule } from 'src/app/oss.module';
|
import { OssModule } from "src/app/oss.module";
|
||||||
|
|
||||||
import { SsoComponent } from './manage/sso.component';
|
import { SsoComponent } from "./manage/sso.component";
|
||||||
import { OrganizationsRoutingModule } from './organizations-routing.module';
|
import { OrganizationsRoutingModule } from "./organizations-routing.module";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [CommonModule, FormsModule, ReactiveFormsModule, OssModule, OrganizationsRoutingModule],
|
||||||
CommonModule,
|
declarations: [SsoComponent],
|
||||||
FormsModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
OssModule,
|
|
||||||
OrganizationsRoutingModule,
|
|
||||||
],
|
|
||||||
declarations: [
|
|
||||||
SsoComponent,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class OrganizationsModule {}
|
export class OrganizationsModule {}
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" id="enabled" [formControl]="enabled" name="Enabled">
|
<input
|
||||||
<label class="form-check-label" for="enabled">{{'enabled' | i18n}}</label>
|
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>
|
</div>
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from "@angular/core";
|
||||||
import { FormBuilder } from '@angular/forms';
|
import { FormBuilder } from "@angular/forms";
|
||||||
|
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
|
||||||
import { PolicyType } from 'jslib-common/enums/policyType';
|
import { PolicyType } from "jslib-common/enums/policyType";
|
||||||
|
|
||||||
import { PolicyRequest } from 'jslib-common/models/request/policyRequest';
|
import { PolicyRequest } from "jslib-common/models/request/policyRequest";
|
||||||
|
|
||||||
import { BasePolicy, BasePolicyComponent } from 'src/app/organizations/policies/base-policy.component';
|
import {
|
||||||
|
BasePolicy,
|
||||||
|
BasePolicyComponent,
|
||||||
|
} from "src/app/organizations/policies/base-policy.component";
|
||||||
|
|
||||||
export class DisablePersonalVaultExportPolicy extends BasePolicy {
|
export class DisablePersonalVaultExportPolicy extends BasePolicy {
|
||||||
name = 'disablePersonalVaultExport';
|
name = "disablePersonalVaultExport";
|
||||||
description = 'disablePersonalVaultExportDesc';
|
description = "disablePersonalVaultExportDesc";
|
||||||
type = PolicyType.DisablePersonalVaultExport;
|
type = PolicyType.DisablePersonalVaultExport;
|
||||||
component = DisablePersonalVaultExportPolicyComponent;
|
component = DisablePersonalVaultExportPolicyComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'policy-disable-personal-vault-export',
|
selector: "policy-disable-personal-vault-export",
|
||||||
templateUrl: 'disable-personal-vault-export.component.html',
|
templateUrl: "disable-personal-vault-export.component.html",
|
||||||
})
|
})
|
||||||
export class DisablePersonalVaultExportPolicyComponent extends BasePolicyComponent {
|
export class DisablePersonalVaultExportPolicyComponent extends BasePolicyComponent {}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,26 +1,46 @@
|
|||||||
<app-callout type="tip" title="{{'prerequisite' | i18n}}">
|
<app-callout type="tip" title="{{ 'prerequisite' | i18n }}">
|
||||||
{{'requireSsoPolicyReq' | i18n}}
|
{{ "requireSsoPolicyReq" | i18n }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" id="enabled" [formControl]="enabled" name="Enabled">
|
<input
|
||||||
<label class="form-check-label" for="enabled">{{'enabled' | i18n}}</label>
|
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>
|
</div>
|
||||||
|
|
||||||
<div [formGroup]="data">
|
<div [formGroup]="data">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="hours">{{'maximumVaultTimeoutLabel' | i18n}}</label>
|
<label for="hours">{{ "maximumVaultTimeoutLabel" | i18n }}</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<input id="hours" class="form-control" type="number" min="0" name="hours" formControlName="hours">
|
<input
|
||||||
<small>{{'hours' | i18n }}</small>
|
id="hours"
|
||||||
|
class="form-control"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
name="hours"
|
||||||
|
formControlName="hours"
|
||||||
|
/>
|
||||||
|
<small>{{ "hours" | i18n }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<input id="minutes" class="form-control" type="number" min="0" max="59" name="minutes"
|
<input
|
||||||
formControlName="minutes">
|
id="minutes"
|
||||||
<small>{{'minutes' | i18n }}</small>
|
class="form-control"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="59"
|
||||||
|
name="minutes"
|
||||||
|
formControlName="minutes"
|
||||||
|
/>
|
||||||
|
<small>{{ "minutes" | i18n }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,33 +1,35 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from "@angular/core";
|
||||||
import { FormBuilder } from '@angular/forms';
|
import { FormBuilder } from "@angular/forms";
|
||||||
|
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
|
||||||
import { PolicyType } from 'jslib-common/enums/policyType';
|
import { PolicyType } from "jslib-common/enums/policyType";
|
||||||
|
|
||||||
import { PolicyRequest } from 'jslib-common/models/request/policyRequest';
|
import { PolicyRequest } from "jslib-common/models/request/policyRequest";
|
||||||
|
|
||||||
import { BasePolicy, BasePolicyComponent } from 'src/app/organizations/policies/base-policy.component';
|
import {
|
||||||
|
BasePolicy,
|
||||||
|
BasePolicyComponent,
|
||||||
|
} from "src/app/organizations/policies/base-policy.component";
|
||||||
|
|
||||||
export class MaximumVaultTimeoutPolicy extends BasePolicy {
|
export class MaximumVaultTimeoutPolicy extends BasePolicy {
|
||||||
name = 'maximumVaultTimeout';
|
name = "maximumVaultTimeout";
|
||||||
description = 'maximumVaultTimeoutDesc';
|
description = "maximumVaultTimeoutDesc";
|
||||||
type = PolicyType.MaximumVaultTimeout;
|
type = PolicyType.MaximumVaultTimeout;
|
||||||
component = MaximumVaultTimeoutPolicyComponent;
|
component = MaximumVaultTimeoutPolicyComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'policy-maximum-timeout',
|
selector: "policy-maximum-timeout",
|
||||||
templateUrl: 'maximum-vault-timeout.component.html',
|
templateUrl: "maximum-vault-timeout.component.html",
|
||||||
})
|
})
|
||||||
export class MaximumVaultTimeoutPolicyComponent extends BasePolicyComponent {
|
export class MaximumVaultTimeoutPolicyComponent extends BasePolicyComponent {
|
||||||
|
data = this.formBuilder.group({
|
||||||
data = this.fb.group({
|
|
||||||
hours: [null],
|
hours: [null],
|
||||||
minutes: [null],
|
minutes: [null],
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(private fb: FormBuilder, private i18nService: I18nService) {
|
constructor(private formBuilder: FormBuilder, private i18nService: I18nService) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,12 +59,12 @@ export class MaximumVaultTimeoutPolicyComponent extends BasePolicyComponent {
|
|||||||
buildRequest(policiesEnabledMap: Map<PolicyType, boolean>): Promise<PolicyRequest> {
|
buildRequest(policiesEnabledMap: Map<PolicyType, boolean>): Promise<PolicyRequest> {
|
||||||
const singleOrgEnabled = policiesEnabledMap.get(PolicyType.SingleOrg) ?? false;
|
const singleOrgEnabled = policiesEnabledMap.get(PolicyType.SingleOrg) ?? false;
|
||||||
if (this.enabled.value && !singleOrgEnabled) {
|
if (this.enabled.value && !singleOrgEnabled) {
|
||||||
throw new Error(this.i18nService.t('requireSsoPolicyReqError'));
|
throw new Error(this.i18nService.t("requireSsoPolicyReqError"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = this.buildRequestData();
|
const data = this.buildRequestData();
|
||||||
if (data?.minutes == null || data?.minutes <= 0) {
|
if (data?.minutes == null || data?.minutes <= 0) {
|
||||||
throw new Error(this.i18nService.t('invalidMaximumVaultTimeout'));
|
throw new Error(this.i18nService.t("invalidMaximumVaultTimeout"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.buildRequest(policiesEnabledMap);
|
return super.buildRequest(policiesEnabledMap);
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="addTitle">
|
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="addTitle">
|
||||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title" id="addTitle">
|
<h2 class="modal-title" id="addTitle">
|
||||||
{{'addExistingOrganization' | i18n}}
|
{{ "addExistingOrganization" | i18n }}
|
||||||
</h2>
|
</h2>
|
||||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="close"
|
||||||
|
data-dismiss="modal"
|
||||||
|
appA11yTitle="{{ 'close' | i18n }}"
|
||||||
|
>
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="card-body text-center" *ngIf="loading">
|
<div class="card-body text-center" *ngIf="loading">
|
||||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||||
{{'loading' | i18n}}
|
{{ "loading" | i18n }}
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="!loading">
|
<ng-container *ngIf="!loading">
|
||||||
<table class="table table-hover table-list">
|
<table class="table table-hover table-list">
|
||||||
@@ -21,10 +26,16 @@
|
|||||||
<app-avatar [data]="o.name" size="25" [circle]="true" [fontSize]="14"></app-avatar>
|
<app-avatar [data]="o.name" size="25" [circle]="true" [fontSize]="14"></app-avatar>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{o.name}}
|
{{ o.name }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-outline-secondary pull-right" (click)="add(o)" [disabled]="formPromise">Add</button>
|
<button
|
||||||
|
class="btn btn-outline-secondary pull-right"
|
||||||
|
(click)="add(o)"
|
||||||
|
[disabled]="formPromise"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,32 +1,21 @@
|
|||||||
import {
|
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||||
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 { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
||||||
import { UserService } from 'jslib-common/abstractions/user.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 { Organization } from "jslib-common/models/domain/organization";
|
||||||
import { Provider } from 'jslib-common/models/domain/provider';
|
import { Provider } from "jslib-common/models/domain/provider";
|
||||||
|
|
||||||
import { PlanType } from 'jslib-common/enums/planType';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'provider-add-organization',
|
selector: "provider-add-organization",
|
||||||
templateUrl: 'add-organization.component.html',
|
templateUrl: "add-organization.component.html",
|
||||||
})
|
})
|
||||||
export class AddOrganizationComponent implements OnInit {
|
export class AddOrganizationComponent implements OnInit {
|
||||||
|
|
||||||
@Input() providerId: string;
|
@Input() providerId: string;
|
||||||
@Input() organizations: Organization[];
|
@Input() organizations: Organization[];
|
||||||
@Output() onAddedOrganization = new EventEmitter();
|
@Output() onAddedOrganization = new EventEmitter();
|
||||||
@@ -35,10 +24,13 @@ export class AddOrganizationComponent implements OnInit {
|
|||||||
formPromise: Promise<any>;
|
formPromise: Promise<any>;
|
||||||
loading = true;
|
loading = true;
|
||||||
|
|
||||||
constructor(private userService: UserService, private providerService: ProviderService,
|
constructor(
|
||||||
private toasterService: ToasterService, private i18nService: I18nService,
|
private providerService: ProviderService,
|
||||||
private platformUtilsService: PlatformUtilsService, private validationService: ValidationService,
|
private webProviderService: WebProviderService,
|
||||||
private apiService: ApiService) { }
|
private i18nService: I18nService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private validationService: ValidationService
|
||||||
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
await this.load();
|
await this.load();
|
||||||
@@ -49,7 +41,7 @@ export class AddOrganizationComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.provider = await this.userService.getProvider(this.providerId);
|
this.provider = await this.providerService.get(this.providerId);
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
@@ -60,15 +52,22 @@ export class AddOrganizationComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const confirmed = await this.platformUtilsService.showDialog(
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
this.i18nService.t('addOrganizationConfirmation', organization.name, this.provider.name), organization.name,
|
this.i18nService.t("addOrganizationConfirmation", organization.name, this.provider.name),
|
||||||
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
organization.name,
|
||||||
|
this.i18nService.t("yes"),
|
||||||
|
this.i18nService.t("no"),
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
|
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.formPromise = this.providerService.addOrganizationToProvider(this.providerId, organization.id);
|
this.formPromise = this.webProviderService.addOrganizationToProvider(
|
||||||
|
this.providerId,
|
||||||
|
organization.id
|
||||||
|
);
|
||||||
await this.formPromise;
|
await this.formPromise;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.validationService.showError(e);
|
this.validationService.showError(e);
|
||||||
@@ -77,7 +76,11 @@ export class AddOrganizationComponent implements OnInit {
|
|||||||
this.formPromise = null;
|
this.formPromise = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.toasterService.popAsync('success', null, this.i18nService.t('organizationJoinedProvider'));
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("organizationJoinedProvider")
|
||||||
|
);
|
||||||
this.onAddedOrganization.emit();
|
this.onAddedOrganization.emit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +1,82 @@
|
|||||||
<div class="page-header d-flex">
|
<div class="page-header d-flex">
|
||||||
<h1>{{'clients' | i18n}}</h1>
|
<h1>{{ "clients" | i18n }}</h1>
|
||||||
|
|
||||||
<div class="ml-auto d-flex">
|
<div class="ml-auto d-flex">
|
||||||
<div>
|
<div>
|
||||||
<label class="sr-only" for="search">{{'search' | i18n}}</label>
|
<label class="sr-only" for="search">{{ "search" | i18n }}</label>
|
||||||
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
|
<input
|
||||||
[(ngModel)]="searchText">
|
type="search"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
id="search"
|
||||||
|
placeholder="{{ 'search' | i18n }}"
|
||||||
|
[(ngModel)]="searchText"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-sm btn-outline-primary ml-3" routerLink="create" *ngIf="manageOrganizations">
|
<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>
|
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||||
{{'newClientOrganization' | i18n}}
|
{{ "newClientOrganization" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
<button class="btn btn-sm btn-outline-primary ml-3" (click)="addExistingOrganization()"
|
<button
|
||||||
*ngIf="manageOrganizations && showAddExisting">
|
class="btn btn-sm btn-outline-primary ml-3"
|
||||||
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
|
(click)="addExistingOrganization()"
|
||||||
{{'addExistingOrganization' | i18n}}
|
*ngIf="manageOrganizations && showAddExisting"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||||
|
{{ "addExistingOrganization" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="loading">
|
<ng-container *ngIf="loading">
|
||||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
<i
|
||||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
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>
|
||||||
|
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngIf="!loading && (clients | search:searchText:'organizationName':'id') as searchedClients">
|
*ngIf="!loading && (clients | search: searchText:'organizationName':'id') as searchedClients"
|
||||||
<p *ngIf="!searchedClients.length">{{'noClientsInList' | i18n}}</p>
|
>
|
||||||
|
<p *ngIf="!searchedClients.length">{{ "noClientsInList" | i18n }}</p>
|
||||||
<ng-container *ngIf="searchedClients.length">
|
<ng-container *ngIf="searchedClients.length">
|
||||||
<table class="table table-hover table-list" infiniteScroll [infiniteScrollDistance]="1"
|
<table
|
||||||
[infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
|
class="table table-hover table-list"
|
||||||
|
infiniteScroll
|
||||||
|
[infiniteScrollDistance]="1"
|
||||||
|
[infiniteScrollDisabled]="!isPaging()"
|
||||||
|
(scrolled)="loadMore()"
|
||||||
|
>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let o of searchedClients">
|
<tr *ngFor="let o of searchedClients">
|
||||||
<td width="30">
|
<td width="30">
|
||||||
<app-avatar [data]="o.organizationName" size="25" [circle]="true" [fontSize]="14"></app-avatar>
|
<app-avatar
|
||||||
|
[data]="o.organizationName"
|
||||||
|
size="25"
|
||||||
|
[circle]="true"
|
||||||
|
[fontSize]="14"
|
||||||
|
></app-avatar>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a [routerLink]="['/organizations', o.organizationId]">{{o.organizationName}}</a>
|
<a [routerLink]="['/organizations', o.organizationId]">{{ o.organizationName }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="table-list-options" *ngIf="manageOrganizations">
|
<td class="table-list-options" *ngIf="manageOrganizations">
|
||||||
<div class="dropdown" appListDropdown>
|
<div class="dropdown" appListDropdown>
|
||||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
|
<button
|
||||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
|
class="btn btn-outline-secondary dropdown-toggle"
|
||||||
appA11yTitle="{{'options' | i18n}}">
|
type="button"
|
||||||
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
|
data-toggle="dropdown"
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded="false"
|
||||||
|
appA11yTitle="{{ 'options' | i18n }}"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu dropdown-menu-right">
|
<div class="dropdown-menu dropdown-menu-right">
|
||||||
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(o)">
|
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(o)">
|
||||||
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
||||||
{{'remove' | i18n}}
|
{{ "remove" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,44 +1,40 @@
|
|||||||
import {
|
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||||
Component,
|
import { ActivatedRoute } from "@angular/router";
|
||||||
OnInit,
|
|
||||||
ViewChild,
|
|
||||||
ViewContainerRef
|
|
||||||
} from '@angular/core';
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
import { ToasterService } from 'angular2-toaster';
|
|
||||||
|
|
||||||
import { first } from 'rxjs/operators';
|
import { first } from "rxjs/operators";
|
||||||
|
|
||||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||||
import { SearchService } from 'jslib-common/abstractions/search.service';
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
import { UserService } from 'jslib-common/abstractions/user.service';
|
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
||||||
|
import { SearchService } from "jslib-common/abstractions/search.service";
|
||||||
|
|
||||||
import { ModalService } from 'jslib-angular/services/modal.service';
|
import { ModalService } from "jslib-angular/services/modal.service";
|
||||||
import { ValidationService } from 'jslib-angular/services/validation.service';
|
import { ValidationService } from "jslib-angular/services/validation.service";
|
||||||
|
|
||||||
import { PlanType } from 'jslib-common/enums/planType';
|
import { PlanType } from "jslib-common/enums/planType";
|
||||||
import { ProviderUserType } from 'jslib-common/enums/providerUserType';
|
import { ProviderUserType } from "jslib-common/enums/providerUserType";
|
||||||
|
|
||||||
import { Organization } from 'jslib-common/models/domain/organization';
|
import { Organization } from "jslib-common/models/domain/organization";
|
||||||
import {
|
import { ProviderOrganizationOrganizationDetailsResponse } from "jslib-common/models/response/provider/providerOrganizationResponse";
|
||||||
ProviderOrganizationOrganizationDetailsResponse
|
|
||||||
} from 'jslib-common/models/response/provider/providerOrganizationResponse';
|
|
||||||
|
|
||||||
import { ProviderService } from '../services/provider.service';
|
import { WebProviderService } from "../services/webProvider.service";
|
||||||
|
|
||||||
import { AddOrganizationComponent } from './add-organization.component';
|
import { AddOrganizationComponent } from "./add-organization.component";
|
||||||
|
|
||||||
const DisallowedPlanTypes = [PlanType.Free, PlanType.FamiliesAnnually2019, PlanType.FamiliesAnnually];
|
const DisallowedPlanTypes = [
|
||||||
|
PlanType.Free,
|
||||||
|
PlanType.FamiliesAnnually2019,
|
||||||
|
PlanType.FamiliesAnnually,
|
||||||
|
];
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: 'clients.component.html',
|
templateUrl: "clients.component.html",
|
||||||
})
|
})
|
||||||
export class ClientsComponent implements OnInit {
|
export class ClientsComponent implements OnInit {
|
||||||
|
@ViewChild("add", { read: ViewContainerRef, static: true }) addModalRef: ViewContainerRef;
|
||||||
@ViewChild('add', { read: ViewContainerRef, static: true }) addModalRef: ViewContainerRef;
|
|
||||||
|
|
||||||
providerId: any;
|
providerId: any;
|
||||||
searchText: string;
|
searchText: string;
|
||||||
@@ -55,20 +51,27 @@ export class ClientsComponent implements OnInit {
|
|||||||
protected actionPromise: Promise<any>;
|
protected actionPromise: Promise<any>;
|
||||||
private pagedClientsCount = 0;
|
private pagedClientsCount = 0;
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute, private userService: UserService,
|
constructor(
|
||||||
private apiService: ApiService, private searchService: SearchService,
|
private route: ActivatedRoute,
|
||||||
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
|
private providerService: ProviderService,
|
||||||
private toasterService: ToasterService, private validationService: ValidationService,
|
private apiService: ApiService,
|
||||||
private providerService: ProviderService, private logService: LogService,
|
private searchService: SearchService,
|
||||||
private modalService: ModalService) { }
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private validationService: ValidationService,
|
||||||
|
private webProviderService: WebProviderService,
|
||||||
|
private logService: LogService,
|
||||||
|
private modalService: ModalService,
|
||||||
|
private organizationService: OrganizationService
|
||||||
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.route.parent.params.subscribe(async params => {
|
this.route.parent.params.subscribe(async (params) => {
|
||||||
this.providerId = params.providerId;
|
this.providerId = params.providerId;
|
||||||
|
|
||||||
await this.load();
|
await this.load();
|
||||||
|
|
||||||
this.route.queryParams.pipe(first()).subscribe(async qParams => {
|
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||||
this.searchText = qParams.search;
|
this.searchText = qParams.search;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -77,12 +80,17 @@ export class ClientsComponent implements OnInit {
|
|||||||
async load() {
|
async load() {
|
||||||
const response = await this.apiService.getProviderClients(this.providerId);
|
const response = await this.apiService.getProviderClients(this.providerId);
|
||||||
this.clients = response.data != null && response.data.length > 0 ? response.data : [];
|
this.clients = response.data != null && response.data.length > 0 ? response.data : [];
|
||||||
this.manageOrganizations = (await this.userService.getProvider(this.providerId)).type === ProviderUserType.ProviderAdmin;
|
this.manageOrganizations =
|
||||||
const candidateOrgs = (await this.userService.getAllOrganizations()).filter(o => o.isOwner && o.providerId == null);
|
(await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin;
|
||||||
const allowedOrgsIds = await Promise.all(candidateOrgs.map(o => this.apiService.getOrganization(o.id))).then(orgs =>
|
const candidateOrgs = (await this.organizationService.getAll()).filter(
|
||||||
orgs.filter(o => !DisallowedPlanTypes.includes(o.planType))
|
(o) => o.isOwner && o.providerId == null
|
||||||
.map(o => o.id));
|
);
|
||||||
this.addableOrganizations = candidateOrgs.filter(o => allowedOrgsIds.includes(o.id));
|
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.showAddExisting = this.addableOrganizations.length !== 0;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
@@ -105,7 +113,6 @@ export class ClientsComponent implements OnInit {
|
|||||||
this.loadMore();
|
this.loadMore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
loadMore() {
|
loadMore() {
|
||||||
if (!this.clients || this.clients.length <= this.pageSize) {
|
if (!this.clients || this.clients.length <= this.pageSize) {
|
||||||
return;
|
return;
|
||||||
@@ -116,14 +123,19 @@ export class ClientsComponent implements OnInit {
|
|||||||
pagedSize = this.pagedClientsCount;
|
pagedSize = this.pagedClientsCount;
|
||||||
}
|
}
|
||||||
if (this.clients.length > pagedLength) {
|
if (this.clients.length > pagedLength) {
|
||||||
this.pagedClients = this.pagedClients.concat(this.clients.slice(pagedLength, pagedLength + pagedSize));
|
this.pagedClients = this.pagedClients.concat(
|
||||||
|
this.clients.slice(pagedLength, pagedLength + pagedSize)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.pagedClientsCount = this.pagedClients.length;
|
this.pagedClientsCount = this.pagedClients.length;
|
||||||
this.didScroll = this.pagedClients.length > this.pageSize;
|
this.didScroll = this.pagedClients.length > this.pageSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addExistingOrganization() {
|
async addExistingOrganization() {
|
||||||
const [modal] = await this.modalService.openViewRef(AddOrganizationComponent, this.addModalRef, comp => {
|
const [modal] = await this.modalService.openViewRef(
|
||||||
|
AddOrganizationComponent,
|
||||||
|
this.addModalRef,
|
||||||
|
(comp) => {
|
||||||
comp.providerId = this.providerId;
|
comp.providerId = this.providerId;
|
||||||
comp.organizations = this.addableOrganizations;
|
comp.organizations = this.addableOrganizations;
|
||||||
comp.onAddedOrganization.subscribe(async () => {
|
comp.onAddedOrganization.subscribe(async () => {
|
||||||
@@ -134,22 +146,34 @@ export class ClientsComponent implements OnInit {
|
|||||||
this.logService.error(`Handled exception: ${e}`);
|
this.logService.error(`Handled exception: ${e}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(organization: ProviderOrganizationOrganizationDetailsResponse) {
|
async remove(organization: ProviderOrganizationOrganizationDetailsResponse) {
|
||||||
const confirmed = await this.platformUtilsService.showDialog(
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
this.i18nService.t('detachOrganizationConfirmation'), organization.organizationName,
|
this.i18nService.t("detachOrganizationConfirmation"),
|
||||||
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
organization.organizationName,
|
||||||
|
this.i18nService.t("yes"),
|
||||||
|
this.i18nService.t("no"),
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
|
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.actionPromise = this.providerService.detachOrganizastion(this.providerId, organization.id);
|
this.actionPromise = this.webProviderService.detachOrganizastion(
|
||||||
|
this.providerId,
|
||||||
|
organization.id
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
await this.actionPromise;
|
await this.actionPromise;
|
||||||
this.toasterService.popAsync('success', null, this.i18nService.t('detachedOrganization', organization.organizationName));
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("detachedOrganization", organization.organizationName)
|
||||||
|
);
|
||||||
await this.load();
|
await this.load();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.validationService.showError(e);
|
this.validationService.showError(e);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1>{{'newClientOrganization' | i18n}}</h1>
|
<h1>{{ "newClientOrganization" | i18n }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<p>{{'newClientOrganizationDesc' | i18n}}</p>
|
<p>{{ "newClientOrganizationDesc" | i18n }}</p>
|
||||||
<app-organization-plans [providerId]="providerId"></app-organization-plans>
|
<app-organization-plans [providerId]="providerId"></app-organization-plans>
|
||||||
|
|||||||
@@ -1,25 +1,22 @@
|
|||||||
import {
|
import { Component, OnInit, ViewChild } from "@angular/core";
|
||||||
Component,
|
import { ActivatedRoute } from "@angular/router";
|
||||||
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({
|
@Component({
|
||||||
selector: 'app-create-organization',
|
selector: "app-create-organization",
|
||||||
templateUrl: 'create-organization.component.html',
|
templateUrl: "create-organization.component.html",
|
||||||
})
|
})
|
||||||
export class CreateOrganizationComponent implements OnInit {
|
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() {
|
ngOnInit() {
|
||||||
this.route.parent.params.subscribe(async params => {
|
this.route.parent.params.subscribe(async (params) => {
|
||||||
this.providerId = params.providerId;
|
this.providerId = params.providerId;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,38 @@
|
|||||||
<div class="mt-5 d-flex justify-content-center" *ngIf="loading">
|
<div class="mt-5 d-flex justify-content-center" *ngIf="loading">
|
||||||
<div>
|
<div>
|
||||||
<img class="mb-4 logo logo-themed" alt="Bitwarden">
|
<img class="mb-4 logo logo-themed" alt="Bitwarden" />
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
<i
|
||||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container" *ngIf="!loading && !authed">
|
<div class="container" *ngIf="!loading && !authed">
|
||||||
<div class="row justify-content-md-center mt-5">
|
<div class="row justify-content-md-center mt-5">
|
||||||
<div class="col-5">
|
<div class="col-5">
|
||||||
<p class="lead text-center mb-4">{{'joinProvider' | i18n}}</p>
|
<p class="lead text-center mb-4">{{ "joinProvider" | i18n }}</p>
|
||||||
<div class="card d-block">
|
<div class="card d-block">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
{{providerName}}
|
{{ providerName }}
|
||||||
<strong class="d-block mt-2">{{email}}</strong>
|
<strong class="d-block mt-2">{{ email }}</strong>
|
||||||
</p>
|
</p>
|
||||||
<p>{{'joinProviderDesc' | i18n}}</p>
|
<p>{{ "joinProviderDesc" | i18n }}</p>
|
||||||
<hr>
|
<hr />
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
|
<a routerLink="/" [queryParams]="{ email: email }" class="btn btn-primary btn-block">
|
||||||
{{'logIn' | i18n}}
|
{{ "logIn" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
<a routerLink="/register" [queryParams]="{email: email}"
|
<a
|
||||||
class="btn btn-primary btn-block ml-2 mt-0">
|
routerLink="/register"
|
||||||
{{'createAccount' | i18n}}
|
[queryParams]="{ email: email }"
|
||||||
|
class="btn btn-primary btn-block ml-2 mt-0"
|
||||||
|
>
|
||||||
|
{{ "createAccount" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,45 +1,53 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from "@angular/core";
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { Toast, ToasterService } from 'angular2-toaster';
|
|
||||||
|
|
||||||
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 { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { StateService } from 'jslib-common/abstractions/state.service';
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
import { UserService } from 'jslib-common/abstractions/user.service';
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
import { ProviderUserAcceptRequest } from 'jslib-common/models/request/provider/providerUserAcceptRequest';
|
import { ProviderUserAcceptRequest } from "jslib-common/models/request/provider/providerUserAcceptRequest";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-accept-provider',
|
selector: "app-accept-provider",
|
||||||
templateUrl: 'accept-provider.component.html',
|
templateUrl: "accept-provider.component.html",
|
||||||
})
|
})
|
||||||
export class AcceptProviderComponent extends BaseAcceptComponent {
|
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,
|
constructor(
|
||||||
userService: UserService, stateService: StateService, private apiService: ApiService) {
|
router: Router,
|
||||||
super(router, toasterService, i18nService, route, userService, stateService);
|
i18nService: I18nService,
|
||||||
|
route: ActivatedRoute,
|
||||||
|
stateService: StateService,
|
||||||
|
private apiService: ApiService,
|
||||||
|
platformUtilService: PlatformUtilsService
|
||||||
|
) {
|
||||||
|
super(router, platformUtilService, i18nService, route, stateService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async authedHandler(qParams: any) {
|
async authedHandler(qParams: any) {
|
||||||
const request = new ProviderUserAcceptRequest();
|
const request = new ProviderUserAcceptRequest();
|
||||||
request.token = qParams.token;
|
request.token = qParams.token;
|
||||||
|
|
||||||
await this.apiService.postProviderUserAccept(qParams.providerId, qParams.providerUserId, request);
|
await this.apiService.postProviderUserAccept(
|
||||||
const toast: Toast = {
|
qParams.providerId,
|
||||||
type: 'success',
|
qParams.providerUserId,
|
||||||
title: this.i18nService.t('inviteAccepted'),
|
request
|
||||||
body: this.i18nService.t('providerInviteAcceptedDesc'),
|
);
|
||||||
timeout: 10000,
|
this.platformUtilService.showToast(
|
||||||
};
|
"success",
|
||||||
this.toasterService.popAsync(toast);
|
this.i18nService.t("inviteAccepted"),
|
||||||
this.router.navigate(['/vault']);
|
this.i18nService.t("providerInviteAcceptedDesc"),
|
||||||
|
{ timeout: 10000 }
|
||||||
|
);
|
||||||
|
this.router.navigate(["/vault"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async unauthedHandler(qParams: any) {
|
async unauthedHandler(qParams: any) {
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
import {
|
import { Component, Input } from "@angular/core";
|
||||||
Component,
|
|
||||||
Input,
|
|
||||||
} from '@angular/core';
|
|
||||||
|
|
||||||
import { ProviderUserBulkConfirmRequest } from 'jslib-common/models/request/provider/providerUserBulkConfirmRequest';
|
import { ProviderUserBulkConfirmRequest } from "jslib-common/models/request/provider/providerUserBulkConfirmRequest";
|
||||||
import { ProviderUserBulkRequest } from 'jslib-common/models/request/provider/providerUserBulkRequest';
|
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 { BulkConfirmComponent as OrganizationBulkConfirmComponent } from "src/app/organizations/manage/bulk/bulk-confirm.component";
|
||||||
import { BulkUserDetails } from 'src/app/organizations/manage/bulk/bulk-status.component';
|
import { BulkUserDetails } from "src/app/organizations/manage/bulk/bulk-status.component";
|
||||||
|
|
||||||
@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 {
|
export class BulkConfirmComponent extends OrganizationBulkConfirmComponent {
|
||||||
|
|
||||||
@Input() providerId: string;
|
@Input() providerId: string;
|
||||||
|
|
||||||
protected isAccepted(user: BulkUserDetails) {
|
protected isAccepted(user: BulkUserDetails) {
|
||||||
@@ -23,7 +19,7 @@ export class BulkConfirmComponent extends OrganizationBulkConfirmComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async getPublicKeys() {
|
protected async getPublicKeys() {
|
||||||
const request = new ProviderUserBulkRequest(this.filteredUsers.map(user => user.id));
|
const request = new ProviderUserBulkRequest(this.filteredUsers.map((user) => user.id));
|
||||||
return await this.apiService.postProviderUsersPublicKey(this.providerId, request);
|
return await this.apiService.postProviderUsersPublicKey(this.providerId, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
import {
|
import { Component, Input } from "@angular/core";
|
||||||
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({
|
@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 {
|
export class BulkRemoveComponent extends OrganizationBulkRemoveComponent {
|
||||||
|
|
||||||
@Input() providerId: string;
|
@Input() providerId: string;
|
||||||
|
|
||||||
async deleteUsers() {
|
async deleteUsers() {
|
||||||
const request = new ProviderUserBulkRequest(this.users.map(user => user.id));
|
const request = new ProviderUserBulkRequest(this.users.map((user) => user.id));
|
||||||
return await this.apiService.deleteManyProviderUsers(this.providerId, request);
|
return await this.apiService.deleteManyProviderUsers(this.providerId, request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +1,107 @@
|
|||||||
<div class="page-header d-flex">
|
<div class="page-header d-flex">
|
||||||
<h1>{{'eventLogs' | i18n}}</h1>
|
<h1>{{ "eventLogs" | i18n }}</h1>
|
||||||
<div class="ml-auto d-flex">
|
<div class="ml-auto d-flex">
|
||||||
<div class="form-inline">
|
<div class="form-inline">
|
||||||
<label class="sr-only" for="start">{{'startDate' | i18n}}</label>
|
<label class="sr-only" for="start">{{ "startDate" | i18n }}</label>
|
||||||
<input type="datetime-local" class="form-control form-control-sm" id="start"
|
<input
|
||||||
placeholder="{{'startDate' | i18n}}" [(ngModel)]="start" placeholder="YYYY-MM-DDTHH:MM"
|
type="datetime-local"
|
||||||
(change)="dirtyDates = true">
|
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>
|
<span class="mx-2">-</span>
|
||||||
<label class="sr-only" for="end">{{'endDate' | i18n}}</label>
|
<label class="sr-only" for="end">{{ "endDate" | i18n }}</label>
|
||||||
<input type="datetime-local" class="form-control form-control-sm" id="end"
|
<input
|
||||||
placeholder="{{'endDate' | i18n}}" [(ngModel)]="end" placeholder="YYYY-MM-DDTHH:MM"
|
type="datetime-local"
|
||||||
(change)="dirtyDates = true">
|
class="form-control form-control-sm"
|
||||||
|
id="end"
|
||||||
|
placeholder="{{ 'endDate' | i18n }}"
|
||||||
|
[(ngModel)]="end"
|
||||||
|
placeholder="YYYY-MM-DDTHH:MM"
|
||||||
|
(change)="dirtyDates = true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<form #refreshForm [appApiAction]="refreshPromise" class="d-inline">
|
<form #refreshForm [appApiAction]="refreshPromise" class="d-inline">
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="loadEvents(true)"
|
<button
|
||||||
[disabled]="loaded && refreshForm.loading">
|
type="button"
|
||||||
<i class="fa fa-refresh fa-fw" aria-hidden="true" [ngClass]="{'fa-spin': loaded && refreshForm.loading}"></i>
|
class="btn btn-sm btn-outline-primary ml-3"
|
||||||
{{'refresh' | i18n}}
|
(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>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<form #exportForm [appApiAction]="exportPromise" class="d-inline">
|
<form #exportForm [appApiAction]="exportPromise" class="d-inline">
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary btn-submit manual ml-3"
|
<button
|
||||||
[ngClass]="{loading:exportForm.loading}" (click)="exportEvents()"
|
type="button"
|
||||||
[disabled]="loaded && exportForm.loading || dirtyDates">
|
class="btn btn-sm btn-outline-primary btn-submit manual ml-3"
|
||||||
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
|
[ngClass]="{ loading: exportForm.loading }"
|
||||||
<span>{{'export' | i18n}}</span>
|
(click)="exportEvents()"
|
||||||
|
[disabled]="(loaded && exportForm.loading) || dirtyDates"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-spinner bwi-spin" aria-hidden="true"></i>
|
||||||
|
<span>{{ "export" | i18n }}</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="!loaded">
|
<ng-container *ngIf="!loaded">
|
||||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
<i
|
||||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
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>
|
||||||
<ng-container *ngIf="loaded">
|
<ng-container *ngIf="loaded">
|
||||||
<p *ngIf="!events || !events.length">{{'noEventsInList' | i18n}}</p>
|
<p *ngIf="!events || !events.length">{{ "noEventsInList" | i18n }}</p>
|
||||||
<table class="table table-hover" *ngIf="events && events.length">
|
<table class="table table-hover" *ngIf="events && events.length">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="border-top-0" width="210">{{'timestamp' | i18n}}</th>
|
<th class="border-top-0" width="210">{{ "timestamp" | i18n }}</th>
|
||||||
<th class="border-top-0" width="40">
|
<th class="border-top-0" width="40">
|
||||||
<span class="sr-only">{{'device' | i18n}}</span>
|
<span class="sr-only">{{ "device" | i18n }}</span>
|
||||||
</th>
|
</th>
|
||||||
<th class="border-top-0" width="150">{{'user' | i18n}}</th>
|
<th class="border-top-0" width="150">{{ "user" | i18n }}</th>
|
||||||
<th class="border-top-0">{{'event' | i18n}}</th>
|
<th class="border-top-0">{{ "event" | i18n }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let e of events">
|
<tr *ngFor="let e of events">
|
||||||
<td>{{e.date | date:'medium'}}</td>
|
<td>{{ e.date | date: "medium" }}</td>
|
||||||
<td>
|
<td>
|
||||||
<i class="text-muted fa fa-lg {{e.appIcon}}" title="{{e.appName}}, {{e.ip}}" aria-hidden="true"></i>
|
<i
|
||||||
<span class="sr-only">{{e.appName}}, {{e.ip}}</span>
|
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>
|
||||||
<td>
|
<td>
|
||||||
<span title="{{e.userEmail}}">{{e.userName}}</span>
|
<span title="{{ e.userEmail }}">{{ e.userName }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td [innerHTML]="e.message"></td>
|
<td [innerHTML]="e.message"></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<button #moreBtn [appApiAction]="morePromise" type="button" class="btn btn-block btn-link btn-submit"
|
<button
|
||||||
(click)="loadEvents(false)" [disabled]="loaded && moreBtn.loading" *ngIf="continuationToken">
|
#moreBtn
|
||||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
[appApiAction]="morePromise"
|
||||||
<span>{{'loadMore' | i18n}}</span>
|
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>
|
</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@@ -1,49 +1,53 @@
|
|||||||
import {
|
import { Component, OnInit } from "@angular/core";
|
||||||
Component,
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
OnInit,
|
|
||||||
} from '@angular/core';
|
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
|
||||||
import { ToasterService } from 'angular2-toaster';
|
|
||||||
|
|
||||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
import { ExportService } from 'jslib-common/abstractions/export.service';
|
import { ExportService } from "jslib-common/abstractions/export.service";
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
import { UserService } from 'jslib-common/abstractions/user.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({
|
@Component({
|
||||||
selector: 'provider-events',
|
selector: "provider-events",
|
||||||
templateUrl: 'events.component.html',
|
templateUrl: "events.component.html",
|
||||||
})
|
})
|
||||||
export class EventsComponent extends BaseEventsComponent implements OnInit {
|
export class EventsComponent extends BaseEventsComponent implements OnInit {
|
||||||
exportFileName: string = 'provider-events';
|
exportFileName: string = "provider-events";
|
||||||
providerId: string;
|
providerId: string;
|
||||||
|
|
||||||
private providerUsersUserIdMap = new Map<string, any>();
|
private providerUsersUserIdMap = new Map<string, any>();
|
||||||
private providerUsersIdMap = new Map<string, any>();
|
private providerUsersIdMap = new Map<string, any>();
|
||||||
|
|
||||||
constructor(private apiService: ApiService, private route: ActivatedRoute, eventService: EventService,
|
constructor(
|
||||||
i18nService: I18nService, toasterService: ToasterService, private userService: UserService,
|
private apiService: ApiService,
|
||||||
exportService: ExportService, platformUtilsService: PlatformUtilsService, private router: Router,
|
private route: ActivatedRoute,
|
||||||
logService: LogService, private userNamePipe: UserNamePipe) {
|
eventService: EventService,
|
||||||
super(eventService, i18nService, toasterService, exportService, platformUtilsService, logService);
|
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() {
|
async ngOnInit() {
|
||||||
this.route.parent.parent.params.subscribe(async params => {
|
this.route.parent.parent.params.subscribe(async (params) => {
|
||||||
this.providerId = params.providerId;
|
this.providerId = params.providerId;
|
||||||
const provider = await this.userService.getProvider(this.providerId);
|
const provider = await this.providerService.get(this.providerId);
|
||||||
if (provider == null || !provider.useEvents) {
|
if (provider == null || !provider.useEvents) {
|
||||||
this.router.navigate(['/providers', this.providerId]);
|
this.router.navigate(["/providers", this.providerId]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.load();
|
await this.load();
|
||||||
@@ -52,7 +56,7 @@ export class EventsComponent extends BaseEventsComponent implements OnInit {
|
|||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
const response = await this.apiService.getProviderUsers(this.providerId);
|
const response = await this.apiService.getProviderUsers(this.providerId);
|
||||||
response.data.forEach(u => {
|
response.data.forEach((u) => {
|
||||||
const name = this.userNamePipe.transform(u);
|
const name = this.userNamePipe.transform(u);
|
||||||
this.providerUsersIdMap.set(u.id, { name: name, email: u.email });
|
this.providerUsersIdMap.set(u.id, { name: name, email: u.email });
|
||||||
this.providerUsersUserIdMap.set(u.userId, { name: name, email: u.email });
|
this.providerUsersUserIdMap.set(u.userId, { name: name, email: u.email });
|
||||||
@@ -62,10 +66,17 @@ export class EventsComponent extends BaseEventsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected requestEvents(startDate: string, endDate: string, continuationToken: string) {
|
protected requestEvents(startDate: string, endDate: string, continuationToken: string) {
|
||||||
return this.apiService.getEventsProvider(this.providerId, startDate, endDate, continuationToken);
|
return this.apiService.getEventsProvider(
|
||||||
|
this.providerId,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
continuationToken
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getUserName(r: EventResponse, userId: string) {
|
protected getUserName(r: EventResponse, userId: string) {
|
||||||
return userId != null && this.providerUsersUserIdMap.has(userId) ? this.providerUsersUserIdMap.get(userId) : null;
|
return userId != null && this.providerUsersUserIdMap.has(userId)
|
||||||
|
? this.providerUsersUserIdMap.get(userId)
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,29 @@
|
|||||||
<div class="container page-content">
|
<div class="container page-content">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-3">
|
<div class="col-3">
|
||||||
<div class="card" *ngIf="provider">
|
<div class="card" *ngIf="provider">
|
||||||
<div class="card-header">{{'manage' | i18n}}</div>
|
<div class="card-header">{{ "manage" | i18n }}</div>
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
<a routerLink="people" class="list-group-item" routerLinkActive="active"
|
<a
|
||||||
*ngIf="provider.canManageUsers">
|
routerLink="people"
|
||||||
{{'people' | i18n}}
|
class="list-group-item"
|
||||||
|
routerLinkActive="active"
|
||||||
|
*ngIf="provider.canManageUsers"
|
||||||
|
>
|
||||||
|
{{ "people" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
<a routerLink="events" class="list-group-item" routerLinkActive="active"
|
<a
|
||||||
*ngIf="provider.canAccessEventLogs && accessEvents">
|
routerLink="events"
|
||||||
{{'eventLogs' | i18n}}
|
class="list-group-item"
|
||||||
|
routerLinkActive="active"
|
||||||
|
*ngIf="provider.canAccessEventLogs && accessEvents"
|
||||||
|
>
|
||||||
|
{{ "eventLogs" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-9 mt-4 mt-md-0">
|
<div class="col-9">
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,26 +1,23 @@
|
|||||||
import {
|
import { Component, OnInit } from "@angular/core";
|
||||||
Component,
|
import { ActivatedRoute } from "@angular/router";
|
||||||
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({
|
@Component({
|
||||||
selector: 'provider-manage',
|
selector: "provider-manage",
|
||||||
templateUrl: 'manage.component.html',
|
templateUrl: "manage.component.html",
|
||||||
})
|
})
|
||||||
export class ManageComponent implements OnInit {
|
export class ManageComponent implements OnInit {
|
||||||
provider: Provider;
|
provider: Provider;
|
||||||
accessEvents = false;
|
accessEvents = false;
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute, private userService: UserService) { }
|
constructor(private route: ActivatedRoute, private providerService: ProviderService) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.route.parent.params.subscribe(async params => {
|
this.route.parent.params.subscribe(async (params) => {
|
||||||
this.provider = await this.userService.getProvider(params.providerId);
|
this.provider = await this.providerService.get(params.providerId);
|
||||||
this.accessEvents = this.provider.useEvents;
|
this.accessEvents = this.provider.useEvents;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,137 +1,221 @@
|
|||||||
<div class="page-header d-flex">
|
<div class="page-header d-flex">
|
||||||
<h1>{{'people' | i18n}}</h1>
|
<h1>{{ "people" | i18n }}</h1>
|
||||||
<div class="ml-auto d-flex">
|
<div class="ml-auto d-flex">
|
||||||
<div class="btn-group btn-group-sm" role="group">
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
<button type="button" class="btn btn-outline-secondary" [ngClass]="{active: status == null}"
|
<button
|
||||||
(click)="filter(null)">
|
type="button"
|
||||||
{{'all' | i18n}}
|
class="btn btn-outline-secondary"
|
||||||
<span class="badge badge-pill badge-info" *ngIf="allCount">{{allCount}}</span>
|
[ngClass]="{ active: status == null }"
|
||||||
|
(click)="filter(null)"
|
||||||
|
>
|
||||||
|
{{ "all" | i18n }}
|
||||||
|
<span class="badge badge-pill badge-info" *ngIf="allCount">{{ allCount }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-outline-secondary"
|
<button
|
||||||
[ngClass]="{active: status == userStatusType.Invited}"
|
type="button"
|
||||||
(click)="filter(userStatusType.Invited)">
|
class="btn btn-outline-secondary"
|
||||||
{{'invited' | i18n}}
|
[ngClass]="{ active: status == userStatusType.Invited }"
|
||||||
<span class="badge badge-pill badge-info" *ngIf="invitedCount">{{invitedCount}}</span>
|
(click)="filter(userStatusType.Invited)"
|
||||||
|
>
|
||||||
|
{{ "invited" | i18n }}
|
||||||
|
<span class="badge badge-pill badge-info" *ngIf="invitedCount">{{ invitedCount }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-outline-secondary"
|
<button
|
||||||
[ngClass]="{active: status == userStatusType.Accepted}"
|
type="button"
|
||||||
(click)="filter(userStatusType.Accepted)">
|
class="btn btn-outline-secondary"
|
||||||
{{'accepted' | i18n}}
|
[ngClass]="{ active: status == userStatusType.Accepted }"
|
||||||
<span class="badge badge-pill badge-warning" *ngIf="acceptedCount">{{acceptedCount}}</span>
|
(click)="filter(userStatusType.Accepted)"
|
||||||
|
>
|
||||||
|
{{ "accepted" | i18n }}
|
||||||
|
<span class="badge badge-pill badge-warning" *ngIf="acceptedCount">{{
|
||||||
|
acceptedCount
|
||||||
|
}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3">
|
<div class="ml-3">
|
||||||
<label class="sr-only" for="search">{{'search' | i18n}}</label>
|
<label class="sr-only" for="search">{{ "search" | i18n }}</label>
|
||||||
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
|
<input
|
||||||
[(ngModel)]="searchText">
|
type="search"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
id="search"
|
||||||
|
placeholder="{{ 'search' | i18n }}"
|
||||||
|
[(ngModel)]="searchText"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown ml-3" appListDropdown>
|
<div class="dropdown ml-3" appListDropdown>
|
||||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="bulkActionsButton"
|
<button
|
||||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" appA11yTitle="{{'options' | i18n}}">
|
class="btn btn-sm btn-outline-secondary dropdown-toggle"
|
||||||
<i class="fa fa-cog" aria-hidden="true"></i>
|
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>
|
</button>
|
||||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="bulkActionsButton">
|
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="bulkActionsButton">
|
||||||
<button class="dropdown-item" appStopClick (click)="bulkReinvite()">
|
<button class="dropdown-item" appStopClick (click)="bulkReinvite()">
|
||||||
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
|
||||||
{{'reinviteSelected' | i18n}}
|
{{ "reinviteSelected" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button class="dropdown-item text-success" appStopClick (click)="bulkConfirm()"
|
<button
|
||||||
*ngIf="showBulkConfirmUsers">
|
class="dropdown-item text-success"
|
||||||
<i class="fa fa-fw fa-check" aria-hidden="true"></i>
|
appStopClick
|
||||||
{{'confirmSelected' | i18n}}
|
(click)="bulkConfirm()"
|
||||||
|
*ngIf="showBulkConfirmUsers"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
||||||
|
{{ "confirmSelected" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button class="dropdown-item text-danger" appStopClick (click)="bulkRemove()">
|
<button class="dropdown-item text-danger" appStopClick (click)="bulkRemove()">
|
||||||
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
||||||
{{'remove' | i18n}}
|
{{ "remove" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<button class="dropdown-item" appStopClick (click)="selectAll(true)">
|
<button class="dropdown-item" appStopClick (click)="selectAll(true)">
|
||||||
<i class="fa fa-fw fa-check-square-o" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-check-square" aria-hidden="true"></i>
|
||||||
{{'selectAll' | i18n}}
|
{{ "selectAll" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button class="dropdown-item" appStopClick (click)="selectAll(false)">
|
<button class="dropdown-item" appStopClick (click)="selectAll(false)">
|
||||||
<i class="fa fa-fw fa-minus-square-o" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-minus-square" aria-hidden="true"></i>
|
||||||
{{'unselectAll' | i18n}}
|
{{ "unselectAll" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="invite()">
|
<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>
|
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||||
{{'inviteUser' | i18n}}
|
{{ "inviteUser" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="loading">
|
<ng-container *ngIf="loading">
|
||||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
<i
|
||||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
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>
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngIf="!loading && (isPaging() ? pagedUsers : users | search:searchText:'name':'email':'id') as searchedUsers">
|
*ngIf="
|
||||||
<p *ngIf="!searchedUsers.length">{{'noUsersInList' | i18n}}</p>
|
!loading &&
|
||||||
|
(isPaging() ? pagedUsers : (users | search: searchText:'name':'email':'id')) as searchedUsers
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<p *ngIf="!searchedUsers.length">{{ "noUsersInList" | i18n }}</p>
|
||||||
<ng-container *ngIf="searchedUsers.length">
|
<ng-container *ngIf="searchedUsers.length">
|
||||||
<app-callout type="info" title="{{'confirmUsers' | i18n}}" icon="fa-check-circle" *ngIf="showConfirmUsers">
|
<app-callout
|
||||||
{{'providerUsersNeedConfirmed' | i18n}}
|
type="info"
|
||||||
|
title="{{ 'confirmUsers' | i18n }}"
|
||||||
|
icon="bwi bwi-check-circle"
|
||||||
|
*ngIf="showConfirmUsers"
|
||||||
|
>
|
||||||
|
{{ "providerUsersNeedConfirmed" | i18n }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
<table class="table table-hover table-list" infiniteScroll [infiniteScrollDistance]="1"
|
<table
|
||||||
[infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
|
class="table table-hover table-list"
|
||||||
|
infiniteScroll
|
||||||
|
[infiniteScrollDistance]="1"
|
||||||
|
[infiniteScrollDisabled]="!isPaging()"
|
||||||
|
(scrolled)="loadMore()"
|
||||||
|
>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let u of searchedUsers">
|
<tr *ngFor="let u of searchedUsers">
|
||||||
<td (click)="checkUser(u)" class="table-list-checkbox">
|
<td (click)="checkUser(u)" class="table-list-checkbox">
|
||||||
<input type="checkbox" [(ngModel)]="u.checked" appStopProp>
|
<input type="checkbox" [(ngModel)]="u.checked" appStopProp />
|
||||||
</td>
|
</td>
|
||||||
<td width="30">
|
<td width="30">
|
||||||
<app-avatar [data]="u | userName" [email]="u.email" size="25" [circle]="true"
|
<app-avatar
|
||||||
[fontSize]="14"></app-avatar>
|
[data]="u | userName"
|
||||||
|
[email]="u.email"
|
||||||
|
size="25"
|
||||||
|
[circle]="true"
|
||||||
|
[fontSize]="14"
|
||||||
|
>
|
||||||
|
</app-avatar>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="#" appStopClick (click)="edit(u)">{{u.email}}</a>
|
<a href="#" appStopClick (click)="edit(u)">{{ u.email }}</a>
|
||||||
<span class="badge badge-secondary"
|
<span class="badge badge-secondary" *ngIf="u.status === userStatusType.Invited">{{
|
||||||
*ngIf="u.status === userStatusType.Invited">{{'invited' | i18n}}</span>
|
"invited" | i18n
|
||||||
<span class="badge badge-warning"
|
}}</span>
|
||||||
*ngIf="u.status === userStatusType.Accepted">{{'accepted' | i18n}}</span>
|
<span class="badge badge-warning" *ngIf="u.status === userStatusType.Accepted">{{
|
||||||
<small class="text-muted d-block" *ngIf="u.name">{{u.name}}</small>
|
"accepted" | i18n
|
||||||
|
}}</span>
|
||||||
|
<small class="text-muted d-block" *ngIf="u.name">{{ u.name }}</small>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<ng-container *ngIf="u.twoFactorEnabled">
|
<ng-container *ngIf="u.twoFactorEnabled">
|
||||||
<i class="fa fa-lock" title="{{'userUsingTwoStep' | i18n}}" aria-hidden="true"></i>
|
<i
|
||||||
<span class="sr-only">{{'userUsingTwoStep' | i18n}}</span>
|
class="bwi bwi-lock"
|
||||||
|
title="{{ 'userUsingTwoStep' | i18n }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
<span class="sr-only">{{ "userUsingTwoStep" | i18n }}</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span *ngIf="u.type === userType.ProviderAdmin">{{'providerAdmin' | i18n}}</span>
|
<span *ngIf="u.type === userType.ProviderAdmin">{{ "providerAdmin" | i18n }}</span>
|
||||||
<span *ngIf="u.type === userType.ServiceUser">{{'serviceUser' | i18n}}</span>
|
<span *ngIf="u.type === userType.ServiceUser">{{ "serviceUser" | i18n }}</span>
|
||||||
<span *ngIf="u.type === userType.Custom">{{'custom' | i18n}}</span>
|
<span *ngIf="u.type === userType.Custom">{{ "custom" | i18n }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="table-list-options">
|
<td class="table-list-options">
|
||||||
<div class="dropdown" appListDropdown>
|
<div class="dropdown" appListDropdown>
|
||||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
|
<button
|
||||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
|
class="btn btn-outline-secondary dropdown-toggle"
|
||||||
appA11yTitle="{{'options' | i18n}}">
|
type="button"
|
||||||
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
|
data-toggle="dropdown"
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded="false"
|
||||||
|
appA11yTitle="{{ 'options' | i18n }}"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu dropdown-menu-right">
|
<div class="dropdown-menu dropdown-menu-right">
|
||||||
<a class="dropdown-item" href="#" appStopClick (click)="reinvite(u)"
|
<a
|
||||||
*ngIf="u.status === userStatusType.Invited">
|
class="dropdown-item"
|
||||||
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
|
href="#"
|
||||||
{{'resendInvitation' | i18n}}
|
appStopClick
|
||||||
|
(click)="reinvite(u)"
|
||||||
|
*ngIf="u.status === userStatusType.Invited"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
|
||||||
|
{{ "resendInvitation" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
<a class="dropdown-item text-success" href="#" appStopClick (click)="confirm(u)"
|
<a
|
||||||
*ngIf="u.status === userStatusType.Accepted">
|
class="dropdown-item text-success"
|
||||||
<i class="fa fa-fw fa-check" aria-hidden="true"></i>
|
href="#"
|
||||||
{{'confirm' | i18n}}
|
appStopClick
|
||||||
|
(click)="confirm(u)"
|
||||||
|
*ngIf="u.status === userStatusType.Accepted"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
||||||
|
{{ "confirm" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
<a class="dropdown-item" href="#" appStopClick (click)="groups(u)" *ngIf="accessGroups">
|
<a
|
||||||
<i class="fa fa-fw fa-sitemap" aria-hidden="true"></i>
|
class="dropdown-item"
|
||||||
{{'groups' | i18n}}
|
href="#"
|
||||||
|
appStopClick
|
||||||
|
(click)="groups(u)"
|
||||||
|
*ngIf="accessGroups"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-fw bwi-sitemap" aria-hidden="true"></i>
|
||||||
|
{{ "groups" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
<a class="dropdown-item" href="#" appStopClick (click)="events(u)"
|
<a
|
||||||
*ngIf="accessEvents && u.status === userStatusType.Confirmed">
|
class="dropdown-item"
|
||||||
<i class="fa fa-fw fa-file-text-o" aria-hidden="true"></i>
|
href="#"
|
||||||
{{'eventLogs' | i18n}}
|
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>
|
||||||
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(u)">
|
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(u)">
|
||||||
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-remove" aria-hidden="true"></i>
|
||||||
{{'remove' | i18n}}
|
{{ "remove" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,81 +1,103 @@
|
|||||||
import {
|
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||||
Component,
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
OnInit,
|
|
||||||
ViewChild,
|
|
||||||
ViewContainerRef
|
|
||||||
} from '@angular/core';
|
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
|
||||||
import { ToasterService } from 'angular2-toaster';
|
|
||||||
|
|
||||||
import { first } from 'rxjs/operators';
|
import { first } from "rxjs/operators";
|
||||||
|
|
||||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
|
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
import { SearchService } from 'jslib-common/abstractions/search.service';
|
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
||||||
import { StorageService } from 'jslib-common/abstractions/storage.service';
|
import { SearchService } from "jslib-common/abstractions/search.service";
|
||||||
import { UserService } from 'jslib-common/abstractions/user.service';
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
|
||||||
import { ModalService } from 'jslib-angular/services/modal.service';
|
import { ModalService } from "jslib-angular/services/modal.service";
|
||||||
import { ValidationService } from 'jslib-angular/services/validation.service';
|
import { ValidationService } from "jslib-angular/services/validation.service";
|
||||||
|
|
||||||
import { ProviderUserStatusType } from 'jslib-common/enums/providerUserStatusType';
|
import { ProviderUserStatusType } from "jslib-common/enums/providerUserStatusType";
|
||||||
import { ProviderUserType } from 'jslib-common/enums/providerUserType';
|
import { ProviderUserType } from "jslib-common/enums/providerUserType";
|
||||||
|
|
||||||
import { SearchPipe } from 'jslib-angular/pipes/search.pipe';
|
import { SearchPipe } from "jslib-angular/pipes/search.pipe";
|
||||||
import { UserNamePipe } from 'jslib-angular/pipes/user-name.pipe';
|
import { UserNamePipe } from "jslib-angular/pipes/user-name.pipe";
|
||||||
|
|
||||||
import { ListResponse } from 'jslib-common/models/response/listResponse';
|
import { ListResponse } from "jslib-common/models/response/listResponse";
|
||||||
import { ProviderUserUserDetailsResponse } from 'jslib-common/models/response/provider/providerUserResponse';
|
import { ProviderUserUserDetailsResponse } from "jslib-common/models/response/provider/providerUserResponse";
|
||||||
|
|
||||||
import { ProviderUserBulkRequest } from 'jslib-common/models/request/provider/providerUserBulkRequest';
|
import { ProviderUserBulkRequest } from "jslib-common/models/request/provider/providerUserBulkRequest";
|
||||||
import { ProviderUserConfirmRequest } from 'jslib-common/models/request/provider/providerUserConfirmRequest';
|
import { ProviderUserConfirmRequest } from "jslib-common/models/request/provider/providerUserConfirmRequest";
|
||||||
import { ProviderUserBulkResponse } from 'jslib-common/models/response/provider/providerUserBulkResponse';
|
import { ProviderUserBulkResponse } from "jslib-common/models/response/provider/providerUserBulkResponse";
|
||||||
|
|
||||||
import { BasePeopleComponent } from 'src/app/common/base.people.component';
|
import { BasePeopleComponent } from "src/app/common/base.people.component";
|
||||||
import { BulkStatusComponent } from 'src/app/organizations/manage/bulk/bulk-status.component';
|
import { BulkStatusComponent } from "src/app/organizations/manage/bulk/bulk-status.component";
|
||||||
import { EntityEventsComponent } from 'src/app/organizations/manage/entity-events.component';
|
import { EntityEventsComponent } from "src/app/organizations/manage/entity-events.component";
|
||||||
import { BulkConfirmComponent } from './bulk/bulk-confirm.component';
|
import { BulkConfirmComponent } from "./bulk/bulk-confirm.component";
|
||||||
import { BulkRemoveComponent } from './bulk/bulk-remove.component';
|
import { BulkRemoveComponent } from "./bulk/bulk-remove.component";
|
||||||
import { UserAddEditComponent } from './user-add-edit.component';
|
import { UserAddEditComponent } from "./user-add-edit.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'provider-people',
|
selector: "provider-people",
|
||||||
templateUrl: 'people.component.html',
|
templateUrl: "people.component.html",
|
||||||
})
|
})
|
||||||
export class PeopleComponent extends BasePeopleComponent<ProviderUserUserDetailsResponse> implements OnInit {
|
export class PeopleComponent
|
||||||
|
extends BasePeopleComponent<ProviderUserUserDetailsResponse>
|
||||||
@ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
implements OnInit
|
||||||
@ViewChild('groupsTemplate', { read: ViewContainerRef, static: true }) groupsModalRef: ViewContainerRef;
|
{
|
||||||
@ViewChild('eventsTemplate', { read: ViewContainerRef, static: true }) eventsModalRef: ViewContainerRef;
|
@ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
||||||
@ViewChild('bulkStatusTemplate', { read: ViewContainerRef, static: true }) bulkStatusModalRef: ViewContainerRef;
|
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
|
||||||
@ViewChild('bulkConfirmTemplate', { read: ViewContainerRef, static: true }) bulkConfirmModalRef: ViewContainerRef;
|
groupsModalRef: ViewContainerRef;
|
||||||
@ViewChild('bulkRemoveTemplate', { read: ViewContainerRef, static: true }) bulkRemoveModalRef: 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;
|
userType = ProviderUserType;
|
||||||
userStatusType = ProviderUserStatusType;
|
userStatusType = ProviderUserStatusType;
|
||||||
providerId: string;
|
providerId: string;
|
||||||
accessEvents = false;
|
accessEvents = false;
|
||||||
|
|
||||||
constructor(apiService: ApiService, private route: ActivatedRoute,
|
constructor(
|
||||||
i18nService: I18nService, modalService: ModalService,
|
apiService: ApiService,
|
||||||
platformUtilsService: PlatformUtilsService, toasterService: ToasterService,
|
private route: ActivatedRoute,
|
||||||
cryptoService: CryptoService, private userService: UserService, private router: Router,
|
i18nService: I18nService,
|
||||||
storageService: StorageService, searchService: SearchService, validationService: ValidationService,
|
modalService: ModalService,
|
||||||
logService: LogService, searchPipe: SearchPipe, userNamePipe: UserNamePipe) {
|
platformUtilsService: PlatformUtilsService,
|
||||||
super(apiService, searchService, i18nService, platformUtilsService, toasterService, cryptoService,
|
cryptoService: CryptoService,
|
||||||
storageService, validationService, modalService, logService, searchPipe, userNamePipe);
|
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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.route.parent.params.subscribe(async params => {
|
this.route.parent.params.subscribe(async (params) => {
|
||||||
this.providerId = params.providerId;
|
this.providerId = params.providerId;
|
||||||
const provider = await this.userService.getProvider(this.providerId);
|
const provider = await this.providerService.get(this.providerId);
|
||||||
|
|
||||||
if (!provider.canManageUsers) {
|
if (!provider.canManageUsers) {
|
||||||
this.router.navigate(['../'], { relativeTo: this.route });
|
this.router.navigate(["../"], { relativeTo: this.route });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,10 +105,10 @@ export class PeopleComponent extends BasePeopleComponent<ProviderUserUserDetails
|
|||||||
|
|
||||||
await this.load();
|
await this.load();
|
||||||
|
|
||||||
this.route.queryParams.pipe(first()).subscribe(async qParams => {
|
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||||
this.searchText = qParams.search;
|
this.searchText = qParams.search;
|
||||||
if (qParams.viewEvents != null) {
|
if (qParams.viewEvents != null) {
|
||||||
const user = this.users.filter(u => u.id === qParams.viewEvents);
|
const user = this.users.filter((u) => u.id === qParams.viewEvents);
|
||||||
if (user.length > 0 && user[0].status === ProviderUserStatusType.Confirmed) {
|
if (user.length > 0 && user[0].status === ProviderUserStatusType.Confirmed) {
|
||||||
this.events(user[0]);
|
this.events(user[0]);
|
||||||
}
|
}
|
||||||
@@ -116,7 +138,10 @@ export class PeopleComponent extends BasePeopleComponent<ProviderUserUserDetails
|
|||||||
}
|
}
|
||||||
|
|
||||||
async edit(user: ProviderUserUserDetailsResponse) {
|
async edit(user: ProviderUserUserDetailsResponse) {
|
||||||
const [modal] = await this.modalService.openViewRef(UserAddEditComponent, this.addEditModalRef, comp => {
|
const [modal] = await this.modalService.openViewRef(
|
||||||
|
UserAddEditComponent,
|
||||||
|
this.addEditModalRef,
|
||||||
|
(comp) => {
|
||||||
comp.name = this.userNamePipe.transform(user);
|
comp.name = this.userNamePipe.transform(user);
|
||||||
comp.providerId = this.providerId;
|
comp.providerId = this.providerId;
|
||||||
comp.providerUserId = user != null ? user.id : null;
|
comp.providerUserId = user != null ? user.id : null;
|
||||||
@@ -128,17 +153,22 @@ export class PeopleComponent extends BasePeopleComponent<ProviderUserUserDetails
|
|||||||
modal.close();
|
modal.close();
|
||||||
this.removeUser(user);
|
this.removeUser(user);
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async events(user: ProviderUserUserDetailsResponse) {
|
async events(user: ProviderUserUserDetailsResponse) {
|
||||||
const [modal] = await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, comp => {
|
const [modal] = await this.modalService.openViewRef(
|
||||||
|
EntityEventsComponent,
|
||||||
|
this.eventsModalRef,
|
||||||
|
(comp) => {
|
||||||
comp.name = this.userNamePipe.transform(user);
|
comp.name = this.userNamePipe.transform(user);
|
||||||
comp.providerId = this.providerId;
|
comp.providerId = this.providerId;
|
||||||
comp.entityId = user.id;
|
comp.entityId = user.id;
|
||||||
comp.showUser = false;
|
comp.showUser = false;
|
||||||
comp.entity = 'user';
|
comp.entity = "user";
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkRemove() {
|
async bulkRemove() {
|
||||||
@@ -146,10 +176,14 @@ export class PeopleComponent extends BasePeopleComponent<ProviderUserUserDetails
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [modal] = await this.modalService.openViewRef(BulkRemoveComponent, this.bulkRemoveModalRef, comp => {
|
const [modal] = await this.modalService.openViewRef(
|
||||||
|
BulkRemoveComponent,
|
||||||
|
this.bulkRemoveModalRef,
|
||||||
|
(comp) => {
|
||||||
comp.providerId = this.providerId;
|
comp.providerId = this.providerId;
|
||||||
comp.users = this.getCheckedUsers();
|
comp.users = this.getCheckedUsers();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
await modal.onClosedPromise();
|
await modal.onClosedPromise();
|
||||||
await this.load();
|
await this.load();
|
||||||
@@ -161,18 +195,26 @@ export class PeopleComponent extends BasePeopleComponent<ProviderUserUserDetails
|
|||||||
}
|
}
|
||||||
|
|
||||||
const users = this.getCheckedUsers();
|
const users = this.getCheckedUsers();
|
||||||
const filteredUsers = users.filter(u => u.status === ProviderUserStatusType.Invited);
|
const filteredUsers = users.filter((u) => u.status === ProviderUserStatusType.Invited);
|
||||||
|
|
||||||
if (filteredUsers.length <= 0) {
|
if (filteredUsers.length <= 0) {
|
||||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
this.platformUtilsService.showToast(
|
||||||
this.i18nService.t('noSelectedUsersApplicable'));
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("noSelectedUsersApplicable")
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const request = new ProviderUserBulkRequest(filteredUsers.map(user => user.id));
|
const request = new ProviderUserBulkRequest(filteredUsers.map((user) => user.id));
|
||||||
const response = this.apiService.postManyProviderUserReinvite(this.providerId, request);
|
const response = this.apiService.postManyProviderUserReinvite(this.providerId, request);
|
||||||
this.showBulkStatus(users, filteredUsers, response, this.i18nService.t('bulkReinviteMessage'));
|
this.showBulkStatus(
|
||||||
|
users,
|
||||||
|
filteredUsers,
|
||||||
|
response,
|
||||||
|
this.i18nService.t("bulkReinviteMessage")
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.validationService.showError(e);
|
this.validationService.showError(e);
|
||||||
}
|
}
|
||||||
@@ -184,21 +226,32 @@ export class PeopleComponent extends BasePeopleComponent<ProviderUserUserDetails
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [modal] = await this.modalService.openViewRef(BulkConfirmComponent, this.bulkConfirmModalRef, comp => {
|
const [modal] = await this.modalService.openViewRef(
|
||||||
|
BulkConfirmComponent,
|
||||||
|
this.bulkConfirmModalRef,
|
||||||
|
(comp) => {
|
||||||
comp.providerId = this.providerId;
|
comp.providerId = this.providerId;
|
||||||
comp.users = this.getCheckedUsers();
|
comp.users = this.getCheckedUsers();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
await modal.onClosedPromise();
|
await modal.onClosedPromise();
|
||||||
await this.load();
|
await this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async showBulkStatus(users: ProviderUserUserDetailsResponse[], filteredUsers: ProviderUserUserDetailsResponse[],
|
private async showBulkStatus(
|
||||||
request: Promise<ListResponse<ProviderUserBulkResponse>>, successfullMessage: string) {
|
users: ProviderUserUserDetailsResponse[],
|
||||||
|
filteredUsers: ProviderUserUserDetailsResponse[],
|
||||||
const [modal, childComponent] = await this.modalService.openViewRef(BulkStatusComponent, this.bulkStatusModalRef, comp => {
|
request: Promise<ListResponse<ProviderUserBulkResponse>>,
|
||||||
|
successfullMessage: string
|
||||||
|
) {
|
||||||
|
const [modal, childComponent] = await this.modalService.openViewRef(
|
||||||
|
BulkStatusComponent,
|
||||||
|
this.bulkStatusModalRef,
|
||||||
|
(comp) => {
|
||||||
comp.loading = true;
|
comp.loading = true;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Workaround to handle closing the modal shortly after it has been opened
|
// Workaround to handle closing the modal shortly after it has been opened
|
||||||
let close = false;
|
let close = false;
|
||||||
@@ -212,13 +265,15 @@ export class PeopleComponent extends BasePeopleComponent<ProviderUserUserDetails
|
|||||||
const response = await request;
|
const response = await request;
|
||||||
|
|
||||||
if (modal) {
|
if (modal) {
|
||||||
const keyedErrors: any = response.data.filter(r => r.error !== '').reduce((a, x) => ({ ...a, [x.id]: x.error }), {});
|
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 }), {});
|
const keyedFilteredUsers: any = filteredUsers.reduce((a, x) => ({ ...a, [x.id]: x }), {});
|
||||||
|
|
||||||
childComponent.users = users.map(user => {
|
childComponent.users = users.map((user) => {
|
||||||
let message = keyedErrors[user.id] ?? successfullMessage;
|
let message = keyedErrors[user.id] ?? successfullMessage;
|
||||||
if (!keyedFilteredUsers.hasOwnProperty(user.id)) {
|
if (!keyedFilteredUsers.hasOwnProperty(user.id)) {
|
||||||
message = this.i18nService.t('bulkFilteredMessage');
|
message = this.i18nService.t("bulkFilteredMessage");
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,68 +1,121 @@
|
|||||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
|
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
|
||||||
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
||||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
<form
|
||||||
|
class="modal-content"
|
||||||
|
#form
|
||||||
|
(ngSubmit)="submit()"
|
||||||
|
[appApiAction]="formPromise"
|
||||||
|
ngNativeValidate
|
||||||
|
>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title" id="userAddEditTitle">
|
<h2 class="modal-title" id="userAddEditTitle">
|
||||||
{{title}}
|
{{ title }}
|
||||||
<small class="text-muted" *ngIf="name">{{name}}</small>
|
<small class="text-muted" *ngIf="name">{{ name }}</small>
|
||||||
</h2>
|
</h2>
|
||||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="close"
|
||||||
|
data-dismiss="modal"
|
||||||
|
appA11yTitle="{{ 'close' | i18n }}"
|
||||||
|
>
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" *ngIf="loading">
|
<div class="modal-body" *ngIf="loading">
|
||||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
<i
|
||||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
class="bwi bwi-spinner bwi-spin text-muted"
|
||||||
|
title="{{ 'loading' | i18n }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" *ngIf="!loading">
|
<div class="modal-body" *ngIf="!loading">
|
||||||
<ng-container *ngIf="!editMode">
|
<ng-container *ngIf="!editMode">
|
||||||
<p>{{'providerInviteUserDesc' | i18n}}</p>
|
<p>{{ "providerInviteUserDesc" | i18n }}</p>
|
||||||
<div class="form-group mb-4">
|
<div class="form-group mb-4">
|
||||||
<label for="emails">{{'email' | i18n}}</label>
|
<label for="emails">{{ "email" | i18n }}</label>
|
||||||
<input id="emails" class="form-control" type="text" name="Emails" [(ngModel)]="emails" required
|
<input
|
||||||
appAutoFocus>
|
id="emails"
|
||||||
<small class="text-muted">{{'inviteMultipleEmailDesc' | i18n : '20'}}</small>
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
name="Emails"
|
||||||
|
[(ngModel)]="emails"
|
||||||
|
required
|
||||||
|
appAutoFocus
|
||||||
|
/>
|
||||||
|
<small class="text-muted">{{ "inviteMultipleEmailDesc" | i18n: "20" }}</small>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<h3>
|
<h3>
|
||||||
{{'userType' | i18n}}
|
{{ "userType" | i18n }}
|
||||||
<a target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}"
|
<a
|
||||||
href="https://bitwarden.com/help/article/user-types-access-control/#user-types">
|
target="_blank"
|
||||||
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
|
rel="noopener"
|
||||||
|
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||||
|
href="https://bitwarden.com/help/provider-users/"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="form-check mt-2 form-check-block">
|
<div class="form-check mt-2 form-check-block">
|
||||||
<input class="form-check-input" type="radio" name="userType" id="userTypeServiceUser"
|
<input
|
||||||
[value]="userType.ServiceUser" [(ngModel)]="type">
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="userType"
|
||||||
|
id="userTypeServiceUser"
|
||||||
|
[value]="userType.ServiceUser"
|
||||||
|
[(ngModel)]="type"
|
||||||
|
/>
|
||||||
<label class="form-check-label" for="userTypeServiceUser">
|
<label class="form-check-label" for="userTypeServiceUser">
|
||||||
{{'serviceUser' | i18n}}
|
{{ "serviceUser" | i18n }}
|
||||||
<small>{{'serviceUserDesc' | i18n}}</small>
|
<small>{{ "serviceUserDesc" | i18n }}</small>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check mt-2 form-check-block">
|
<div class="form-check mt-2 form-check-block">
|
||||||
<input class="form-check-input" type="radio" name="userType" id="userTypeProviderAdmin"
|
<input
|
||||||
[value]="userType.ProviderAdmin" [(ngModel)]="type">
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="userType"
|
||||||
|
id="userTypeProviderAdmin"
|
||||||
|
[value]="userType.ProviderAdmin"
|
||||||
|
[(ngModel)]="type"
|
||||||
|
/>
|
||||||
<label class="form-check-label" for="userTypeProviderAdmin">
|
<label class="form-check-label" for="userTypeProviderAdmin">
|
||||||
{{'providerAdmin' | i18n}}
|
{{ "providerAdmin" | i18n }}
|
||||||
<small>{{'providerAdminDesc' | i18n}}</small>
|
<small>{{ "providerAdminDesc" | i18n }}</small>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||||
<span>{{'save' | i18n}}</span>
|
<span>{{ "save" | i18n }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||||
{{'cancel' | i18n}}
|
{{ "cancel" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<div class="ml-auto">
|
<div class="ml-auto">
|
||||||
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
|
<button
|
||||||
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"
|
#deleteBtn
|
||||||
[appApiAction]="deletePromise">
|
type="button"
|
||||||
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading" aria-hidden="true"></i>
|
(click)="delete()"
|
||||||
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"
|
class="btn btn-outline-danger"
|
||||||
title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,28 +1,20 @@
|
|||||||
import {
|
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||||
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 { ProviderUserInviteRequest } from "jslib-common/models/request/provider/providerUserInviteRequest";
|
||||||
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 { 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({
|
@Component({
|
||||||
selector: 'provider-user-add-edit',
|
selector: "provider-user-add-edit",
|
||||||
templateUrl: 'user-add-edit.component.html',
|
templateUrl: "user-add-edit.component.html",
|
||||||
})
|
})
|
||||||
export class UserAddEditComponent implements OnInit {
|
export class UserAddEditComponent implements OnInit {
|
||||||
@Input() name: string;
|
@Input() name: string;
|
||||||
@@ -38,21 +30,24 @@ export class UserAddEditComponent implements OnInit {
|
|||||||
type: ProviderUserType = ProviderUserType.ServiceUser;
|
type: ProviderUserType = ProviderUserType.ServiceUser;
|
||||||
permissions = new PermissionsApi();
|
permissions = new PermissionsApi();
|
||||||
showCustom = false;
|
showCustom = false;
|
||||||
access: 'all' | 'selected' = 'selected';
|
access: "all" | "selected" = "selected";
|
||||||
formPromise: Promise<any>;
|
formPromise: Promise<any>;
|
||||||
deletePromise: Promise<any>;
|
deletePromise: Promise<any>;
|
||||||
userType = ProviderUserType;
|
userType = ProviderUserType;
|
||||||
|
|
||||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
constructor(
|
||||||
private toasterService: ToasterService, private platformUtilsService: PlatformUtilsService,
|
private apiService: ApiService,
|
||||||
private logService: LogService) { }
|
private i18nService: I18nService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private logService: LogService
|
||||||
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.editMode = this.loading = this.providerUserId != null;
|
this.editMode = this.loading = this.providerUserId != null;
|
||||||
|
|
||||||
if (this.editMode) {
|
if (this.editMode) {
|
||||||
this.editMode = true;
|
this.editMode = true;
|
||||||
this.title = this.i18nService.t('editUser');
|
this.title = this.i18nService.t("editUser");
|
||||||
try {
|
try {
|
||||||
const user = await this.apiService.getProviderUser(this.providerId, this.providerUserId);
|
const user = await this.apiService.getProviderUser(this.providerId, this.providerUserId);
|
||||||
this.type = user.type;
|
this.type = user.type;
|
||||||
@@ -60,7 +55,7 @@ export class UserAddEditComponent implements OnInit {
|
|||||||
this.logService.error(e);
|
this.logService.error(e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.title = this.i18nService.t('inviteUser');
|
this.title = this.i18nService.t("inviteUser");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
@@ -71,7 +66,11 @@ export class UserAddEditComponent implements OnInit {
|
|||||||
if (this.editMode) {
|
if (this.editMode) {
|
||||||
const request = new ProviderUserUpdateRequest();
|
const request = new ProviderUserUpdateRequest();
|
||||||
request.type = this.type;
|
request.type = this.type;
|
||||||
this.formPromise = this.apiService.putProviderUser(this.providerId, this.providerUserId, request);
|
this.formPromise = this.apiService.putProviderUser(
|
||||||
|
this.providerId,
|
||||||
|
this.providerUserId,
|
||||||
|
request
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const request = new ProviderUserInviteRequest();
|
const request = new ProviderUserInviteRequest();
|
||||||
request.emails = this.emails.trim().split(/\s*,\s*/);
|
request.emails = this.emails.trim().split(/\s*,\s*/);
|
||||||
@@ -79,8 +78,11 @@ export class UserAddEditComponent implements OnInit {
|
|||||||
this.formPromise = this.apiService.postProviderUserInvite(this.providerId, request);
|
this.formPromise = this.apiService.postProviderUserInvite(this.providerId, request);
|
||||||
}
|
}
|
||||||
await this.formPromise;
|
await this.formPromise;
|
||||||
this.toasterService.popAsync('success', null,
|
this.platformUtilsService.showToast(
|
||||||
this.i18nService.t(this.editMode ? 'editedUserId' : 'invitedUsers', this.name));
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.name)
|
||||||
|
);
|
||||||
this.onSavedUser.emit();
|
this.onSavedUser.emit();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logService.error(e);
|
this.logService.error(e);
|
||||||
@@ -93,8 +95,12 @@ export class UserAddEditComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const confirmed = await this.platformUtilsService.showDialog(
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
this.i18nService.t('removeUserConfirmation'), this.name,
|
this.i18nService.t("removeUserConfirmation"),
|
||||||
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
this.name,
|
||||||
|
this.i18nService.t("yes"),
|
||||||
|
this.i18nService.t("no"),
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -102,11 +108,14 @@ export class UserAddEditComponent implements OnInit {
|
|||||||
try {
|
try {
|
||||||
this.deletePromise = this.apiService.deleteProviderUser(this.providerId, this.providerUserId);
|
this.deletePromise = this.apiService.deleteProviderUser(this.providerId, this.providerUserId);
|
||||||
await this.deletePromise;
|
await this.deletePromise;
|
||||||
this.toasterService.popAsync('success', null, this.i18nService.t('removedUserId', this.name));
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("removedUserId", this.name)
|
||||||
|
);
|
||||||
this.onDeletedUser.emit();
|
this.onDeletedUser.emit();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logService.error(e);
|
this.logService.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,33 +5,33 @@
|
|||||||
<div class="my-auto d-flex align-items-center pl-1">
|
<div class="my-auto d-flex align-items-center pl-1">
|
||||||
<app-avatar [data]="provider.name" size="45" [circle]="true"></app-avatar>
|
<app-avatar [data]="provider.name" size="45" [circle]="true"></app-avatar>
|
||||||
<div class="org-name ml-3">
|
<div class="org-name ml-3">
|
||||||
<span>{{provider.name}}</span>
|
<span>{{ provider.name }}</span>
|
||||||
<small class="text-muted">{{'provider' | i18n}}</small>
|
<small class="text-muted">{{ "provider" | i18n }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3 card border-danger text-danger bg-transparent" *ngIf="!provider.enabled">
|
<div class="ml-3 card border-danger text-danger bg-transparent" *ngIf="!provider.enabled">
|
||||||
<div class="card-body py-2">
|
<div class="card-body py-2">
|
||||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
<i class="bwi bwi-exclamation-triangle" aria-hidden="true"></i>
|
||||||
{{'providerIsDisabled' | i18n}}
|
{{ "providerIsDisabled" | i18n }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="nav nav-tabs" *ngIf="showMenuBar">
|
<ul class="nav nav-tabs" *ngIf="showMenuBar">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="clients" routerLinkActive="active">
|
<a class="nav-link" routerLink="clients" routerLinkActive="active">
|
||||||
<i class="fa fa-university" aria-hidden="true"></i>
|
<i class="bwi bwi-bank" aria-hidden="true"></i>
|
||||||
{{'clients' | i18n}}
|
{{ "clients" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" *ngIf="showManageTab">
|
<li class="nav-item" *ngIf="showManageTab">
|
||||||
<a class="nav-link" [routerLink]="manageRoute" routerLinkActive="active">
|
<a class="nav-link" [routerLink]="manageRoute" routerLinkActive="active">
|
||||||
<i class="fa fa-sliders" aria-hidden="true"></i>
|
<i class="bwi bwi-sliders" aria-hidden="true"></i>
|
||||||
{{'manage' | i18n}}
|
{{ "manage" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" *ngIf="showSettingsTab">
|
<li class="nav-item" *ngIf="showSettingsTab">
|
||||||
<a class="nav-link" routerLink="settings" routerLinkActive="active">
|
<a class="nav-link" routerLink="settings" routerLinkActive="active">
|
||||||
<i class="fa fa-cogs" aria-hidden="true"></i>
|
<i class="bwi bwi-cogs" aria-hidden="true"></i>
|
||||||
{{'settings' | i18n}}
|
{{ "settings" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,31 +1,30 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from "@angular/core";
|
||||||
import { ActivatedRoute } from '@angular/router';
|
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({
|
@Component({
|
||||||
selector: 'providers-layout',
|
selector: "providers-layout",
|
||||||
templateUrl: 'providers-layout.component.html',
|
templateUrl: "providers-layout.component.html",
|
||||||
})
|
})
|
||||||
export class ProvidersLayoutComponent {
|
export class ProvidersLayoutComponent {
|
||||||
|
|
||||||
provider: Provider;
|
provider: Provider;
|
||||||
private providerId: string;
|
private providerId: string;
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute, private userService: UserService) { }
|
constructor(private route: ActivatedRoute, private providerService: ProviderService) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
document.body.classList.remove('layout_frontend');
|
document.body.classList.remove("layout_frontend");
|
||||||
this.route.params.subscribe(async params => {
|
this.route.params.subscribe(async (params) => {
|
||||||
this.providerId = params.providerId;
|
this.providerId = params.providerId;
|
||||||
await this.load();
|
await this.load();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
this.provider = await this.userService.getProvider(this.providerId);
|
this.provider = await this.providerService.get(this.providerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
get showMenuBar() {
|
get showMenuBar() {
|
||||||
@@ -43,9 +42,9 @@ export class ProvidersLayoutComponent {
|
|||||||
get manageRoute(): string {
|
get manageRoute(): string {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case this.provider.canManageUsers:
|
case this.provider.canManageUsers:
|
||||||
return 'manage/people';
|
return "manage/people";
|
||||||
case this.provider.canAccessEventLogs:
|
case this.provider.canAccessEventLogs:
|
||||||
return 'manage/events';
|
return "manage/events";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,110 +1,110 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from "@angular/core";
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from "@angular/router";
|
||||||
|
|
||||||
import { AuthGuardService } from 'jslib-angular/services/auth-guard.service';
|
import { AuthGuardService } from "jslib-angular/services/auth-guard.service";
|
||||||
import { Permissions } from 'jslib-common/enums/permissions';
|
import { Permissions } from "jslib-common/enums/permissions";
|
||||||
|
|
||||||
import { AddOrganizationComponent } from './clients/add-organization.component';
|
import { AddOrganizationComponent } from "./clients/add-organization.component";
|
||||||
import { ClientsComponent } from './clients/clients.component';
|
import { ClientsComponent } from "./clients/clients.component";
|
||||||
import { CreateOrganizationComponent } from './clients/create-organization.component';
|
import { CreateOrganizationComponent } from "./clients/create-organization.component";
|
||||||
import { AcceptProviderComponent } from './manage/accept-provider.component';
|
import { AcceptProviderComponent } from "./manage/accept-provider.component";
|
||||||
import { EventsComponent } from './manage/events.component';
|
import { EventsComponent } from "./manage/events.component";
|
||||||
import { ManageComponent } from './manage/manage.component';
|
import { ManageComponent } from "./manage/manage.component";
|
||||||
import { PeopleComponent } from './manage/people.component';
|
import { PeopleComponent } from "./manage/people.component";
|
||||||
import { ProvidersLayoutComponent } from './providers-layout.component';
|
import { ProvidersLayoutComponent } from "./providers-layout.component";
|
||||||
import { SettingsComponent } from './settings/settings.component';
|
import { SettingsComponent } from "./settings/settings.component";
|
||||||
import { SetupProviderComponent } from './setup/setup-provider.component';
|
import { SetupProviderComponent } from "./setup/setup-provider.component";
|
||||||
import { SetupComponent } from './setup/setup.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 { ProvidersComponent } from "src/app/providers/providers.component";
|
||||||
import { ProviderGuardService } from './services/provider-guard.service';
|
import { ProviderGuardService } from "./services/provider-guard.service";
|
||||||
import { ProviderTypeGuardService } from './services/provider-type-guard.service';
|
import { ProviderTypeGuardService } from "./services/provider-type-guard.service";
|
||||||
import { AccountComponent } from './settings/account.component';
|
import { AccountComponent } from "./settings/account.component";
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: "",
|
||||||
canActivate: [AuthGuardService],
|
canActivate: [AuthGuardService],
|
||||||
component: ProvidersComponent,
|
component: ProvidersComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: "",
|
||||||
component: FrontendLayoutComponent,
|
component: FrontendLayoutComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'setup-provider',
|
path: "setup-provider",
|
||||||
component: SetupProviderComponent,
|
component: SetupProviderComponent,
|
||||||
data: { titleId: 'setupProvider' },
|
data: { titleId: "setupProvider" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'accept-provider',
|
path: "accept-provider",
|
||||||
component: AcceptProviderComponent,
|
component: AcceptProviderComponent,
|
||||||
data: { titleId: 'acceptProvider' },
|
data: { titleId: "acceptProvider" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: "",
|
||||||
canActivate: [AuthGuardService],
|
canActivate: [AuthGuardService],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'setup',
|
path: "setup",
|
||||||
component: SetupComponent,
|
component: SetupComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ':providerId',
|
path: ":providerId",
|
||||||
component: ProvidersLayoutComponent,
|
component: ProvidersLayoutComponent,
|
||||||
canActivate: [ProviderGuardService],
|
canActivate: [ProviderGuardService],
|
||||||
children: [
|
children: [
|
||||||
{ path: '', pathMatch: 'full', redirectTo: 'clients' },
|
{ path: "", pathMatch: "full", redirectTo: "clients" },
|
||||||
{ path: 'clients/create', component: CreateOrganizationComponent },
|
{ path: "clients/create", component: CreateOrganizationComponent },
|
||||||
{ path: 'clients', component: ClientsComponent, data: { titleId: 'clients' } },
|
{ path: "clients", component: ClientsComponent, data: { titleId: "clients" } },
|
||||||
{
|
{
|
||||||
path: 'manage',
|
path: "manage",
|
||||||
component: ManageComponent,
|
component: ManageComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: "",
|
||||||
pathMatch: 'full',
|
pathMatch: "full",
|
||||||
redirectTo: 'people',
|
redirectTo: "people",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'people',
|
path: "people",
|
||||||
component: PeopleComponent,
|
component: PeopleComponent,
|
||||||
canActivate: [ProviderTypeGuardService],
|
canActivate: [ProviderTypeGuardService],
|
||||||
data: {
|
data: {
|
||||||
titleId: 'people',
|
titleId: "people",
|
||||||
permissions: [Permissions.ManageUsers],
|
permissions: [Permissions.ManageUsers],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'events',
|
path: "events",
|
||||||
component: EventsComponent,
|
component: EventsComponent,
|
||||||
canActivate: [ProviderTypeGuardService],
|
canActivate: [ProviderTypeGuardService],
|
||||||
data: {
|
data: {
|
||||||
titleId: 'eventLogs',
|
titleId: "eventLogs",
|
||||||
permissions: [Permissions.AccessEventLogs],
|
permissions: [Permissions.AccessEventLogs],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'settings',
|
path: "settings",
|
||||||
component: SettingsComponent,
|
component: SettingsComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: "",
|
||||||
pathMatch: 'full',
|
pathMatch: "full",
|
||||||
redirectTo: 'account',
|
redirectTo: "account",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'account',
|
path: "account",
|
||||||
component: AccountComponent,
|
component: AccountComponent,
|
||||||
canActivate: [ProviderTypeGuardService],
|
canActivate: [ProviderTypeGuardService],
|
||||||
data: {
|
data: {
|
||||||
titleId: 'myProvider',
|
titleId: "myProvider",
|
||||||
permissions: [Permissions.ManageProvider],
|
permissions: [Permissions.ManageProvider],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -120,4 +120,4 @@ const routes: Routes = [
|
|||||||
imports: [RouterModule.forChild(routes)],
|
imports: [RouterModule.forChild(routes)],
|
||||||
exports: [RouterModule],
|
exports: [RouterModule],
|
||||||
})
|
})
|
||||||
export class ProvidersRoutingModule { }
|
export class ProvidersRoutingModule {}
|
||||||
|
|||||||
@@ -1,44 +1,39 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from "@angular/common";
|
||||||
import { ComponentFactoryResolver } from '@angular/core';
|
import { ComponentFactoryResolver } from "@angular/core";
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from "@angular/core";
|
||||||
import { FormsModule } from '@angular/forms';
|
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 { ProviderGuardService } from "./services/provider-guard.service";
|
||||||
import { ProviderTypeGuardService } from './services/provider-type-guard.service';
|
import { ProviderTypeGuardService } from "./services/provider-type-guard.service";
|
||||||
import { ProviderService } from './services/provider.service';
|
import { WebProviderService } from "./services/webProvider.service";
|
||||||
|
|
||||||
import { ProvidersLayoutComponent } from './providers-layout.component';
|
import { ProvidersLayoutComponent } from "./providers-layout.component";
|
||||||
import { ProvidersRoutingModule } from './providers-routing.module';
|
import { ProvidersRoutingModule } from "./providers-routing.module";
|
||||||
|
|
||||||
import { AddOrganizationComponent } from './clients/add-organization.component';
|
import { AddOrganizationComponent } from "./clients/add-organization.component";
|
||||||
import { ClientsComponent } from './clients/clients.component';
|
import { ClientsComponent } from "./clients/clients.component";
|
||||||
import { CreateOrganizationComponent } from './clients/create-organization.component';
|
import { CreateOrganizationComponent } from "./clients/create-organization.component";
|
||||||
|
|
||||||
import { AcceptProviderComponent } from './manage/accept-provider.component';
|
import { AcceptProviderComponent } from "./manage/accept-provider.component";
|
||||||
import { BulkConfirmComponent } from './manage/bulk/bulk-confirm.component';
|
import { BulkConfirmComponent } from "./manage/bulk/bulk-confirm.component";
|
||||||
import { BulkRemoveComponent } from './manage/bulk/bulk-remove.component';
|
import { BulkRemoveComponent } from "./manage/bulk/bulk-remove.component";
|
||||||
import { EventsComponent } from './manage/events.component';
|
import { EventsComponent } from "./manage/events.component";
|
||||||
import { ManageComponent } from './manage/manage.component';
|
import { ManageComponent } from "./manage/manage.component";
|
||||||
import { PeopleComponent } from './manage/people.component';
|
import { PeopleComponent } from "./manage/people.component";
|
||||||
import { UserAddEditComponent } from './manage/user-add-edit.component';
|
import { UserAddEditComponent } from "./manage/user-add-edit.component";
|
||||||
|
|
||||||
import { AccountComponent } from './settings/account.component';
|
import { AccountComponent } from "./settings/account.component";
|
||||||
import { SettingsComponent } from './settings/settings.component';
|
import { SettingsComponent } from "./settings/settings.component";
|
||||||
|
|
||||||
import { SetupProviderComponent } from './setup/setup-provider.component';
|
import { SetupProviderComponent } from "./setup/setup-provider.component";
|
||||||
import { SetupComponent } from './setup/setup.component';
|
import { SetupComponent } from "./setup/setup.component";
|
||||||
|
|
||||||
import { OssModule } from 'src/app/oss.module';
|
import { OssModule } from "src/app/oss.module";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [CommonModule, FormsModule, OssModule, ProvidersRoutingModule],
|
||||||
CommonModule,
|
|
||||||
FormsModule,
|
|
||||||
OssModule,
|
|
||||||
ProvidersRoutingModule,
|
|
||||||
],
|
|
||||||
declarations: [
|
declarations: [
|
||||||
AcceptProviderComponent,
|
AcceptProviderComponent,
|
||||||
AccountComponent,
|
AccountComponent,
|
||||||
@@ -56,14 +51,13 @@ import { OssModule } from 'src/app/oss.module';
|
|||||||
SetupProviderComponent,
|
SetupProviderComponent,
|
||||||
UserAddEditComponent,
|
UserAddEditComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [WebProviderService, ProviderGuardService, ProviderTypeGuardService],
|
||||||
ProviderService,
|
|
||||||
ProviderGuardService,
|
|
||||||
ProviderTypeGuardService,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class ProvidersModule {
|
export class ProvidersModule {
|
||||||
constructor(modalService: ModalService, componentFactoryResolver: ComponentFactoryResolver) {
|
constructor(modalService: ModalService, componentFactoryResolver: ComponentFactoryResolver) {
|
||||||
modalService.registerComponentFactoryResolver(AddOrganizationComponent, componentFactoryResolver);
|
modalService.registerComponentFactoryResolver(
|
||||||
|
AddOrganizationComponent,
|
||||||
|
componentFactoryResolver
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,28 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from "@angular/core";
|
||||||
import {
|
import { ActivatedRouteSnapshot, CanActivate, Router } from "@angular/router";
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
CanActivate,
|
|
||||||
Router,
|
|
||||||
} from '@angular/router';
|
|
||||||
|
|
||||||
import { ToasterService } from 'angular2-toaster';
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
||||||
import { UserService } from 'jslib-common/abstractions/user.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ProviderGuardService implements CanActivate {
|
export class ProviderGuardService implements CanActivate {
|
||||||
constructor(private userService: UserService, private router: Router,
|
constructor(
|
||||||
private toasterService: ToasterService, private i18nService: I18nService) { }
|
private router: Router,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private providerService: ProviderService
|
||||||
|
) {}
|
||||||
|
|
||||||
async canActivate(route: ActivatedRouteSnapshot) {
|
async canActivate(route: ActivatedRouteSnapshot) {
|
||||||
const provider = await this.userService.getProvider(route.params.providerId);
|
const provider = await this.providerService.get(route.params.providerId);
|
||||||
if (provider == null) {
|
if (provider == null) {
|
||||||
this.router.navigate(['/']);
|
this.router.navigate(["/"]);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!provider.isProviderAdmin && !provider.enabled) {
|
if (!provider.isProviderAdmin && !provider.enabled) {
|
||||||
this.toasterService.popAsync('error', null, this.i18nService.t('providerIsDisabled'));
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("providerIsDisabled"));
|
||||||
this.router.navigate(['/']);
|
this.router.navigate(["/"]);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from "@angular/core";
|
||||||
import {
|
import { ActivatedRouteSnapshot, CanActivate, Router } from "@angular/router";
|
||||||
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()
|
@Injectable()
|
||||||
export class ProviderTypeGuardService implements CanActivate {
|
export class ProviderTypeGuardService implements CanActivate {
|
||||||
constructor(private userService: UserService, private router: Router) { }
|
constructor(private providerService: ProviderService, private router: Router) {}
|
||||||
|
|
||||||
async canActivate(route: ActivatedRouteSnapshot) {
|
async canActivate(route: ActivatedRouteSnapshot) {
|
||||||
const provider = await this.userService.getProvider(route.params.providerId);
|
const provider = await this.providerService.get(route.params.providerId);
|
||||||
const permissions = route.data == null ? null : route.data.permissions as Permissions[];
|
const permissions = route.data == null ? null : (route.data.permissions as Permissions[]);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(permissions.indexOf(Permissions.AccessEventLogs) !== -1 && provider.canAccessEventLogs) ||
|
(permissions.indexOf(Permissions.AccessEventLogs) !== -1 && provider.canAccessEventLogs) ||
|
||||||
@@ -25,7 +21,7 @@ export class ProviderTypeGuardService implements CanActivate {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.router.navigate(['/providers', provider.id]);
|
this.router.navigate(["/providers", provider.id]);
|
||||||
return false;
|
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">
|
<div class="page-header">
|
||||||
<h1>{{'myProvider' | i18n}}</h1>
|
<h1>{{ "myProvider" | i18n }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="loading">
|
<div *ngIf="loading">
|
||||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
<i
|
||||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
class="bwi bwi-spinner bwi-spin text-muted"
|
||||||
|
title="{{ 'loading' | i18n }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||||
</div>
|
</div>
|
||||||
<form *ngIf="provider && !loading" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
<form
|
||||||
|
*ngIf="provider && !loading"
|
||||||
|
#form
|
||||||
|
(ngSubmit)="submit()"
|
||||||
|
[appApiAction]="formPromise"
|
||||||
|
ngNativeValidate
|
||||||
|
>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 mt-4 mt-md-0">
|
<div class="col-6">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="name">{{'providerName' | i18n}}</label>
|
<label for="name">{{ "providerName" | i18n }}</label>
|
||||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="provider.name"
|
<input
|
||||||
[disabled]="selfHosted">
|
id="name"
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
name="Name"
|
||||||
|
[(ngModel)]="provider.name"
|
||||||
|
[disabled]="selfHosted"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="billingEmail">{{'billingEmail' | i18n}}</label>
|
<label for="billingEmail">{{ "billingEmail" | i18n }}</label>
|
||||||
<input id="billingEmail" class="form-control" type="text" name="BillingEmail"
|
<input
|
||||||
[(ngModel)]="provider.billingEmail" [disabled]="selfHosted">
|
id="billingEmail"
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
name="BillingEmail"
|
||||||
|
[(ngModel)]="provider.billingEmail"
|
||||||
|
[disabled]="selfHosted"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 mt-4 mt-md-0">
|
<div class="col-6">
|
||||||
<app-avatar data="{{provider.name}}" dynamic="true" size="75" fontSize="35"></app-avatar>
|
<app-avatar data="{{ provider.name }}" dynamic="true" size="75" fontSize="35"></app-avatar>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||||
<span>{{'save' | i18n}}</span>
|
<span>{{ "save" | i18n }}</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from "@angular/core";
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { ToasterService } from 'angular2-toaster';
|
|
||||||
|
|
||||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { LogService } from 'jslib-common/abstractions/log.service';
|
import { LogService } from "jslib-common/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
import { SyncService } from 'jslib-common/abstractions/sync.service';
|
import { SyncService } from "jslib-common/abstractions/sync.service";
|
||||||
|
|
||||||
import { 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({
|
@Component({
|
||||||
selector: 'provider-account',
|
selector: "provider-account",
|
||||||
templateUrl: 'account.component.html',
|
templateUrl: "account.component.html",
|
||||||
})
|
})
|
||||||
export class AccountComponent {
|
export class AccountComponent {
|
||||||
selfHosted = false;
|
selfHosted = false;
|
||||||
@@ -25,14 +24,18 @@ export class AccountComponent {
|
|||||||
|
|
||||||
private providerId: string;
|
private providerId: string;
|
||||||
|
|
||||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
constructor(
|
||||||
private toasterService: ToasterService, private route: ActivatedRoute,
|
private apiService: ApiService,
|
||||||
private syncService: SyncService, private platformUtilsService: PlatformUtilsService,
|
private i18nService: I18nService,
|
||||||
private logService: LogService) { }
|
private route: ActivatedRoute,
|
||||||
|
private syncService: SyncService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private logService: LogService
|
||||||
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.selfHosted = this.platformUtilsService.isSelfHost();
|
this.selfHosted = this.platformUtilsService.isSelfHost();
|
||||||
this.route.parent.parent.params.subscribe(async params => {
|
this.route.parent.parent.params.subscribe(async (params) => {
|
||||||
this.providerId = params.providerId;
|
this.providerId = params.providerId;
|
||||||
try {
|
try {
|
||||||
this.provider = await this.apiService.getProvider(this.providerId);
|
this.provider = await this.apiService.getProvider(this.providerId);
|
||||||
@@ -54,7 +57,7 @@ export class AccountComponent {
|
|||||||
return this.syncService.fullSync(true);
|
return this.syncService.fullSync(true);
|
||||||
});
|
});
|
||||||
await this.formPromise;
|
await this.formPromise;
|
||||||
this.toasterService.popAsync('success', null, this.i18nService.t('providerUpdated'));
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("providerUpdated"));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logService.error(`Handled exception: ${e}`);
|
this.logService.error(`Handled exception: ${e}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">{{'settings' | i18n}}</div>
|
<div class="card-header">{{ "settings" | i18n }}</div>
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
<a routerLink="account" class="list-group-item" routerLinkActive="active">
|
<a routerLink="account" class="list-group-item" routerLinkActive="active">
|
||||||
{{'myProvider' | i18n}}
|
{{ "myProvider" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from "@angular/core";
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
|
||||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
import { UserService } from 'jslib-common/abstractions/user.service';
|
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'provider-settings',
|
selector: "provider-settings",
|
||||||
templateUrl: 'settings.component.html',
|
templateUrl: "settings.component.html",
|
||||||
})
|
})
|
||||||
export class SettingsComponent {
|
export class SettingsComponent {
|
||||||
constructor(private route: ActivatedRoute, private userService: UserService,
|
constructor(
|
||||||
private platformUtilsService: PlatformUtilsService) { }
|
private route: ActivatedRoute,
|
||||||
|
private providerService: ProviderService,
|
||||||
|
private platformUtilsService: PlatformUtilsService
|
||||||
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.route.parent.params.subscribe(async params => {
|
this.route.parent.params.subscribe(async (params) => {
|
||||||
const provider = await this.userService.getProvider(params.providerId);
|
const provider = await this.providerService.get(params.providerId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,27 @@
|
|||||||
<div class="mt-5 d-flex justify-content-center" *ngIf="loading">
|
<div class="mt-5 d-flex justify-content-center" *ngIf="loading">
|
||||||
<div>
|
<div>
|
||||||
<img class="mb-4 logo logo-themed" alt="Bitwarden">
|
<img class="mb-4 logo logo-themed" alt="Bitwarden" />
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
<i
|
||||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container" *ngIf="!loading && !authed">
|
<div class="container" *ngIf="!loading && !authed">
|
||||||
<div class="row justify-content-md-center mt-5">
|
<div class="row justify-content-md-center mt-5">
|
||||||
<div class="col-5">
|
<div class="col-5">
|
||||||
<p class="lead text-center mb-4">{{'setupProvider' | i18n}}</p>
|
<p class="lead text-center mb-4">{{ "setupProvider" | i18n }}</p>
|
||||||
<div class="card d-block">
|
<div class="card d-block">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p>{{'setupProviderLoginDesc' | i18n}}</p>
|
<p>{{ "setupProviderLoginDesc" | i18n }}</p>
|
||||||
<hr>
|
<hr />
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
|
<a routerLink="/" [queryParams]="{ email: email }" class="btn btn-primary btn-block">
|
||||||
{{'logIn' | i18n}}
|
{{ "logIn" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
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({
|
@Component({
|
||||||
selector: 'app-setup-provider',
|
selector: "app-setup-provider",
|
||||||
templateUrl: 'setup-provider.component.html',
|
templateUrl: "setup-provider.component.html",
|
||||||
})
|
})
|
||||||
export class SetupProviderComponent extends BaseAcceptComponent {
|
export class SetupProviderComponent extends BaseAcceptComponent {
|
||||||
|
failedShortMessage = "inviteAcceptFailedShort";
|
||||||
|
failedMessage = "inviteAcceptFailed";
|
||||||
|
|
||||||
failedShortMessage = 'inviteAcceptFailedShort';
|
requiredParameters = ["providerId", "email", "token"];
|
||||||
failedMessage = 'inviteAcceptFailed';
|
|
||||||
|
|
||||||
requiredParameters = ['providerId', 'email', 'token'];
|
|
||||||
|
|
||||||
async authedHandler(qParams: any) {
|
async authedHandler(qParams: any) {
|
||||||
this.router.navigate(['/providers/setup'], {queryParams: qParams});
|
this.router.navigate(["/providers/setup"], { queryParams: qParams });
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
|
|||||||
@@ -1,30 +1,37 @@
|
|||||||
<app-navbar></app-navbar>
|
<app-navbar></app-navbar>
|
||||||
<div class="container page-content">
|
<div class="container page-content">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1>{{'setupProvider' | i18n}}</h1>
|
<h1>{{ "setupProvider" | i18n }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<p>{{'setupProviderDesc' | i18n}}</p>
|
<p>{{ "setupProviderDesc" | i18n }}</p>
|
||||||
|
|
||||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="loading">
|
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="loading">
|
||||||
<h2 class="mt-5">{{'generalInformation' | i18n}}</h2>
|
<h2 class="mt-5">{{ "generalInformation" | i18n }}</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="form-group col-md-6 mt-4 mt-md-0">
|
<div class="form-group col-6">
|
||||||
<label for="name">{{'providerName' | i18n}}</label>
|
<label for="name">{{ "providerName" | i18n }}</label>
|
||||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required>
|
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group col-md-6 mt-4 mt-md-0">
|
<div class="form-group col-6">
|
||||||
<label for="billingEmail">{{'billingEmail' | i18n}}</label>
|
<label for="billingEmail">{{ "billingEmail" | i18n }}</label>
|
||||||
<input id="billingEmail" class="form-control" type="text" name="BillingEmail" [(ngModel)]="billingEmail" required>
|
<input
|
||||||
|
id="billingEmail"
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
name="BillingEmail"
|
||||||
|
[(ngModel)]="billingEmail"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||||
<span>{{'submit' | i18n}}</span>
|
<span>{{ "submit" | i18n }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" *ngIf="showCancel">
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" *ngIf="showCancel">
|
||||||
{{'cancel' | i18n}}
|
{{ "cancel" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,29 +1,20 @@
|
|||||||
import {
|
import { Component, OnInit } from "@angular/core";
|
||||||
Component,
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
OnInit,
|
|
||||||
} from '@angular/core';
|
|
||||||
import {
|
|
||||||
ActivatedRoute,
|
|
||||||
Router,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
|
||||||
Toast,
|
|
||||||
ToasterService,
|
|
||||||
} from 'angular2-toaster';
|
|
||||||
|
|
||||||
import { first } from 'rxjs/operators';
|
import { first } from "rxjs/operators";
|
||||||
|
|
||||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
|
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||||
|
|
||||||
import { ValidationService } from 'jslib-angular/services/validation.service';
|
import { ValidationService } from "jslib-angular/services/validation.service";
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { SyncService } from 'jslib-common/abstractions/sync.service';
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
import { ProviderSetupRequest } from 'jslib-common/models/request/provider/providerSetupRequest';
|
import { SyncService } from "jslib-common/abstractions/sync.service";
|
||||||
|
import { ProviderSetupRequest } from "jslib-common/models/request/provider/providerSetupRequest";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'provider-setup',
|
selector: "provider-setup",
|
||||||
templateUrl: 'setup.component.html',
|
templateUrl: "setup.component.html",
|
||||||
})
|
})
|
||||||
export class SetupComponent implements OnInit {
|
export class SetupComponent implements OnInit {
|
||||||
loading = true;
|
loading = true;
|
||||||
@@ -36,25 +27,30 @@ export class SetupComponent implements OnInit {
|
|||||||
name: string;
|
name: string;
|
||||||
billingEmail: string;
|
billingEmail: string;
|
||||||
|
|
||||||
constructor(private router: Router, private toasterService: ToasterService,
|
constructor(
|
||||||
private i18nService: I18nService, private route: ActivatedRoute,
|
private router: Router,
|
||||||
private cryptoService: CryptoService, private apiService: ApiService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private syncService: SyncService, private validationService: ValidationService) { }
|
private i18nService: I18nService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private cryptoService: CryptoService,
|
||||||
|
private apiService: ApiService,
|
||||||
|
private syncService: SyncService,
|
||||||
|
private validationService: ValidationService
|
||||||
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
document.body.classList.remove('layout_frontend');
|
document.body.classList.remove("layout_frontend");
|
||||||
this.route.queryParams.pipe(first()).subscribe(async qParams => {
|
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||||
const error = qParams.providerId == null || qParams.email == null || qParams.token == null;
|
const error = qParams.providerId == null || qParams.email == null || qParams.token == null;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
const toast: Toast = {
|
this.platformUtilsService.showToast(
|
||||||
type: 'error',
|
"error",
|
||||||
title: null,
|
null,
|
||||||
body: this.i18nService.t('emergencyInviteAcceptFailed'),
|
this.i18nService.t("emergencyInviteAcceptFailed"),
|
||||||
timeout: 10000,
|
{ timeout: 10000 }
|
||||||
};
|
);
|
||||||
this.toasterService.popAsync(toast);
|
this.router.navigate(["/"]);
|
||||||
this.router.navigate(['/']);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,11 +61,11 @@ export class SetupComponent implements OnInit {
|
|||||||
try {
|
try {
|
||||||
const provider = await this.apiService.getProvider(this.providerId);
|
const provider = await this.apiService.getProvider(this.providerId);
|
||||||
if (provider.name != null) {
|
if (provider.name != null) {
|
||||||
this.router.navigate(['/providers', provider.id], { replaceUrl: true });
|
this.router.navigate(["/providers", provider.id], { replaceUrl: true });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.validationService.showError(e);
|
this.validationService.showError(e);
|
||||||
this.router.navigate(['/']);
|
this.router.navigate(["/"]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -92,10 +88,10 @@ export class SetupComponent implements OnInit {
|
|||||||
request.key = key;
|
request.key = key;
|
||||||
|
|
||||||
const provider = await this.apiService.postProviderSetup(this.providerId, request);
|
const provider = await this.apiService.postProviderSetup(this.providerId, request);
|
||||||
this.toasterService.popAsync('success', null, this.i18nService.t('providerSetup'));
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("providerSetup"));
|
||||||
await this.syncService.fullSync(true);
|
await this.syncService.fullSync(true);
|
||||||
|
|
||||||
this.router.navigate(['/providers', provider.id]);
|
this.router.navigate(["/providers", provider.id]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.validationService.showError(e);
|
this.validationService.showError(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
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.entry["app/main"] = "./bitwarden_license/src/app/main.ts";
|
||||||
webpackConfig.plugins[webpackConfig.plugins.length -1] = new AngularCompilerPlugin({
|
webpackConfig.plugins[webpackConfig.plugins.length - 1] = new AngularWebpackPlugin({
|
||||||
tsConfigPath: 'tsconfig.json',
|
tsConfigPath: "tsconfig.json",
|
||||||
entryModule: 'bitwarden_license/src/app/app.module#AppModule',
|
entryModule: "bitwarden_license/src/app/app.module#AppModule",
|
||||||
sourceMap: true,
|
sourceMap: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
14
config.js
14
config.js
@@ -1,8 +1,13 @@
|
|||||||
function load(envName) {
|
function load(envName) {
|
||||||
return {
|
return {
|
||||||
...require('./config/base.json'),
|
...require("./config/base.json"),
|
||||||
...loadConfig(envName),
|
...loadConfig(envName),
|
||||||
...loadConfig('local'),
|
...loadConfig("local"),
|
||||||
|
dev: {
|
||||||
|
...require("./config/base.json").dev,
|
||||||
|
...loadConfig(envName).dev,
|
||||||
|
...loadConfig("local").dev,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,8 +24,7 @@ function loadConfig(configName) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.code === "MODULE_NOT_FOUND") {
|
if (e instanceof Error && e.code === "MODULE_NOT_FOUND") {
|
||||||
return {};
|
return {};
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,5 +32,5 @@ function loadConfig(configName) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
load,
|
load,
|
||||||
log
|
log,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,5 +5,8 @@
|
|||||||
"paypal": {
|
"paypal": {
|
||||||
"businessId": "AD3LAUZSNVPJY",
|
"businessId": "AD3LAUZSNVPJY",
|
||||||
"buttonAction": "https://www.sandbox.paypal.com/cgi-bin/webscr"
|
"buttonAction": "https://www.sandbox.paypal.com/cgi-bin/webscr"
|
||||||
|
},
|
||||||
|
"dev": {
|
||||||
|
"allowedHosts": "auto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,10 @@
|
|||||||
"paypal": {
|
"paypal": {
|
||||||
"businessId": "4ZDA7DLUUJGMN",
|
"businessId": "4ZDA7DLUUJGMN",
|
||||||
"buttonAction": "https://www.paypal.com/cgi-bin/webscr"
|
"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,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"urls": {
|
||||||
|
"notifications": "http://localhost:61840"
|
||||||
|
},
|
||||||
|
"dev": {
|
||||||
"proxyApi": "http://localhost:4000",
|
"proxyApi": "http://localhost:4000",
|
||||||
"proxyIdentity": "http://localhost:33656",
|
"proxyIdentity": "http://localhost:33656",
|
||||||
"proxyEvents": "http://localhost:46273",
|
"proxyEvents": "http://localhost:46273",
|
||||||
"proxyNotifications": "http://localhost:61840",
|
"proxyNotifications": "http://localhost:61840"
|
||||||
"allowedHosts": [],
|
|
||||||
"urls": {
|
|
||||||
"notifications": "http://localhost:61840"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,10 @@
|
|||||||
"urls": {
|
"urls": {
|
||||||
"icons": "https://icons.qa.bitwarden.pw",
|
"icons": "https://icons.qa.bitwarden.pw",
|
||||||
"notifications": "https://notifications.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
|
project_id_env: _CROWDIN_PROJECT_ID
|
||||||
api_token_env: CROWDIN_API_TOKEN
|
api_token_env: CROWDIN_API_TOKEN
|
||||||
|
preserve_hierarchy: true
|
||||||
files:
|
files:
|
||||||
- source: /src/locales/en/messages.json
|
- source: /src/locales/en/messages.json
|
||||||
|
dest: /src/locales/en/%file_name%.%file_extension%
|
||||||
translation: /src/locales/%two_letters_code%/%original_file_name%
|
translation: /src/locales/%two_letters_code%/%original_file_name%
|
||||||
update_option: update_as_unapproved
|
update_option: update_as_unapproved
|
||||||
languages_mapping:
|
languages_mapping:
|
||||||
|
|||||||
2
jslib
2
jslib
Submodule jslib updated: 815b436f7c...92a65b7b36
13702
package-lock.json
generated
13702
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
75
package.json
75
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bitwarden-web",
|
"name": "@bitwarden/web-vault",
|
||||||
"version": "2.23.0",
|
"version": "2.25.1",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"repository": "https://github.com/bitwarden/web",
|
"repository": "https://github.com/bitwarden/web",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -29,44 +29,57 @@
|
|||||||
"dist:bit:selfhost": "npm run build:bit:selfhost:prod",
|
"dist:bit:selfhost": "npm run build:bit:selfhost:prod",
|
||||||
"deploy": "npm run dist:bit && gh-pages -d build",
|
"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",
|
"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' || true",
|
"lint": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' && prettier --check .",
|
||||||
"lint:fix": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' --fix"
|
"lint:fix": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' --fix",
|
||||||
|
"prettier": "prettier --write .",
|
||||||
|
"prepare": "husky install"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/compiler-cli": "^11.2.11",
|
"@angular/compiler-cli": "^12.2.13",
|
||||||
"@ngtools/webpack": "^11.2.10",
|
"@ngtools/webpack": "^12.2.13",
|
||||||
"@types/jquery": "^3.5.5",
|
"@types/jquery": "^3.5.5",
|
||||||
"@types/node": "^14.17.2",
|
"@types/node": "^16.11.12",
|
||||||
"@types/webcrypto": "^0.0.28",
|
"@types/webcrypto": "^0.0.28",
|
||||||
"@types/webpack": "^4.4.27",
|
"@types/webpack": "^5.28.0",
|
||||||
"clean-webpack-plugin": "^3.0.0",
|
"buffer": "^6.0.3",
|
||||||
"copy-webpack-plugin": "^6.4.0",
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
|
"copy-webpack-plugin": "^10.0.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"css-loader": "^5.2.3",
|
"css-loader": "^6.5.1",
|
||||||
"del": "^6.0.0",
|
|
||||||
"file-loader": "^6.2.0",
|
|
||||||
"gh-pages": "^3.1.0",
|
"gh-pages": "^3.1.0",
|
||||||
"html-loader": "^1.3.2",
|
"html-loader": "^3.0.1",
|
||||||
"html-webpack-injector": "1.1.4",
|
"html-webpack-injector": "1.1.4",
|
||||||
"html-webpack-plugin": "^4.5.1",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"mini-css-extract-plugin": "^1.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": "^1.32.10",
|
||||||
"sass-loader": "^10.1.1",
|
"sass-loader": "^12.4.0",
|
||||||
"style-loader": "^2.0.0",
|
"style-loader": "^3.3.1",
|
||||||
"tapable": "^1.1.3",
|
"terser-webpack-plugin": "^5.2.5",
|
||||||
"terser-webpack-plugin": "^4.2.3",
|
"ts-loader": "^9.2.5",
|
||||||
"ts-loader": "^8.1.0",
|
|
||||||
"tslint": "^6.1.3",
|
"tslint": "^6.1.3",
|
||||||
"tslint-loader": "^3.5.4",
|
"tslint-loader": "^3.5.4",
|
||||||
"typescript": "4.1.5",
|
"typescript": "4.3.5",
|
||||||
"webpack": "^4.46.0",
|
"util": "^0.12.4",
|
||||||
"webpack-cli": "^4.6.0",
|
"webpack": "^5.64.4",
|
||||||
"webpack-dev-server": "^3.11.2"
|
"webpack-cli": "^4.9.1",
|
||||||
|
"webpack-dev-server": "^4.6.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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-angular": "file:jslib/angular",
|
||||||
"@bitwarden/jslib-common": "file:jslib/common",
|
"@bitwarden/jslib-common": "file:jslib/common",
|
||||||
"angular2-toaster": "11.0.1",
|
|
||||||
"bootstrap": "4.6.0",
|
"bootstrap": "4.6.0",
|
||||||
"braintree-web-drop-in": "1.30.1",
|
"braintree-web-drop-in": "1.30.1",
|
||||||
"browser-hrtime": "^1.1.8",
|
"browser-hrtime": "^1.1.8",
|
||||||
@@ -75,14 +88,20 @@
|
|||||||
"font-awesome": "4.7.0",
|
"font-awesome": "4.7.0",
|
||||||
"jquery": "3.6.0",
|
"jquery": "3.6.0",
|
||||||
"ngx-infinite-scroll": "^10.0.1",
|
"ngx-infinite-scroll": "^10.0.1",
|
||||||
|
"ngx-toastr": "14.1.4",
|
||||||
"popper.js": "1.16.1",
|
"popper.js": "1.16.1",
|
||||||
"qrious": "4.0.2",
|
"qrious": "4.0.2",
|
||||||
|
"rxjs": "^7.4.0",
|
||||||
"sweetalert2": "^10.16.6",
|
"sweetalert2": "^10.16.6",
|
||||||
"webcrypto-shim": "0.1.7",
|
"webcrypto-shim": "0.1.7",
|
||||||
"whatwg-fetch": "3.6.2"
|
"whatwg-fetch": "3.6.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "~14",
|
"node": "~16",
|
||||||
"npm": "~7"
|
"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;
|
||||||
|
}
|
||||||
|
});
|
||||||
46
src/404.html
46
src/404.html
@@ -1,24 +1,26 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
<link href="/404/bootstrap.min.css" rel="stylesheet" type="text/css"
|
<link
|
||||||
integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l">
|
href="/404/bootstrap.min.css"
|
||||||
<link href="/404/font-awesome.min.css" rel="stylesheet" type="text/css"
|
rel="stylesheet"
|
||||||
integrity="sha512-SfTiTlX6kk+qitfevl/7LibUOeJWlt9rbyDn92a1DqWOw9vWG2MFoays0sgObmWazO5BQPiFucnnEAjpAB+/Sw==">
|
type="text/css"
|
||||||
<link href="/404/styles.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="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="32x32" href="/images/icons/favicon-32x32.png" />
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/favicon-16x16.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="mask-icon" href="/images/icons/safari-pinned-tab.svg" color="#175DDC" />
|
||||||
<link rel="manifest" href="/manifest.json">
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
|
||||||
<title>Page not found!</title>
|
<title>Page not found!</title>
|
||||||
<meta name="description" content="404 Page Not Found">
|
<meta name="description" content="404 Page Not Found" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -26,8 +28,7 @@
|
|||||||
<div class="container inner banner">
|
<div class="container inner banner">
|
||||||
<div class="row align-items-center">
|
<div class="row align-items-center">
|
||||||
<div class="col brand">
|
<div class="col brand">
|
||||||
<i class="fa fa-shield"></i>
|
<i class="bwi bwi-shield"></i> <strong>bit</strong>warden
|
||||||
<strong>bit</strong>warden</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,14 +38,15 @@
|
|||||||
<p>Sorry, but the page you were looking for could not be found.</p>
|
<p>Sorry, but the page you were looking for could not be found.</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="/">
|
<a href="/">
|
||||||
<img src="/images/404.png" class="img-fluid" alt="404 image" width="80%"/>
|
<img src="/images/404.png" class="img-fluid" alt="404 image" width="80%" />
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p>You can <a href="/">return to the web vault</a>, check our <a href="https://status.bitwarden.com/">status page</a>
|
<p>
|
||||||
or <a href="https://bitwarden.com/contact/">contact us</a>.</p>
|
You can <a href="/">return to the web vault</a>, check our
|
||||||
</div>
|
<a href="https://status.bitwarden.com/">status page</a> or
|
||||||
<div class="container footer text-muted content">
|
<a href="https://bitwarden.com/contact/">contact us</a>.
|
||||||
© Copyright 2021 Bitwarden, Inc.
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="container footer text-muted content">© Copyright 2022 Bitwarden, Inc.</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
4
src/404/font-awesome.min.css
vendored
4
src/404/font-awesome.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -1,88 +1,90 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Open Sans';
|
font-family: "Open Sans";
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
src: url(../fonts/Open_Sans-italic-300.woff) format('woff');
|
src: url(../fonts/Open_Sans-italic-300.woff) format("woff");
|
||||||
unicode-range: U+0-10FFFF;
|
unicode-range: U+0-10FFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Open Sans';
|
font-family: "Open Sans";
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: url(../fonts/Open_Sans-italic-400.woff) format('woff');
|
src: url(../fonts/Open_Sans-italic-400.woff) format("woff");
|
||||||
unicode-range: U+0-10FFFF;
|
unicode-range: U+0-10FFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Open Sans';
|
font-family: "Open Sans";
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
src: url(../fonts/Open_Sans-italic-600.woff) format('woff');
|
src: url(../fonts/Open_Sans-italic-600.woff) format("woff");
|
||||||
unicode-range: U+0-10FFFF;
|
unicode-range: U+0-10FFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Open Sans';
|
font-family: "Open Sans";
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
src: url(../fonts/Open_Sans-italic-700.woff) format('woff');
|
src: url(../fonts/Open_Sans-italic-700.woff) format("woff");
|
||||||
unicode-range: U+0-10FFFF;
|
unicode-range: U+0-10FFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Open Sans';
|
font-family: "Open Sans";
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
src: url(../fonts/Open_Sans-italic-800.woff) format('woff');
|
src: url(../fonts/Open_Sans-italic-800.woff) format("woff");
|
||||||
unicode-range: U+0-10FFFF;
|
unicode-range: U+0-10FFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Open Sans';
|
font-family: "Open Sans";
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
src: url(../fonts/Open_Sans-normal-300.woff) format('woff');
|
src: url(../fonts/Open_Sans-normal-300.woff) format("woff");
|
||||||
unicode-range: U+0-10FFFF;
|
unicode-range: U+0-10FFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Open Sans';
|
font-family: "Open Sans";
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: url(../fonts/Open_Sans-normal-400.woff) format('woff');
|
src: url(../fonts/Open_Sans-normal-400.woff) format("woff");
|
||||||
unicode-range: U+0-10FFFF;
|
unicode-range: U+0-10FFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Open Sans';
|
font-family: "Open Sans";
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
src: url(../fonts/Open_Sans-normal-600.woff) format('woff');
|
src: url(../fonts/Open_Sans-normal-600.woff) format("woff");
|
||||||
unicode-range: U+0-10FFFF;
|
unicode-range: U+0-10FFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Open Sans';
|
font-family: "Open Sans";
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
src: url(../fonts/Open_Sans-normal-700.woff) format('woff');
|
src: url(../fonts/Open_Sans-normal-700.woff) format("woff");
|
||||||
unicode-range: U+0-10FFFF;
|
unicode-range: U+0-10FFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Open Sans';
|
font-family: "Open Sans";
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
src: url(../fonts/Open_Sans-normal-800.woff) format('woff');
|
src: url(../fonts/Open_Sans-normal-800.woff) format("woff");
|
||||||
unicode-range: U+0-10FFFF;
|
unicode-range: U+0-10FFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Open Sans';
|
font-family: "Open Sans";
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body, .row {
|
html,
|
||||||
|
body,
|
||||||
|
.row {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
@@ -98,11 +100,11 @@ h2 {
|
|||||||
font-size: 23px;
|
font-size: 23px;
|
||||||
line-height: 25px;
|
line-height: 25px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
|
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner {
|
.banner {
|
||||||
background-color: #175DDC;
|
background-color: #175ddc;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,3 +119,33 @@ h2 {
|
|||||||
padding: 40px 0 40px 0;
|
padding: 40px 0 40px 0;
|
||||||
border-top: 1px solid #dee2e6;
|
border-top: 1px solid #dee2e6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Bitwarden icons, manually copied */
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "bwi-font";
|
||||||
|
src: url(../images/bwi-font.svg) format("svg"), url(../fonts/bwi-font.ttf) format("truetype"),
|
||||||
|
url(../fonts/bwi-font.woff) format("woff"), url(../fonts/bwi-font.woff2) format("woff2");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bwi {
|
||||||
|
/* use !important to prevent issues with browser extensions that change fonts */
|
||||||
|
font-family: "bwi-font" !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-shield:before {
|
||||||
|
content: "\e932";
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,30 +1,37 @@
|
|||||||
<div class="mt-5 d-flex justify-content-center" *ngIf="loading">
|
<div class="mt-5 d-flex justify-content-center" *ngIf="loading">
|
||||||
<div>
|
<div>
|
||||||
<img class="mb-4 logo logo-themed" alt="Bitwarden">
|
<img class="mb-4 logo logo-themed" alt="Bitwarden" />
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
<i
|
||||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container" *ngIf="!loading && !authed">
|
<div class="container" *ngIf="!loading && !authed">
|
||||||
<div class="row justify-content-md-center mt-5">
|
<div class="row justify-content-md-center mt-5">
|
||||||
<div class="col-5">
|
<div class="col-5">
|
||||||
<p class="lead text-center mb-4">{{'emergencyAccess' | i18n}}</p>
|
<p class="lead text-center mb-4">{{ "emergencyAccess" | i18n }}</p>
|
||||||
<div class="card d-block">
|
<div class="card d-block">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
{{name}}
|
{{ name }}
|
||||||
</p>
|
</p>
|
||||||
<p>{{'acceptEmergencyAccess' | i18n}}</p>
|
<p>{{ "acceptEmergencyAccess" | i18n }}</p>
|
||||||
<hr>
|
<hr />
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
|
<a routerLink="/" [queryParams]="{ email: email }" class="btn btn-primary btn-block">
|
||||||
{{'logIn' | i18n}}
|
{{ "logIn" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
<a routerLink="/register" [queryParams]="{email: email}"
|
<a
|
||||||
class="btn btn-primary btn-block ml-2 mt-0">
|
routerLink="/register"
|
||||||
{{'createAccount' | i18n}}
|
[queryParams]="{ email: email }"
|
||||||
|
class="btn btn-primary btn-block ml-2 mt-0"
|
||||||
|
>
|
||||||
|
{{ "createAccount" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,38 +1,33 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from "@angular/core";
|
||||||
import {
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
ActivatedRoute,
|
|
||||||
Router,
|
|
||||||
} from '@angular/router';
|
|
||||||
|
|
||||||
import {
|
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||||
Toast,
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
ToasterService,
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
} from 'angular2-toaster';
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
import { EmergencyAccessAcceptRequest } from "jslib-common/models/request/emergencyAccessAcceptRequest";
|
||||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
import { BaseAcceptComponent } from "../common/base.accept.component";
|
||||||
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 { EmergencyAccessAcceptRequest } from 'jslib-common/models/request/emergencyAccessAcceptRequest';
|
|
||||||
import { BaseAcceptComponent } from '../common/base.accept.component';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-accept-emergency',
|
selector: "app-accept-emergency",
|
||||||
templateUrl: 'accept-emergency.component.html',
|
templateUrl: "accept-emergency.component.html",
|
||||||
})
|
})
|
||||||
export class AcceptEmergencyComponent extends BaseAcceptComponent {
|
export class AcceptEmergencyComponent extends BaseAcceptComponent {
|
||||||
|
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
protected requiredParameters: string[] = ['id', 'name', 'email', 'token'];
|
protected requiredParameters: string[] = ["id", "name", "email", "token"];
|
||||||
protected failedShortMessage = 'emergencyInviteAcceptFailedShort';
|
protected failedShortMessage = "emergencyInviteAcceptFailedShort";
|
||||||
protected failedMessage = 'emergencyInviteAcceptFailed';
|
protected failedMessage = "emergencyInviteAcceptFailed";
|
||||||
|
|
||||||
constructor(router: Router, toasterService: ToasterService,
|
constructor(
|
||||||
i18nService: I18nService, route: ActivatedRoute,
|
router: Router,
|
||||||
private apiService: ApiService, userService: UserService,
|
platformUtilsService: PlatformUtilsService,
|
||||||
stateService: StateService) {
|
i18nService: I18nService,
|
||||||
super(router, toasterService, i18nService, route, userService, stateService);
|
route: ActivatedRoute,
|
||||||
|
private apiService: ApiService,
|
||||||
|
stateService: StateService
|
||||||
|
) {
|
||||||
|
super(router, platformUtilsService, i18nService, route, stateService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async authedHandler(qParams: any): Promise<void> {
|
async authedHandler(qParams: any): Promise<void> {
|
||||||
@@ -40,21 +35,20 @@ export class AcceptEmergencyComponent extends BaseAcceptComponent {
|
|||||||
request.token = qParams.token;
|
request.token = qParams.token;
|
||||||
this.actionPromise = this.apiService.postEmergencyAccessAccept(qParams.id, request);
|
this.actionPromise = this.apiService.postEmergencyAccessAccept(qParams.id, request);
|
||||||
await this.actionPromise;
|
await this.actionPromise;
|
||||||
const toast: Toast = {
|
this.platformUtilService.showToast(
|
||||||
type: 'success',
|
"success",
|
||||||
title: this.i18nService.t('inviteAccepted'),
|
this.i18nService.t("inviteAccepted"),
|
||||||
body: this.i18nService.t('emergencyInviteAcceptedDesc'),
|
this.i18nService.t("emergencyInviteAcceptedDesc"),
|
||||||
timeout: 10000,
|
{ timeout: 10000 }
|
||||||
};
|
);
|
||||||
this.toasterService.popAsync(toast);
|
this.router.navigate(["/vault"]);
|
||||||
this.router.navigate(['/vault']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async unauthedHandler(qParams: any): Promise<void> {
|
async unauthedHandler(qParams: any): Promise<void> {
|
||||||
this.name = qParams.name;
|
this.name = qParams.name;
|
||||||
if (this.name != null) {
|
if (this.name != null) {
|
||||||
// Fix URL encoding of space issue with Angular
|
// Fix URL encoding of space issue with Angular
|
||||||
this.name = this.name.replace(/\+/g, ' ');
|
this.name = this.name.replace(/\+/g, " ");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,38 @@
|
|||||||
<div class="mt-5 d-flex justify-content-center" *ngIf="loading">
|
<div class="mt-5 d-flex justify-content-center" *ngIf="loading">
|
||||||
<div>
|
<div>
|
||||||
<img src="../../images/logo-dark@2x.png" class="mb-4 logo" alt="Bitwarden">
|
<img src="../../images/logo-dark@2x.png" class="mb-4 logo" alt="Bitwarden" />
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
<i
|
||||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container" *ngIf="!loading && !authed">
|
<div class="container" *ngIf="!loading && !authed">
|
||||||
<div class="row justify-content-md-center mt-5">
|
<div class="row justify-content-md-center mt-5">
|
||||||
<div class="col-5">
|
<div class="col-5">
|
||||||
<p class="lead text-center mb-4">{{'joinOrganization' | i18n}}</p>
|
<p class="lead text-center mb-4">{{ "joinOrganization" | i18n }}</p>
|
||||||
<div class="card d-block">
|
<div class="card d-block">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
{{orgName}}
|
{{ orgName }}
|
||||||
<strong class="d-block mt-2">{{email}}</strong>
|
<strong class="d-block mt-2">{{ email }}</strong>
|
||||||
</p>
|
</p>
|
||||||
<p>{{'joinOrganizationDesc' | i18n}}</p>
|
<p>{{ "joinOrganizationDesc" | i18n }}</p>
|
||||||
<hr>
|
<hr />
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
|
<a routerLink="/" [queryParams]="{ email: email }" class="btn btn-primary btn-block">
|
||||||
{{'logIn' | i18n}}
|
{{ "logIn" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
<a routerLink="/register" [queryParams]="{email: email}"
|
<a
|
||||||
class="btn btn-primary btn-block ml-2 mt-0">
|
routerLink="/register"
|
||||||
{{'createAccount' | i18n}}
|
[queryParams]="{ email: email }"
|
||||||
|
class="btn btn-primary btn-block ml-2 mt-0"
|
||||||
|
>
|
||||||
|
{{ "createAccount" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user