mirror of
https://github.com/bitwarden/web
synced 2025-12-06 00:03:28 +00:00
Compare commits
4 Commits
v2.28.0
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ca2d19091 | ||
|
|
53e18abe4f | ||
|
|
d10afbc130 | ||
|
|
bc8e90ecc2 |
@@ -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 = 2
|
indent_size = 4
|
||||||
|
|
||||||
[*.{ts}]
|
[*.{ts}]
|
||||||
quote_type = single
|
quote_type = single
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
**/dist
|
|
||||||
**/build
|
|
||||||
jslib
|
|
||||||
webpack.config.js
|
|
||||||
scripts/optimize.js
|
|
||||||
config.js
|
|
||||||
|
|
||||||
**/node_modules
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"root": true,
|
|
||||||
"env": {
|
|
||||||
"browser": true
|
|
||||||
},
|
|
||||||
"extends": ["./jslib/shared/eslintrc.json"],
|
|
||||||
"rules": {
|
|
||||||
"import/order": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"alphabetize": {
|
|
||||||
"order": "asc"
|
|
||||||
},
|
|
||||||
"newlines-between": "always",
|
|
||||||
"pathGroups": [
|
|
||||||
{
|
|
||||||
"pattern": "jslib-*/**",
|
|
||||||
"group": "external",
|
|
||||||
"position": "after"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pattern": "src/**/*",
|
|
||||||
"group": "parent",
|
|
||||||
"position": "before"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"pathGroupsExcludedImportTypes": ["builtin"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# Apply Prettier https://github.com/bitwarden/web/pull/1347
|
|
||||||
56477eb39cfd8a73c9920577d24d75fed36e2cf5
|
|
||||||
4
.gitattributes
vendored
4
.gitattributes
vendored
@@ -1 +1,3 @@
|
|||||||
* text=auto eol=lf
|
*.sh eol=lf
|
||||||
|
.dockerignore eol=lf
|
||||||
|
dockerfile eol=lf
|
||||||
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -6,7 +6,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thanks for taking the time to fill out this bug report!
|
Thanks for taking the time to fill out this bug report!
|
||||||
|
|
||||||
Please do not submit feature requests. The [Community Forums](https://community.bitwarden.com) has a section for submitting, voting for, and discussing product feature requests.
|
Please do not submit feature requests. The [Community Forums](https://community.bitwarden.com) has a section for submitting, voting for, and discussing product feature requests.
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: reproduce
|
id: reproduce
|
||||||
|
|||||||
32
.github/PULL_REQUEST_TEMPLATE.md
vendored
32
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,32 +0,0 @@
|
|||||||
## 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)
|
|
||||||
280
.github/workflows/build.yml
vendored
280
.github/workflows/build.yml
vendored
@@ -9,11 +9,8 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
push:
|
push:
|
||||||
branches-ignore:
|
branches-ignore:
|
||||||
- "l10n_master"
|
- 'l10n_master'
|
||||||
- "gh-pages"
|
- 'gh-pages'
|
||||||
paths-ignore:
|
|
||||||
- '.github/workflows/**'
|
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cloc:
|
cloc:
|
||||||
@@ -32,27 +29,6 @@ jobs:
|
|||||||
run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
|
run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
|
||||||
|
|
||||||
|
|
||||||
lint:
|
|
||||||
name: Lint
|
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
steps:
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
|
||||||
|
|
||||||
- name: Cache npm
|
|
||||||
id: npm-cache
|
|
||||||
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
|
||||||
with:
|
|
||||||
path: "~/.npm"
|
|
||||||
key: ${{ runner.os }}-npm-lint-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Run linter
|
|
||||||
run: npm run lint
|
|
||||||
|
|
||||||
|
|
||||||
setup:
|
setup:
|
||||||
name: Setup
|
name: Setup
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
@@ -64,27 +40,32 @@ jobs:
|
|||||||
|
|
||||||
- name: Get GitHub sha as version
|
- name: Get GitHub sha as version
|
||||||
id: version
|
id: version
|
||||||
run: echo "::set-output name=value::${GITHUB_SHA:0:7}"
|
run: |
|
||||||
|
echo "::set-output name=value::${GITHUB_SHA:0:7}"
|
||||||
|
|
||||||
|
|
||||||
build-oss-selfhost:
|
build-oss-selfhost:
|
||||||
name: Build OSS zip
|
name: Build OSS zip
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs:
|
needs: setup
|
||||||
- setup
|
|
||||||
- lint
|
|
||||||
env:
|
env:
|
||||||
_VERSION: ${{ needs.setup.outputs.version }}
|
_VERSION: ${{ needs.setup.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
|
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||||
with:
|
with:
|
||||||
cache: 'npm'
|
node-version: '14'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
|
||||||
node-version: "16"
|
- name: Update NPM
|
||||||
|
run: |
|
||||||
|
npm install -g npm@7
|
||||||
|
|
||||||
|
- name: Cache npm
|
||||||
|
id: npm-cache
|
||||||
|
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
||||||
|
with:
|
||||||
|
path: '~/.npm'
|
||||||
|
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
@@ -96,6 +77,9 @@ jobs:
|
|||||||
echo "GitHub ref: $GITHUB_REF"
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
@@ -115,21 +99,25 @@ jobs:
|
|||||||
build-cloud:
|
build-cloud:
|
||||||
name: Build Cloud zip
|
name: Build Cloud zip
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs:
|
needs: setup
|
||||||
- setup
|
|
||||||
- lint
|
|
||||||
env:
|
env:
|
||||||
_VERSION: ${{ needs.setup.outputs.version }}
|
_VERSION: ${{ needs.setup.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
|
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||||
with:
|
with:
|
||||||
cache: 'npm'
|
node-version: '14'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
|
||||||
node-version: "16"
|
- name: Update NPM
|
||||||
|
run: |
|
||||||
|
npm install -g npm@7
|
||||||
|
|
||||||
|
- name: Cache npm
|
||||||
|
id: npm-cache
|
||||||
|
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
||||||
|
with:
|
||||||
|
path: '~/.npm'
|
||||||
|
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
@@ -141,6 +129,9 @@ jobs:
|
|||||||
echo "GitHub ref: $GITHUB_REF"
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
@@ -160,21 +151,25 @@ jobs:
|
|||||||
build-commercial-selfhost:
|
build-commercial-selfhost:
|
||||||
name: Build SelfHost Docker image
|
name: Build SelfHost Docker image
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs:
|
needs: setup
|
||||||
- setup
|
|
||||||
- lint
|
|
||||||
env:
|
env:
|
||||||
_VERSION: ${{ needs.setup.outputs.version }}
|
_VERSION: ${{ needs.setup.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
|
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||||
with:
|
with:
|
||||||
cache: 'npm'
|
node-version: '14'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
|
||||||
node-version: "16"
|
- name: Update NPM
|
||||||
|
run: |
|
||||||
|
npm install -g npm@7
|
||||||
|
|
||||||
|
- name: Cache npm
|
||||||
|
id: npm-cache
|
||||||
|
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
||||||
|
with:
|
||||||
|
path: '~/.npm'
|
||||||
|
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
@@ -187,13 +182,19 @@ jobs:
|
|||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
- name: Setup DCT
|
- name: Setup DCT
|
||||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
|
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
|
||||||
id: setup-dct
|
id: setup-dct
|
||||||
uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff
|
uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff
|
||||||
with:
|
with:
|
||||||
azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||||
azure-keyvault-name: "bitwarden-prod-kv"
|
azure-keyvault-name: "bitwarden-prod-kv"
|
||||||
|
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||||
|
|
||||||
|
- name: Restore
|
||||||
|
run: dotnet tool restore
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
@@ -227,81 +228,48 @@ 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-rc'
|
|
||||||
run: docker tag bitwarden/web bitwarden/web:hotfix-rc
|
|
||||||
|
|
||||||
- name: List Docker images
|
- name: List Docker images
|
||||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
|
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
|
||||||
run: docker images
|
run: docker images
|
||||||
|
|
||||||
- name: Push rc image
|
- name: Push rc images
|
||||||
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 image
|
- name: Push dev images
|
||||||
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: Push hotfix image
|
|
||||||
if: github.ref == 'refs/heads/hotfix-rc'
|
|
||||||
run: docker push bitwarden/web:hotfix-rc
|
|
||||||
env:
|
|
||||||
DOCKER_CONTENT_TRUST: 1
|
|
||||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
|
|
||||||
|
|
||||||
- name: Log out of Docker
|
|
||||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
|
|
||||||
run: |
|
|
||||||
docker logout
|
|
||||||
echo "DOCKER_CONTENT_TRUST=0" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Login to Azure - QA Subscription
|
|
||||||
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
|
||||||
with:
|
|
||||||
creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }}
|
|
||||||
|
|
||||||
- name: Login to Azure ACR
|
|
||||||
run: az acr login -n bitwardenqa
|
|
||||||
|
|
||||||
- name: Tag and Push RC to Azure ACR QA registry
|
|
||||||
env:
|
|
||||||
REGISTRY: bitwardenqa.azurecr.io
|
|
||||||
run: |
|
|
||||||
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") # slash safe branch name
|
|
||||||
if [[ "$IMAGE_TAG" == "master" ]]; then
|
|
||||||
IMAGE_TAG=dev
|
|
||||||
fi
|
|
||||||
docker tag bitwarden/web \
|
|
||||||
$REGISTRY/web-sh:$IMAGE_TAG
|
|
||||||
docker push $REGISTRY/web-sh:$IMAGE_TAG
|
|
||||||
|
|
||||||
- name: Log out of Docker
|
- name: Log out of Docker
|
||||||
|
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
|
||||||
run: docker logout
|
run: docker logout
|
||||||
|
|
||||||
|
|
||||||
build-qa:
|
build-qa:
|
||||||
name: Build Docker images for QA environment
|
name: Build Docker images for QA environment
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs:
|
|
||||||
- setup
|
|
||||||
- lint
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
|
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||||
with:
|
with:
|
||||||
cache: 'npm'
|
node-version: '14'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
|
||||||
node-version: "16"
|
- name: Update NPM
|
||||||
|
run: |
|
||||||
|
npm install -g npm@7
|
||||||
|
|
||||||
|
- name: Cache npm
|
||||||
|
id: npm-cache
|
||||||
|
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
||||||
|
with:
|
||||||
|
path: '~/.npm'
|
||||||
|
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
@@ -321,6 +289,12 @@ jobs:
|
|||||||
- name: Log into container registry
|
- name: Log into container registry
|
||||||
run: az acr login -n bitwardenqa
|
run: az acr login -n bitwardenqa
|
||||||
|
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||||
|
|
||||||
|
- name: Restore
|
||||||
|
run: dotnet tool restore
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
@@ -344,7 +318,7 @@ jobs:
|
|||||||
- name: Get image tag
|
- name: Get image tag
|
||||||
id: image-tag
|
id: image-tag
|
||||||
run: |
|
run: |
|
||||||
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g")
|
IMAGE_TAG=$(echo "$GITHUB_REF" | awk '{split($0, a, "/"); print a[3];}')
|
||||||
TAG_EXTENSION=${{ github.event.inputs.custom_tag_extension }}
|
TAG_EXTENSION=${{ github.event.inputs.custom_tag_extension }}
|
||||||
|
|
||||||
if [[ $TAG_EXTENSION ]]; then
|
if [[ $TAG_EXTENSION ]]; then
|
||||||
@@ -381,24 +355,35 @@ jobs:
|
|||||||
name: Test code on Windows
|
name: Test code on Windows
|
||||||
runs-on: windows-2019
|
runs-on: windows-2019
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
|
||||||
|
|
||||||
- name: Set up NuGet
|
- name: Set up NuGet
|
||||||
uses: nuget/setup-nuget@04b0c2b8d1b97922f67eca497d7cf0bf17b8ffe1
|
uses: nuget/setup-nuget@04b0c2b8d1b97922f67eca497d7cf0bf17b8ffe1
|
||||||
with:
|
with:
|
||||||
nuget-version: "latest"
|
nuget-version: 'latest'
|
||||||
|
|
||||||
|
- name: Set up MSBuild
|
||||||
|
uses: microsoft/setup-msbuild@c26a08ba26249b81327e26f6ef381897b6a8754d
|
||||||
|
|
||||||
|
- name: Cache npm
|
||||||
|
id: npm-cache
|
||||||
|
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
|
||||||
|
with:
|
||||||
|
path: '~/.npm'
|
||||||
|
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
|
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||||
with:
|
with:
|
||||||
cache: 'npm'
|
node-version: '14'
|
||||||
cache-dependency-path: '**/package-lock.json'
|
|
||||||
node-version: "16"
|
- name: Update NPM
|
||||||
|
run: |
|
||||||
|
npm install -g npm@7
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
nuget help | grep Version
|
nuget help | grep Version
|
||||||
|
msbuild -version
|
||||||
|
dotnet --info
|
||||||
node --version
|
node --version
|
||||||
npm --version
|
npm --version
|
||||||
echo "GitHub ref: $GITHUB_REF"
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
@@ -407,52 +392,19 @@ jobs:
|
|||||||
GITHUB_REF: ${{ github.ref }}
|
GITHUB_REF: ${{ github.ref }}
|
||||||
GITHUB_EVENT: ${{ github.event_name }}
|
GITHUB_EVENT: ${{ github.event_name }}
|
||||||
|
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
|
- name: NPM install
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
- 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
|
||||||
if: always()
|
if: always()
|
||||||
@@ -460,31 +412,25 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- cloc
|
- cloc
|
||||||
- setup
|
- setup
|
||||||
- lint
|
|
||||||
- build-oss-selfhost
|
- build-oss-selfhost
|
||||||
- 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
|
||||||
if: ${{ (github.ref == 'refs/heads/master') || (github.ref == 'refs/heads/rc') }}
|
if: ${{ (github.ref == 'refs/heads/master') || (github.ref == 'refs/heads/rc') }}
|
||||||
env:
|
env:
|
||||||
CLOC_STATUS: ${{ needs.cloc.result }}
|
CLOC_STATUS: ${{ needs.cloc.result }}
|
||||||
LINT_STATUS: ${{ needs.lint.result }}
|
|
||||||
SETUP_STATUS: ${{ needs.setup.result }}
|
SETUP_STATUS: ${{ needs.setup.result }}
|
||||||
BUILD_OSS_SELFHOST_STATUS: ${{ needs.build-oss-selfhost.result }}
|
BUILD_OSS_SELFHOST_STATUS: ${{ needs.build-oss-selfhost.result }}
|
||||||
BUILD_CLOUD_STATUS: ${{ needs.build-cloud.result }}
|
BUILD_CLOUD_STATUS: ${{ needs.build-cloud.result }}
|
||||||
BUILD_COMMERCIAL_SELFHOST_STATUS: ${{ needs.build-commercial-selfhost.result }}
|
BUILD_COMMERCIAL_SELFHOST_STATUS: ${{ needs.build-commercial-selfhost.result }}
|
||||||
BUILD_QA_STATUS: ${{ needs.build-qa.result }}
|
BUILD_QA: ${{ needs.build-qa.result }}
|
||||||
CROWDIN_PUSH_STATUS: ${{ needs.crowdin-push.result }}
|
WINDOWS: ${{ needs.windows.result }}
|
||||||
WINDOWS_STATUS: ${{ needs.windows.result }}
|
|
||||||
run: |
|
run: |
|
||||||
if [ "$CLOC_STATUS" = "failure" ]; then
|
if [ "$CLOC_STATUS" = "failure" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
elif [ "$LINT_STATUS" = "failure" ]; then
|
|
||||||
exit 1
|
|
||||||
elif [ "$SETUP_STATUS" = "failure" ]; then
|
elif [ "$SETUP_STATUS" = "failure" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
elif [ "$BUILD_OSS_SELFHOST_STATUS" = "failure" ]; then
|
elif [ "$BUILD_OSS_SELFHOST_STATUS" = "failure" ]; then
|
||||||
@@ -493,11 +439,9 @@ 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_STATUS" = "failure" ]; then
|
elif [ "$BUILD_QA" = "failure" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
elif [ "$CROWDIN_PUSH_STATUS" = "failure" ]; then
|
elif [ "$WINDOWS" = "failure" ]; then
|
||||||
exit 1
|
|
||||||
elif [ "$WINDOWS_STATUS" = "failure" ]; then
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
---
|
---
|
||||||
name: Crowdin Pull
|
name: Crowdin Sync
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs: {}
|
inputs: {}
|
||||||
schedule:
|
# schedule:
|
||||||
- cron: "0 0 * * 5"
|
# - cron: '0 0 * * *'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
crowdin-pull:
|
crowdin-sync:
|
||||||
name: Pull
|
name: Autosync
|
||||||
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 # v1.3.2
|
uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea
|
||||||
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 }}
|
||||||
16
.github/workflows/enforce-labels.yml
vendored
16
.github/workflows/enforce-labels.yml
vendored
@@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
name: Enforce PR labels
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [labeled, unlabeled, opened, edited, synchronize]
|
|
||||||
jobs:
|
|
||||||
enforce-label:
|
|
||||||
name: EnforceLabel
|
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
steps:
|
|
||||||
- name: Enforce Label
|
|
||||||
uses: yogevbd/enforce-label-action@8d1e1709b1011e6d90400a0e6cf7c0b77aa5efeb
|
|
||||||
with:
|
|
||||||
BANNED_LABELS: "hold"
|
|
||||||
BANNED_LABELS_DESCRIPTION: "PRs on hold cannot be merged"
|
|
||||||
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: "bw-env-qa"
|
_QA_CLUSTER_RESOURCE_GROUP: "bitwarden-devops"
|
||||||
_QA_CLUSTER_NAME: "bw-aks-qa"
|
_QA_CLUSTER_NAME: "dev-aks"
|
||||||
_QA_K8S_NAMESPACE: "bw-qa"
|
_QA_K8S_NAMESPACE: "bw-qa"
|
||||||
_QA_K8S_APP_NAME: "bw-web"
|
_QA_K8S_APP_NAME: "bw-web"
|
||||||
|
|
||||||
@@ -23,7 +23,8 @@ jobs:
|
|||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
||||||
|
|
||||||
- name: Setup
|
- name: Setup
|
||||||
run: export PATH=$PATH:~/work/web/web
|
run:
|
||||||
|
export PATH=$PATH:~/work/web/web
|
||||||
|
|
||||||
- name: Login to Azure
|
- name: Login to Azure
|
||||||
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
||||||
@@ -35,16 +36,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: "qa-aks-kubectl-credentials"
|
secrets: "dev-aks-kubectl-credentials"
|
||||||
|
|
||||||
- name: Login with qa-aks-kubectl-credentials SP
|
- name: Login to dev-aks-kubectl SP
|
||||||
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
||||||
with:
|
with:
|
||||||
creds: ${{ env.qa-aks-kubectl-credentials }}
|
creds: ${{ env.dev-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
|
||||||
@@ -54,7 +55,7 @@ jobs:
|
|||||||
- name: Get image tag
|
- name: Get image tag
|
||||||
id: image_tag
|
id: image_tag
|
||||||
run: |
|
run: |
|
||||||
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g")
|
IMAGE_TAG=$(echo "$GITHUB_REF" | awk '{split($0, a, "/"); print a[3];}')
|
||||||
TAG_EXTENSION=${{ github.event.inputs.image_extension }}
|
TAG_EXTENSION=${{ github.event.inputs.image_extension }}
|
||||||
|
|
||||||
if [[ $TAG_EXTENSION ]]; then
|
if [[ $TAG_EXTENSION ]]; then
|
||||||
|
|||||||
151
.github/workflows/release.yml
vendored
151
.github/workflows/release.yml
vendored
@@ -3,16 +3,7 @@ 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
|
|
||||||
- Dry Run
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
setup:
|
setup:
|
||||||
@@ -21,20 +12,18 @@ 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
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
|
||||||
run: |
|
run: |
|
||||||
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then
|
if [[ "$GITHUB_REF" != "refs/heads/rc" ]]; then
|
||||||
echo "==================================="
|
echo "==================================="
|
||||||
echo "[!] Can only release from the 'rc' or 'hotfix-rc' branches"
|
echo "[!] Can only release from rc branch"
|
||||||
echo "==================================="
|
echo "==================================="
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # 2.4.0
|
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # 2.3.4
|
||||||
|
|
||||||
- name: Check Release Version
|
- name: Check Release Version
|
||||||
id: version
|
id: version
|
||||||
@@ -44,8 +33,7 @@ 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" ] && \
|
if [ "v$version" == "$previous_release_tag_version" ]; then
|
||||||
[ "${{ 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
|
||||||
@@ -53,21 +41,13 @@ 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: Release self-host docker
|
name: Build 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 }}
|
||||||
_RELEASE_OPTION: ${{ github.event.inputs.release_type }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
@@ -75,12 +55,7 @@ jobs:
|
|||||||
docker --version
|
docker --version
|
||||||
echo "GitHub ref: $GITHUB_REF"
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
echo "Github Release Option: $_RELEASE_OPTION"
|
|
||||||
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
|
||||||
|
|
||||||
########## DockerHub ##########
|
|
||||||
- name: Setup DCT
|
- name: Setup DCT
|
||||||
id: setup-dct
|
id: setup-dct
|
||||||
uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff
|
uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff
|
||||||
@@ -88,70 +63,27 @@ jobs:
|
|||||||
azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||||
azure-keyvault-name: "bitwarden-prod-kv"
|
azure-keyvault-name: "bitwarden-prod-kv"
|
||||||
|
|
||||||
- name: Pull latest selfhost image
|
- name: Checkout repo
|
||||||
run: |
|
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||||
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
|
|
||||||
docker pull bitwarden/web:latest
|
|
||||||
else
|
|
||||||
docker pull bitwarden/web:$_BRANCH_NAME
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Tag version and latest
|
- name: Pull latest selfhost rc image
|
||||||
run: |
|
run: docker pull bitwarden/web:rc
|
||||||
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
|
|
||||||
docker tag bitwarden/web:latest bitwarden/web:dryrun
|
|
||||||
else
|
|
||||||
docker tag bitwarden/web:$_BRANCH_NAME bitwarden/web:$_RELEASE_VERSION
|
|
||||||
docker tag bitwarden/web:$_BRANCH_NAME bitwarden/web:latest
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Push version and latest image
|
- name: Tag version
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
run: |
|
||||||
|
docker tag bitwarden/web:rc bitwarden/web:latest
|
||||||
|
docker tag bitwarden/web:rc bitwarden/web:$_RELEASE_VERSION
|
||||||
|
|
||||||
|
- name: List Docker images
|
||||||
|
run: docker images
|
||||||
|
|
||||||
|
- name: Push images
|
||||||
|
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 and disable Docker Notary
|
|
||||||
run: |
|
|
||||||
docker logout
|
|
||||||
echo "DOCKER_CONTENT_TRUST=0" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
########## ACR ##########
|
|
||||||
- name: Login to Azure - QA Subscription
|
|
||||||
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
|
||||||
with:
|
|
||||||
creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }}
|
|
||||||
|
|
||||||
- name: Login to Azure ACR
|
|
||||||
run: az acr login -n bitwardenqa
|
|
||||||
|
|
||||||
- name: Tag version and latest
|
|
||||||
env:
|
|
||||||
REGISTRY: bitwardenqa.azurecr.io
|
|
||||||
run: |
|
|
||||||
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
|
|
||||||
docker tag bitwarden/web:latest $REGISTRY/web:dryrun
|
|
||||||
else
|
|
||||||
docker tag bitwarden/web:$_BRANCH_NAME $REGISTRY/web:$_RELEASE_VERSION
|
|
||||||
docker tag bitwarden/web:$_BRANCH_NAME $REGISTRY/web:latest
|
|
||||||
|
|
||||||
docker tag bitwarden/web:$_BRANCH_NAME $REGISTRY/web-sh:$_RELEASE_VERSION
|
|
||||||
docker tag bitwarden/web:$_BRANCH_NAME $REGISTRY/web-sh:latest
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Push version and latest image
|
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
|
||||||
env:
|
|
||||||
REGISTRY: bitwardenqa.azurecr.io
|
|
||||||
run: |
|
|
||||||
docker push $REGISTRY/web:$_RELEASE_VERSION
|
|
||||||
docker push $REGISTRY/web:latest
|
|
||||||
|
|
||||||
docker push $REGISTRY/web-sh:$_RELEASE_VERSION
|
|
||||||
docker push $REGISTRY/web-sh:latest
|
|
||||||
|
|
||||||
- name: Log out of Docker
|
- name: Log out of Docker
|
||||||
run: docker logout
|
run: docker logout
|
||||||
@@ -168,7 +100,7 @@ jobs:
|
|||||||
_TAG_VERSION: ${{ needs.setup.outputs.tag_version }}
|
_TAG_VERSION: ${{ needs.setup.outputs.tag_version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repo
|
- name: Checkout Repo
|
||||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
|
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
||||||
with:
|
with:
|
||||||
ref: gh-pages
|
ref: gh-pages
|
||||||
|
|
||||||
@@ -176,9 +108,7 @@ 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@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
|
|
||||||
|
|
||||||
- name: Setup git config
|
- name: Setup git config
|
||||||
run: |
|
run: |
|
||||||
@@ -192,7 +122,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: ${{ needs.setup.outputs.branch_name }}
|
branch: rc
|
||||||
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
|
||||||
@@ -200,7 +130,7 @@ jobs:
|
|||||||
run: unzip web-*-cloud-COMMERCIAL.zip
|
run: unzip web-*-cloud-COMMERCIAL.zip
|
||||||
|
|
||||||
- name: Deploy GitHub Pages
|
- name: Deploy GitHub Pages
|
||||||
uses: crazy-max/ghaction-github-pages@a117e4aa1fb4854d021546d2abdfac95be568a3a # v2.6.0
|
uses: crazy-max/ghaction-github-pages@db4476a01402e1a7ce05f41832040eef16d14925 # v2.5.0
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
@@ -208,10 +138,8 @@ jobs:
|
|||||||
build_dir: build
|
build_dir: build
|
||||||
keep_history: true
|
keep_history: true
|
||||||
commit_message: "Staging deploy ${{ needs.setup.outputs.release_version }}"
|
commit_message: "Staging deploy ${{ needs.setup.outputs.release_version }}"
|
||||||
dry_run: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
|
||||||
|
|
||||||
- name: Create Deploy PR
|
- name: Create Deploy PR
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
|
||||||
env:
|
env:
|
||||||
PR_BRANCH: deploy-${{ env._TAG_VERSION }}
|
PR_BRANCH: deploy-${{ env._TAG_VERSION }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -235,9 +163,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: ${{ needs.setup.outputs.branch_name }}
|
branch: rc
|
||||||
artifacts: "web-*-selfhosted-COMMERCIAL.zip,
|
artifacts: "web-*-selfhosted-COMMERCIAL.zip,
|
||||||
web-*-selfhosted-open-source.zip"
|
web-*-selfhosted-open-source.zip"
|
||||||
|
|
||||||
- name: Rename assets
|
- name: Rename assets
|
||||||
run: |
|
run: |
|
||||||
@@ -245,31 +173,12 @@ jobs:
|
|||||||
mv web-*-selfhosted-open-source.zip web-${{ needs.setup.outputs.release_version }}-selfhosted-open-source.zip
|
mv web-*-selfhosted-open-source.zip web-${{ needs.setup.outputs.release_version }}-selfhosted-open-source.zip
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
uses: ncipollo/release-action@95215a3cb6e6a1908b3c44e00b4fdb15548b1e09
|
||||||
uses: ncipollo/release-action@40bb172bd05f266cf9ba4ff965cb61e9ee5f6d01
|
|
||||||
with:
|
with:
|
||||||
name: "Version ${{ needs.setup.outputs.release_version }}"
|
name: "Version ${{ needs.setup.outputs.release_version }}"
|
||||||
commit: ${{ github.sha }}
|
commit: ${{ github.sha }}
|
||||||
tag: "${{ needs.setup.outputs.tag_version }}"
|
tag: "${{ needs.setup.outputs.tag_version }}"
|
||||||
body: "<insert release notes here>"
|
body: "<insert release notes here>"
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
dry-run:
|
|
||||||
name: Dry Run Cleanup
|
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
|
||||||
env:
|
|
||||||
_TAG_VERSION: ${{ needs.setup.outputs.tag_version }}
|
|
||||||
needs:
|
|
||||||
- setup
|
|
||||||
- release
|
|
||||||
steps:
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # 2.4.0
|
|
||||||
|
|
||||||
- name: Remove deploy branch
|
|
||||||
run: git push origin --delete deploy-$_TAG_VERSION
|
|
||||||
|
|||||||
71
.github/workflows/version-bump.yml
vendored
71
.github/workflows/version-bump.yml
vendored
@@ -1,71 +0,0 @@
|
|||||||
---
|
|
||||||
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 }}"
|
|
||||||
11
.github/workflows/workflow-linter.yml
vendored
11
.github/workflows/workflow-linter.yml
vendored
@@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
name: Workflow Linter
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- .github/workflows/**
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
call-workflow:
|
|
||||||
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@master
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
npx lint-staged
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# Build directories
|
|
||||||
build
|
|
||||||
dist
|
|
||||||
|
|
||||||
jslib
|
|
||||||
|
|
||||||
# External libraries / auto synced locales
|
|
||||||
src/locales
|
|
||||||
src/404/*.min.css
|
|
||||||
|
|
||||||
# Github Workflows
|
|
||||||
.github/workflows
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"printWidth": 100
|
|
||||||
}
|
|
||||||
@@ -6,12 +6,17 @@ 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
|
|
||||||
- **Report a bug or submit a bugfix:** Use Github issues and pull requests
|
* **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 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
|
* **Report a bug or submit a bugfix:** Use Github issues and pull requests
|
||||||
- **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
|
||||||
|
|
||||||
@@ -19,9 +24,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)
|
||||||
|
|
||||||
|
|||||||
38
README.md
38
README.md
@@ -23,8 +23,8 @@
|
|||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org) v16.13.1 or greater
|
- [Node.js](https://nodejs.org) v14.17 or greater
|
||||||
- NPM v8
|
- NPM v7
|
||||||
|
|
||||||
### Run the app
|
### Run the app
|
||||||
|
|
||||||
@@ -41,52 +41,28 @@ If you want to point the development web vault to the production APIs, you can r
|
|||||||
|
|
||||||
```
|
```
|
||||||
npm install
|
npm install
|
||||||
ENV=cloud npm run build:oss:watch
|
ENV=production 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": {}
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Where the `urls` object is defined by the [Urls type in jslib](https://github.com/bitwarden/jslib/blob/master/common/src/abstractions/environment.service.ts).
|
Where the `urls` object is defined by the [Urls type in jslib](https://github.com/bitwarden/jslib/blob/master/common/src/abstractions/environment.service.ts).
|
||||||
|
|
||||||
## We're Hiring!
|
|
||||||
|
|
||||||
Interested in contributing in a big way? Consider joining our team! We're hiring for many positions. Please take a look at our [Careers page](https://bitwarden.com/careers/) to see what opportunities are currently open as well as what it's like to work at Bitwarden.
|
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file.
|
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
|
|
||||||
```
|
|
||||||
|
|||||||
42
SECURITY.md
42
SECURITY.md
@@ -1,11 +1,39 @@
|
|||||||
Bitwarden believes that working with security researchers across the globe is crucial to keeping our users safe. If you believe you've found a security issue in our product or service, we encourage you to please submit a report through our [HackerOne Program](https://hackerone.com/bitwarden/). We welcome working with you to resolve the issue promptly. Thanks in advance!
|
Bitwarden believes that working with security researchers across the globe is crucial to keeping our
|
||||||
|
users safe. If you believe you've found a security issue in our product or service, we encourage you to
|
||||||
|
notify us. We welcome working with you to resolve the issue promptly. Thanks in advance!
|
||||||
|
|
||||||
# Disclosure Policy
|
# Disclosure Policy
|
||||||
|
|
||||||
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every effort to quickly resolve the issue.
|
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every
|
||||||
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a third-party. We may publicly disclose the issue before resolving it, if appropriate.
|
effort to quickly resolve the issue.
|
||||||
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or degradation of our service. Only interact with accounts you own or with explicit permission of the account holder.
|
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a
|
||||||
- If you would like to encrypt your report, please use the PGP key with long ID `0xDE6887086F892325FEC04CC0D847525B6931381F` (available in the public keyserver pool).
|
third-party. We may publicly disclose the issue before resolving it, if appropriate.
|
||||||
|
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or
|
||||||
|
degradation of our service. Only interact with accounts you own or with explicit permission of the
|
||||||
|
account holder.
|
||||||
|
- If you would like to encrypt your report, please use the PGP key with long ID
|
||||||
|
`0xDE6887086F892325FEC04CC0D847525B6931381F` (available in the public keyserver pool).
|
||||||
|
|
||||||
|
# In-scope
|
||||||
|
|
||||||
|
- Security issues in any current release of Bitwarden. This includes the web vault, browser extension,
|
||||||
|
and mobile apps (iOS and Android). Product downloads are available at https://bitwarden.com. Source
|
||||||
|
code is available at https://github.com/bitwarden.
|
||||||
|
|
||||||
|
# Exclusions
|
||||||
|
|
||||||
|
The following bug classes are out-of scope:
|
||||||
|
|
||||||
|
- Bugs that are already reported on any of Bitwarden's issue trackers (https://github.com/bitwarden),
|
||||||
|
or that we already know of. Note that some of our issue tracking is private.
|
||||||
|
- Issues in an upstream software dependency (ex: Xamarin, ASP.NET) which are already reported to the
|
||||||
|
upstream maintainer.
|
||||||
|
- Attacks requiring physical access to a user's device.
|
||||||
|
- Self-XSS
|
||||||
|
- Issues related to software or protocols not under Bitwarden's control
|
||||||
|
- Vulnerabilities in outdated versions of Bitwarden
|
||||||
|
- Missing security best practices that do not directly lead to a vulnerability
|
||||||
|
- Issues that do not have any impact on the general public
|
||||||
|
|
||||||
While researching, we'd like to ask you to refrain from:
|
While researching, we'd like to ask you to refrain from:
|
||||||
|
|
||||||
@@ -14,8 +42,4 @@ While researching, we'd like to ask you to refrain from:
|
|||||||
- Social engineering (including phishing) of Bitwarden staff or contractors
|
- Social engineering (including phishing) of Bitwarden staff or contractors
|
||||||
- Any physical attempts against Bitwarden property or data centers
|
- Any physical attempts against Bitwarden property or data centers
|
||||||
|
|
||||||
# We want to help you!
|
|
||||||
|
|
||||||
If you have something that you feel is close to exploitation, or if you'd like some information regarding the internal API, or generally have any questions regarding the app that would help in your efforts, please email us at https://bitwarden.com/contact and ask for that information. As stated above, Bitwarden wants to help you find issues, and is more than willing to help.
|
|
||||||
|
|
||||||
Thank you for helping keep Bitwarden and our users safe!
|
Thank you for helping keep Bitwarden and our users safe!
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
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,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forChild(routes)],
|
imports: [RouterModule.forChild(routes)],
|
||||||
exports: [RouterModule],
|
exports: [RouterModule],
|
||||||
})
|
})
|
||||||
export class AppRoutingModule {}
|
export class AppRoutingModule { }
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
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() {
|
|
||||||
super.ngOnInit();
|
|
||||||
|
|
||||||
this.policyListService.addPolicies([
|
ngOnInit() {
|
||||||
new MaximumVaultTimeoutPolicy(),
|
super.ngOnInit();
|
||||||
new DisablePersonalVaultExportPolicy(),
|
|
||||||
]);
|
this.policyListService.addPolicies([
|
||||||
}
|
new MaximumVaultTimeoutPolicy(),
|
||||||
|
new DisablePersonalVaultExportPolicy(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,44 @@
|
|||||||
import { DragDropModule } from "@angular/cdk/drag-drop";
|
import { ToasterModule } from 'angular2-toaster';
|
||||||
import { NgModule } from "@angular/core";
|
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||||
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 { JslibModule } from "jslib-angular/jslib.module";
|
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
import { OssRoutingModule } from "src/app/oss-routing.module";
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { ServicesModule } from "src/app/services/services.module";
|
import { AppComponent } from './app.component';
|
||||||
import { WildcardRoutingModule } from "src/app/wildcard-routing.module";
|
import { OrganizationsModule } from './organizations/organizations.module';
|
||||||
|
import { DisablePersonalVaultExportPolicyComponent } from './policies/disable-personal-vault-export.component';
|
||||||
|
import { MaximumVaultTimeoutPolicyComponent } from './policies/maximum-vault-timeout.component';
|
||||||
|
|
||||||
import { AppRoutingModule } from "./app-routing.module";
|
import { OssRoutingModule } from 'src/app/oss-routing.module';
|
||||||
import { AppComponent } from "./app.component";
|
import { OssModule } from 'src/app/oss.module';
|
||||||
import { OrganizationsModule } from "./organizations/organizations.module";
|
import { ServicesModule } from 'src/app/services/services.module';
|
||||||
import { DisablePersonalVaultExportPolicyComponent } from "./policies/disable-personal-vault-export.component";
|
import { WildcardRoutingModule } from 'src/app/wildcard-routing.module';
|
||||||
import { MaximumVaultTimeoutPolicyComponent } from "./policies/maximum-vault-timeout.component";
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
JslibModule,
|
OssModule,
|
||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
ServicesModule,
|
ServicesModule,
|
||||||
InfiniteScrollModule,
|
ToasterModule.forRoot(),
|
||||||
DragDropModule,
|
InfiniteScrollModule,
|
||||||
AppRoutingModule,
|
DragDropModule,
|
||||||
OssRoutingModule,
|
AppRoutingModule,
|
||||||
OrganizationsModule, // Must be after OssRoutingModule for competing routes to resolve properly
|
OssRoutingModule,
|
||||||
RouterModule,
|
OrganizationsModule,
|
||||||
WildcardRoutingModule, // Needs to be last to catch all non-existing routes
|
RouterModule,
|
||||||
],
|
WildcardRoutingModule, // Needs to be last to catch all non-existing routes
|
||||||
declarations: [
|
],
|
||||||
AppComponent,
|
declarations: [
|
||||||
DisablePersonalVaultExportPolicyComponent,
|
AppComponent,
|
||||||
MaximumVaultTimeoutPolicyComponent,
|
MaximumVaultTimeoutPolicyComponent,
|
||||||
],
|
DisablePersonalVaultExportPolicyComponent,
|
||||||
bootstrap: [AppComponent],
|
],
|
||||||
|
bootstrap: [AppComponent],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule { }
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
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';
|
||||||
|
|
||||||
require("src/scss/styles.scss");
|
// tslint:disable-next-line
|
||||||
require("src/scss/tailwind.css");
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });
|
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
import { Directive, Input, OnInit, Self } from "@angular/core";
|
|
||||||
import { ControlValueAccessor, FormControl, NgControl, Validators } from "@angular/forms";
|
|
||||||
|
|
||||||
import { dirtyRequired } from "jslib-angular/validators/dirty.validator";
|
|
||||||
|
|
||||||
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
|
|
||||||
@Directive()
|
|
||||||
export abstract class BaseCvaComponent implements ControlValueAccessor, OnInit {
|
|
||||||
get describedById() {
|
|
||||||
return this.showDescribedBy ? this.controlId + "Desc" : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
get showDescribedBy() {
|
|
||||||
return this.helperText != null || this.controlDir.control.hasError("required");
|
|
||||||
}
|
|
||||||
|
|
||||||
get isRequired() {
|
|
||||||
return (
|
|
||||||
this.controlDir.control.hasValidator(Validators.required) ||
|
|
||||||
this.controlDir.control.hasValidator(dirtyRequired)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Input() label: string;
|
|
||||||
@Input() controlId: string;
|
|
||||||
@Input() helperText: string;
|
|
||||||
|
|
||||||
internalControl = new FormControl("");
|
|
||||||
|
|
||||||
protected onChange: any;
|
|
||||||
protected onTouched: any;
|
|
||||||
|
|
||||||
constructor(@Self() public controlDir: NgControl) {
|
|
||||||
this.controlDir.valueAccessor = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.internalControl.valueChanges.subscribe(this.onValueChangesInternal);
|
|
||||||
}
|
|
||||||
|
|
||||||
onBlurInternal() {
|
|
||||||
this.onTouched();
|
|
||||||
}
|
|
||||||
|
|
||||||
// CVA interfaces
|
|
||||||
writeValue(value: string) {
|
|
||||||
this.internalControl.setValue(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
registerOnChange(fn: any) {
|
|
||||||
this.onChange = fn;
|
|
||||||
}
|
|
||||||
|
|
||||||
registerOnTouched(fn: any) {
|
|
||||||
this.onTouched = fn;
|
|
||||||
}
|
|
||||||
|
|
||||||
setDisabledState(isDisabled: boolean) {
|
|
||||||
if (isDisabled) {
|
|
||||||
this.internalControl.disable();
|
|
||||||
} else {
|
|
||||||
this.internalControl.enable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected onValueChangesInternal: any = (value: string) => this.onChange(value);
|
|
||||||
// End CVA interfaces
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<div class="form-group">
|
|
||||||
<div class="form-check">
|
|
||||||
<input
|
|
||||||
class="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
[attr.id]="controlId"
|
|
||||||
[attr.aria-describedby]="describedById"
|
|
||||||
[formControl]="internalControl"
|
|
||||||
(blur)="onBlurInternal()"
|
|
||||||
/>
|
|
||||||
<label class="form-check-label" [attr.for]="controlId">{{ label }}</label>
|
|
||||||
</div>
|
|
||||||
<small *ngIf="showDescribedBy" [attr.id]="describedById" class="form-text text-muted">{{
|
|
||||||
helperText
|
|
||||||
}}</small>
|
|
||||||
</div>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { Component } from "@angular/core";
|
|
||||||
|
|
||||||
import { BaseCvaComponent } from "./base-cva.component";
|
|
||||||
|
|
||||||
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
|
|
||||||
@Component({
|
|
||||||
selector: "app-input-checkbox",
|
|
||||||
templateUrl: "input-checkbox.component.html",
|
|
||||||
})
|
|
||||||
export class InputCheckboxComponent extends BaseCvaComponent {}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<div class="form-group">
|
|
||||||
<label>{{ label }}</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input class="form-control" readonly [value]="controlValue" />
|
|
||||||
<div class="input-group-append" *ngIf="showLaunch">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-secondary"
|
|
||||||
appA11yTitle="{{ 'launch' | i18n }}"
|
|
||||||
(click)="launchUri(controlValue)"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="input-group-append" *ngIf="showCopy">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-secondary"
|
|
||||||
appA11yTitle="{{ 'copyValue' | i18n }}"
|
|
||||||
(click)="copy(controlValue)"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { Component, Input } from "@angular/core";
|
|
||||||
|
|
||||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
|
||||||
|
|
||||||
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
|
|
||||||
@Component({
|
|
||||||
selector: "app-input-text-readonly",
|
|
||||||
templateUrl: "input-text-readonly.component.html",
|
|
||||||
})
|
|
||||||
export class InputTextReadOnlyComponent {
|
|
||||||
@Input() controlValue: string;
|
|
||||||
@Input() label: string;
|
|
||||||
@Input() showCopy = true;
|
|
||||||
@Input() showLaunch = false;
|
|
||||||
|
|
||||||
constructor(private platformUtilsService: PlatformUtilsService) {}
|
|
||||||
|
|
||||||
copy(value: string) {
|
|
||||||
this.platformUtilsService.copyToClipboard(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
launchUri(url: string) {
|
|
||||||
this.platformUtilsService.launchUri(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<div class="form-group">
|
|
||||||
<label [attr.for]="controlId">
|
|
||||||
{{ label }}
|
|
||||||
<small *ngIf="isRequired" class="text-muted form-text d-inline"
|
|
||||||
>({{ "required" | i18n }})</small
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
[formControl]="internalControl"
|
|
||||||
class="form-control"
|
|
||||||
[attr.id]="controlId"
|
|
||||||
[attr.aria-describedby]="describedById"
|
|
||||||
[attr.aria-invalid]="controlDir.control.invalid"
|
|
||||||
(blur)="onBlurInternal()"
|
|
||||||
/>
|
|
||||||
<div *ngIf="showDescribedBy" [attr.id]="describedById">
|
|
||||||
<small
|
|
||||||
*ngIf="helperText != null && !controlDir.control.hasError(helperTextSameAsError)"
|
|
||||||
class="form-text text-muted"
|
|
||||||
>
|
|
||||||
{{ helperText }}
|
|
||||||
</small>
|
|
||||||
<small class="error-inline" *ngIf="controlDir.control.hasError('required')" role="alert">
|
|
||||||
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
|
|
||||||
<span class="sr-only">{{ "error" | i18n }}:</span>
|
|
||||||
{{
|
|
||||||
controlDir.control.hasError(helperTextSameAsError)
|
|
||||||
? helperText
|
|
||||||
: ("fieldRequiredError" | i18n: label)
|
|
||||||
}}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { Component, Input, OnInit } from "@angular/core";
|
|
||||||
|
|
||||||
import { BaseCvaComponent } from "./base-cva.component";
|
|
||||||
|
|
||||||
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
|
|
||||||
@Component({
|
|
||||||
selector: "app-input-text[label][controlId]",
|
|
||||||
templateUrl: "input-text.component.html",
|
|
||||||
})
|
|
||||||
export class InputTextComponent extends BaseCvaComponent implements OnInit {
|
|
||||||
@Input() helperTextSameAsError: string;
|
|
||||||
@Input() requiredErrorMessage: string;
|
|
||||||
@Input() stripSpaces = false;
|
|
||||||
|
|
||||||
transformValue: (value: string) => string = null;
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
super.ngOnInit();
|
|
||||||
if (this.stripSpaces) {
|
|
||||||
this.transformValue = this.doStripSpaces;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writeValue(value: string) {
|
|
||||||
this.internalControl.setValue(value == null ? "" : value);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected onValueChangesInternal: any = (value: string) => {
|
|
||||||
let newValue = value;
|
|
||||||
if (this.transformValue != null) {
|
|
||||||
newValue = this.transformValue(value);
|
|
||||||
this.internalControl.setValue(newValue, { emitEvent: false });
|
|
||||||
}
|
|
||||||
this.onChange(newValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
protected onValueChangeInternal(value: string) {
|
|
||||||
let newValue = value;
|
|
||||||
if (this.transformValue != null) {
|
|
||||||
newValue = this.transformValue(value);
|
|
||||||
this.internalControl.setValue(newValue, { emitEvent: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private doStripSpaces(value: string) {
|
|
||||||
return value.replace(/ /g, "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<div class="form-group">
|
|
||||||
<label [attr.for]="controlId">
|
|
||||||
{{ label }}
|
|
||||||
<small *ngIf="isRequired" class="text-muted form-text d-inline"
|
|
||||||
>({{ "required" | i18n }})</small
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
class="form-control"
|
|
||||||
[attr.id]="controlId"
|
|
||||||
[attr.aria-invalid]="controlDir.control.invalid"
|
|
||||||
[formControl]="internalControl"
|
|
||||||
(blur)="onBlurInternal()"
|
|
||||||
>
|
|
||||||
<option *ngFor="let o of selectOptions" [ngValue]="o.value" disabled="{{ o.disabled }}">
|
|
||||||
{{ o.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { Component, Input } from "@angular/core";
|
|
||||||
|
|
||||||
import { SelectOptions } from "jslib-angular/interfaces/selectOptions";
|
|
||||||
|
|
||||||
import { BaseCvaComponent } from "./base-cva.component";
|
|
||||||
|
|
||||||
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
|
|
||||||
@Component({
|
|
||||||
selector: "app-select",
|
|
||||||
templateUrl: "select.component.html",
|
|
||||||
})
|
|
||||||
export class SelectComponent extends BaseCvaComponent {
|
|
||||||
@Input() selectOptions: SelectOptions[];
|
|
||||||
}
|
|
||||||
@@ -1,448 +1,284 @@
|
|||||||
<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
|
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
class="bwi bwi-spinner bwi-spin text-muted"
|
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||||
title="{{ 'loading' | i18n }}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<form
|
<form #form (ngSubmit)="submit()" [formGroup]="data" [appApiAction]="formPromise" *ngIf="!loading">
|
||||||
#form
|
<div class="form-group">
|
||||||
(ngSubmit)="submit()"
|
<div class="form-check">
|
||||||
[formGroup]="ssoConfigForm"
|
<input class="form-check-input" type="checkbox" id="enabled" [formControl]="enabled" name="Enabled">
|
||||||
[appApiAction]="formPromise"
|
<label class="form-check-label" for="enabled">{{'enabled' | i18n}}</label>
|
||||||
*ngIf="!loading"
|
</div>
|
||||||
>
|
</div>
|
||||||
<p>
|
|
||||||
{{ "ssoPolicyHelpStart" | i18n }}
|
|
||||||
<a routerLink="../policies">{{ "ssoPolicyHelpLink" | i18n }}</a>
|
|
||||||
{{ "ssoPolicyHelpEnd" | i18n }}
|
|
||||||
<br />
|
|
||||||
{{ "ssoPolicyHelpKeyConnector" | i18n }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Root form -->
|
|
||||||
<ng-container>
|
|
||||||
<app-input-checkbox
|
|
||||||
controlId="enabled"
|
|
||||||
[formControl]="enabled"
|
|
||||||
[label]="'allowSso' | i18n"
|
|
||||||
[helperText]="'allowSsoDesc' | i18n"
|
|
||||||
></app-input-checkbox>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{ "memberDecryptionOption" | i18n }}</label>
|
<label for="type">{{'type' | i18n}}</label>
|
||||||
<div class="form-check form-check-block">
|
<select class="form-control" id="type" formControlName="configType">
|
||||||
<input
|
<option value="0" disabled>{{'selectType' | i18n}}</option>
|
||||||
class="form-check-input"
|
<option value="1">OpenID Connect</option>
|
||||||
type="radio"
|
<option value="2">SAML 2.0</option>
|
||||||
id="memberDecryptionPass"
|
</select>
|
||||||
[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>
|
</div>
|
||||||
|
|
||||||
<!-- Key Connector -->
|
<!-- OIDC -->
|
||||||
<ng-container *ngIf="ssoConfigForm.get('keyConnectorEnabled').value">
|
<div *ngIf="data.value.configType == 1">
|
||||||
<app-callout type="warning" [useAlertRole]="true">
|
<div class="config-section">
|
||||||
{{ "keyConnectorWarning" | i18n }}
|
<h2>{{'openIdConnectConfig' | i18n}}</h2>
|
||||||
</app-callout>
|
<div class="form-group">
|
||||||
|
<label>{{'callbackPath' | i18n}}</label>
|
||||||
<div class="form-group">
|
<div class="input-group">
|
||||||
<label for="keyConnectorUrl">
|
<input class="form-control" readonly [value]="callbackPath">
|
||||||
{{ "keyConnectorUrl" | i18n }}
|
<div class="input-group-append">
|
||||||
<small class="text-muted form-text d-inline">({{ "required" | i18n }})</small>
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
</label>
|
appA11yTitle="{{'copyValue' | i18n}}"
|
||||||
<div class="input-group">
|
(click)="copy(callbackPath)">
|
||||||
<input
|
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
||||||
class="form-control"
|
</button>
|
||||||
formControlName="keyConnectorUrl"
|
</div>
|
||||||
id="keyConnectorUrl"
|
</div>
|
||||||
aria-describedby="keyConnectorUrlDesc"
|
</div>
|
||||||
(change)="haveTestedKeyConnector = false"
|
<div class="form-group">
|
||||||
appInputStripSpaces
|
<label>{{'signedOutCallbackPath' | i18n}}</label>
|
||||||
appA11yInvalid
|
<div class="input-group">
|
||||||
/>
|
<input class="form-control" readonly [value]="signedOutCallbackPath">
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<button
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
type="button"
|
appA11yTitle="{{'copyValue' | i18n}}"
|
||||||
class="btn btn-outline-secondary"
|
(click)="copy(signedOutCallbackPath)">
|
||||||
(click)="validateKeyConnectorUrl()"
|
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
||||||
[disabled]="!enableTestKeyConnector"
|
</button>
|
||||||
>
|
</div>
|
||||||
<i
|
</div>
|
||||||
class="bwi bwi-spinner bwi-spin"
|
</div>
|
||||||
title="{{ 'loading' | i18n }}"
|
<div class="form-group">
|
||||||
aria-hidden="true"
|
<label>{{'authority' | i18n}}</label>
|
||||||
*ngIf="keyConnectorUrl.pending"
|
<input class="form-control" formControlName="authority">
|
||||||
></i>
|
</div>
|
||||||
<span *ngIf="!keyConnectorUrl.pending">
|
<div class="form-group">
|
||||||
{{ "keyConnectorTest" | i18n }}
|
<label>{{'clientId' | i18n}}</label>
|
||||||
</span>
|
<input class="form-control" formControlName="clientId">
|
||||||
</button>
|
</div>
|
||||||
</div>
|
<div class="form-group">
|
||||||
|
<label>{{'clientSecret' | i18n}}</label>
|
||||||
|
<input class="form-control" formControlName="clientSecret">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{'metadataAddress' | i18n}}</label>
|
||||||
|
<input class="form-control" formControlName="metadataAddress">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{'oidcRedirectBehavior' | i18n}}</label>
|
||||||
|
<select class="form-control" formControlName="redirectBehavior">
|
||||||
|
<option value="0">Redirect GET</option>
|
||||||
|
<option value="1">Form POST</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="getClaimsFromUserInfoEndpoint"
|
||||||
|
formControlName="getClaimsFromUserInfoEndpoint">
|
||||||
|
<label class="form-check-label" for="getClaimsFromUserInfoEndpoint">
|
||||||
|
{{'getClaimsFromUserInfoEndpoint' | i18n}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{'additionalScopes' | i18n}}</label>
|
||||||
|
<input class="form-control" formControlName="additionalScopes">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{'additionalUserIdClaimTypes' | i18n}}</label>
|
||||||
|
<input class="form-control" formControlName="additionalUserIdClaimTypes">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{'additionalEmailClaimTypes' | i18n}}</label>
|
||||||
|
<input class="form-control" formControlName="additionalEmailClaimTypes">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{'additionalNameClaimTypes' | i18n}}</label>
|
||||||
|
<input class="form-control" formControlName="additionalNameClaimTypes">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{'acrValues' | i18n}}</label>
|
||||||
|
<input class="form-control" formControlName="acrValues">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{'expectedReturnAcrValue' | i18n}}</label>
|
||||||
|
<input class="form-control" formControlName="expectedReturnAcrValue">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="haveTestedKeyConnector" id="keyConnectorUrlDesc" aria-live="polite">
|
</div>
|
||||||
<small
|
|
||||||
class="error-inline"
|
<div *ngIf="data.value.configType == 2">
|
||||||
*ngIf="keyConnectorUrl.hasError('invalidUrl'); else keyConnectorSuccess"
|
<!-- SAML2 SP -->
|
||||||
>
|
<div class="config-section">
|
||||||
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
|
<h2>{{'samlSpConfig' | i18n}}</h2>
|
||||||
<span class="sr-only">{{ "error" | i18n }}:</span>
|
<div class="form-group">
|
||||||
{{ "keyConnectorTestFail" | i18n }}
|
<label>{{'spEntityId' | i18n}}</label>
|
||||||
</small>
|
<div class="input-group">
|
||||||
<ng-template #keyConnectorSuccess>
|
<input class="form-control" readonly [value]="spEntityId" >
|
||||||
<small class="text-success">
|
<div class="input-group-append">
|
||||||
<i class="bwi bwi-check-circle" aria-hidden="true"></i>
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
{{ "keyConnectorTestSuccess" | i18n }}
|
appA11yTitle="{{'copyValue' | i18n}}"
|
||||||
</small>
|
(click)="copy(spEntityId)">
|
||||||
</ng-template>
|
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{'spMetadataUrl' | i18n}}</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input class="form-control" readonly [value]="spMetadataUrl">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
|
appA11yTitle="{{'launch' | i18n}}"
|
||||||
|
(click)="launchUri(spMetadataUrl)">
|
||||||
|
<i class="fa fa-lg fa-external-link" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
|
appA11yTitle="{{'copyValue' | i18n}}"
|
||||||
|
(click)="copy(spMetadataUrl)">
|
||||||
|
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{'spAcsUrl' | i18n}}</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input class="form-control" readonly [value]="spAcsUrl">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
|
appA11yTitle="{{'copyValue' | i18n}}"
|
||||||
|
(click)="copy(spAcsUrl)">
|
||||||
|
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{'spNameIdFormat' | i18n}}</label>
|
||||||
|
<select class="form-control" formControlName="spNameIdFormat">
|
||||||
|
<option value="0">Not Configured</option>
|
||||||
|
<option value="1">Unspecified</option>
|
||||||
|
<option value="2">Email Address</option>
|
||||||
|
<option value="3">X.509 Subject Name</option>
|
||||||
|
<option value="4">Windows Domain Qualified Name</option>
|
||||||
|
<option value="5">Kerberos Principal Name</option>
|
||||||
|
<option value="6">Entity Identifier</option>
|
||||||
|
<option value="7">Persistent</option>
|
||||||
|
<option value="8">Transient</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{'spOutboundSigningAlgorithm' | i18n}}</label>
|
||||||
|
<select class="form-control" formControlName="spOutboundSigningAlgorithm">
|
||||||
|
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{o}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{'spSigningBehavior' | i18n}}</label>
|
||||||
|
<select class="form-control" formControlName="spSigningBehavior">
|
||||||
|
<option value="0">If IdP Wants Authn Requests Signed</option>
|
||||||
|
<option value="1">Always</option>
|
||||||
|
<option value="3">Never</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{'spMinIncomingSigningAlgorithm' | i18n}}</label>
|
||||||
|
<select class="form-control" formControlName="spMinIncomingSigningAlgorithm">
|
||||||
|
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{o}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="spWantAssertionsSigned" formControlName="spWantAssertionsSigned">
|
||||||
|
<label class="form-check-label" for="spWantAssertionsSigned">{{'spWantAssertionsSigned' | i18n}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="spValidateCertificates" formControlName="spValidateCertificates">
|
||||||
|
<label class="form-check-label" for="spValidateCertificates">{{'spValidateCertificates' | i18n}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<app-select
|
<!-- SAML2 IDP -->
|
||||||
controlId="type"
|
<div class="config-section">
|
||||||
[label]="'type' | i18n"
|
<h2>{{'samlIdpConfig' | i18n}}</h2>
|
||||||
[selectOptions]="ssoTypeOptions"
|
|
||||||
formControlName="configType"
|
|
||||||
>
|
|
||||||
</app-select>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- OIDC -->
|
<div class="form-group">
|
||||||
<div
|
<label>{{'idpEntityId' | i18n}}</label>
|
||||||
*ngIf="ssoConfigForm.get('configType').value === ssoType.OpenIdConnect"
|
<input class="form-control" formControlName="idpEntityId">
|
||||||
[formGroup]="openIdForm"
|
</div>
|
||||||
>
|
<div class="form-group">
|
||||||
<div class="config-section">
|
<label>{{'idpBindingType' | i18n}}</label>
|
||||||
<h2 class="secondary-header">{{ "openIdConnectConfig" | i18n }}</h2>
|
<select class="form-control" formControlName="idpBindingType">
|
||||||
|
<option value="1">Redirect</option>
|
||||||
<app-input-text-readonly
|
<option value="2">HTTP POST</option>
|
||||||
[label]="'callbackPath' | i18n"
|
<option value="4">Artifact</option>
|
||||||
[controlValue]="callbackPath"
|
</select>
|
||||||
></app-input-text-readonly>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
<app-input-text-readonly
|
<label>{{'idpSingleSignOnServiceUrl' | i18n}}</label>
|
||||||
[label]="'signedOutCallbackPath' | i18n"
|
<input class="form-control" formControlName="idpSingleSignOnServiceUrl">
|
||||||
[controlValue]="signedOutCallbackPath"
|
</div>
|
||||||
></app-input-text-readonly>
|
<div class="form-group">
|
||||||
|
<label>{{'idpSingleLogoutServiceUrl' | i18n}}</label>
|
||||||
<app-input-text
|
<input class="form-control" formControlName="idpSingleLogoutServiceUrl">
|
||||||
[label]="'authority' | i18n"
|
</div>
|
||||||
controlId="authority"
|
<div class="form-group">
|
||||||
[stripSpaces]="true"
|
<label>{{'idpArtifactResolutionServiceUrl' | i18n}}</label>
|
||||||
formControlName="authority"
|
<input class="form-control" formControlName="idpArtifactResolutionServiceUrl">
|
||||||
></app-input-text>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
<app-input-text
|
<label>{{'idpX509PublicCert' | i18n}}</label>
|
||||||
[label]="'clientId' | i18n"
|
<textarea formControlName="idpX509PublicCert" class="form-control form-control-sm text-monospace" rows="6"></textarea>
|
||||||
controlId="clientId"
|
</div>
|
||||||
[stripSpaces]="true"
|
<div class="form-group">
|
||||||
formControlName="clientId"
|
<label>{{'idpOutboundSigningAlgorithm' | i18n}}</label>
|
||||||
></app-input-text>
|
<select class="form-control" formControlName="idpOutboundSigningAlgorithm">
|
||||||
|
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{o}}</option>
|
||||||
<app-input-text
|
</select>
|
||||||
[label]="'clientSecret' | i18n"
|
</div>
|
||||||
controlId="clientSecret"
|
<div class="form-group">
|
||||||
[stripSpaces]="true"
|
<div class="form-check">
|
||||||
formControlName="clientSecret"
|
<input class="form-check-input" type="checkbox" id="idpAllowUnsolicitedAuthnResponse"
|
||||||
></app-input-text>
|
formControlName="idpAllowUnsolicitedAuthnResponse">
|
||||||
|
<label class="form-check-label" for="idpAllowUnsolicitedAuthnResponse">
|
||||||
<app-input-text
|
{{'idpAllowUnsolicitedAuthnResponse' | i18n}}
|
||||||
[label]="'metadataAddress' | i18n"
|
</label>
|
||||||
controlId="metadataAddress"
|
</div>
|
||||||
[stripSpaces]="true"
|
</div>
|
||||||
[helperText]="'openIdAuthorityRequired' | i18n"
|
<div class="form-group">
|
||||||
formControlName="metadataAddress"
|
<div class="form-check">
|
||||||
></app-input-text>
|
<input class="form-check-input" type="checkbox" id="idpDisableOutboundLogoutRequests"
|
||||||
|
formControlName="idpDisableOutboundLogoutRequests">
|
||||||
<app-select
|
<label class="form-check-label" for="idpDisableOutboundLogoutRequests">
|
||||||
controlId="redirectBehavior"
|
{{'idpDisableOutboundLogoutRequests' | i18n}}
|
||||||
[label]="'oidcRedirectBehavior' | i18n"
|
</label>
|
||||||
[selectOptions]="connectRedirectOptions"
|
</div>
|
||||||
formControlName="redirectBehavior"
|
</div>
|
||||||
>
|
<div class="form-group">
|
||||||
</app-select>
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="idpWantAuthnRequestsSigned"
|
||||||
<app-input-checkbox
|
formControlName="idpWantAuthnRequestsSigned">
|
||||||
controlId="getClaimsFromUserInfoEndpoint"
|
<label class="form-check-label" for="idpWantAuthnRequestsSigned">
|
||||||
formControlName="getClaimsFromUserInfoEndpoint"
|
{{'idpWantAuthnRequestsSigned' | i18n}}
|
||||||
[label]="'getClaimsFromUserInfoEndpoint' | i18n"
|
</label>
|
||||||
></app-input-checkbox>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Optional customizations -->
|
</div>
|
||||||
<div
|
|
||||||
class="section-header d-flex flex-row align-items-center mt-3 mb-3"
|
|
||||||
(click)="toggleOpenIdCustomizations()"
|
|
||||||
>
|
|
||||||
<h3 class="mb-0 mr-2" id="customizations-header">
|
|
||||||
{{ "openIdOptionalCustomizations" | i18n }}
|
|
||||||
</h3>
|
|
||||||
<button
|
|
||||||
class="mb-1 btn btn-link"
|
|
||||||
type="button"
|
|
||||||
appStopClick
|
|
||||||
role="button"
|
|
||||||
aria-controls="customizations"
|
|
||||||
[attr.aria-expanded]="showOpenIdCustomizations"
|
|
||||||
aria-labelledby="customizations-header"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="bwi"
|
|
||||||
aria-hidden="true"
|
|
||||||
[ngClass]="{
|
|
||||||
'bwi-angle-down': !showOpenIdCustomizations,
|
|
||||||
'bwi-chevron-up': showOpenIdCustomizations
|
|
||||||
}"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="customizations" [hidden]="!showOpenIdCustomizations">
|
|
||||||
<app-input-text
|
|
||||||
[label]="'additionalScopes' | i18n"
|
|
||||||
controlId="additionalScopes"
|
|
||||||
[helperText]="'separateMultipleWithComma' | i18n"
|
|
||||||
formControlName="additionalScopes"
|
|
||||||
></app-input-text>
|
|
||||||
|
|
||||||
<app-input-text
|
|
||||||
[label]="'additionalUserIdClaimTypes' | i18n"
|
|
||||||
controlId="additionalUserIdClaimTypes"
|
|
||||||
[helperText]="'separateMultipleWithComma' | i18n"
|
|
||||||
formControlName="additionalUserIdClaimTypes"
|
|
||||||
></app-input-text>
|
|
||||||
|
|
||||||
<app-input-text
|
|
||||||
[label]="'additionalEmailClaimTypes' | i18n"
|
|
||||||
controlId="additionalEmailClaimTypes"
|
|
||||||
[helperText]="'separateMultipleWithComma' | i18n"
|
|
||||||
formControlName="additionalEmailClaimTypes"
|
|
||||||
></app-input-text>
|
|
||||||
|
|
||||||
<app-input-text
|
|
||||||
[label]="'additionalNameClaimTypes' | i18n"
|
|
||||||
controlId="additionalNameClaimTypes"
|
|
||||||
[helperText]="'separateMultipleWithComma' | i18n"
|
|
||||||
formControlName="additionalNameClaimTypes"
|
|
||||||
></app-input-text>
|
|
||||||
|
|
||||||
<app-input-text
|
|
||||||
[label]="'acrValues' | i18n"
|
|
||||||
controlId="acrValues"
|
|
||||||
helperText="acr_values"
|
|
||||||
formControlName="acrValues"
|
|
||||||
></app-input-text>
|
|
||||||
|
|
||||||
<app-input-text
|
|
||||||
[label]="'expectedReturnAcrValue' | i18n"
|
|
||||||
controlId="expectedReturnAcrValue"
|
|
||||||
helperText="acr_validation"
|
|
||||||
formControlName="expectedReturnAcrValue"
|
|
||||||
></app-input-text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- SAML2 SP -->
|
|
||||||
<div *ngIf="ssoConfigForm.get('configType').value === ssoType.Saml2" [formGroup]="samlForm">
|
|
||||||
<!-- SAML2 SP -->
|
|
||||||
<div class="config-section">
|
|
||||||
<h2 class="secondary-header">{{ "samlSpConfig" | i18n }}</h2>
|
|
||||||
|
|
||||||
<app-input-text-readonly
|
|
||||||
[label]="'spEntityId' | i18n"
|
|
||||||
[controlValue]="spEntityId"
|
|
||||||
></app-input-text-readonly>
|
|
||||||
|
|
||||||
<app-input-text-readonly
|
|
||||||
[label]="'spMetadataUrl' | i18n"
|
|
||||||
[controlValue]="spMetadataUrl"
|
|
||||||
[showLaunch]="true"
|
|
||||||
></app-input-text-readonly>
|
|
||||||
|
|
||||||
<app-input-text-readonly
|
|
||||||
[label]="'spAcsUrl' | i18n"
|
|
||||||
[controlValue]="spAcsUrl"
|
|
||||||
></app-input-text-readonly>
|
|
||||||
|
|
||||||
<app-select
|
|
||||||
controlId="spNameIdFormat"
|
|
||||||
[label]="'spNameIdFormat' | i18n"
|
|
||||||
[selectOptions]="saml2NameIdFormatOptions"
|
|
||||||
formControlName="spNameIdFormat"
|
|
||||||
>
|
|
||||||
</app-select>
|
|
||||||
|
|
||||||
<app-select
|
|
||||||
controlId="spOutboundSigningAlgorithm"
|
|
||||||
[label]="'spOutboundSigningAlgorithm' | i18n"
|
|
||||||
[selectOptions]="samlSigningAlgorithmOptions"
|
|
||||||
formControlName="spOutboundSigningAlgorithm"
|
|
||||||
>
|
|
||||||
</app-select>
|
|
||||||
|
|
||||||
<app-select
|
|
||||||
controlId="spSigningBehavior"
|
|
||||||
[label]="'spSigningBehavior' | i18n"
|
|
||||||
[selectOptions]="saml2SigningBehaviourOptions"
|
|
||||||
formControlName="spSigningBehavior"
|
|
||||||
>
|
|
||||||
</app-select>
|
|
||||||
|
|
||||||
<app-select
|
|
||||||
controlId="spMinIncomingSigningAlgorithm"
|
|
||||||
[label]="'spMinIncomingSigningAlgorithm' | i18n"
|
|
||||||
[selectOptions]="samlSigningAlgorithmOptions"
|
|
||||||
formControlName="spMinIncomingSigningAlgorithm"
|
|
||||||
>
|
|
||||||
</app-select>
|
|
||||||
|
|
||||||
<app-input-checkbox
|
|
||||||
controlId="spWantAssertionsSigned"
|
|
||||||
formControlName="spWantAssertionsSigned"
|
|
||||||
[label]="'spWantAssertionsSigned' | i18n"
|
|
||||||
></app-input-checkbox>
|
|
||||||
|
|
||||||
<app-input-checkbox
|
|
||||||
controlId="spValidateCertificates"
|
|
||||||
formControlName="spValidateCertificates"
|
|
||||||
[label]="'spValidateCertificates' | i18n"
|
|
||||||
></app-input-checkbox>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- SAML2 IDP -->
|
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||||
<div class="config-section">
|
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
<h2 class="secondary-header">{{ "samlIdpConfig" | i18n }}</h2>
|
<span>{{'save' | i18n}}</span>
|
||||||
|
</button>
|
||||||
<app-input-text
|
|
||||||
[label]="'idpEntityId' | i18n"
|
|
||||||
controlId="idpEntityId"
|
|
||||||
formControlName="idpEntityId"
|
|
||||||
></app-input-text>
|
|
||||||
|
|
||||||
<app-select
|
|
||||||
controlId="idpBindingType"
|
|
||||||
[label]="'idpBindingType' | i18n"
|
|
||||||
[selectOptions]="saml2BindingTypeOptions"
|
|
||||||
formControlName="idpBindingType"
|
|
||||||
>
|
|
||||||
</app-select>
|
|
||||||
|
|
||||||
<app-input-text
|
|
||||||
[label]="'idpSingleSignOnServiceUrl' | i18n"
|
|
||||||
controlId="idpSingleSignOnServiceUrl"
|
|
||||||
[helperText]="'idpSingleSignOnServiceUrlRequired' | i18n"
|
|
||||||
[stripSpaces]="true"
|
|
||||||
formControlName="idpSingleSignOnServiceUrl"
|
|
||||||
></app-input-text>
|
|
||||||
|
|
||||||
<app-input-text
|
|
||||||
[label]="'idpSingleLogoutServiceUrl' | i18n"
|
|
||||||
controlId="idpSingleLogoutServiceUrl"
|
|
||||||
[stripSpaces]="true"
|
|
||||||
formControlName="idpSingleLogoutServiceUrl"
|
|
||||||
></app-input-text>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="idpX509PublicCert">
|
|
||||||
{{ "idpX509PublicCert" | i18n }}
|
|
||||||
<small class="text-muted form-text d-inline">({{ "required" | i18n }})</small>
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
formControlName="idpX509PublicCert"
|
|
||||||
class="form-control form-control-sm text-monospace"
|
|
||||||
rows="6"
|
|
||||||
id="idpX509PublicCert"
|
|
||||||
appA11yInvalid
|
|
||||||
aria-describedby="idpX509PublicCertDesc"
|
|
||||||
></textarea>
|
|
||||||
<small
|
|
||||||
id="idpX509PublicCertDesc"
|
|
||||||
class="error-inline"
|
|
||||||
role="alert"
|
|
||||||
*ngIf="samlForm.get('idpX509PublicCert').hasError('required')"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
|
|
||||||
<span class="sr-only">{{ "error" | i18n }}:</span>
|
|
||||||
{{ "fieldRequiredError" | i18n: ("idpX509PublicCert" | i18n) }}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<app-select
|
|
||||||
controlId="idpOutboundSigningAlgorithm"
|
|
||||||
[label]="'idpOutboundSigningAlgorithm' | i18n"
|
|
||||||
[selectOptions]="samlSigningAlgorithmOptions"
|
|
||||||
formControlName="idpOutboundSigningAlgorithm"
|
|
||||||
>
|
|
||||||
</app-select>
|
|
||||||
|
|
||||||
<!--TODO: Uncomment once Unsolicited IdP Response is supported-->
|
|
||||||
<!-- <app-input-checkbox
|
|
||||||
controlId="idpAllowUnsolicitedAuthnResponse"
|
|
||||||
formControlName="idpAllowUnsolicitedAuthnResponse"
|
|
||||||
[label]="'idpAllowUnsolicitedAuthnResponse' | i18n"
|
|
||||||
></app-input-checkbox> -->
|
|
||||||
|
|
||||||
<app-input-checkbox
|
|
||||||
controlId="idpAllowOutboundLogoutRequests"
|
|
||||||
formControlName="idpAllowOutboundLogoutRequests"
|
|
||||||
[label]="'idpAllowOutboundLogoutRequests' | i18n"
|
|
||||||
></app-input-checkbox>
|
|
||||||
|
|
||||||
<app-input-checkbox
|
|
||||||
controlId="idpWantAuthnRequestsSigned"
|
|
||||||
formControlName="idpWantAuthnRequestsSigned"
|
|
||||||
[label]="'idpSignAuthenticationRequests' | i18n"
|
|
||||||
></app-input-checkbox>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
|
||||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
|
||||||
<span>{{ "save" | i18n }}</span>
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
id="errorSummary"
|
|
||||||
class="error-summary text-danger"
|
|
||||||
*ngIf="this.getErrorCount(ssoConfigForm) as errorCount"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
|
|
||||||
<span class="sr-only">{{ "error" | i18n }}:</span>
|
|
||||||
{{
|
|
||||||
(errorCount === 1 ? "formErrorSummarySingle" : "formErrorSummaryPlural") | i18n: errorCount
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,318 +1,121 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
|
||||||
import { AbstractControl, FormBuilder, FormGroup } from "@angular/forms";
|
|
||||||
import { ActivatedRoute } from "@angular/router";
|
|
||||||
|
|
||||||
import { SelectOptions } from "jslib-angular/interfaces/selectOptions";
|
|
||||||
import { dirtyRequired } from "jslib-angular/validators/dirty.validator";
|
|
||||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
|
||||||
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
|
||||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
|
||||||
import {
|
import {
|
||||||
OpenIdConnectRedirectBehavior,
|
Component,
|
||||||
Saml2BindingType,
|
OnInit,
|
||||||
Saml2NameIdFormat,
|
} from '@angular/core';
|
||||||
Saml2SigningBehavior,
|
import { FormBuilder } from '@angular/forms';
|
||||||
SsoType,
|
import { ActivatedRoute } from '@angular/router';
|
||||||
} from "jslib-common/enums/ssoEnums";
|
|
||||||
import { Utils } from "jslib-common/misc/utils";
|
|
||||||
import { SsoConfigApi } from "jslib-common/models/api/ssoConfigApi";
|
|
||||||
import { Organization } from "jslib-common/models/domain/organization";
|
|
||||||
import { OrganizationSsoRequest } from "jslib-common/models/request/organization/organizationSsoRequest";
|
|
||||||
import { OrganizationSsoResponse } from "jslib-common/models/response/organization/organizationSsoResponse";
|
|
||||||
import { SsoConfigView } from "jslib-common/models/view/ssoConfigView";
|
|
||||||
|
|
||||||
const defaultSigningAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
|
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||||
|
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||||
|
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||||
|
import { 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 {
|
||||||
readonly ssoType = SsoType;
|
|
||||||
|
|
||||||
readonly ssoTypeOptions: SelectOptions[] = [
|
samlSigningAlgorithms = [
|
||||||
{ name: this.i18nService.t("selectType"), value: SsoType.None, disabled: true },
|
'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
|
||||||
{ name: "OpenID Connect", value: SsoType.OpenIdConnect },
|
'http://www.w3.org/2000/09/xmldsig#rsa-sha384',
|
||||||
{ name: "SAML 2.0", value: SsoType.Saml2 },
|
'http://www.w3.org/2000/09/xmldsig#rsa-sha512',
|
||||||
];
|
'http://www.w3.org/2000/09/xmldsig#rsa-sha1',
|
||||||
|
];
|
||||||
|
|
||||||
readonly samlSigningAlgorithms = [
|
loading = true;
|
||||||
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
|
organizationId: string;
|
||||||
"http://www.w3.org/2000/09/xmldsig#rsa-sha384",
|
formPromise: Promise<any>;
|
||||||
"http://www.w3.org/2000/09/xmldsig#rsa-sha512",
|
|
||||||
"http://www.w3.org/2000/09/xmldsig#rsa-sha1",
|
|
||||||
];
|
|
||||||
|
|
||||||
readonly saml2SigningBehaviourOptions: SelectOptions[] = [
|
callbackPath: string;
|
||||||
{
|
signedOutCallbackPath: string;
|
||||||
name: "If IdP Wants Authn Requests Signed",
|
spEntityId: string;
|
||||||
value: Saml2SigningBehavior.IfIdpWantAuthnRequestsSigned,
|
spMetadataUrl: string;
|
||||||
},
|
spAcsUrl: string;
|
||||||
{ name: "Always", value: Saml2SigningBehavior.Always },
|
|
||||||
{ name: "Never", value: Saml2SigningBehavior.Never },
|
|
||||||
];
|
|
||||||
readonly saml2BindingTypeOptions: SelectOptions[] = [
|
|
||||||
{ name: "Redirect", value: Saml2BindingType.HttpRedirect },
|
|
||||||
{ name: "HTTP POST", value: Saml2BindingType.HttpPost },
|
|
||||||
];
|
|
||||||
readonly saml2NameIdFormatOptions: SelectOptions[] = [
|
|
||||||
{ name: "Not Configured", value: Saml2NameIdFormat.NotConfigured },
|
|
||||||
{ name: "Unspecified", value: Saml2NameIdFormat.Unspecified },
|
|
||||||
{ name: "Email Address", value: Saml2NameIdFormat.EmailAddress },
|
|
||||||
{ name: "X.509 Subject Name", value: Saml2NameIdFormat.X509SubjectName },
|
|
||||||
{ name: "Windows Domain Qualified Name", value: Saml2NameIdFormat.WindowsDomainQualifiedName },
|
|
||||||
{ name: "Kerberos Principal Name", value: Saml2NameIdFormat.KerberosPrincipalName },
|
|
||||||
{ name: "Entity Identifier", value: Saml2NameIdFormat.EntityIdentifier },
|
|
||||||
{ name: "Persistent", value: Saml2NameIdFormat.Persistent },
|
|
||||||
{ name: "Transient", value: Saml2NameIdFormat.Transient },
|
|
||||||
];
|
|
||||||
|
|
||||||
readonly connectRedirectOptions: SelectOptions[] = [
|
enabled = this.fb.control(false);
|
||||||
{ name: "Redirect GET", value: OpenIdConnectRedirectBehavior.RedirectGet },
|
data = this.fb.group({
|
||||||
{ name: "Form POST", value: OpenIdConnectRedirectBehavior.FormPost },
|
configType: [],
|
||||||
];
|
|
||||||
|
|
||||||
showOpenIdCustomizations = false;
|
// OpenId
|
||||||
|
authority: [],
|
||||||
|
clientId: [],
|
||||||
|
clientSecret: [],
|
||||||
|
metadataAddress: [],
|
||||||
|
redirectBehavior: [],
|
||||||
|
getClaimsFromUserInfoEndpoint: [],
|
||||||
|
additionalScopes: [],
|
||||||
|
additionalUserIdClaimTypes: [],
|
||||||
|
additionalEmailClaimTypes: [],
|
||||||
|
additionalNameClaimTypes: [],
|
||||||
|
acrValues: [],
|
||||||
|
expectedReturnAcrValue: [],
|
||||||
|
|
||||||
loading = true;
|
// SAML
|
||||||
haveTestedKeyConnector = false;
|
spNameIdFormat: [],
|
||||||
organizationId: string;
|
spOutboundSigningAlgorithm: [],
|
||||||
organization: Organization;
|
spSigningBehavior: [],
|
||||||
formPromise: Promise<any>;
|
spMinIncomingSigningAlgorithm: [],
|
||||||
|
spWantAssertionsSigned: [],
|
||||||
|
spValidateCertificates: [],
|
||||||
|
|
||||||
callbackPath: string;
|
idpEntityId: [],
|
||||||
signedOutCallbackPath: string;
|
idpBindingType: [],
|
||||||
spEntityId: string;
|
idpSingleSignOnServiceUrl: [],
|
||||||
spMetadataUrl: string;
|
idpSingleLogoutServiceUrl: [],
|
||||||
spAcsUrl: string;
|
idpArtifactResolutionServiceUrl: [],
|
||||||
|
idpX509PublicCert: [],
|
||||||
enabled = this.formBuilder.control(false);
|
idpOutboundSigningAlgorithm: [],
|
||||||
|
idpAllowUnsolicitedAuthnResponse: [],
|
||||||
openIdForm = this.formBuilder.group(
|
idpDisableOutboundLogoutRequests: [],
|
||||||
{
|
idpWantAuthnRequestsSigned: [],
|
||||||
authority: ["", dirtyRequired],
|
|
||||||
clientId: ["", dirtyRequired],
|
|
||||||
clientSecret: ["", dirtyRequired],
|
|
||||||
metadataAddress: [],
|
|
||||||
redirectBehavior: [OpenIdConnectRedirectBehavior.RedirectGet, dirtyRequired],
|
|
||||||
getClaimsFromUserInfoEndpoint: [],
|
|
||||||
additionalScopes: [],
|
|
||||||
additionalUserIdClaimTypes: [],
|
|
||||||
additionalEmailClaimTypes: [],
|
|
||||||
additionalNameClaimTypes: [],
|
|
||||||
acrValues: [],
|
|
||||||
expectedReturnAcrValue: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
updateOn: "blur",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
samlForm = this.formBuilder.group(
|
|
||||||
{
|
|
||||||
spNameIdFormat: [Saml2NameIdFormat.NotConfigured],
|
|
||||||
spOutboundSigningAlgorithm: [defaultSigningAlgorithm],
|
|
||||||
spSigningBehavior: [Saml2SigningBehavior.IfIdpWantAuthnRequestsSigned],
|
|
||||||
spMinIncomingSigningAlgorithm: [defaultSigningAlgorithm],
|
|
||||||
spWantAssertionsSigned: [],
|
|
||||||
spValidateCertificates: [],
|
|
||||||
|
|
||||||
idpEntityId: ["", dirtyRequired],
|
|
||||||
idpBindingType: [Saml2BindingType.HttpRedirect],
|
|
||||||
idpSingleSignOnServiceUrl: [],
|
|
||||||
idpSingleLogoutServiceUrl: [],
|
|
||||||
idpX509PublicCert: ["", dirtyRequired],
|
|
||||||
idpOutboundSigningAlgorithm: [defaultSigningAlgorithm],
|
|
||||||
idpAllowUnsolicitedAuthnResponse: [],
|
|
||||||
idpAllowOutboundLogoutRequests: [true],
|
|
||||||
idpWantAuthnRequestsSigned: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
updateOn: "blur",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
ssoConfigForm = this.formBuilder.group({
|
|
||||||
configType: [SsoType.None],
|
|
||||||
keyConnectorEnabled: [false],
|
|
||||||
keyConnectorUrl: [""],
|
|
||||||
openId: this.openIdForm,
|
|
||||||
saml: this.samlForm,
|
|
||||||
});
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private formBuilder: FormBuilder,
|
|
||||||
private route: ActivatedRoute,
|
|
||||||
private apiService: ApiService,
|
|
||||||
private platformUtilsService: PlatformUtilsService,
|
|
||||||
private i18nService: I18nService,
|
|
||||||
private organizationService: OrganizationService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async ngOnInit() {
|
|
||||||
this.ssoConfigForm.get("configType").valueChanges.subscribe((newType: SsoType) => {
|
|
||||||
if (newType === SsoType.OpenIdConnect) {
|
|
||||||
this.openIdForm.enable();
|
|
||||||
this.samlForm.disable();
|
|
||||||
} else if (newType === SsoType.Saml2) {
|
|
||||||
this.openIdForm.disable();
|
|
||||||
this.samlForm.enable();
|
|
||||||
} else {
|
|
||||||
this.openIdForm.disable();
|
|
||||||
this.samlForm.disable();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.samlForm
|
constructor(private fb: FormBuilder, private route: ActivatedRoute, private apiService: ApiService,
|
||||||
.get("spSigningBehavior")
|
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService) { }
|
||||||
.valueChanges.subscribe(() =>
|
|
||||||
this.samlForm.get("idpX509PublicCert").updateValueAndValidity()
|
|
||||||
);
|
|
||||||
|
|
||||||
this.route.parent.parent.params.subscribe(async (params) => {
|
async ngOnInit() {
|
||||||
this.organizationId = params.organizationId;
|
this.route.parent.parent.params.subscribe(async params => {
|
||||||
await this.load();
|
this.organizationId = params.organizationId;
|
||||||
});
|
await this.load();
|
||||||
}
|
});
|
||||||
|
|
||||||
async load() {
|
|
||||||
this.organization = await this.organizationService.get(this.organizationId);
|
|
||||||
const ssoSettings = await this.apiService.getOrganizationSso(this.organizationId);
|
|
||||||
this.populateForm(ssoSettings);
|
|
||||||
|
|
||||||
this.callbackPath = ssoSettings.urls.callbackPath;
|
|
||||||
this.signedOutCallbackPath = ssoSettings.urls.signedOutCallbackPath;
|
|
||||||
this.spEntityId = ssoSettings.urls.spEntityId;
|
|
||||||
this.spMetadataUrl = ssoSettings.urls.spMetadataUrl;
|
|
||||||
this.spAcsUrl = ssoSettings.urls.spAcsUrl;
|
|
||||||
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async submit() {
|
|
||||||
this.validateForm(this.ssoConfigForm);
|
|
||||||
|
|
||||||
if (this.ssoConfigForm.get("keyConnectorEnabled").value) {
|
|
||||||
await this.validateKeyConnectorUrl();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.ssoConfigForm.valid) {
|
async load() {
|
||||||
this.readOutErrors();
|
const ssoSettings = await this.apiService.getOrganizationSso(this.organizationId);
|
||||||
return;
|
|
||||||
|
this.data.patchValue(ssoSettings.data);
|
||||||
|
this.enabled.setValue(ssoSettings.enabled);
|
||||||
|
|
||||||
|
this.callbackPath = ssoSettings.urls.callbackPath;
|
||||||
|
this.signedOutCallbackPath = ssoSettings.urls.signedOutCallbackPath;
|
||||||
|
this.spEntityId = ssoSettings.urls.spEntityId;
|
||||||
|
this.spMetadataUrl = ssoSettings.urls.spMetadataUrl;
|
||||||
|
this.spAcsUrl = ssoSettings.urls.spAcsUrl;
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = new OrganizationSsoRequest();
|
copy(value: string) {
|
||||||
request.enabled = this.enabled.value;
|
this.platformUtilsService.copyToClipboard(value);
|
||||||
request.data = SsoConfigApi.fromView(this.ssoConfigForm.value as SsoConfigView);
|
|
||||||
|
|
||||||
this.formPromise = this.apiService.postOrganizationSso(this.organizationId, request);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await this.formPromise;
|
|
||||||
this.populateForm(response);
|
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("ssoSettingsSaved"));
|
|
||||||
} catch {
|
|
||||||
// Logged by appApiAction, do nothing
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.formPromise = null;
|
launchUri(url: string) {
|
||||||
}
|
this.platformUtilsService.launchUri(url);
|
||||||
|
|
||||||
async validateKeyConnectorUrl() {
|
|
||||||
if (this.haveTestedKeyConnector) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.keyConnectorUrl.markAsPending();
|
async submit() {
|
||||||
|
const request = new OrganizationSsoRequest();
|
||||||
|
request.enabled = this.enabled.value;
|
||||||
|
request.data = this.data.value;
|
||||||
|
|
||||||
try {
|
this.formPromise = this.apiService.postOrganizationSso(this.organizationId, request);
|
||||||
await this.apiService.getKeyConnectorAlive(this.keyConnectorUrl.value);
|
|
||||||
this.keyConnectorUrl.updateValueAndValidity();
|
const response = await this.formPromise;
|
||||||
} catch {
|
this.data.patchValue(response.data);
|
||||||
this.keyConnectorUrl.setErrors({
|
this.enabled.setValue(response.enabled);
|
||||||
invalidUrl: true,
|
|
||||||
});
|
this.formPromise = null;
|
||||||
|
this.platformUtilsService.showToast('success', null, this.i18nService.t('ssoSettingsSaved'));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.haveTestedKeyConnector = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleOpenIdCustomizations() {
|
|
||||||
this.showOpenIdCustomizations = !this.showOpenIdCustomizations;
|
|
||||||
}
|
|
||||||
|
|
||||||
getErrorCount(form: FormGroup): number {
|
|
||||||
return Object.values(form.controls).reduce((acc: number, control: AbstractControl) => {
|
|
||||||
if (control instanceof FormGroup) {
|
|
||||||
return acc + this.getErrorCount(control);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (control.errors == null) {
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
return acc + Object.keys(control.errors).length;
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
get enableTestKeyConnector() {
|
|
||||||
return (
|
|
||||||
this.ssoConfigForm.get("keyConnectorEnabled").value &&
|
|
||||||
!Utils.isNullOrWhitespace(this.keyConnectorUrl?.value)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get keyConnectorUrl() {
|
|
||||||
return this.ssoConfigForm.get("keyConnectorUrl");
|
|
||||||
}
|
|
||||||
|
|
||||||
get samlSigningAlgorithmOptions(): SelectOptions[] {
|
|
||||||
return this.samlSigningAlgorithms.map((algorithm) => ({ name: algorithm, value: algorithm }));
|
|
||||||
}
|
|
||||||
|
|
||||||
private validateForm(form: FormGroup) {
|
|
||||||
Object.values(form.controls).forEach((control: AbstractControl) => {
|
|
||||||
if (control.disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (control instanceof FormGroup) {
|
|
||||||
this.validateForm(control);
|
|
||||||
} else {
|
|
||||||
control.markAsDirty();
|
|
||||||
control.markAsTouched();
|
|
||||||
control.updateValueAndValidity();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private populateForm(ssoSettings: OrganizationSsoResponse) {
|
|
||||||
this.enabled.setValue(ssoSettings.enabled);
|
|
||||||
if (ssoSettings.data != null) {
|
|
||||||
const ssoConfigView = new SsoConfigView(ssoSettings.data);
|
|
||||||
this.ssoConfigForm.patchValue(ssoConfigView);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readOutErrors() {
|
|
||||||
const errorText = this.i18nService.t("error");
|
|
||||||
const errorCount = this.getErrorCount(this.ssoConfigForm);
|
|
||||||
const errorCountText = this.i18nService.t(
|
|
||||||
errorCount === 1 ? "formErrorSummarySingle" : "formErrorSummaryPlural",
|
|
||||||
errorCount.toString()
|
|
||||||
);
|
|
||||||
|
|
||||||
const div = document.createElement("div");
|
|
||||||
div.className = "sr-only";
|
|
||||||
div.id = "srErrorCount";
|
|
||||||
div.setAttribute("aria-live", "polite");
|
|
||||||
div.innerText = errorText + ": " + errorCountText;
|
|
||||||
|
|
||||||
const existing = document.getElementById("srErrorCount");
|
|
||||||
if (existing != null) {
|
|
||||||
existing.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.append(div);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +1,54 @@
|
|||||||
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 { OrganizationLayoutComponent } from "src/app/layouts/organization-layout.component";
|
import { Permissions } from 'jslib-common/enums/permissions';
|
||||||
import { ManageComponent } from "src/app/organizations/manage/manage.component";
|
|
||||||
import { OrganizationGuardService } from "src/app/services/organization-guard.service";
|
|
||||||
import { OrganizationTypeGuardService } from "src/app/services/organization-type-guard.service";
|
|
||||||
|
|
||||||
import { SsoComponent } from "./manage/sso.component";
|
import { OrganizationLayoutComponent } from 'src/app/layouts/organization-layout.component';
|
||||||
|
import { ManageComponent } from 'src/app/organizations/manage/manage.component';
|
||||||
|
import { OrganizationGuardService } from 'src/app/services/organization-guard.service';
|
||||||
|
import { OrganizationTypeGuardService } from 'src/app/services/organization-type-guard.service';
|
||||||
|
|
||||||
|
import { SsoComponent } from './manage/sso.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: "organizations/:organizationId",
|
path: 'organizations/:organizationId',
|
||||||
component: OrganizationLayoutComponent,
|
component: OrganizationLayoutComponent,
|
||||||
canActivate: [AuthGuardService, OrganizationGuardService],
|
canActivate: [AuthGuardService, OrganizationGuardService],
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "manage",
|
|
||||||
component: ManageComponent,
|
|
||||||
canActivate: [OrganizationTypeGuardService],
|
|
||||||
data: {
|
|
||||||
permissions: [
|
|
||||||
Permissions.CreateNewCollections,
|
|
||||||
Permissions.EditAnyCollection,
|
|
||||||
Permissions.DeleteAnyCollection,
|
|
||||||
Permissions.EditAssignedCollections,
|
|
||||||
Permissions.DeleteAssignedCollections,
|
|
||||||
Permissions.AccessEventLogs,
|
|
||||||
Permissions.ManageGroups,
|
|
||||||
Permissions.ManageUsers,
|
|
||||||
Permissions.ManagePolicies,
|
|
||||||
Permissions.ManageSso,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "sso",
|
path: 'manage',
|
||||||
component: SsoComponent,
|
component: ManageComponent,
|
||||||
},
|
canActivate: [OrganizationTypeGuardService],
|
||||||
|
data: {
|
||||||
|
permissions: [
|
||||||
|
Permissions.CreateNewCollections,
|
||||||
|
Permissions.EditAnyCollection,
|
||||||
|
Permissions.DeleteAnyCollection,
|
||||||
|
Permissions.EditAssignedCollections,
|
||||||
|
Permissions.DeleteAssignedCollections,
|
||||||
|
Permissions.AccessEventLogs,
|
||||||
|
Permissions.ManageGroups,
|
||||||
|
Permissions.ManageUsers,
|
||||||
|
Permissions.ManagePolicies,
|
||||||
|
Permissions.ManageSso,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'sso',
|
||||||
|
component: SsoComponent,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forChild(routes)],
|
imports: [RouterModule.forChild(routes)],
|
||||||
exports: [RouterModule],
|
exports: [RouterModule],
|
||||||
})
|
})
|
||||||
export class OrganizationsRoutingModule {}
|
export class OrganizationsRoutingModule { }
|
||||||
|
|||||||
@@ -1,32 +1,22 @@
|
|||||||
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 { JslibModule } from "jslib-angular/jslib.module";
|
import { OssModule } from 'src/app/oss.module';
|
||||||
|
|
||||||
import { InputCheckboxComponent } from "./components/input-checkbox.component";
|
import { SsoComponent } from './manage/sso.component';
|
||||||
import { InputTextReadOnlyComponent } from "./components/input-text-readonly.component";
|
import { OrganizationsRoutingModule } from './organizations-routing.module';
|
||||||
import { InputTextComponent } from "./components/input-text.component";
|
|
||||||
import { SelectComponent } from "./components/select.component";
|
|
||||||
import { SsoComponent } from "./manage/sso.component";
|
|
||||||
import { OrganizationsRoutingModule } from "./organizations-routing.module";
|
|
||||||
|
|
||||||
// Form components are for use in the SSO Configuration Form only and should not be exported for use elsewhere.
|
|
||||||
// They will be deprecated by the Component Library.
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
JslibModule,
|
OssModule,
|
||||||
OrganizationsRoutingModule,
|
OrganizationsRoutingModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
InputCheckboxComponent,
|
SsoComponent,
|
||||||
InputTextComponent,
|
],
|
||||||
InputTextReadOnlyComponent,
|
|
||||||
SelectComponent,
|
|
||||||
SsoComponent,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class OrganizationsModule {}
|
export class OrganizationsModule {}
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input class="form-check-input" type="checkbox" id="enabled" [formControl]="enabled" name="Enabled">
|
||||||
class="form-check-input"
|
<label class="form-check-label" for="enabled">{{'enabled' | i18n}}</label>
|
||||||
type="checkbox"
|
</div>
|
||||||
id="enabled"
|
|
||||||
[formControl]="enabled"
|
|
||||||
name="Enabled"
|
|
||||||
/>
|
|
||||||
<label class="form-check-label" for="enabled">{{ "enabled" | i18n }}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from '@angular/core';
|
||||||
|
import { FormBuilder } from '@angular/forms';
|
||||||
|
|
||||||
import { PolicyType } from "jslib-common/enums/policyType";
|
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||||
|
|
||||||
import {
|
import { PolicyType } from 'jslib-common/enums/policyType';
|
||||||
BasePolicy,
|
|
||||||
BasePolicyComponent,
|
import { PolicyRequest } from 'jslib-common/models/request/policyRequest';
|
||||||
} 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,47 +1,27 @@
|
|||||||
<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
|
<input class="form-check-input" type="checkbox" id="enabled" [formControl]="enabled" name="Enabled">
|
||||||
class="form-check-input"
|
<label class="form-check-label" for="enabled">{{'enabled' | i18n}}</label>
|
||||||
type="checkbox"
|
</div>
|
||||||
id="enabled"
|
|
||||||
[formControl]="enabled"
|
|
||||||
name="Enabled"
|
|
||||||
/>
|
|
||||||
<label class="form-check-label" for="enabled">{{ "enabled" | i18n }}</label>
|
|
||||||
</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
|
<input id="hours" class="form-control" type="number" min="0" name="hours" formControlName="hours">
|
||||||
id="hours"
|
<small>{{'hours' | i18n }}</small>
|
||||||
class="form-control"
|
</div>
|
||||||
type="number"
|
<div class="col-6">
|
||||||
min="0"
|
<input id="minutes" class="form-control" type="number" min="0" max="59" name="minutes"
|
||||||
name="hours"
|
formControlName="minutes">
|
||||||
formControlName="hours"
|
<small>{{'minutes' | i18n }}</small>
|
||||||
/>
|
</div>
|
||||||
<small>{{ "hours" | i18n }}</small>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="col-6">
|
|
||||||
<input
|
|
||||||
id="minutes"
|
|
||||||
class="form-control"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="59"
|
|
||||||
name="minutes"
|
|
||||||
formControlName="minutes"
|
|
||||||
/>
|
|
||||||
<small>{{ "minutes" | i18n }}</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,70 +1,70 @@
|
|||||||
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 { PolicyRequest } from "jslib-common/models/request/policyRequest";
|
|
||||||
|
|
||||||
import {
|
import { PolicyType } from 'jslib-common/enums/policyType';
|
||||||
BasePolicy,
|
|
||||||
BasePolicyComponent,
|
import { PolicyRequest } from 'jslib-common/models/request/policyRequest';
|
||||||
} 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({
|
|
||||||
hours: [null],
|
|
||||||
minutes: [null],
|
|
||||||
});
|
|
||||||
|
|
||||||
constructor(private formBuilder: FormBuilder, private i18nService: I18nService) {
|
data = this.fb.group({
|
||||||
super();
|
hours: [null],
|
||||||
}
|
minutes: [null],
|
||||||
|
|
||||||
loadData() {
|
|
||||||
const minutes = this.policyResponse.data?.minutes;
|
|
||||||
|
|
||||||
if (minutes == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.data.patchValue({
|
|
||||||
hours: Math.floor(minutes / 60),
|
|
||||||
minutes: minutes % 60,
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
buildRequestData() {
|
constructor(private fb: FormBuilder, private i18nService: I18nService) {
|
||||||
if (this.data.value.hours == null && this.data.value.minutes == null) {
|
super();
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
loadData() {
|
||||||
minutes: this.data.value.hours * 60 + this.data.value.minutes,
|
const minutes = this.policyResponse.data?.minutes;
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
buildRequest(policiesEnabledMap: Map<PolicyType, boolean>): Promise<PolicyRequest> {
|
if (minutes == null) {
|
||||||
const singleOrgEnabled = policiesEnabledMap.get(PolicyType.SingleOrg) ?? false;
|
return;
|
||||||
if (this.enabled.value && !singleOrgEnabled) {
|
}
|
||||||
throw new Error(this.i18nService.t("requireSsoPolicyReqError"));
|
|
||||||
|
this.data.patchValue({
|
||||||
|
hours: Math.floor(minutes / 60),
|
||||||
|
minutes: minutes % 60,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = this.buildRequestData();
|
buildRequestData() {
|
||||||
if (data?.minutes == null || data?.minutes <= 0) {
|
if (this.data.value.hours == null && this.data.value.minutes == null) {
|
||||||
throw new Error(this.i18nService.t("invalidMaximumVaultTimeout"));
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
minutes: this.data.value.hours * 60 + this.data.value.minutes,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.buildRequest(policiesEnabledMap);
|
buildRequest(policiesEnabledMap: Map<PolicyType, boolean>): Promise<PolicyRequest> {
|
||||||
}
|
const singleOrgEnabled = policiesEnabledMap.get(PolicyType.SingleOrg) ?? false;
|
||||||
|
if (this.enabled.value && !singleOrgEnabled) {
|
||||||
|
throw new Error(this.i18nService.t('requireSsoPolicyReqError'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = this.buildRequestData();
|
||||||
|
if (data?.minutes == null || data?.minutes <= 0) {
|
||||||
|
throw new Error(this.i18nService.t('invalidMaximumVaultTimeout'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.buildRequest(policiesEnabledMap);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,35 @@
|
|||||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="addTitle">
|
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="addTitle">
|
||||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
<div class="modal-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
|
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||||
type="button"
|
<span aria-hidden="true">×</span>
|
||||||
class="close"
|
|
||||||
data-dismiss="modal"
|
|
||||||
appA11yTitle="{{ 'close' | i18n }}"
|
|
||||||
>
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="card-body text-center" *ngIf="loading">
|
|
||||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
|
||||||
{{ "loading" | i18n }}
|
|
||||||
</div>
|
|
||||||
<ng-container *ngIf="!loading">
|
|
||||||
<table class="table table-hover table-list">
|
|
||||||
<tr *ngFor="let o of organizations">
|
|
||||||
<td width="30">
|
|
||||||
<app-avatar [data]="o.name" size="25" [circle]="true" [fontSize]="14"></app-avatar>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ o.name }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button
|
|
||||||
class="btn btn-outline-secondary pull-right"
|
|
||||||
(click)="add(o)"
|
|
||||||
[disabled]="formPromise"
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
<div class="modal-body">
|
||||||
</table>
|
<div class="card-body text-center" *ngIf="loading">
|
||||||
</ng-container>
|
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
</div>
|
{{'loading' | i18n}}
|
||||||
|
</div>
|
||||||
|
<ng-container *ngIf="!loading">
|
||||||
|
<table class="table table-hover table-list">
|
||||||
|
<tr *ngFor="let o of organizations">
|
||||||
|
<td width="30">
|
||||||
|
<app-avatar [data]="o.name" size="25" [circle]="true" [fontSize]="14"></app-avatar>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{o.name}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-outline-secondary pull-right" (click)="add(o)" [disabled]="formPromise">Add</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,84 +1,83 @@
|
|||||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
import {
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
OnInit,
|
||||||
|
Output
|
||||||
|
} from '@angular/core';
|
||||||
|
import { ToasterService } from 'angular2-toaster';
|
||||||
|
|
||||||
import { ValidationService } from "jslib-angular/services/validation.service";
|
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
import { 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 { Organization } from "jslib-common/models/domain/organization";
|
|
||||||
import { Provider } from "jslib-common/models/domain/provider";
|
|
||||||
|
|
||||||
import { WebProviderService } from "../services/webProvider.service";
|
import { ValidationService } from 'jslib-angular/services/validation.service';
|
||||||
|
|
||||||
|
import { ProviderService } from '../services/provider.service';
|
||||||
|
|
||||||
|
import { Organization } from 'jslib-common/models/domain/organization';
|
||||||
|
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() organizations: Organization[];
|
|
||||||
@Output() onAddedOrganization = new EventEmitter();
|
|
||||||
|
|
||||||
provider: Provider;
|
@Input() providerId: string;
|
||||||
formPromise: Promise<any>;
|
@Input() organizations: Organization[];
|
||||||
loading = true;
|
@Output() onAddedOrganization = new EventEmitter();
|
||||||
|
|
||||||
constructor(
|
provider: Provider;
|
||||||
private providerService: ProviderService,
|
formPromise: Promise<any>;
|
||||||
private webProviderService: WebProviderService,
|
loading = true;
|
||||||
private i18nService: I18nService,
|
|
||||||
private platformUtilsService: PlatformUtilsService,
|
|
||||||
private validationService: ValidationService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async ngOnInit() {
|
constructor(private userService: UserService, private providerService: ProviderService,
|
||||||
await this.load();
|
private toasterService: ToasterService, private i18nService: I18nService,
|
||||||
}
|
private platformUtilsService: PlatformUtilsService, private validationService: ValidationService,
|
||||||
|
private apiService: ApiService) { }
|
||||||
|
|
||||||
async load() {
|
async ngOnInit() {
|
||||||
if (this.providerId == null) {
|
await this.load();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.provider = await this.providerService.get(this.providerId);
|
async load() {
|
||||||
|
if (this.providerId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.loading = false;
|
this.provider = await this.userService.getProvider(this.providerId);
|
||||||
}
|
|
||||||
|
|
||||||
async add(organization: Organization) {
|
this.loading = false;
|
||||||
if (this.formPromise) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmed = await this.platformUtilsService.showDialog(
|
async add(organization: Organization) {
|
||||||
this.i18nService.t("addOrganizationConfirmation", organization.name, this.provider.name),
|
if (this.formPromise) {
|
||||||
organization.name,
|
return;
|
||||||
this.i18nService.t("yes"),
|
}
|
||||||
this.i18nService.t("no"),
|
|
||||||
"warning"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!confirmed) {
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
return false;
|
this.i18nService.t('addOrganizationConfirmation', organization.name, this.provider.name), organization.name,
|
||||||
|
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.formPromise = this.providerService.addOrganizationToProvider(this.providerId, organization.id);
|
||||||
|
await this.formPromise;
|
||||||
|
} catch (e) {
|
||||||
|
this.validationService.showError(e);
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
this.formPromise = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toasterService.popAsync('success', null, this.i18nService.t('organizationJoinedProvider'));
|
||||||
|
this.onAddedOrganization.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
this.formPromise = this.webProviderService.addOrganizationToProvider(
|
|
||||||
this.providerId,
|
|
||||||
organization.id
|
|
||||||
);
|
|
||||||
await this.formPromise;
|
|
||||||
} catch (e) {
|
|
||||||
this.validationService.showError(e);
|
|
||||||
return;
|
|
||||||
} finally {
|
|
||||||
this.formPromise = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.platformUtilsService.showToast(
|
|
||||||
"success",
|
|
||||||
null,
|
|
||||||
this.i18nService.t("organizationJoinedProvider")
|
|
||||||
);
|
|
||||||
this.onAddedOrganization.emit();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,90 +1,62 @@
|
|||||||
<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
|
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
|
||||||
type="search"
|
[(ngModel)]="searchText">
|
||||||
class="form-control form-control-sm"
|
</div>
|
||||||
id="search"
|
<a class="btn btn-sm btn-outline-primary ml-3" routerLink="create" *ngIf="manageOrganizations">
|
||||||
placeholder="{{ 'search' | i18n }}"
|
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
|
||||||
[(ngModel)]="searchText"
|
{{'newClientOrganization' | i18n}}
|
||||||
/>
|
</a>
|
||||||
|
<button class="btn btn-sm btn-outline-primary ml-3" (click)="addExistingOrganization()"
|
||||||
|
*ngIf="manageOrganizations && showAddExisting">
|
||||||
|
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
|
||||||
|
{{'addExistingOrganization' | i18n}}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-sm btn-outline-primary ml-3" routerLink="create" *ngIf="manageOrganizations">
|
|
||||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
|
||||||
{{ "newClientOrganization" | i18n }}
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
class="btn btn-sm btn-outline-primary ml-3"
|
|
||||||
(click)="addExistingOrganization()"
|
|
||||||
*ngIf="manageOrganizations && showAddExisting"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
|
||||||
{{ "addExistingOrganization" | i18n }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="loading">
|
<ng-container *ngIf="loading">
|
||||||
<i
|
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
class="bwi bwi-spinner bwi-spin text-muted"
|
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||||
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"
|
<tbody>
|
||||||
infiniteScroll
|
<tr *ngFor="let o of searchedClients">
|
||||||
[infiniteScrollDistance]="1"
|
<td width="30">
|
||||||
[infiniteScrollDisabled]="!isPaging()"
|
<app-avatar [data]="o.organizationName" size="25" [circle]="true" [fontSize]="14"></app-avatar>
|
||||||
(scrolled)="loadMore()"
|
</td>
|
||||||
>
|
<td>
|
||||||
<tbody>
|
<a [routerLink]="['/organizations', o.organizationId]">{{o.organizationName}}</a>
|
||||||
<tr *ngFor="let o of searchedClients">
|
</td>
|
||||||
<td width="30">
|
<td class="table-list-options" *ngIf="manageOrganizations">
|
||||||
<app-avatar
|
<div class="dropdown" appListDropdown>
|
||||||
[data]="o.organizationName"
|
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
|
||||||
size="25"
|
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
|
||||||
[circle]="true"
|
appA11yTitle="{{'options' | i18n}}">
|
||||||
[fontSize]="14"
|
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
|
||||||
></app-avatar>
|
</button>
|
||||||
</td>
|
<div class="dropdown-menu dropdown-menu-right">
|
||||||
<td>
|
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(o)">
|
||||||
<a [routerLink]="['/organizations', o.organizationId]">{{ o.organizationName }}</a>
|
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
|
||||||
</td>
|
{{'remove' | i18n}}
|
||||||
<td class="table-list-options" *ngIf="manageOrganizations">
|
</a>
|
||||||
<div class="dropdown" appListDropdown>
|
</div>
|
||||||
<button
|
</div>
|
||||||
class="btn btn-outline-secondary dropdown-toggle"
|
</td>
|
||||||
type="button"
|
</tr>
|
||||||
data-toggle="dropdown"
|
</tbody>
|
||||||
aria-haspopup="true"
|
</table>
|
||||||
aria-expanded="false"
|
</ng-container>
|
||||||
appA11yTitle="{{ 'options' | i18n }}"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<div class="dropdown-menu dropdown-menu-right">
|
|
||||||
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(o)">
|
|
||||||
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
|
||||||
{{ "remove" | i18n }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-template #add></ng-template>
|
<ng-template #add></ng-template>
|
||||||
|
|||||||
@@ -1,179 +1,159 @@
|
|||||||
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
import {
|
||||||
import { ActivatedRoute } from "@angular/router";
|
Component,
|
||||||
import { first } from "rxjs/operators";
|
OnInit,
|
||||||
|
ViewChild,
|
||||||
|
ViewContainerRef
|
||||||
|
} from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { ToasterService } from 'angular2-toaster';
|
||||||
|
|
||||||
import { ModalService } from "jslib-angular/services/modal.service";
|
import { first } from 'rxjs/operators';
|
||||||
import { ValidationService } from "jslib-angular/services/validation.service";
|
|
||||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
|
||||||
import { LogService } from "jslib-common/abstractions/log.service";
|
|
||||||
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
|
||||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
|
||||||
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
|
||||||
import { SearchService } from "jslib-common/abstractions/search.service";
|
|
||||||
import { PlanType } from "jslib-common/enums/planType";
|
|
||||||
import { ProviderUserType } from "jslib-common/enums/providerUserType";
|
|
||||||
import { Organization } from "jslib-common/models/domain/organization";
|
|
||||||
import { ProviderOrganizationOrganizationDetailsResponse } from "jslib-common/models/response/provider/providerOrganizationResponse";
|
|
||||||
|
|
||||||
import { WebProviderService } from "../services/webProvider.service";
|
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||||
|
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||||
|
import { LogService } from 'jslib-common/abstractions/log.service';
|
||||||
|
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||||
|
import { SearchService } from 'jslib-common/abstractions/search.service';
|
||||||
|
import { UserService } from 'jslib-common/abstractions/user.service';
|
||||||
|
|
||||||
import { AddOrganizationComponent } from "./add-organization.component";
|
import { ModalService } from 'jslib-angular/services/modal.service';
|
||||||
|
import { ValidationService } from 'jslib-angular/services/validation.service';
|
||||||
|
|
||||||
const DisallowedPlanTypes = [
|
import { PlanType } from 'jslib-common/enums/planType';
|
||||||
PlanType.Free,
|
import { ProviderUserType } from 'jslib-common/enums/providerUserType';
|
||||||
PlanType.FamiliesAnnually2019,
|
|
||||||
PlanType.FamiliesAnnually,
|
import { Organization } from 'jslib-common/models/domain/organization';
|
||||||
];
|
import {
|
||||||
|
ProviderOrganizationOrganizationDetailsResponse
|
||||||
|
} from 'jslib-common/models/response/provider/providerOrganizationResponse';
|
||||||
|
|
||||||
|
import { ProviderService } from '../services/provider.service';
|
||||||
|
|
||||||
|
import { AddOrganizationComponent } from './add-organization.component';
|
||||||
|
|
||||||
|
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;
|
|
||||||
|
|
||||||
providerId: any;
|
@ViewChild('add', { read: ViewContainerRef, static: true }) addModalRef: ViewContainerRef;
|
||||||
searchText: string;
|
|
||||||
addableOrganizations: Organization[];
|
|
||||||
loading = true;
|
|
||||||
manageOrganizations = false;
|
|
||||||
showAddExisting = false;
|
|
||||||
|
|
||||||
clients: ProviderOrganizationOrganizationDetailsResponse[];
|
providerId: any;
|
||||||
pagedClients: ProviderOrganizationOrganizationDetailsResponse[];
|
searchText: string;
|
||||||
|
addableOrganizations: Organization[];
|
||||||
|
loading = true;
|
||||||
|
manageOrganizations = false;
|
||||||
|
showAddExisting = false;
|
||||||
|
|
||||||
protected didScroll = false;
|
clients: ProviderOrganizationOrganizationDetailsResponse[];
|
||||||
protected pageSize = 100;
|
pagedClients: ProviderOrganizationOrganizationDetailsResponse[];
|
||||||
protected actionPromise: Promise<any>;
|
|
||||||
private pagedClientsCount = 0;
|
|
||||||
|
|
||||||
constructor(
|
protected didScroll = false;
|
||||||
private route: ActivatedRoute,
|
protected pageSize = 100;
|
||||||
private providerService: ProviderService,
|
protected actionPromise: Promise<any>;
|
||||||
private apiService: ApiService,
|
private pagedClientsCount = 0;
|
||||||
private searchService: SearchService,
|
|
||||||
private platformUtilsService: PlatformUtilsService,
|
|
||||||
private i18nService: I18nService,
|
|
||||||
private validationService: ValidationService,
|
|
||||||
private webProviderService: WebProviderService,
|
|
||||||
private logService: LogService,
|
|
||||||
private modalService: ModalService,
|
|
||||||
private organizationService: OrganizationService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async ngOnInit() {
|
constructor(private route: ActivatedRoute, private userService: UserService,
|
||||||
this.route.parent.params.subscribe(async (params) => {
|
private apiService: ApiService, private searchService: SearchService,
|
||||||
this.providerId = params.providerId;
|
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
|
||||||
|
private toasterService: ToasterService, private validationService: ValidationService,
|
||||||
|
private providerService: ProviderService, private logService: LogService,
|
||||||
|
private modalService: ModalService) { }
|
||||||
|
|
||||||
await this.load();
|
async ngOnInit() {
|
||||||
|
this.route.parent.params.subscribe(async params => {
|
||||||
|
this.providerId = params.providerId;
|
||||||
|
|
||||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
|
||||||
this.searchText = qParams.search;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async load() {
|
|
||||||
const response = await this.apiService.getProviderClients(this.providerId);
|
|
||||||
this.clients = response.data != null && response.data.length > 0 ? response.data : [];
|
|
||||||
this.manageOrganizations =
|
|
||||||
(await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin;
|
|
||||||
const candidateOrgs = (await this.organizationService.getAll()).filter(
|
|
||||||
(o) => o.isOwner && o.providerId == null
|
|
||||||
);
|
|
||||||
const allowedOrgsIds = await Promise.all(
|
|
||||||
candidateOrgs.map((o) => this.apiService.getOrganization(o.id))
|
|
||||||
).then((orgs) =>
|
|
||||||
orgs.filter((o) => !DisallowedPlanTypes.includes(o.planType)).map((o) => o.id)
|
|
||||||
);
|
|
||||||
this.addableOrganizations = candidateOrgs.filter((o) => allowedOrgsIds.includes(o.id));
|
|
||||||
|
|
||||||
this.showAddExisting = this.addableOrganizations.length !== 0;
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
isPaging() {
|
|
||||||
const searching = this.isSearching();
|
|
||||||
if (searching && this.didScroll) {
|
|
||||||
this.resetPaging();
|
|
||||||
}
|
|
||||||
return !searching && this.clients && this.clients.length > this.pageSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
isSearching() {
|
|
||||||
return this.searchService.isSearchable(this.searchText);
|
|
||||||
}
|
|
||||||
|
|
||||||
async resetPaging() {
|
|
||||||
this.pagedClients = [];
|
|
||||||
this.loadMore();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMore() {
|
|
||||||
if (!this.clients || this.clients.length <= this.pageSize) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const pagedLength = this.pagedClients.length;
|
|
||||||
let pagedSize = this.pageSize;
|
|
||||||
if (pagedLength === 0 && this.pagedClientsCount > this.pageSize) {
|
|
||||||
pagedSize = this.pagedClientsCount;
|
|
||||||
}
|
|
||||||
if (this.clients.length > pagedLength) {
|
|
||||||
this.pagedClients = this.pagedClients.concat(
|
|
||||||
this.clients.slice(pagedLength, pagedLength + pagedSize)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.pagedClientsCount = this.pagedClients.length;
|
|
||||||
this.didScroll = this.pagedClients.length > this.pageSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
async addExistingOrganization() {
|
|
||||||
const [modal] = await this.modalService.openViewRef(
|
|
||||||
AddOrganizationComponent,
|
|
||||||
this.addModalRef,
|
|
||||||
(comp) => {
|
|
||||||
comp.providerId = this.providerId;
|
|
||||||
comp.organizations = this.addableOrganizations;
|
|
||||||
comp.onAddedOrganization.subscribe(async () => {
|
|
||||||
try {
|
|
||||||
await this.load();
|
await this.load();
|
||||||
modal.close();
|
|
||||||
} catch (e) {
|
this.route.queryParams.pipe(first()).subscribe(async qParams => {
|
||||||
this.logService.error(`Handled exception: ${e}`);
|
this.searchText = qParams.search;
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async remove(organization: ProviderOrganizationOrganizationDetailsResponse) {
|
|
||||||
const confirmed = await this.platformUtilsService.showDialog(
|
|
||||||
this.i18nService.t("detachOrganizationConfirmation"),
|
|
||||||
organization.organizationName,
|
|
||||||
this.i18nService.t("yes"),
|
|
||||||
this.i18nService.t("no"),
|
|
||||||
"warning"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!confirmed) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.actionPromise = this.webProviderService.detachOrganizastion(
|
async load() {
|
||||||
this.providerId,
|
const response = await this.apiService.getProviderClients(this.providerId);
|
||||||
organization.id
|
this.clients = response.data != null && response.data.length > 0 ? response.data : [];
|
||||||
);
|
this.manageOrganizations = (await this.userService.getProvider(this.providerId)).type === ProviderUserType.ProviderAdmin;
|
||||||
try {
|
const candidateOrgs = (await this.userService.getAllOrganizations()).filter(o => o.isOwner && o.providerId == null);
|
||||||
await this.actionPromise;
|
const allowedOrgsIds = await Promise.all(candidateOrgs.map(o => this.apiService.getOrganization(o.id))).then(orgs =>
|
||||||
this.platformUtilsService.showToast(
|
orgs.filter(o => !DisallowedPlanTypes.includes(o.planType))
|
||||||
"success",
|
.map(o => o.id));
|
||||||
null,
|
this.addableOrganizations = candidateOrgs.filter(o => allowedOrgsIds.includes(o.id));
|
||||||
this.i18nService.t("detachedOrganization", organization.organizationName)
|
|
||||||
);
|
this.showAddExisting = this.addableOrganizations.length !== 0;
|
||||||
await this.load();
|
this.loading = false;
|
||||||
} catch (e) {
|
}
|
||||||
this.validationService.showError(e);
|
|
||||||
|
isPaging() {
|
||||||
|
const searching = this.isSearching();
|
||||||
|
if (searching && this.didScroll) {
|
||||||
|
this.resetPaging();
|
||||||
|
}
|
||||||
|
return !searching && this.clients && this.clients.length > this.pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSearching() {
|
||||||
|
return this.searchService.isSearchable(this.searchText);
|
||||||
|
}
|
||||||
|
|
||||||
|
async resetPaging() {
|
||||||
|
this.pagedClients = [];
|
||||||
|
this.loadMore();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
loadMore() {
|
||||||
|
if (!this.clients || this.clients.length <= this.pageSize) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pagedLength = this.pagedClients.length;
|
||||||
|
let pagedSize = this.pageSize;
|
||||||
|
if (pagedLength === 0 && this.pagedClientsCount > this.pageSize) {
|
||||||
|
pagedSize = this.pagedClientsCount;
|
||||||
|
}
|
||||||
|
if (this.clients.length > pagedLength) {
|
||||||
|
this.pagedClients = this.pagedClients.concat(this.clients.slice(pagedLength, pagedLength + pagedSize));
|
||||||
|
}
|
||||||
|
this.pagedClientsCount = this.pagedClients.length;
|
||||||
|
this.didScroll = this.pagedClients.length > this.pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addExistingOrganization() {
|
||||||
|
const [modal] = await this.modalService.openViewRef(AddOrganizationComponent, this.addModalRef, comp => {
|
||||||
|
comp.providerId = this.providerId;
|
||||||
|
comp.organizations = this.addableOrganizations;
|
||||||
|
comp.onAddedOrganization.subscribe(async () => {
|
||||||
|
try {
|
||||||
|
await this.load();
|
||||||
|
modal.close();
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(`Handled exception: ${e}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(organization: ProviderOrganizationOrganizationDetailsResponse) {
|
||||||
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
|
this.i18nService.t('detachOrganizationConfirmation'), organization.organizationName,
|
||||||
|
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.actionPromise = this.providerService.detachOrganizastion(this.providerId, organization.id);
|
||||||
|
try {
|
||||||
|
await this.actionPromise;
|
||||||
|
this.toasterService.popAsync('success', null, this.i18nService.t('detachedOrganization', organization.organizationName));
|
||||||
|
await this.load();
|
||||||
|
} catch (e) {
|
||||||
|
this.validationService.showError(e);
|
||||||
|
}
|
||||||
|
this.actionPromise = null;
|
||||||
}
|
}
|
||||||
this.actionPromise = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,23 +1,26 @@
|
|||||||
import { Component, OnInit, ViewChild } from "@angular/core";
|
import {
|
||||||
import { ActivatedRoute } from "@angular/router";
|
Component,
|
||||||
|
OnInit,
|
||||||
|
ViewChild,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
import { OrganizationPlansComponent } from "src/app/settings/organization-plans.component";
|
import { OrganizationPlansComponent } from 'src/app/settings/organization-plans.component';
|
||||||
|
|
||||||
@Component({
|
@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 })
|
@ViewChild(OrganizationPlansComponent, { static: true }) orgPlansComponent: OrganizationPlansComponent;
|
||||||
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,42 +1,35 @@
|
|||||||
<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
|
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
|
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||||
title="{{ 'loading' | i18n }}"
|
</p>
|
||||||
aria-hidden="true"
|
</div>
|
||||||
></i>
|
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
|
||||||
</p>
|
|
||||||
</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
|
<a routerLink="/register" [queryParams]="{email: email}"
|
||||||
routerLink="/register"
|
class="btn btn-primary btn-block ml-2 mt-0">
|
||||||
[queryParams]="{ email: email }"
|
{{'createAccount' | i18n}}
|
||||||
class="btn btn-primary btn-block ml-2 mt-0"
|
</a>
|
||||||
>
|
</div>
|
||||||
{{ "createAccount" | i18n }}
|
</div>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,55 +1,48 @@
|
|||||||
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 { ApiService } from "jslib-common/abstractions/api.service";
|
import { BaseAcceptComponent } from 'src/app/common/base.accept.component';
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
|
||||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
|
||||||
import { StateService } from "jslib-common/abstractions/state.service";
|
|
||||||
import { ProviderUserAcceptRequest } from "jslib-common/models/request/provider/providerUserAcceptRequest";
|
|
||||||
|
|
||||||
import { BaseAcceptComponent } from "src/app/common/base.accept.component";
|
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||||
|
|
||||||
|
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||||
|
import { StateService } from 'jslib-common/abstractions/state.service';
|
||||||
|
import { UserService } from 'jslib-common/abstractions/user.service';
|
||||||
|
import { ProviderUserAcceptRequest } from 'jslib-common/models/request/provider/providerUserAcceptRequest';
|
||||||
|
|
||||||
@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(
|
constructor(router: Router, toasterService: ToasterService, i18nService: I18nService, route: ActivatedRoute,
|
||||||
router: Router,
|
userService: UserService, stateService: StateService, private apiService: ApiService) {
|
||||||
i18nService: I18nService,
|
super(router, toasterService, i18nService, route, userService, stateService);
|
||||||
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(
|
await this.apiService.postProviderUserAccept(qParams.providerId, qParams.providerUserId, request);
|
||||||
qParams.providerId,
|
const toast: Toast = {
|
||||||
qParams.providerUserId,
|
type: 'success',
|
||||||
request
|
title: this.i18nService.t('inviteAccepted'),
|
||||||
);
|
body: this.i18nService.t('providerInviteAcceptedDesc'),
|
||||||
this.platformUtilService.showToast(
|
timeout: 10000,
|
||||||
"success",
|
};
|
||||||
this.i18nService.t("inviteAccepted"),
|
this.toasterService.popAsync(toast);
|
||||||
this.i18nService.t("providerInviteAcceptedDesc"),
|
this.router.navigate(['/vault']);
|
||||||
{ timeout: 10000 }
|
}
|
||||||
);
|
|
||||||
this.router.navigate(["/vault"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async unauthedHandler(qParams: any) {
|
async unauthedHandler(qParams: any) {
|
||||||
this.providerName = qParams.providerName;
|
this.providerName = qParams.providerName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,38 @@
|
|||||||
import { Component, Input } from "@angular/core";
|
import {
|
||||||
|
Component,
|
||||||
|
Input,
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
import { ProviderUserStatusType } from "jslib-common/enums/providerUserStatusType";
|
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 { BulkConfirmComponent as OrganizationBulkConfirmComponent } from "src/app/organizations/manage/bulk/bulk-confirm.component";
|
import { ProviderUserStatusType } from 'jslib-common/enums/providerUserStatusType';
|
||||||
import { BulkUserDetails } from "src/app/organizations/manage/bulk/bulk-status.component";
|
|
||||||
|
import { BulkConfirmComponent as OrganizationBulkConfirmComponent } from 'src/app/organizations/manage/bulk/bulk-confirm.component';
|
||||||
|
import { BulkUserDetails } from 'src/app/organizations/manage/bulk/bulk-status.component';
|
||||||
|
|
||||||
@Component({
|
@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;
|
|
||||||
|
|
||||||
protected isAccepted(user: BulkUserDetails) {
|
@Input() providerId: string;
|
||||||
return user.status === ProviderUserStatusType.Accepted;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async getPublicKeys() {
|
protected isAccepted(user: BulkUserDetails) {
|
||||||
const request = new ProviderUserBulkRequest(this.filteredUsers.map((user) => user.id));
|
return user.status === ProviderUserStatusType.Accepted;
|
||||||
return await this.apiService.postProviderUsersPublicKey(this.providerId, request);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected getCryptoKey() {
|
protected async getPublicKeys() {
|
||||||
return this.cryptoService.getProviderKey(this.providerId);
|
const request = new ProviderUserBulkRequest(this.filteredUsers.map(user => user.id));
|
||||||
}
|
return await this.apiService.postProviderUsersPublicKey(this.providerId, request);
|
||||||
|
}
|
||||||
|
|
||||||
protected async postConfirmRequest(userIdsWithKeys: any[]) {
|
protected getCryptoKey() {
|
||||||
const request = new ProviderUserBulkConfirmRequest(userIdsWithKeys);
|
return this.cryptoService.getProviderKey(this.providerId);
|
||||||
return await this.apiService.postProviderUserBulkConfirm(this.providerId, request);
|
}
|
||||||
}
|
|
||||||
|
protected async postConfirmRequest(userIdsWithKeys: any[]) {
|
||||||
|
const request = new ProviderUserBulkConfirmRequest(userIdsWithKeys);
|
||||||
|
return await this.apiService.postProviderUserBulkConfirm(this.providerId, request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import { Component, Input } from "@angular/core";
|
import {
|
||||||
|
Component,
|
||||||
|
Input,
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
import { ProviderUserBulkRequest } from "jslib-common/models/request/provider/providerUserBulkRequest";
|
import { ProviderUserBulkRequest } from 'jslib-common/models/request/provider/providerUserBulkRequest';
|
||||||
|
|
||||||
import { BulkRemoveComponent as OrganizationBulkRemoveComponent } from "src/app/organizations/manage/bulk/bulk-remove.component";
|
import { BulkRemoveComponent as OrganizationBulkRemoveComponent } from 'src/app/organizations/manage/bulk/bulk-remove.component';
|
||||||
|
|
||||||
@Component({
|
@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;
|
|
||||||
|
|
||||||
async deleteUsers() {
|
@Input() providerId: string;
|
||||||
const request = new ProviderUserBulkRequest(this.users.map((user) => user.id));
|
|
||||||
return await this.apiService.deleteManyProviderUsers(this.providerId, request);
|
async deleteUsers() {
|
||||||
}
|
const request = new ProviderUserBulkRequest(this.users.map(user => user.id));
|
||||||
|
return await this.apiService.deleteManyProviderUsers(this.providerId, request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,107 +1,68 @@
|
|||||||
<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
|
<input type="datetime-local" class="form-control form-control-sm" id="start"
|
||||||
type="datetime-local"
|
placeholder="{{'startDate' | i18n}}" [(ngModel)]="start" placeholder="YYYY-MM-DDTHH:MM"
|
||||||
class="form-control form-control-sm"
|
(change)="dirtyDates = true">
|
||||||
id="start"
|
<span class="mx-2">-</span>
|
||||||
placeholder="{{ 'startDate' | i18n }}"
|
<label class="sr-only" for="end">{{'endDate' | i18n}}</label>
|
||||||
[(ngModel)]="start"
|
<input type="datetime-local" class="form-control form-control-sm" id="end"
|
||||||
placeholder="YYYY-MM-DDTHH:MM"
|
placeholder="{{'endDate' | i18n}}" [(ngModel)]="end" placeholder="YYYY-MM-DDTHH:MM"
|
||||||
(change)="dirtyDates = true"
|
(change)="dirtyDates = true">
|
||||||
/>
|
</div>
|
||||||
<span class="mx-2">-</span>
|
<form #refreshForm [appApiAction]="refreshPromise" class="d-inline">
|
||||||
<label class="sr-only" for="end">{{ "endDate" | i18n }}</label>
|
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="loadEvents(true)"
|
||||||
<input
|
[disabled]="loaded && refreshForm.loading">
|
||||||
type="datetime-local"
|
<i class="fa fa-refresh fa-fw" aria-hidden="true" [ngClass]="{'fa-spin': loaded && refreshForm.loading}"></i>
|
||||||
class="form-control form-control-sm"
|
{{'refresh' | i18n}}
|
||||||
id="end"
|
</button>
|
||||||
placeholder="{{ 'endDate' | i18n }}"
|
</form>
|
||||||
[(ngModel)]="end"
|
<form #exportForm [appApiAction]="exportPromise" class="d-inline">
|
||||||
placeholder="YYYY-MM-DDTHH:MM"
|
<button type="button" class="btn btn-sm btn-outline-primary btn-submit manual ml-3"
|
||||||
(change)="dirtyDates = true"
|
[ngClass]="{loading:exportForm.loading}" (click)="exportEvents()"
|
||||||
/>
|
[disabled]="loaded && exportForm.loading || dirtyDates">
|
||||||
|
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
|
||||||
|
<span>{{'export' | i18n}}</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<form #refreshForm [appApiAction]="refreshPromise" class="d-inline">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-sm btn-outline-primary ml-3"
|
|
||||||
(click)="loadEvents(true)"
|
|
||||||
[disabled]="loaded && refreshForm.loading"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-refresh bwi-fw"
|
|
||||||
aria-hidden="true"
|
|
||||||
[ngClass]="{ 'bwi-spin': loaded && refreshForm.loading }"
|
|
||||||
></i>
|
|
||||||
{{ "refresh" | i18n }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<form #exportForm [appApiAction]="exportPromise" class="d-inline">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-sm btn-outline-primary btn-submit manual ml-3"
|
|
||||||
[ngClass]="{ loading: exportForm.loading }"
|
|
||||||
(click)="exportEvents()"
|
|
||||||
[disabled]="(loaded && exportForm.loading) || dirtyDates"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-spinner bwi-spin" aria-hidden="true"></i>
|
|
||||||
<span>{{ "export" | i18n }}</span>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="!loaded">
|
<ng-container *ngIf="!loaded">
|
||||||
<i
|
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
class="bwi bwi-spinner bwi-spin text-muted"
|
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||||
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
|
<i class="text-muted fa fa-lg {{e.appIcon}}" title="{{e.appName}}, {{e.ip}}" aria-hidden="true"></i>
|
||||||
class="text-muted bwi bwi-lg {{ e.appIcon }}"
|
<span class="sr-only">{{e.appName}}, {{e.ip}}</span>
|
||||||
title="{{ e.appName }}, {{ e.ip }}"
|
</td>
|
||||||
aria-hidden="true"
|
<td>
|
||||||
></i>
|
<span title="{{e.userEmail}}">{{e.userName}}</span>
|
||||||
<span class="sr-only">{{ e.appName }}, {{ e.ip }}</span>
|
</td>
|
||||||
</td>
|
<td [innerHTML]="e.message"></td>
|
||||||
<td>
|
</tr>
|
||||||
<span title="{{ e.userEmail }}">{{ e.userName }}</span>
|
</tbody>
|
||||||
</td>
|
</table>
|
||||||
<td [innerHTML]="e.message"></td>
|
<button #moreBtn [appApiAction]="morePromise" type="button" class="btn btn-block btn-link btn-submit"
|
||||||
</tr>
|
(click)="loadEvents(false)" [disabled]="loaded && moreBtn.loading" *ngIf="continuationToken">
|
||||||
</tbody>
|
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
</table>
|
<span>{{'loadMore' | i18n}}</span>
|
||||||
<button
|
</button>
|
||||||
#moreBtn
|
|
||||||
[appApiAction]="morePromise"
|
|
||||||
type="button"
|
|
||||||
class="btn btn-block btn-link btn-submit"
|
|
||||||
(click)="loadEvents(false)"
|
|
||||||
[disabled]="loaded && moreBtn.loading"
|
|
||||||
*ngIf="continuationToken"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
|
||||||
<span>{{ "loadMore" | i18n }}</span>
|
|
||||||
</button>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@@ -1,79 +1,71 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
import {
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
Component,
|
||||||
|
OnInit,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { ToasterService } from 'angular2-toaster';
|
||||||
|
|
||||||
import { UserNamePipe } from "jslib-angular/pipes/user-name.pipe";
|
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 { EventResponse } from "jslib-common/models/response/eventResponse";
|
|
||||||
|
|
||||||
import { BaseEventsComponent } from "src/app/common/base.events.component";
|
import { UserNamePipe } from 'jslib-angular/pipes/user-name.pipe';
|
||||||
import { EventService } from "src/app/services/event.service";
|
|
||||||
|
import { EventResponse } from 'jslib-common/models/response/eventResponse';
|
||||||
|
|
||||||
|
import { EventService } from 'src/app/services/event.service';
|
||||||
|
|
||||||
|
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 = "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(
|
constructor(private apiService: ApiService, private route: ActivatedRoute, eventService: EventService,
|
||||||
private apiService: ApiService,
|
i18nService: I18nService, toasterService: ToasterService, private userService: UserService,
|
||||||
private route: ActivatedRoute,
|
exportService: ExportService, platformUtilsService: PlatformUtilsService, private router: Router,
|
||||||
eventService: EventService,
|
logService: LogService, private userNamePipe: UserNamePipe) {
|
||||||
i18nService: I18nService,
|
super(eventService, i18nService, toasterService, exportService, platformUtilsService, logService);
|
||||||
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.providerService.get(this.providerId);
|
const provider = await this.userService.getProvider(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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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 });
|
||||||
});
|
});
|
||||||
await this.loadEvents(true);
|
await this.loadEvents(true);
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected requestEvents(startDate: string, endDate: string, continuationToken: string) {
|
protected requestEvents(startDate: string, endDate: string, continuationToken: string) {
|
||||||
return this.apiService.getEventsProvider(
|
return this.apiService.getEventsProvider(this.providerId, startDate, endDate, continuationToken);
|
||||||
this.providerId,
|
}
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
continuationToken
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getUserName(r: EventResponse, userId: string) {
|
protected getUserName(r: EventResponse, userId: string) {
|
||||||
return userId != null && this.providerUsersUserIdMap.has(userId)
|
return userId != null && this.providerUsersUserIdMap.has(userId) ? this.providerUsersUserIdMap.get(userId) : null;
|
||||||
? this.providerUsersUserIdMap.get(userId)
|
}
|
||||||
: null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,22 @@
|
|||||||
<div class="container page-content">
|
<div class="container page-content">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-3">
|
<div class="col-md-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
|
<a routerLink="people" class="list-group-item" routerLinkActive="active"
|
||||||
routerLink="people"
|
*ngIf="provider.canManageUsers">
|
||||||
class="list-group-item"
|
{{'people' | i18n}}
|
||||||
routerLinkActive="active"
|
</a>
|
||||||
*ngIf="provider.canManageUsers"
|
<a routerLink="events" class="list-group-item" routerLinkActive="active"
|
||||||
>
|
*ngIf="provider.canAccessEventLogs && accessEvents">
|
||||||
{{ "people" | i18n }}
|
{{'eventLogs' | i18n}}
|
||||||
</a>
|
</a>
|
||||||
<a
|
</div>
|
||||||
routerLink="events"
|
</div>
|
||||||
class="list-group-item"
|
</div>
|
||||||
routerLinkActive="active"
|
<div class="col-md-9 mt-4 mt-md-0">
|
||||||
*ngIf="provider.canAccessEventLogs && accessEvents"
|
<router-outlet></router-outlet>
|
||||||
>
|
|
||||||
{{ "eventLogs" | i18n }}
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-9">
|
|
||||||
<router-outlet></router-outlet>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,23 +1,27 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
import {
|
||||||
import { ActivatedRoute } from "@angular/router";
|
Component,
|
||||||
|
OnInit,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
import { UserService } from 'jslib-common/abstractions/user.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 providerService: ProviderService) {}
|
constructor(private route: ActivatedRoute, private userService: UserService) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.route.parent.params.subscribe(async (params) => {
|
this.route.parent.params.subscribe(async params => {
|
||||||
this.provider = await this.providerService.get(params.providerId);
|
this.provider = await this.userService.getProvider(params.providerId);
|
||||||
this.accessEvents = this.provider.useEvents;
|
this.accessEvents = this.provider.useEvents;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,229 +1,145 @@
|
|||||||
<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
|
<button type="button" class="btn btn-outline-secondary" [ngClass]="{active: status == null}"
|
||||||
type="button"
|
(click)="filter(null)">
|
||||||
class="btn btn-outline-secondary"
|
{{'all' | i18n}}
|
||||||
[ngClass]="{ active: status == null }"
|
<span class="badge badge-pill badge-info" *ngIf="allCount">{{allCount}}</span>
|
||||||
(click)="filter(null)"
|
</button>
|
||||||
>
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
{{ "all" | i18n }}
|
[ngClass]="{active: status == userStatusType.Invited}"
|
||||||
<span class="badge badge-pill badge-info" *ngIf="allCount">{{ allCount }}</span>
|
(click)="filter(userStatusType.Invited)">
|
||||||
</button>
|
{{'invited' | i18n}}
|
||||||
<button
|
<span class="badge badge-pill badge-info" *ngIf="invitedCount">{{invitedCount}}</span>
|
||||||
type="button"
|
</button>
|
||||||
class="btn btn-outline-secondary"
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
[ngClass]="{ active: status == userStatusType.Invited }"
|
[ngClass]="{active: status == userStatusType.Accepted}"
|
||||||
(click)="filter(userStatusType.Invited)"
|
(click)="filter(userStatusType.Accepted)">
|
||||||
>
|
{{'accepted' | i18n}}
|
||||||
{{ "invited" | i18n }}
|
<span class="badge badge-pill badge-warning" *ngIf="acceptedCount">{{acceptedCount}}</span>
|
||||||
<span class="badge badge-pill badge-info" *ngIf="invitedCount">{{ invitedCount }}</span>
|
</button>
|
||||||
</button>
|
</div>
|
||||||
<button
|
<div class="ml-3">
|
||||||
type="button"
|
<label class="sr-only" for="search">{{'search' | i18n}}</label>
|
||||||
class="btn btn-outline-secondary"
|
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
|
||||||
[ngClass]="{ active: status == userStatusType.Accepted }"
|
[(ngModel)]="searchText">
|
||||||
(click)="filter(userStatusType.Accepted)"
|
</div>
|
||||||
>
|
<div class="dropdown ml-3" appListDropdown>
|
||||||
{{ "accepted" | i18n }}
|
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="bulkActionsButton"
|
||||||
<span class="badge badge-pill badge-warning" *ngIf="acceptedCount">{{
|
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" appA11yTitle="{{'options' | i18n}}">
|
||||||
acceptedCount
|
<i class="fa fa-cog" aria-hidden="true"></i>
|
||||||
}}</span>
|
</button>
|
||||||
</button>
|
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="bulkActionsButton">
|
||||||
|
<button class="dropdown-item" appStopClick (click)="bulkReinvite()">
|
||||||
|
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
|
||||||
|
{{'reinviteSelected' | i18n}}
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item text-success" appStopClick (click)="bulkConfirm()"
|
||||||
|
*ngIf="showBulkConfirmUsers">
|
||||||
|
<i class="fa fa-fw fa-check" aria-hidden="true"></i>
|
||||||
|
{{'confirmSelected' | i18n}}
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item text-danger" appStopClick (click)="bulkRemove()">
|
||||||
|
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
|
||||||
|
{{'remove' | i18n}}
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<button class="dropdown-item" appStopClick (click)="selectAll(true)">
|
||||||
|
<i class="fa fa-fw fa-check-square-o" aria-hidden="true"></i>
|
||||||
|
{{'selectAll' | i18n}}
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item" appStopClick (click)="selectAll(false)">
|
||||||
|
<i class="fa fa-fw fa-minus-square-o" aria-hidden="true"></i>
|
||||||
|
{{'unselectAll' | i18n}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="invite()">
|
||||||
|
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
|
||||||
|
{{'inviteUser' | i18n}}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3">
|
|
||||||
<label class="sr-only" for="search">{{ "search" | i18n }}</label>
|
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
class="form-control form-control-sm"
|
|
||||||
id="search"
|
|
||||||
placeholder="{{ 'search' | i18n }}"
|
|
||||||
[(ngModel)]="searchText"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="dropdown ml-3" appListDropdown>
|
|
||||||
<button
|
|
||||||
class="btn btn-sm btn-outline-secondary dropdown-toggle"
|
|
||||||
type="button"
|
|
||||||
id="bulkActionsButton"
|
|
||||||
data-toggle="dropdown"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-expanded="false"
|
|
||||||
appA11yTitle="{{ 'options' | i18n }}"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-cog" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="bulkActionsButton">
|
|
||||||
<button class="dropdown-item" appStopClick (click)="bulkReinvite()">
|
|
||||||
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
|
|
||||||
{{ "reinviteSelected" | i18n }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="dropdown-item text-success"
|
|
||||||
appStopClick
|
|
||||||
(click)="bulkConfirm()"
|
|
||||||
*ngIf="showBulkConfirmUsers"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
|
||||||
{{ "confirmSelected" | i18n }}
|
|
||||||
</button>
|
|
||||||
<button class="dropdown-item text-danger" appStopClick (click)="bulkRemove()">
|
|
||||||
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
|
||||||
{{ "remove" | i18n }}
|
|
||||||
</button>
|
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
<button class="dropdown-item" appStopClick (click)="selectAll(true)">
|
|
||||||
<i class="bwi bwi-fw bwi-check-square" aria-hidden="true"></i>
|
|
||||||
{{ "selectAll" | i18n }}
|
|
||||||
</button>
|
|
||||||
<button class="dropdown-item" appStopClick (click)="selectAll(false)">
|
|
||||||
<i class="bwi bwi-fw bwi-minus-square" aria-hidden="true"></i>
|
|
||||||
{{ "unselectAll" | i18n }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="invite()">
|
|
||||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
|
||||||
{{ "inviteUser" | i18n }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="loading">
|
<ng-container *ngIf="loading">
|
||||||
<i
|
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
class="bwi bwi-spinner bwi-spin text-muted"
|
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||||
title="{{ 'loading' | i18n }}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngIf="
|
*ngIf="!loading && (isPaging() ? pagedUsers : users | search:searchText:'name':'email':'id') as searchedUsers">
|
||||||
!loading &&
|
<p *ngIf="!searchedUsers.length">{{'noUsersInList' | i18n}}</p>
|
||||||
(isPaging() ? pagedUsers : (users | search: searchText:'name':'email':'id')) as searchedUsers
|
<ng-container *ngIf="searchedUsers.length">
|
||||||
"
|
<app-callout type="info" title="{{'confirmUsers' | i18n}}" icon="fa-check-circle" *ngIf="showConfirmUsers">
|
||||||
>
|
{{'providerUsersNeedConfirmed' | i18n}}
|
||||||
<p *ngIf="!searchedUsers.length">{{ "noUsersInList" | i18n }}</p>
|
</app-callout>
|
||||||
<ng-container *ngIf="searchedUsers.length">
|
<table class="table table-hover table-list" infiniteScroll [infiniteScrollDistance]="1"
|
||||||
<app-callout
|
[infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
|
||||||
type="info"
|
<tbody>
|
||||||
title="{{ 'confirmUsers' | i18n }}"
|
<tr *ngFor="let u of searchedUsers">
|
||||||
icon="bwi bwi-check-circle"
|
<td (click)="checkUser(u)" class="table-list-checkbox">
|
||||||
*ngIf="showConfirmUsers"
|
<input type="checkbox" [(ngModel)]="u.checked" appStopProp>
|
||||||
>
|
</td>
|
||||||
{{ "providerUsersNeedConfirmed" | i18n }}
|
<td width="30">
|
||||||
</app-callout>
|
<app-avatar [data]="u | userName" [email]="u.email" size="25" [circle]="true"
|
||||||
<table
|
[fontSize]="14"></app-avatar>
|
||||||
class="table table-hover table-list"
|
</td>
|
||||||
infiniteScroll
|
<td>
|
||||||
[infiniteScrollDistance]="1"
|
<a href="#" appStopClick (click)="edit(u)">{{u.email}}</a>
|
||||||
[infiniteScrollDisabled]="!isPaging()"
|
<span class="badge badge-secondary"
|
||||||
(scrolled)="loadMore()"
|
*ngIf="u.status === userStatusType.Invited">{{'invited' | i18n}}</span>
|
||||||
>
|
<span class="badge badge-warning"
|
||||||
<tbody>
|
*ngIf="u.status === userStatusType.Accepted">{{'accepted' | i18n}}</span>
|
||||||
<tr *ngFor="let u of searchedUsers">
|
<small class="text-muted d-block" *ngIf="u.name">{{u.name}}</small>
|
||||||
<td (click)="checkUser(u)" class="table-list-checkbox">
|
</td>
|
||||||
<input type="checkbox" [(ngModel)]="u.checked" appStopProp />
|
<td>
|
||||||
</td>
|
<ng-container *ngIf="u.twoFactorEnabled">
|
||||||
<td width="30">
|
<i class="fa fa-lock" title="{{'userUsingTwoStep' | i18n}}" aria-hidden="true"></i>
|
||||||
<app-avatar
|
<span class="sr-only">{{'userUsingTwoStep' | i18n}}</span>
|
||||||
[data]="u | userName"
|
</ng-container>
|
||||||
[email]="u.email"
|
</td>
|
||||||
size="25"
|
<td>
|
||||||
[circle]="true"
|
<span *ngIf="u.type === userType.ProviderAdmin">{{'providerAdmin' | i18n}}</span>
|
||||||
[fontSize]="14"
|
<span *ngIf="u.type === userType.ServiceUser">{{'serviceUser' | i18n}}</span>
|
||||||
>
|
<span *ngIf="u.type === userType.Custom">{{'custom' | i18n}}</span>
|
||||||
</app-avatar>
|
</td>
|
||||||
</td>
|
<td class="table-list-options">
|
||||||
<td>
|
<div class="dropdown" appListDropdown>
|
||||||
<a href="#" appStopClick (click)="edit(u)">{{ u.email }}</a>
|
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
|
||||||
<span class="badge badge-secondary" *ngIf="u.status === userStatusType.Invited">{{
|
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
|
||||||
"invited" | i18n
|
appA11yTitle="{{'options' | i18n}}">
|
||||||
}}</span>
|
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
|
||||||
<span class="badge badge-warning" *ngIf="u.status === userStatusType.Accepted">{{
|
</button>
|
||||||
"accepted" | i18n
|
<div class="dropdown-menu dropdown-menu-right">
|
||||||
}}</span>
|
<a class="dropdown-item" href="#" appStopClick (click)="reinvite(u)"
|
||||||
<small class="text-muted d-block" *ngIf="u.name">{{ u.name }}</small>
|
*ngIf="u.status === userStatusType.Invited">
|
||||||
</td>
|
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
|
||||||
<td>
|
{{'resendInvitation' | i18n}}
|
||||||
<ng-container *ngIf="u.twoFactorEnabled">
|
</a>
|
||||||
<i
|
<a class="dropdown-item text-success" href="#" appStopClick (click)="confirm(u)"
|
||||||
class="bwi bwi-lock"
|
*ngIf="u.status === userStatusType.Accepted">
|
||||||
title="{{ 'userUsingTwoStep' | i18n }}"
|
<i class="fa fa-fw fa-check" aria-hidden="true"></i>
|
||||||
aria-hidden="true"
|
{{'confirm' | i18n}}
|
||||||
></i>
|
</a>
|
||||||
<span class="sr-only">{{ "userUsingTwoStep" | i18n }}</span>
|
<a class="dropdown-item" href="#" appStopClick (click)="groups(u)" *ngIf="accessGroups">
|
||||||
</ng-container>
|
<i class="fa fa-fw fa-sitemap" aria-hidden="true"></i>
|
||||||
</td>
|
{{'groups' | i18n}}
|
||||||
<td>
|
</a>
|
||||||
<span *ngIf="u.type === userType.ProviderAdmin">{{ "providerAdmin" | i18n }}</span>
|
<a class="dropdown-item" href="#" appStopClick (click)="events(u)"
|
||||||
<span *ngIf="u.type === userType.ServiceUser">{{ "serviceUser" | i18n }}</span>
|
*ngIf="accessEvents && u.status === userStatusType.Confirmed">
|
||||||
<span *ngIf="u.type === userType.Custom">{{ "custom" | i18n }}</span>
|
<i class="fa fa-fw fa-file-text-o" aria-hidden="true"></i>
|
||||||
</td>
|
{{'eventLogs' | i18n}}
|
||||||
<td class="table-list-options">
|
</a>
|
||||||
<div class="dropdown" appListDropdown>
|
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(u)">
|
||||||
<button
|
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
|
||||||
class="btn btn-outline-secondary dropdown-toggle"
|
{{'remove' | i18n}}
|
||||||
type="button"
|
</a>
|
||||||
data-toggle="dropdown"
|
</div>
|
||||||
aria-haspopup="true"
|
</div>
|
||||||
aria-expanded="false"
|
</td>
|
||||||
appA11yTitle="{{ 'options' | i18n }}"
|
</tr>
|
||||||
>
|
</tbody>
|
||||||
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
|
</table>
|
||||||
</button>
|
</ng-container>
|
||||||
<div class="dropdown-menu dropdown-menu-right">
|
|
||||||
<a
|
|
||||||
class="dropdown-item"
|
|
||||||
href="#"
|
|
||||||
appStopClick
|
|
||||||
(click)="reinvite(u)"
|
|
||||||
*ngIf="u.status === userStatusType.Invited"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
|
|
||||||
{{ "resendInvitation" | i18n }}
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="dropdown-item text-success"
|
|
||||||
href="#"
|
|
||||||
appStopClick
|
|
||||||
(click)="confirm(u)"
|
|
||||||
*ngIf="u.status === userStatusType.Accepted"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
|
||||||
{{ "confirm" | i18n }}
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="dropdown-item"
|
|
||||||
href="#"
|
|
||||||
appStopClick
|
|
||||||
(click)="groups(u)"
|
|
||||||
*ngIf="accessGroups"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-fw bwi-sitemap" aria-hidden="true"></i>
|
|
||||||
{{ "groups" | i18n }}
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="dropdown-item"
|
|
||||||
href="#"
|
|
||||||
appStopClick
|
|
||||||
(click)="events(u)"
|
|
||||||
*ngIf="accessEvents && u.status === userStatusType.Confirmed"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>
|
|
||||||
{{ "eventLogs" | i18n }}
|
|
||||||
</a>
|
|
||||||
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(u)">
|
|
||||||
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
|
||||||
{{ "remove" | i18n }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #addEdit></ng-template>
|
<ng-template #addEdit></ng-template>
|
||||||
<ng-template #eventsTemplate></ng-template>
|
<ng-template #eventsTemplate></ng-template>
|
||||||
|
|||||||
@@ -1,284 +1,237 @@
|
|||||||
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
import {
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
Component,
|
||||||
import { first } from "rxjs/operators";
|
OnInit,
|
||||||
|
ViewChild,
|
||||||
|
ViewContainerRef
|
||||||
|
} from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { ToasterService } from 'angular2-toaster';
|
||||||
|
|
||||||
import { SearchPipe } from "jslib-angular/pipes/search.pipe";
|
import { first } from 'rxjs/operators';
|
||||||
import { UserNamePipe } from "jslib-angular/pipes/user-name.pipe";
|
|
||||||
import { ModalService } from "jslib-angular/services/modal.service";
|
|
||||||
import { ValidationService } from "jslib-angular/services/validation.service";
|
|
||||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
|
||||||
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
|
||||||
import { LogService } from "jslib-common/abstractions/log.service";
|
|
||||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
|
||||||
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
|
||||||
import { SearchService } from "jslib-common/abstractions/search.service";
|
|
||||||
import { StateService } from "jslib-common/abstractions/state.service";
|
|
||||||
import { ProviderUserStatusType } from "jslib-common/enums/providerUserStatusType";
|
|
||||||
import { ProviderUserType } from "jslib-common/enums/providerUserType";
|
|
||||||
import { ProviderUserBulkRequest } from "jslib-common/models/request/provider/providerUserBulkRequest";
|
|
||||||
import { ProviderUserConfirmRequest } from "jslib-common/models/request/provider/providerUserConfirmRequest";
|
|
||||||
import { ListResponse } from "jslib-common/models/response/listResponse";
|
|
||||||
import { ProviderUserBulkResponse } from "jslib-common/models/response/provider/providerUserBulkResponse";
|
|
||||||
import { ProviderUserUserDetailsResponse } from "jslib-common/models/response/provider/providerUserResponse";
|
|
||||||
|
|
||||||
import { BasePeopleComponent } from "src/app/common/base.people.component";
|
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||||
import { BulkStatusComponent } from "src/app/organizations/manage/bulk/bulk-status.component";
|
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
|
||||||
import { EntityEventsComponent } from "src/app/organizations/manage/entity-events.component";
|
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||||
|
import { LogService } from 'jslib-common/abstractions/log.service';
|
||||||
|
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||||
|
import { SearchService } from 'jslib-common/abstractions/search.service';
|
||||||
|
import { StorageService } from 'jslib-common/abstractions/storage.service';
|
||||||
|
import { UserService } from 'jslib-common/abstractions/user.service';
|
||||||
|
|
||||||
import { BulkConfirmComponent } from "./bulk/bulk-confirm.component";
|
import { ModalService } from 'jslib-angular/services/modal.service';
|
||||||
import { BulkRemoveComponent } from "./bulk/bulk-remove.component";
|
import { ValidationService } from 'jslib-angular/services/validation.service';
|
||||||
import { UserAddEditComponent } from "./user-add-edit.component";
|
|
||||||
|
import { ProviderUserStatusType } from 'jslib-common/enums/providerUserStatusType';
|
||||||
|
import { ProviderUserType } from 'jslib-common/enums/providerUserType';
|
||||||
|
|
||||||
|
import { SearchPipe } from 'jslib-angular/pipes/search.pipe';
|
||||||
|
import { UserNamePipe } from 'jslib-angular/pipes/user-name.pipe';
|
||||||
|
|
||||||
|
import { ListResponse } from 'jslib-common/models/response/listResponse';
|
||||||
|
import { ProviderUserUserDetailsResponse } from 'jslib-common/models/response/provider/providerUserResponse';
|
||||||
|
|
||||||
|
import { ProviderUserBulkRequest } from 'jslib-common/models/request/provider/providerUserBulkRequest';
|
||||||
|
import { ProviderUserConfirmRequest } from 'jslib-common/models/request/provider/providerUserConfirmRequest';
|
||||||
|
import { ProviderUserBulkResponse } from 'jslib-common/models/response/provider/providerUserBulkResponse';
|
||||||
|
|
||||||
|
import { BasePeopleComponent } from 'src/app/common/base.people.component';
|
||||||
|
import { BulkStatusComponent } from 'src/app/organizations/manage/bulk/bulk-status.component';
|
||||||
|
import { EntityEventsComponent } from 'src/app/organizations/manage/entity-events.component';
|
||||||
|
import { BulkConfirmComponent } from './bulk/bulk-confirm.component';
|
||||||
|
import { BulkRemoveComponent } from './bulk/bulk-remove.component';
|
||||||
|
import { UserAddEditComponent } from './user-add-edit.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "provider-people",
|
selector: 'provider-people',
|
||||||
templateUrl: "people.component.html",
|
templateUrl: 'people.component.html',
|
||||||
})
|
})
|
||||||
export class PeopleComponent
|
export class PeopleComponent extends BasePeopleComponent<ProviderUserUserDetailsResponse> implements OnInit {
|
||||||
extends BasePeopleComponent<ProviderUserUserDetailsResponse>
|
|
||||||
implements OnInit
|
|
||||||
{
|
|
||||||
@ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
|
||||||
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
|
|
||||||
groupsModalRef: ViewContainerRef;
|
|
||||||
@ViewChild("eventsTemplate", { read: ViewContainerRef, static: true })
|
|
||||||
eventsModalRef: ViewContainerRef;
|
|
||||||
@ViewChild("bulkStatusTemplate", { read: ViewContainerRef, static: true })
|
|
||||||
bulkStatusModalRef: ViewContainerRef;
|
|
||||||
@ViewChild("bulkConfirmTemplate", { read: ViewContainerRef, static: true })
|
|
||||||
bulkConfirmModalRef: ViewContainerRef;
|
|
||||||
@ViewChild("bulkRemoveTemplate", { read: ViewContainerRef, static: true })
|
|
||||||
bulkRemoveModalRef: ViewContainerRef;
|
|
||||||
|
|
||||||
userType = ProviderUserType;
|
@ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
||||||
userStatusType = ProviderUserStatusType;
|
@ViewChild('groupsTemplate', { read: ViewContainerRef, static: true }) groupsModalRef: ViewContainerRef;
|
||||||
providerId: string;
|
@ViewChild('eventsTemplate', { read: ViewContainerRef, static: true }) eventsModalRef: ViewContainerRef;
|
||||||
accessEvents = false;
|
@ViewChild('bulkStatusTemplate', { read: ViewContainerRef, static: true }) bulkStatusModalRef: ViewContainerRef;
|
||||||
|
@ViewChild('bulkConfirmTemplate', { read: ViewContainerRef, static: true }) bulkConfirmModalRef: ViewContainerRef;
|
||||||
|
@ViewChild('bulkRemoveTemplate', { read: ViewContainerRef, static: true }) bulkRemoveModalRef: ViewContainerRef;
|
||||||
|
|
||||||
constructor(
|
userType = ProviderUserType;
|
||||||
apiService: ApiService,
|
userStatusType = ProviderUserStatusType;
|
||||||
private route: ActivatedRoute,
|
providerId: string;
|
||||||
i18nService: I18nService,
|
accessEvents = false;
|
||||||
modalService: ModalService,
|
|
||||||
platformUtilsService: PlatformUtilsService,
|
|
||||||
cryptoService: CryptoService,
|
|
||||||
private router: Router,
|
|
||||||
searchService: SearchService,
|
|
||||||
validationService: ValidationService,
|
|
||||||
logService: LogService,
|
|
||||||
searchPipe: SearchPipe,
|
|
||||||
userNamePipe: UserNamePipe,
|
|
||||||
stateService: StateService,
|
|
||||||
private providerService: ProviderService
|
|
||||||
) {
|
|
||||||
super(
|
|
||||||
apiService,
|
|
||||||
searchService,
|
|
||||||
i18nService,
|
|
||||||
platformUtilsService,
|
|
||||||
cryptoService,
|
|
||||||
validationService,
|
|
||||||
modalService,
|
|
||||||
logService,
|
|
||||||
searchPipe,
|
|
||||||
userNamePipe,
|
|
||||||
stateService
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
constructor(apiService: ApiService, private route: ActivatedRoute,
|
||||||
this.route.parent.params.subscribe(async (params) => {
|
i18nService: I18nService, modalService: ModalService,
|
||||||
this.providerId = params.providerId;
|
platformUtilsService: PlatformUtilsService, toasterService: ToasterService,
|
||||||
const provider = await this.providerService.get(this.providerId);
|
cryptoService: CryptoService, private userService: UserService, private router: Router,
|
||||||
|
storageService: StorageService, searchService: SearchService, validationService: ValidationService,
|
||||||
|
logService: LogService, searchPipe: SearchPipe, userNamePipe: UserNamePipe) {
|
||||||
|
super(apiService, searchService, i18nService, platformUtilsService, toasterService, cryptoService,
|
||||||
|
storageService, validationService, modalService, logService, searchPipe, userNamePipe);
|
||||||
|
}
|
||||||
|
|
||||||
if (!provider.canManageUsers) {
|
ngOnInit() {
|
||||||
this.router.navigate(["../"], { relativeTo: this.route });
|
this.route.parent.params.subscribe(async params => {
|
||||||
return;
|
this.providerId = params.providerId;
|
||||||
}
|
const provider = await this.userService.getProvider(this.providerId);
|
||||||
|
|
||||||
this.accessEvents = provider.useEvents;
|
if (!provider.canManageUsers) {
|
||||||
|
this.router.navigate(['../'], { relativeTo: this.route });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this.load();
|
this.accessEvents = provider.useEvents;
|
||||||
|
|
||||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
await this.load();
|
||||||
this.searchText = qParams.search;
|
|
||||||
if (qParams.viewEvents != null) {
|
this.route.queryParams.pipe(first()).subscribe(async qParams => {
|
||||||
const user = this.users.filter((u) => u.id === qParams.viewEvents);
|
this.searchText = qParams.search;
|
||||||
if (user.length > 0 && user[0].status === ProviderUserStatusType.Confirmed) {
|
if (qParams.viewEvents != null) {
|
||||||
this.events(user[0]);
|
const user = this.users.filter(u => u.id === qParams.viewEvents);
|
||||||
}
|
if (user.length > 0 && user[0].status === ProviderUserStatusType.Confirmed) {
|
||||||
|
this.events(user[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getUsers(): Promise<ListResponse<ProviderUserUserDetailsResponse>> {
|
||||||
|
return this.apiService.getProviderUsers(this.providerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteUser(id: string): Promise<any> {
|
||||||
|
return this.apiService.deleteProviderUser(this.providerId, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
reinviteUser(id: string): Promise<any> {
|
||||||
|
return this.apiService.postProviderUserReinvite(this.providerId, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirmUser(user: ProviderUserUserDetailsResponse, publicKey: Uint8Array): Promise<any> {
|
||||||
|
const providerKey = await this.cryptoService.getProviderKey(this.providerId);
|
||||||
|
const key = await this.cryptoService.rsaEncrypt(providerKey.key, publicKey.buffer);
|
||||||
|
const request = new ProviderUserConfirmRequest();
|
||||||
|
request.key = key.encryptedString;
|
||||||
|
await this.apiService.postProviderUserConfirm(this.providerId, user.id, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
async edit(user: ProviderUserUserDetailsResponse) {
|
||||||
|
const [modal] = await this.modalService.openViewRef(UserAddEditComponent, this.addEditModalRef, comp => {
|
||||||
|
comp.name = this.userNamePipe.transform(user);
|
||||||
|
comp.providerId = this.providerId;
|
||||||
|
comp.providerUserId = user != null ? user.id : null;
|
||||||
|
comp.onSavedUser.subscribe(() => {
|
||||||
|
modal.close();
|
||||||
|
this.load();
|
||||||
|
});
|
||||||
|
comp.onDeletedUser.subscribe(() => {
|
||||||
|
modal.close();
|
||||||
|
this.removeUser(user);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async events(user: ProviderUserUserDetailsResponse) {
|
||||||
|
const [modal] = await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, comp => {
|
||||||
|
comp.name = this.userNamePipe.transform(user);
|
||||||
|
comp.providerId = this.providerId;
|
||||||
|
comp.entityId = user.id;
|
||||||
|
comp.showUser = false;
|
||||||
|
comp.entity = 'user';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async bulkRemove() {
|
||||||
|
if (this.actionPromise != null) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getUsers(): Promise<ListResponse<ProviderUserUserDetailsResponse>> {
|
const [modal] = await this.modalService.openViewRef(BulkRemoveComponent, this.bulkRemoveModalRef, comp => {
|
||||||
return this.apiService.getProviderUsers(this.providerId);
|
comp.providerId = this.providerId;
|
||||||
}
|
comp.users = this.getCheckedUsers();
|
||||||
|
|
||||||
deleteUser(id: string): Promise<any> {
|
|
||||||
return this.apiService.deleteProviderUser(this.providerId, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
reinviteUser(id: string): Promise<any> {
|
|
||||||
return this.apiService.postProviderUserReinvite(this.providerId, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async confirmUser(user: ProviderUserUserDetailsResponse, publicKey: Uint8Array): Promise<any> {
|
|
||||||
const providerKey = await this.cryptoService.getProviderKey(this.providerId);
|
|
||||||
const key = await this.cryptoService.rsaEncrypt(providerKey.key, publicKey.buffer);
|
|
||||||
const request = new ProviderUserConfirmRequest();
|
|
||||||
request.key = key.encryptedString;
|
|
||||||
await this.apiService.postProviderUserConfirm(this.providerId, user.id, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
async edit(user: ProviderUserUserDetailsResponse) {
|
|
||||||
const [modal] = await this.modalService.openViewRef(
|
|
||||||
UserAddEditComponent,
|
|
||||||
this.addEditModalRef,
|
|
||||||
(comp) => {
|
|
||||||
comp.name = this.userNamePipe.transform(user);
|
|
||||||
comp.providerId = this.providerId;
|
|
||||||
comp.providerUserId = user != null ? user.id : null;
|
|
||||||
comp.onSavedUser.subscribe(() => {
|
|
||||||
modal.close();
|
|
||||||
this.load();
|
|
||||||
});
|
});
|
||||||
comp.onDeletedUser.subscribe(() => {
|
|
||||||
modal.close();
|
await modal.onClosedPromise();
|
||||||
this.removeUser(user);
|
await this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async bulkReinvite() {
|
||||||
|
if (this.actionPromise != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = this.getCheckedUsers();
|
||||||
|
const filteredUsers = users.filter(u => u.status === ProviderUserStatusType.Invited);
|
||||||
|
|
||||||
|
if (filteredUsers.length <= 0) {
|
||||||
|
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||||
|
this.i18nService.t('noSelectedUsersApplicable'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new ProviderUserBulkRequest(filteredUsers.map(user => user.id));
|
||||||
|
const response = this.apiService.postManyProviderUserReinvite(this.providerId, request);
|
||||||
|
this.showBulkStatus(users, filteredUsers, response, this.i18nService.t('bulkReinviteMessage'));
|
||||||
|
} catch (e) {
|
||||||
|
this.validationService.showError(e);
|
||||||
|
}
|
||||||
|
this.actionPromise = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async bulkConfirm() {
|
||||||
|
if (this.actionPromise != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [modal] = await this.modalService.openViewRef(BulkConfirmComponent, this.bulkConfirmModalRef, comp => {
|
||||||
|
comp.providerId = this.providerId;
|
||||||
|
comp.users = this.getCheckedUsers();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async events(user: ProviderUserUserDetailsResponse) {
|
await modal.onClosedPromise();
|
||||||
await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, (comp) => {
|
await this.load();
|
||||||
comp.name = this.userNamePipe.transform(user);
|
|
||||||
comp.providerId = this.providerId;
|
|
||||||
comp.entityId = user.id;
|
|
||||||
comp.showUser = false;
|
|
||||||
comp.entity = "user";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async bulkRemove() {
|
|
||||||
if (this.actionPromise != null) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const [modal] = await this.modalService.openViewRef(
|
private async showBulkStatus(users: ProviderUserUserDetailsResponse[], filteredUsers: ProviderUserUserDetailsResponse[],
|
||||||
BulkRemoveComponent,
|
request: Promise<ListResponse<ProviderUserBulkResponse>>, successfullMessage: string) {
|
||||||
this.bulkRemoveModalRef,
|
|
||||||
(comp) => {
|
|
||||||
comp.providerId = this.providerId;
|
|
||||||
comp.users = this.getCheckedUsers();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await modal.onClosedPromise();
|
const [modal, childComponent] = await this.modalService.openViewRef(BulkStatusComponent, this.bulkStatusModalRef, comp => {
|
||||||
await this.load();
|
comp.loading = true;
|
||||||
}
|
|
||||||
|
|
||||||
async bulkReinvite() {
|
|
||||||
if (this.actionPromise != null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const users = this.getCheckedUsers();
|
|
||||||
const filteredUsers = users.filter((u) => u.status === ProviderUserStatusType.Invited);
|
|
||||||
|
|
||||||
if (filteredUsers.length <= 0) {
|
|
||||||
this.platformUtilsService.showToast(
|
|
||||||
"error",
|
|
||||||
this.i18nService.t("errorOccurred"),
|
|
||||||
this.i18nService.t("noSelectedUsersApplicable")
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const request = new ProviderUserBulkRequest(filteredUsers.map((user) => user.id));
|
|
||||||
const response = this.apiService.postManyProviderUserReinvite(this.providerId, request);
|
|
||||||
this.showBulkStatus(
|
|
||||||
users,
|
|
||||||
filteredUsers,
|
|
||||||
response,
|
|
||||||
this.i18nService.t("bulkReinviteMessage")
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
this.validationService.showError(e);
|
|
||||||
}
|
|
||||||
this.actionPromise = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async bulkConfirm() {
|
|
||||||
if (this.actionPromise != null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [modal] = await this.modalService.openViewRef(
|
|
||||||
BulkConfirmComponent,
|
|
||||||
this.bulkConfirmModalRef,
|
|
||||||
(comp) => {
|
|
||||||
comp.providerId = this.providerId;
|
|
||||||
comp.users = this.getCheckedUsers();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await modal.onClosedPromise();
|
|
||||||
await this.load();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async showBulkStatus(
|
|
||||||
users: ProviderUserUserDetailsResponse[],
|
|
||||||
filteredUsers: ProviderUserUserDetailsResponse[],
|
|
||||||
request: Promise<ListResponse<ProviderUserBulkResponse>>,
|
|
||||||
successfullMessage: string
|
|
||||||
) {
|
|
||||||
const [modal, childComponent] = await this.modalService.openViewRef(
|
|
||||||
BulkStatusComponent,
|
|
||||||
this.bulkStatusModalRef,
|
|
||||||
(comp) => {
|
|
||||||
comp.loading = true;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Workaround to handle closing the modal shortly after it has been opened
|
|
||||||
let close = false;
|
|
||||||
modal.onShown.subscribe(() => {
|
|
||||||
if (close) {
|
|
||||||
modal.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await request;
|
|
||||||
|
|
||||||
if (modal) {
|
|
||||||
const keyedErrors: any = response.data
|
|
||||||
.filter((r) => r.error !== "")
|
|
||||||
.reduce((a, x) => ({ ...a, [x.id]: x.error }), {});
|
|
||||||
const keyedFilteredUsers: any = filteredUsers.reduce((a, x) => ({ ...a, [x.id]: x }), {});
|
|
||||||
|
|
||||||
childComponent.users = users.map((user) => {
|
|
||||||
let message = keyedErrors[user.id] ?? successfullMessage;
|
|
||||||
// eslint-disable-next-line
|
|
||||||
if (!keyedFilteredUsers.hasOwnProperty(user.id)) {
|
|
||||||
message = this.i18nService.t("bulkFilteredMessage");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
user: user,
|
|
||||||
error: keyedErrors.hasOwnProperty(user.id), // eslint-disable-line
|
|
||||||
message: message,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
childComponent.loading = false;
|
|
||||||
}
|
// Workaround to handle closing the modal shortly after it has been opened
|
||||||
} catch {
|
let close = false;
|
||||||
close = true;
|
modal.onShown.subscribe(() => {
|
||||||
modal.close();
|
if (close) {
|
||||||
|
modal.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await request;
|
||||||
|
|
||||||
|
if (modal) {
|
||||||
|
const keyedErrors: any = response.data.filter(r => r.error !== '').reduce((a, x) => ({ ...a, [x.id]: x.error }), {});
|
||||||
|
const keyedFilteredUsers: any = filteredUsers.reduce((a, x) => ({ ...a, [x.id]: x }), {});
|
||||||
|
|
||||||
|
childComponent.users = users.map(user => {
|
||||||
|
let message = keyedErrors[user.id] ?? successfullMessage;
|
||||||
|
if (!keyedFilteredUsers.hasOwnProperty(user.id)) {
|
||||||
|
message = this.i18nService.t('bulkFilteredMessage');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: user,
|
||||||
|
error: keyedErrors.hasOwnProperty(user.id),
|
||||||
|
message: message,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
childComponent.loading = false;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
close = true;
|
||||||
|
modal.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,124 +1,71 @@
|
|||||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
|
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
|
||||||
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
||||||
<form
|
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||||
class="modal-content"
|
<div class="modal-header">
|
||||||
#form
|
<h2 class="modal-title" id="userAddEditTitle">
|
||||||
(ngSubmit)="submit()"
|
{{title}}
|
||||||
[appApiAction]="formPromise"
|
<small class="text-muted" *ngIf="name">{{name}}</small>
|
||||||
ngNativeValidate
|
</h2>
|
||||||
>
|
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||||
<div class="modal-header">
|
<span aria-hidden="true">×</span>
|
||||||
<h2 class="modal-title" id="userAddEditTitle">
|
</button>
|
||||||
{{ title }}
|
</div>
|
||||||
<small class="text-muted" *ngIf="name">{{ name }}</small>
|
<div class="modal-body" *ngIf="loading">
|
||||||
</h2>
|
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
<button
|
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||||
type="button"
|
</div>
|
||||||
class="close"
|
<div class="modal-body" *ngIf="!loading">
|
||||||
data-dismiss="modal"
|
<ng-container *ngIf="!editMode">
|
||||||
appA11yTitle="{{ 'close' | i18n }}"
|
<p>{{'providerInviteUserDesc' | i18n}}</p>
|
||||||
>
|
<div class="form-group mb-4">
|
||||||
<span aria-hidden="true">×</span>
|
<label for="emails">{{'email' | i18n}}</label>
|
||||||
</button>
|
<input id="emails" class="form-control" type="text" name="Emails" [(ngModel)]="emails" required
|
||||||
</div>
|
appAutoFocus>
|
||||||
<div class="modal-body" *ngIf="loading">
|
<small class="text-muted">{{'inviteMultipleEmailDesc' | i18n : '20'}}</small>
|
||||||
<i
|
</div>
|
||||||
class="bwi bwi-spinner bwi-spin text-muted"
|
</ng-container>
|
||||||
title="{{ 'loading' | i18n }}"
|
<h3>
|
||||||
aria-hidden="true"
|
{{'userType' | i18n}}
|
||||||
></i>
|
<a target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}"
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
href="https://bitwarden.com/help/article/user-types-access-control/#user-types">
|
||||||
</div>
|
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
|
||||||
<div class="modal-body" *ngIf="!loading">
|
</a>
|
||||||
<ng-container *ngIf="!editMode">
|
</h3>
|
||||||
<p>{{ "providerInviteUserDesc" | i18n }}</p>
|
<div class="form-check mt-2 form-check-block">
|
||||||
<div class="form-group mb-4">
|
<input class="form-check-input" type="radio" name="userType" id="userTypeServiceUser"
|
||||||
<label for="emails">{{ "email" | i18n }}</label>
|
[value]="userType.ServiceUser" [(ngModel)]="type">
|
||||||
<input
|
<label class="form-check-label" for="userTypeServiceUser">
|
||||||
id="emails"
|
{{'serviceUser' | i18n}}
|
||||||
class="form-control"
|
<small>{{'serviceUserDesc' | i18n}}</small>
|
||||||
type="text"
|
</label>
|
||||||
name="Emails"
|
</div>
|
||||||
[(ngModel)]="emails"
|
<div class="form-check mt-2 form-check-block">
|
||||||
required
|
<input class="form-check-input" type="radio" name="userType" id="userTypeProviderAdmin"
|
||||||
appAutoFocus
|
[value]="userType.ProviderAdmin" [(ngModel)]="type">
|
||||||
/>
|
<label class="form-check-label" for="userTypeProviderAdmin">
|
||||||
<small class="text-muted">{{ "inviteMultipleEmailDesc" | i18n: "20" }}</small>
|
{{'providerAdmin' | i18n}}
|
||||||
</div>
|
<small>{{'providerAdminDesc' | i18n}}</small>
|
||||||
</ng-container>
|
</label>
|
||||||
<h3>
|
</div>
|
||||||
{{ "userType" | i18n }}
|
</div>
|
||||||
<a
|
<div class="modal-footer">
|
||||||
target="_blank"
|
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||||
rel="noopener"
|
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
<span>{{'save' | i18n}}</span>
|
||||||
href="https://bitwarden.com/help/provider-users/"
|
</button>
|
||||||
>
|
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
{{'cancel' | i18n}}
|
||||||
</a>
|
</button>
|
||||||
</h3>
|
<div class="ml-auto">
|
||||||
<div class="form-check mt-2 form-check-block">
|
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
|
||||||
<input
|
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"
|
||||||
class="form-check-input"
|
[appApiAction]="deletePromise">
|
||||||
type="radio"
|
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading" aria-hidden="true"></i>
|
||||||
name="userType"
|
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"
|
||||||
id="userTypeServiceUser"
|
title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
[value]="userType.ServiceUser"
|
</button>
|
||||||
[(ngModel)]="type"
|
</div>
|
||||||
/>
|
</div>
|
||||||
<label class="form-check-label" for="userTypeServiceUser">
|
</form>
|
||||||
{{ "serviceUser" | i18n }}
|
</div>
|
||||||
<small>{{ "serviceUserDesc" | i18n }}</small>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check mt-2 form-check-block">
|
|
||||||
<input
|
|
||||||
class="form-check-input"
|
|
||||||
type="radio"
|
|
||||||
name="userType"
|
|
||||||
id="userTypeProviderAdmin"
|
|
||||||
[value]="userType.ProviderAdmin"
|
|
||||||
[(ngModel)]="type"
|
|
||||||
/>
|
|
||||||
<label class="form-check-label" for="userTypeProviderAdmin">
|
|
||||||
{{ "providerAdmin" | i18n }}
|
|
||||||
<small>{{ "providerAdminDesc" | i18n }}</small>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
|
||||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
|
||||||
<span>{{ "save" | i18n }}</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
|
||||||
{{ "cancel" | i18n }}
|
|
||||||
</button>
|
|
||||||
<div class="ml-auto">
|
|
||||||
<button
|
|
||||||
#deleteBtn
|
|
||||||
type="button"
|
|
||||||
(click)="delete()"
|
|
||||||
class="btn btn-outline-danger"
|
|
||||||
appA11yTitle="{{ 'delete' | i18n }}"
|
|
||||||
*ngIf="editMode"
|
|
||||||
[disabled]="deleteBtn.loading"
|
|
||||||
[appApiAction]="deletePromise"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-trash bwi-lg bwi-fw"
|
|
||||||
[hidden]="deleteBtn.loading"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
|
||||||
[hidden]="!deleteBtn.loading"
|
|
||||||
title="{{ 'loading' | i18n }}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,118 +1,112 @@
|
|||||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
import {
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
OnInit,
|
||||||
|
Output,
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
import { ToasterService } from 'angular2-toaster';
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
|
||||||
import { LogService } from "jslib-common/abstractions/log.service";
|
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||||
import { ProviderUserType } from "jslib-common/enums/providerUserType";
|
import { LogService } from 'jslib-common/abstractions/log.service';
|
||||||
import { PermissionsApi } from "jslib-common/models/api/permissionsApi";
|
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||||
import { ProviderUserInviteRequest } from "jslib-common/models/request/provider/providerUserInviteRequest";
|
|
||||||
import { ProviderUserUpdateRequest } from "jslib-common/models/request/provider/providerUserUpdateRequest";
|
import { ProviderUserInviteRequest } from 'jslib-common/models/request/provider/providerUserInviteRequest';
|
||||||
|
|
||||||
|
import { PermissionsApi } from 'jslib-common/models/api/permissionsApi';
|
||||||
|
|
||||||
|
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;
|
||||||
@Input() providerUserId: string;
|
@Input() providerUserId: string;
|
||||||
@Input() providerId: string;
|
@Input() providerId: string;
|
||||||
@Output() onSavedUser = new EventEmitter();
|
@Output() onSavedUser = new EventEmitter();
|
||||||
@Output() onDeletedUser = new EventEmitter();
|
@Output() onDeletedUser = new EventEmitter();
|
||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
editMode = false;
|
editMode: boolean = false;
|
||||||
title: string;
|
title: string;
|
||||||
emails: string;
|
emails: string;
|
||||||
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(
|
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||||
private apiService: ApiService,
|
private toasterService: ToasterService, private platformUtilsService: PlatformUtilsService,
|
||||||
private i18nService: I18nService,
|
private logService: LogService) { }
|
||||||
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;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
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;
|
async submit() {
|
||||||
}
|
try {
|
||||||
|
if (this.editMode) {
|
||||||
async submit() {
|
const request = new ProviderUserUpdateRequest();
|
||||||
try {
|
request.type = this.type;
|
||||||
if (this.editMode) {
|
this.formPromise = this.apiService.putProviderUser(this.providerId, this.providerUserId, request);
|
||||||
const request = new ProviderUserUpdateRequest();
|
} else {
|
||||||
request.type = this.type;
|
const request = new ProviderUserInviteRequest();
|
||||||
this.formPromise = this.apiService.putProviderUser(
|
request.emails = this.emails.trim().split(/\s*,\s*/);
|
||||||
this.providerId,
|
request.type = this.type;
|
||||||
this.providerUserId,
|
this.formPromise = this.apiService.postProviderUserInvite(this.providerId, request);
|
||||||
request
|
}
|
||||||
);
|
await this.formPromise;
|
||||||
} else {
|
this.toasterService.popAsync('success', null,
|
||||||
const request = new ProviderUserInviteRequest();
|
this.i18nService.t(this.editMode ? 'editedUserId' : 'invitedUsers', this.name));
|
||||||
request.emails = this.emails.trim().split(/\s*,\s*/);
|
this.onSavedUser.emit();
|
||||||
request.type = this.type;
|
} catch (e) {
|
||||||
this.formPromise = this.apiService.postProviderUserInvite(this.providerId, request);
|
this.logService.error(e);
|
||||||
}
|
}
|
||||||
await this.formPromise;
|
|
||||||
this.platformUtilsService.showToast(
|
|
||||||
"success",
|
|
||||||
null,
|
|
||||||
this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.name)
|
|
||||||
);
|
|
||||||
this.onSavedUser.emit();
|
|
||||||
} catch (e) {
|
|
||||||
this.logService.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete() {
|
|
||||||
if (!this.editMode) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmed = await this.platformUtilsService.showDialog(
|
async delete() {
|
||||||
this.i18nService.t("removeUserConfirmation"),
|
if (!this.editMode) {
|
||||||
this.name,
|
return;
|
||||||
this.i18nService.t("yes"),
|
}
|
||||||
this.i18nService.t("no"),
|
|
||||||
"warning"
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
);
|
this.i18nService.t('removeUserConfirmation'), this.name,
|
||||||
if (!confirmed) {
|
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||||
return false;
|
if (!confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.deletePromise = this.apiService.deleteProviderUser(this.providerId, this.providerUserId);
|
||||||
|
await this.deletePromise;
|
||||||
|
this.toasterService.popAsync('success', null, this.i18nService.t('removedUserId', this.name));
|
||||||
|
this.onDeletedUser.emit();
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
this.deletePromise = this.apiService.deleteProviderUser(this.providerId, this.providerUserId);
|
|
||||||
await this.deletePromise;
|
|
||||||
this.platformUtilsService.showToast(
|
|
||||||
"success",
|
|
||||||
null,
|
|
||||||
this.i18nService.t("removedUserId", this.name)
|
|
||||||
);
|
|
||||||
this.onDeletedUser.emit();
|
|
||||||
} catch (e) {
|
|
||||||
this.logService.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,44 @@
|
|||||||
<app-navbar></app-navbar>
|
<app-navbar></app-navbar>
|
||||||
<div class="org-nav" *ngIf="provider">
|
<div class="org-nav" *ngIf="provider">
|
||||||
<div class="container d-flex">
|
<div class="container d-flex">
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<div class="my-auto d-flex align-items-center pl-1">
|
<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 class="ml-3 card border-danger text-danger bg-transparent" *ngIf="!provider.enabled">
|
||||||
|
<div class="card-body py-2">
|
||||||
|
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||||
|
{{'providerIsDisabled' | i18n}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="nav nav-tabs" *ngIf="showMenuBar">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" routerLink="clients" routerLinkActive="active">
|
||||||
|
<i class="fa fa-university" aria-hidden="true"></i>
|
||||||
|
{{'clients' | i18n}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" *ngIf="showManageTab">
|
||||||
|
<a class="nav-link" [routerLink]="manageRoute" routerLinkActive="active">
|
||||||
|
<i class="fa fa-sliders" aria-hidden="true"></i>
|
||||||
|
{{'manage' | i18n}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" *ngIf="showSettingsTab">
|
||||||
|
<a class="nav-link" routerLink="settings" routerLinkActive="active">
|
||||||
|
<i class="fa fa-cogs" aria-hidden="true"></i>
|
||||||
|
{{'settings' | i18n}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3 card border-danger text-danger bg-transparent" *ngIf="!provider.enabled">
|
|
||||||
<div class="card-body py-2">
|
|
||||||
<i class="bwi bwi-exclamation-triangle" aria-hidden="true"></i>
|
|
||||||
{{ "providerIsDisabled" | i18n }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ul class="nav nav-tabs" *ngIf="showMenuBar">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" routerLink="clients" routerLinkActive="active">
|
|
||||||
<i class="bwi bwi-bank" aria-hidden="true"></i>
|
|
||||||
{{ "clients" | i18n }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item" *ngIf="showManageTab">
|
|
||||||
<a class="nav-link" [routerLink]="manageRoute" routerLinkActive="active">
|
|
||||||
<i class="bwi bwi-sliders" aria-hidden="true"></i>
|
|
||||||
{{ "manage" | i18n }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item" *ngIf="showSettingsTab">
|
|
||||||
<a class="nav-link" routerLink="settings" routerLinkActive="active">
|
|
||||||
<i class="bwi bwi-cogs" aria-hidden="true"></i>
|
|
||||||
{{ "settings" | i18n }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="container page-content">
|
<div class="container page-content">
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</div>
|
</div>
|
||||||
<app-footer></app-footer>
|
<app-footer></app-footer>
|
||||||
|
|||||||
@@ -1,49 +1,51 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from '@angular/core';
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
import { UserService } from 'jslib-common/abstractions/user.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;
|
|
||||||
private providerId: string;
|
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute, private providerService: ProviderService) {}
|
provider: Provider;
|
||||||
|
private providerId: string;
|
||||||
|
|
||||||
ngOnInit() {
|
constructor(private route: ActivatedRoute, private userService: UserService) { }
|
||||||
document.body.classList.remove("layout_frontend");
|
|
||||||
this.route.params.subscribe(async (params) => {
|
|
||||||
this.providerId = params.providerId;
|
|
||||||
await this.load();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async load() {
|
ngOnInit() {
|
||||||
this.provider = await this.providerService.get(this.providerId);
|
document.body.classList.remove('layout_frontend');
|
||||||
}
|
this.route.params.subscribe(async params => {
|
||||||
|
this.providerId = params.providerId;
|
||||||
get showMenuBar() {
|
await this.load();
|
||||||
return this.showManageTab || this.showSettingsTab;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get showManageTab() {
|
async load() {
|
||||||
return this.provider.canManageUsers || this.provider.canAccessEventLogs;
|
this.provider = await this.userService.getProvider(this.providerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
get showSettingsTab() {
|
get showMenuBar() {
|
||||||
return this.provider.isProviderAdmin;
|
return this.showManageTab || this.showSettingsTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
get manageRoute(): string {
|
get showManageTab() {
|
||||||
switch (true) {
|
return this.provider.canManageUsers || this.provider.canAccessEventLogs;
|
||||||
case this.provider.canManageUsers:
|
}
|
||||||
return "manage/people";
|
|
||||||
case this.provider.canAccessEventLogs:
|
get showSettingsTab() {
|
||||||
return "manage/events";
|
return this.provider.isProviderAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
get manageRoute(): string {
|
||||||
|
switch (true) {
|
||||||
|
case this.provider.canManageUsers:
|
||||||
|
return 'manage/people';
|
||||||
|
case this.provider.canAccessEventLogs:
|
||||||
|
return 'manage/events';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,121 +1,123 @@
|
|||||||
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 { FrontendLayoutComponent } from "src/app/layouts/frontend-layout.component";
|
import { AddOrganizationComponent } from './clients/add-organization.component';
|
||||||
import { ProvidersComponent } from "src/app/providers/providers.component";
|
import { ClientsComponent } from './clients/clients.component';
|
||||||
|
import { CreateOrganizationComponent } from './clients/create-organization.component';
|
||||||
|
import { AcceptProviderComponent } from './manage/accept-provider.component';
|
||||||
|
import { EventsComponent } from './manage/events.component';
|
||||||
|
import { ManageComponent } from './manage/manage.component';
|
||||||
|
import { PeopleComponent } from './manage/people.component';
|
||||||
|
import { ProvidersLayoutComponent } from './providers-layout.component';
|
||||||
|
import { SettingsComponent } from './settings/settings.component';
|
||||||
|
import { SetupProviderComponent } from './setup/setup-provider.component';
|
||||||
|
import { SetupComponent } from './setup/setup.component';
|
||||||
|
|
||||||
import { ClientsComponent } from "./clients/clients.component";
|
import { FrontendLayoutComponent } from 'src/app/layouts/frontend-layout.component';
|
||||||
import { CreateOrganizationComponent } from "./clients/create-organization.component";
|
|
||||||
import { AcceptProviderComponent } from "./manage/accept-provider.component";
|
import { ProvidersComponent } from 'src/app/providers/providers.component';
|
||||||
import { EventsComponent } from "./manage/events.component";
|
import { ProviderGuardService } from './services/provider-guard.service';
|
||||||
import { ManageComponent } from "./manage/manage.component";
|
import { ProviderTypeGuardService } from './services/provider-type-guard.service';
|
||||||
import { PeopleComponent } from "./manage/people.component";
|
import { AccountComponent } from './settings/account.component';
|
||||||
import { ProvidersLayoutComponent } from "./providers-layout.component";
|
|
||||||
import { ProviderGuardService } from "./services/provider-guard.service";
|
|
||||||
import { ProviderTypeGuardService } from "./services/provider-type-guard.service";
|
|
||||||
import { AccountComponent } from "./settings/account.component";
|
|
||||||
import { SettingsComponent } from "./settings/settings.component";
|
|
||||||
import { SetupProviderComponent } from "./setup/setup-provider.component";
|
|
||||||
import { SetupComponent } from "./setup/setup.component";
|
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: "",
|
path: '',
|
||||||
canActivate: [AuthGuardService],
|
canActivate: [AuthGuardService],
|
||||||
component: ProvidersComponent,
|
component: ProvidersComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "",
|
path: '',
|
||||||
component: FrontendLayoutComponent,
|
component: FrontendLayoutComponent,
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "setup-provider",
|
|
||||||
component: SetupProviderComponent,
|
|
||||||
data: { titleId: "setupProvider" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "accept-provider",
|
|
||||||
component: AcceptProviderComponent,
|
|
||||||
data: { titleId: "acceptProvider" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "",
|
|
||||||
canActivate: [AuthGuardService],
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "setup",
|
|
||||||
component: SetupComponent,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ":providerId",
|
|
||||||
component: ProvidersLayoutComponent,
|
|
||||||
canActivate: [ProviderGuardService],
|
|
||||||
children: [
|
children: [
|
||||||
{ path: "", pathMatch: "full", redirectTo: "clients" },
|
{
|
||||||
{ path: "clients/create", component: CreateOrganizationComponent },
|
path: 'setup-provider',
|
||||||
{ path: "clients", component: ClientsComponent, data: { titleId: "clients" } },
|
component: SetupProviderComponent,
|
||||||
{
|
data: { titleId: 'setupProvider' },
|
||||||
path: "manage",
|
},
|
||||||
component: ManageComponent,
|
{
|
||||||
children: [
|
path: 'accept-provider',
|
||||||
{
|
component: AcceptProviderComponent,
|
||||||
path: "",
|
data: { titleId: 'acceptProvider' },
|
||||||
pathMatch: "full",
|
},
|
||||||
redirectTo: "people",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "people",
|
|
||||||
component: PeopleComponent,
|
|
||||||
canActivate: [ProviderTypeGuardService],
|
|
||||||
data: {
|
|
||||||
titleId: "people",
|
|
||||||
permissions: [Permissions.ManageUsers],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "events",
|
|
||||||
component: EventsComponent,
|
|
||||||
canActivate: [ProviderTypeGuardService],
|
|
||||||
data: {
|
|
||||||
titleId: "eventLogs",
|
|
||||||
permissions: [Permissions.AccessEventLogs],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "settings",
|
|
||||||
component: SettingsComponent,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "",
|
|
||||||
pathMatch: "full",
|
|
||||||
redirectTo: "account",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "account",
|
|
||||||
component: AccountComponent,
|
|
||||||
canActivate: [ProviderTypeGuardService],
|
|
||||||
data: {
|
|
||||||
titleId: "myProvider",
|
|
||||||
permissions: [Permissions.ManageProvider],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
{
|
||||||
},
|
path: '',
|
||||||
|
canActivate: [AuthGuardService],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'setup',
|
||||||
|
component: SetupComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':providerId',
|
||||||
|
component: ProvidersLayoutComponent,
|
||||||
|
canActivate: [ProviderGuardService],
|
||||||
|
children: [
|
||||||
|
{ path: '', pathMatch: 'full', redirectTo: 'clients' },
|
||||||
|
{ path: 'clients/create', component: CreateOrganizationComponent },
|
||||||
|
{ path: 'clients', component: ClientsComponent, data: { titleId: 'clients' } },
|
||||||
|
{
|
||||||
|
path: 'manage',
|
||||||
|
component: ManageComponent,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
pathMatch: 'full',
|
||||||
|
redirectTo: 'people',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'people',
|
||||||
|
component: PeopleComponent,
|
||||||
|
canActivate: [ProviderTypeGuardService],
|
||||||
|
data: {
|
||||||
|
titleId: 'people',
|
||||||
|
permissions: [Permissions.ManageUsers],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'events',
|
||||||
|
component: EventsComponent,
|
||||||
|
canActivate: [ProviderTypeGuardService],
|
||||||
|
data: {
|
||||||
|
titleId: 'eventLogs',
|
||||||
|
permissions: [Permissions.AccessEventLogs],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'settings',
|
||||||
|
component: SettingsComponent,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
pathMatch: 'full',
|
||||||
|
redirectTo: 'account',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'account',
|
||||||
|
component: AccountComponent,
|
||||||
|
canActivate: [ProviderTypeGuardService],
|
||||||
|
data: {
|
||||||
|
titleId: 'myProvider',
|
||||||
|
permissions: [Permissions.ManageProvider],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forChild(routes)],
|
imports: [RouterModule.forChild(routes)],
|
||||||
exports: [RouterModule],
|
exports: [RouterModule],
|
||||||
})
|
})
|
||||||
export class ProvidersRoutingModule {}
|
export class ProvidersRoutingModule { }
|
||||||
|
|||||||
@@ -1,58 +1,69 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from '@angular/common';
|
||||||
import { ComponentFactoryResolver, NgModule } from "@angular/core";
|
import { ComponentFactoryResolver } from '@angular/core';
|
||||||
import { FormsModule } from "@angular/forms";
|
import { NgModule } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
|
||||||
import { JslibModule } from "jslib-angular/jslib.module";
|
import { ModalService } from 'jslib-angular/services/modal.service';
|
||||||
import { ModalService } from "jslib-angular/services/modal.service";
|
|
||||||
|
|
||||||
import { OssModule } from "src/app/oss.module";
|
import { ProviderGuardService } from './services/provider-guard.service';
|
||||||
|
import { ProviderTypeGuardService } from './services/provider-type-guard.service';
|
||||||
|
import { ProviderService } from './services/provider.service';
|
||||||
|
|
||||||
import { AddOrganizationComponent } from "./clients/add-organization.component";
|
import { ProvidersLayoutComponent } from './providers-layout.component';
|
||||||
import { ClientsComponent } from "./clients/clients.component";
|
import { ProvidersRoutingModule } from './providers-routing.module';
|
||||||
import { CreateOrganizationComponent } from "./clients/create-organization.component";
|
|
||||||
import { AcceptProviderComponent } from "./manage/accept-provider.component";
|
import { AddOrganizationComponent } from './clients/add-organization.component';
|
||||||
import { BulkConfirmComponent } from "./manage/bulk/bulk-confirm.component";
|
import { ClientsComponent } from './clients/clients.component';
|
||||||
import { BulkRemoveComponent } from "./manage/bulk/bulk-remove.component";
|
import { CreateOrganizationComponent } from './clients/create-organization.component';
|
||||||
import { EventsComponent } from "./manage/events.component";
|
|
||||||
import { ManageComponent } from "./manage/manage.component";
|
import { AcceptProviderComponent } from './manage/accept-provider.component';
|
||||||
import { PeopleComponent } from "./manage/people.component";
|
import { BulkConfirmComponent } from './manage/bulk/bulk-confirm.component';
|
||||||
import { UserAddEditComponent } from "./manage/user-add-edit.component";
|
import { BulkRemoveComponent } from './manage/bulk/bulk-remove.component';
|
||||||
import { ProvidersLayoutComponent } from "./providers-layout.component";
|
import { EventsComponent } from './manage/events.component';
|
||||||
import { ProvidersRoutingModule } from "./providers-routing.module";
|
import { ManageComponent } from './manage/manage.component';
|
||||||
import { ProviderGuardService } from "./services/provider-guard.service";
|
import { PeopleComponent } from './manage/people.component';
|
||||||
import { ProviderTypeGuardService } from "./services/provider-type-guard.service";
|
import { UserAddEditComponent } from './manage/user-add-edit.component';
|
||||||
import { WebProviderService } from "./services/webProvider.service";
|
|
||||||
import { AccountComponent } from "./settings/account.component";
|
import { AccountComponent } from './settings/account.component';
|
||||||
import { SettingsComponent } from "./settings/settings.component";
|
import { SettingsComponent } from './settings/settings.component';
|
||||||
import { SetupProviderComponent } from "./setup/setup-provider.component";
|
|
||||||
import { SetupComponent } from "./setup/setup.component";
|
import { SetupProviderComponent } from './setup/setup-provider.component';
|
||||||
|
import { SetupComponent } from './setup/setup.component';
|
||||||
|
|
||||||
|
import { OssModule } from 'src/app/oss.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [CommonModule, FormsModule, OssModule, JslibModule, ProvidersRoutingModule],
|
imports: [
|
||||||
declarations: [
|
CommonModule,
|
||||||
AcceptProviderComponent,
|
FormsModule,
|
||||||
AccountComponent,
|
OssModule,
|
||||||
AddOrganizationComponent,
|
ProvidersRoutingModule,
|
||||||
BulkConfirmComponent,
|
],
|
||||||
BulkRemoveComponent,
|
declarations: [
|
||||||
ClientsComponent,
|
AcceptProviderComponent,
|
||||||
CreateOrganizationComponent,
|
AccountComponent,
|
||||||
EventsComponent,
|
AddOrganizationComponent,
|
||||||
ManageComponent,
|
BulkConfirmComponent,
|
||||||
PeopleComponent,
|
BulkRemoveComponent,
|
||||||
ProvidersLayoutComponent,
|
ClientsComponent,
|
||||||
SettingsComponent,
|
CreateOrganizationComponent,
|
||||||
SetupComponent,
|
EventsComponent,
|
||||||
SetupProviderComponent,
|
ManageComponent,
|
||||||
UserAddEditComponent,
|
PeopleComponent,
|
||||||
],
|
ProvidersLayoutComponent,
|
||||||
providers: [WebProviderService, ProviderGuardService, ProviderTypeGuardService],
|
SettingsComponent,
|
||||||
|
SetupComponent,
|
||||||
|
SetupProviderComponent,
|
||||||
|
UserAddEditComponent,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
ProviderService,
|
||||||
|
ProviderGuardService,
|
||||||
|
ProviderTypeGuardService,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class ProvidersModule {
|
export class ProvidersModule {
|
||||||
constructor(modalService: ModalService, componentFactoryResolver: ComponentFactoryResolver) {
|
constructor(modalService: ModalService, componentFactoryResolver: ComponentFactoryResolver) {
|
||||||
modalService.registerComponentFactoryResolver(
|
modalService.registerComponentFactoryResolver(AddOrganizationComponent, componentFactoryResolver);
|
||||||
AddOrganizationComponent,
|
}
|
||||||
componentFactoryResolver
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,32 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from '@angular/core';
|
||||||
import { ActivatedRouteSnapshot, CanActivate, Router } from "@angular/router";
|
import {
|
||||||
|
ActivatedRouteSnapshot,
|
||||||
|
CanActivate,
|
||||||
|
Router,
|
||||||
|
} from '@angular/router';
|
||||||
|
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
import { ToasterService } from 'angular2-toaster';
|
||||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
|
||||||
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||||
|
import { UserService } from 'jslib-common/abstractions/user.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ProviderGuardService implements CanActivate {
|
export class ProviderGuardService implements CanActivate {
|
||||||
constructor(
|
constructor(private userService: UserService, private router: Router,
|
||||||
private router: Router,
|
private toasterService: ToasterService, private i18nService: I18nService) { }
|
||||||
private platformUtilsService: PlatformUtilsService,
|
|
||||||
private i18nService: I18nService,
|
|
||||||
private providerService: ProviderService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async canActivate(route: ActivatedRouteSnapshot) {
|
async canActivate(route: ActivatedRouteSnapshot) {
|
||||||
const provider = await this.providerService.get(route.params.providerId);
|
const provider = await this.userService.getProvider(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.platformUtilsService.showToast("error", null, this.i18nService.t("providerIsDisabled"));
|
this.toasterService.popAsync('error', null, this.i18nService.t('providerIsDisabled'));
|
||||||
this.router.navigate(["/"]);
|
this.router.navigate(['/']);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,31 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from '@angular/core';
|
||||||
import { ActivatedRouteSnapshot, CanActivate, Router } from "@angular/router";
|
import {
|
||||||
|
ActivatedRouteSnapshot,
|
||||||
|
CanActivate,
|
||||||
|
Router,
|
||||||
|
} from '@angular/router';
|
||||||
|
|
||||||
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
import { UserService } from 'jslib-common/abstractions/user.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 providerService: ProviderService, private router: Router) {}
|
constructor(private userService: UserService, private router: Router) { }
|
||||||
|
|
||||||
async canActivate(route: ActivatedRouteSnapshot) {
|
async canActivate(route: ActivatedRouteSnapshot) {
|
||||||
const provider = await this.providerService.get(route.params.providerId);
|
const provider = await this.userService.getProvider(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) ||
|
||||||
(permissions.indexOf(Permissions.ManageProvider) !== -1 && provider.isProviderAdmin) ||
|
(permissions.indexOf(Permissions.ManageProvider) !== -1 && provider.isProviderAdmin) ||
|
||||||
(permissions.indexOf(Permissions.ManageUsers) !== -1 && provider.canManageUsers)
|
(permissions.indexOf(Permissions.ManageUsers) !== -1 && provider.canManageUsers)
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.router.navigate(['/providers', provider.id]);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.router.navigate(["/providers", provider.id]);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,35 +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 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,52 +1,30 @@
|
|||||||
<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
|
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
class="bwi bwi-spinner bwi-spin text-muted"
|
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||||
title="{{ 'loading' | i18n }}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<form
|
<form *ngIf="provider && !loading" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||||
*ngIf="provider && !loading"
|
<div class="row">
|
||||||
#form
|
<div class="col-md-6 mt-4 mt-md-0">
|
||||||
(ngSubmit)="submit()"
|
<div class="form-group">
|
||||||
[appApiAction]="formPromise"
|
<label for="name">{{'providerName' | i18n}}</label>
|
||||||
ngNativeValidate
|
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="provider.name"
|
||||||
>
|
[disabled]="selfHosted">
|
||||||
<div class="row">
|
</div>
|
||||||
<div class="col-6">
|
<div class="form-group">
|
||||||
<div class="form-group">
|
<label for="billingEmail">{{'billingEmail' | i18n}}</label>
|
||||||
<label for="name">{{ "providerName" | i18n }}</label>
|
<input id="billingEmail" class="form-control" type="text" name="BillingEmail"
|
||||||
<input
|
[(ngModel)]="provider.billingEmail" [disabled]="selfHosted">
|
||||||
id="name"
|
</div>
|
||||||
class="form-control"
|
</div>
|
||||||
type="text"
|
<div class="col-md-6 mt-4 mt-md-0">
|
||||||
name="Name"
|
<app-avatar data="{{provider.name}}" dynamic="true" size="75" fontSize="35"></app-avatar>
|
||||||
[(ngModel)]="provider.name"
|
</div>
|
||||||
[disabled]="selfHosted"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="billingEmail">{{ "billingEmail" | i18n }}</label>
|
|
||||||
<input
|
|
||||||
id="billingEmail"
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
name="BillingEmail"
|
|
||||||
[(ngModel)]="provider.billingEmail"
|
|
||||||
[disabled]="selfHosted"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||||
<app-avatar data="{{ provider.name }}" dynamic="true" size="75" fontSize="35"></app-avatar>
|
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
</div>
|
<span>{{'save' | i18n}}</span>
|
||||||
</div>
|
</button>
|
||||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
|
||||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
|
||||||
<span>{{ "save" | i18n }}</span>
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,63 +1,62 @@
|
|||||||
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 { ProviderResponse } from "jslib-common/models/response/provider/providerResponse";
|
import { ProviderUpdateRequest } from 'jslib-common/models/request/provider/providerUpdateRequest';
|
||||||
|
|
||||||
|
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;
|
||||||
loading = true;
|
loading = true;
|
||||||
provider: ProviderResponse;
|
provider: ProviderResponse;
|
||||||
formPromise: Promise<any>;
|
formPromise: Promise<any>;
|
||||||
taxFormPromise: Promise<any>;
|
taxFormPromise: Promise<any>;
|
||||||
|
|
||||||
private providerId: string;
|
private providerId: string;
|
||||||
|
|
||||||
constructor(
|
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||||
private apiService: ApiService,
|
private toasterService: ToasterService, private route: ActivatedRoute,
|
||||||
private i18nService: I18nService,
|
private syncService: SyncService, private platformUtilsService: PlatformUtilsService,
|
||||||
private route: ActivatedRoute,
|
private logService: LogService) { }
|
||||||
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);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logService.error(`Handled exception: ${e}`);
|
this.logService.error(`Handled exception: ${e}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit() {
|
async submit() {
|
||||||
try {
|
try {
|
||||||
const request = new ProviderUpdateRequest();
|
const request = new ProviderUpdateRequest();
|
||||||
request.name = this.provider.name;
|
request.name = this.provider.name;
|
||||||
request.businessName = this.provider.businessName;
|
request.businessName = this.provider.businessName;
|
||||||
request.billingEmail = this.provider.billingEmail;
|
request.billingEmail = this.provider.billingEmail;
|
||||||
|
|
||||||
this.formPromise = this.apiService.putProvider(this.providerId, request).then(() => {
|
this.formPromise = this.apiService.putProvider(this.providerId, request).then(() => {
|
||||||
return this.syncService.fullSync(true);
|
return this.syncService.fullSync(true);
|
||||||
});
|
});
|
||||||
await this.formPromise;
|
await this.formPromise;
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("providerUpdated"));
|
this.toasterService.popAsync('success', null, this.i18nService.t('providerUpdated'));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logService.error(`Handled exception: ${e}`);
|
this.logService.error(`Handled exception: ${e}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<div class="container page-content">
|
<div class="container page-content">
|
||||||
<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 class="col-9">
|
||||||
|
<router-outlet></router-outlet>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-9">
|
|
||||||
<router-outlet></router-outlet>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from '@angular/core';
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
import { ProviderService } from "jslib-common/abstractions/provider.service";
|
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||||
|
import { UserService } from 'jslib-common/abstractions/user.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 providerService: ProviderService) {}
|
constructor(private route: ActivatedRoute, private userService: UserService,
|
||||||
|
private platformUtilsService: PlatformUtilsService) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.route.parent.params.subscribe(async (params) => {
|
this.route.parent.params.subscribe(async params => {
|
||||||
await this.providerService.get(params.providerId);
|
const provider = await this.userService.getProvider(params.providerId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +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
|
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
|
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||||
title="{{ 'loading' | i18n }}"
|
</p>
|
||||||
aria-hidden="true"
|
</div>
|
||||||
></i>
|
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
|
||||||
</p>
|
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
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";
|
|
||||||
|
|
||||||
requiredParameters = ["providerId", "email", "token"];
|
failedShortMessage = 'inviteAcceptFailedShort';
|
||||||
|
failedMessage = 'inviteAcceptFailed';
|
||||||
|
|
||||||
async authedHandler(qParams: any) {
|
requiredParameters = ['providerId', 'email', 'token'];
|
||||||
this.router.navigate(["/providers/setup"], { queryParams: qParams });
|
|
||||||
}
|
|
||||||
|
|
||||||
async unauthedHandler(qParams: any) {
|
async authedHandler(qParams: any) {
|
||||||
// Empty
|
this.router.navigate(['/providers/setup'], {queryParams: qParams});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tslint:disable-next-line
|
||||||
|
async unauthedHandler(qParams: any) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,32 @@
|
|||||||
<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>
|
|
||||||
<p>{{ "setupProviderDesc" | i18n }}</p>
|
|
||||||
|
|
||||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="loading">
|
|
||||||
<h2 class="mt-5">{{ "generalInformation" | i18n }}</h2>
|
|
||||||
<div class="row">
|
|
||||||
<div class="form-group col-6">
|
|
||||||
<label for="name">{{ "providerName" | i18n }}</label>
|
|
||||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group col-6">
|
|
||||||
<label for="billingEmail">{{ "billingEmail" | i18n }}</label>
|
|
||||||
<input
|
|
||||||
id="billingEmail"
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
name="BillingEmail"
|
|
||||||
[(ngModel)]="billingEmail"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p>{{'setupProviderDesc' | i18n}}</p>
|
||||||
|
|
||||||
<div class="mt-4">
|
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="loading">
|
||||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
<h2 class="mt-5">{{'generalInformation' | i18n}}</h2>
|
||||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
<div class="row">
|
||||||
<span>{{ "submit" | i18n }}</span>
|
<div class="form-group col-md-6 mt-4 mt-md-0">
|
||||||
</button>
|
<label for="name">{{'providerName' | i18n}}</label>
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" *ngIf="showCancel">
|
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required>
|
||||||
{{ "cancel" | i18n }}
|
</div>
|
||||||
</button>
|
<div class="form-group col-md-6 mt-4 mt-md-0">
|
||||||
</div>
|
<label for="billingEmail">{{'billingEmail' | i18n}}</label>
|
||||||
</form>
|
<input id="billingEmail" class="form-control" type="text" name="BillingEmail" [(ngModel)]="billingEmail" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||||
|
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
|
<span>{{'submit' | i18n}}</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" *ngIf="showCancel">
|
||||||
|
{{'cancel' | i18n}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<app-footer></app-footer>
|
<app-footer></app-footer>
|
||||||
|
|||||||
@@ -1,97 +1,103 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
import {
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
Component,
|
||||||
import { first } from "rxjs/operators";
|
OnInit,
|
||||||
|
} from '@angular/core';
|
||||||
|
import {
|
||||||
|
ActivatedRoute,
|
||||||
|
Router,
|
||||||
|
} from '@angular/router';
|
||||||
|
import {
|
||||||
|
Toast,
|
||||||
|
ToasterService,
|
||||||
|
} from 'angular2-toaster';
|
||||||
|
|
||||||
import { ValidationService } from "jslib-angular/services/validation.service";
|
import { first } from 'rxjs/operators';
|
||||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
|
||||||
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
|
||||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
|
||||||
import { SyncService } from "jslib-common/abstractions/sync.service";
|
import { ValidationService } from 'jslib-angular/services/validation.service';
|
||||||
import { ProviderSetupRequest } from "jslib-common/models/request/provider/providerSetupRequest";
|
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||||
|
import { SyncService } from 'jslib-common/abstractions/sync.service';
|
||||||
|
import { ProviderSetupRequest } from 'jslib-common/models/request/provider/providerSetupRequest';
|
||||||
|
|
||||||
@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;
|
||||||
authed = false;
|
authed = false;
|
||||||
email: string;
|
email: string;
|
||||||
formPromise: Promise<any>;
|
formPromise: Promise<any>;
|
||||||
|
|
||||||
providerId: string;
|
providerId: string;
|
||||||
token: string;
|
token: string;
|
||||||
name: string;
|
name: string;
|
||||||
billingEmail: string;
|
billingEmail: string;
|
||||||
|
|
||||||
constructor(
|
constructor(private router: Router, private toasterService: ToasterService,
|
||||||
private router: Router,
|
private i18nService: I18nService, private route: ActivatedRoute,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private cryptoService: CryptoService, private apiService: ApiService,
|
||||||
private i18nService: I18nService,
|
private syncService: SyncService, private validationService: ValidationService) { }
|
||||||
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) {
|
||||||
this.platformUtilsService.showToast(
|
const toast: Toast = {
|
||||||
"error",
|
type: 'error',
|
||||||
null,
|
title: null,
|
||||||
this.i18nService.t("emergencyInviteAcceptFailed"),
|
body: this.i18nService.t('emergencyInviteAcceptFailed'),
|
||||||
{ timeout: 10000 }
|
timeout: 10000,
|
||||||
);
|
};
|
||||||
this.router.navigate(["/"]);
|
this.toasterService.popAsync(toast);
|
||||||
return;
|
this.router.navigate(['/']);
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.providerId = qParams.providerId;
|
this.providerId = qParams.providerId;
|
||||||
this.token = qParams.token;
|
this.token = qParams.token;
|
||||||
|
|
||||||
// Check if provider exists, redirect if it does
|
// Check if provider exists, redirect if it does
|
||||||
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(['/']);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit() {
|
async submit() {
|
||||||
this.formPromise = this.doSubmit();
|
this.formPromise = this.doSubmit();
|
||||||
await this.formPromise;
|
await this.formPromise;
|
||||||
this.formPromise = null;
|
this.formPromise = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async doSubmit() {
|
async doSubmit() {
|
||||||
try {
|
try {
|
||||||
const shareKey = await this.cryptoService.makeShareKey();
|
const shareKey = await this.cryptoService.makeShareKey();
|
||||||
const key = shareKey[0].encryptedString;
|
const key = shareKey[0].encryptedString;
|
||||||
|
|
||||||
const request = new ProviderSetupRequest();
|
const request = new ProviderSetupRequest();
|
||||||
request.name = this.name;
|
request.name = this.name;
|
||||||
request.billingEmail = this.billingEmail;
|
request.billingEmail = this.billingEmail;
|
||||||
request.token = this.token;
|
request.token = this.token;
|
||||||
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.platformUtilsService.showToast("success", null, this.i18nService.t("providerSetup"));
|
this.toasterService.popAsync('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,12 +1,12 @@
|
|||||||
const { AngularWebpackPlugin } = require("@ngtools/webpack");
|
const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin;
|
||||||
|
|
||||||
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 AngularWebpackPlugin({
|
webpackConfig.plugins[webpackConfig.plugins.length -1] = new AngularCompilerPlugin({
|
||||||
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,
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = webpackConfig;
|
module.exports = webpackConfig;
|
||||||
|
|||||||
44
config.js
44
config.js
@@ -1,36 +1,32 @@
|
|||||||
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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function log(configObj) {
|
function log(configObj) {
|
||||||
const repeatNum = 50;
|
const repeatNum = 50;
|
||||||
console.log(`${"=".repeat(repeatNum)}\nenvConfig`);
|
console.log(`${"=".repeat(repeatNum)}\nenvConfig`);
|
||||||
console.log(JSON.stringify(configObj, null, 2));
|
console.log(JSON.stringify(configObj, null, 2));
|
||||||
console.log(`${"=".repeat(repeatNum)}`);
|
console.log(`${"=".repeat(repeatNum)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadConfig(configName) {
|
function loadConfig(configName) {
|
||||||
try {
|
try {
|
||||||
return require(`./config/${configName}.json`);
|
return require(`./config/${configName}.json`);
|
||||||
} 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 {
|
}
|
||||||
throw e;
|
else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
load,
|
load,
|
||||||
log,
|
log
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
{
|
{
|
||||||
"urls": {},
|
"urls": {},
|
||||||
"stripeKey": "pk_test_KPoCfZXu7mznb9uSCPZ2JpTD",
|
"stripeKey": "pk_test_KPoCfZXu7mznb9uSCPZ2JpTD",
|
||||||
"braintreeKey": "sandbox_r72q8jq6_9pnxkwm75f87sdc2",
|
"braintreeKey": "sandbox_r72q8jq6_9pnxkwm75f87sdc2",
|
||||||
"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": {
|
|
||||||
"port": 8080,
|
|
||||||
"allowedHosts": "auto"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
{
|
{
|
||||||
"urls": {
|
"urls": {
|
||||||
"icons": "https://icons.bitwarden.net",
|
"icons": "https://icons.bitwarden.net",
|
||||||
"notifications": "https://notifications.bitwarden.com"
|
"notifications": "https://notifications.bitwarden.com"
|
||||||
},
|
},
|
||||||
"stripeKey": "pk_live_bpN0P37nMxrMQkcaHXtAybJk",
|
"stripeKey": "pk_live_bpN0P37nMxrMQkcaHXtAybJk",
|
||||||
"braintreeKey": "production_qfbsv8kc_njj2zjtyngtjmbjd",
|
"braintreeKey": "production_qfbsv8kc_njj2zjtyngtjmbjd",
|
||||||
"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,11 +1,10 @@
|
|||||||
{
|
{
|
||||||
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
{
|
{
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
1
config/self-hosted.json
Normal file
1
config/self-hosted.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"dev": {
|
|
||||||
"proxyApi": "http://localhost:4001",
|
|
||||||
"proxyIdentity": "http://localhost:33657",
|
|
||||||
"proxyEvents": "http://localhost:46274",
|
|
||||||
"proxyNotifications": "http://localhost:61841",
|
|
||||||
"port": 8081
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
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: e595c0548e...815b436f7c
18148
package-lock.json
generated
18148
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
94
package.json
94
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@bitwarden/web-vault",
|
"name": "bitwarden-web",
|
||||||
"version": "2.28.0",
|
"version": "2.23.0",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"repository": "https://github.com/bitwarden/web",
|
"repository": "https://github.com/bitwarden/web",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -29,90 +29,60 @@
|
|||||||
"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": "eslint . && prettier --check .",
|
"lint": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' || true",
|
||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' --fix"
|
||||||
"prettier": "prettier --write .",
|
|
||||||
"prepare": "husky install"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/compiler-cli": "^12.2.13",
|
"@angular/compiler-cli": "^11.2.11",
|
||||||
"@ngtools/webpack": "^12.2.13",
|
"@ngtools/webpack": "^11.2.10",
|
||||||
"@types/jquery": "^3.5.5",
|
"@types/jquery": "^3.5.5",
|
||||||
"@types/node": "^16.11.12",
|
"@types/node": "^14.17.2",
|
||||||
"@types/webcrypto": "^0.0.28",
|
"@types/webcrypto": "^0.0.28",
|
||||||
"@types/webpack": "^5.28.0",
|
"@types/webpack": "^4.4.27",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
"@typescript-eslint/parser": "^5.10.1",
|
"copy-webpack-plugin": "^6.4.0",
|
||||||
"autoprefixer": "^10.4.2",
|
|
||||||
"buffer": "^6.0.3",
|
|
||||||
"clean-webpack-plugin": "^4.0.0",
|
|
||||||
"copy-webpack-plugin": "^10.0.0",
|
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"css-loader": "^6.5.1",
|
"css-loader": "^5.2.3",
|
||||||
"eslint": "^8.7.0",
|
"del": "^6.0.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"file-loader": "^6.2.0",
|
||||||
"eslint-import-resolver-typescript": "^2.5.0",
|
|
||||||
"eslint-plugin-import": "^2.25.4",
|
|
||||||
"gh-pages": "^3.1.0",
|
"gh-pages": "^3.1.0",
|
||||||
"html-loader": "^3.0.1",
|
"html-loader": "^1.3.2",
|
||||||
"html-webpack-injector": "1.1.4",
|
"html-webpack-injector": "1.1.4",
|
||||||
"html-webpack-plugin": "^5.5.0",
|
"html-webpack-plugin": "^4.5.1",
|
||||||
"husky": "^7.0.4",
|
"mini-css-extract-plugin": "^1.5.0",
|
||||||
"lint-staged": "^12.1.2",
|
|
||||||
"mini-css-extract-plugin": "^2.4.5",
|
|
||||||
"postcss": "^8.4.6",
|
|
||||||
"postcss-loader": "^6.2.1",
|
|
||||||
"prettier": "2.5.1",
|
|
||||||
"process": "^0.11.10",
|
|
||||||
"rimraf": "^3.0.2",
|
|
||||||
"sass": "^1.32.10",
|
"sass": "^1.32.10",
|
||||||
"sass-loader": "^12.4.0",
|
"sass-loader": "^10.1.1",
|
||||||
"style-loader": "^3.3.1",
|
"style-loader": "^2.0.0",
|
||||||
"tailwindcss": "^3.0.18",
|
"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",
|
||||||
"typescript": "4.3.5",
|
"tslint": "^6.1.3",
|
||||||
"util": "^0.12.4",
|
"tslint-loader": "^3.5.4",
|
||||||
"webpack": "^5.64.4",
|
"typescript": "4.1.5",
|
||||||
"webpack-cli": "^4.9.1",
|
"webpack": "^4.46.0",
|
||||||
"webpack-dev-server": "^4.6.0"
|
"webpack-cli": "^4.6.0",
|
||||||
|
"webpack-dev-server": "^3.11.2"
|
||||||
},
|
},
|
||||||
"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",
|
||||||
"core-js": "^3.11.0",
|
"core-js": "^3.11.0",
|
||||||
"date-input-polyfill": "^2.14.0",
|
"date-input-polyfill": "^2.14.0",
|
||||||
|
"font-awesome": "4.7.0",
|
||||||
"jquery": "3.6.0",
|
"jquery": "3.6.0",
|
||||||
"jszip": "^3.7.1",
|
|
||||||
"ngx-infinite-scroll": "^10.0.1",
|
"ngx-infinite-scroll": "^10.0.1",
|
||||||
"ngx-toastr": "14.1.4",
|
|
||||||
"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"
|
||||||
"zone.js": "0.11.4"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "~16",
|
"node": "~14",
|
||||||
"npm": "~8"
|
"npm": "~7"
|
||||||
},
|
|
||||||
"lint-staged": {
|
|
||||||
"./!(jslib)**": "prettier --ignore-unknown --write",
|
|
||||||
"*.ts": "eslint --fix",
|
|
||||||
"*.png": "node scripts/optimize.js"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
/* eslint-disable no-undef */
|
|
||||||
module.exports = {
|
|
||||||
plugins: [require("tailwindcss"), require("autoprefixer"), require("postcss-nested")],
|
|
||||||
};
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
const child_process = require("child_process");
|
|
||||||
const path = require("path");
|
|
||||||
|
|
||||||
const images = process.argv.slice(2);
|
|
||||||
|
|
||||||
images.forEach((img) => {
|
|
||||||
switch (img.split(".").pop()) {
|
|
||||||
case "png":
|
|
||||||
child_process.execSync(
|
|
||||||
`npx @squoosh/cli --oxipng {} --output-dir "${path.dirname(img)}" "${img}"`
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case "jpg":
|
|
||||||
child_process.execSync(
|
|
||||||
`npx @squoosh/cli --mozjpeg {"quality":85,"baseline":false,"arithmetic":false,"progressive":true,"optimize_coding":true,"smoothing":0,"color_space":3,"quant_table":3,"trellis_multipass":false,"trellis_opt_zero":false,"trellis_opt_table":false,"trellis_loops":1,"auto_subsample":true,"chroma_subsample":2,"separate_chroma_quality":false,"chroma_quality":75} --output-dir "${path.dirname(
|
|
||||||
img
|
|
||||||
)}" "${img}"`
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
86
src/404.html
86
src/404.html
@@ -1,52 +1,50 @@
|
|||||||
<!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
|
<link href="/404/bootstrap.min.css" rel="stylesheet" type="text/css"
|
||||||
href="/404/bootstrap.min.css"
|
integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l">
|
||||||
rel="stylesheet"
|
<link href="/404/font-awesome.min.css" rel="stylesheet" type="text/css"
|
||||||
type="text/css"
|
integrity="sha512-SfTiTlX6kk+qitfevl/7LibUOeJWlt9rbyDn92a1DqWOw9vWG2MFoays0sgObmWazO5BQPiFucnnEAjpAB+/Sw==">
|
||||||
integrity="sha384-hA/ESrxp2b05ywLtD9YwM6m+pNyLRY4+ruk6dWK00SM4k6SQs0bfrITJVSf6uZyH"
|
<link href="/404/styles.css" rel="stylesheet" type="text/css">
|
||||||
/>
|
|
||||||
<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>
|
||||||
<div class="banner">
|
<div class="banner">
|
||||||
<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="bwi bwi-shield"></i> <strong>bit</strong>warden
|
<i class="fa fa-shield"></i>
|
||||||
</div>
|
<strong>bit</strong>warden</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="container inner content">
|
||||||
</div>
|
<h2>Page not found!</h2>
|
||||||
<div class="container inner content">
|
<p>Sorry, but the page you were looking for could not be found.</p>
|
||||||
<h2>Page not found!</h2>
|
<p>
|
||||||
<p>Sorry, but the page you were looking for could not be found.</p>
|
<a href="/">
|
||||||
<p>
|
<img src="/images/404.png" class="img-fluid" alt="404 image" width="80%"/>
|
||||||
<a href="/">
|
</a>
|
||||||
<img src="/images/404.png" class="img-fluid" alt="404 image" width="80%" />
|
</p>
|
||||||
</a>
|
<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>
|
||||||
<p>
|
</div>
|
||||||
You can <a href="/">return to the web vault</a>, check our
|
<div class="container footer text-muted content">
|
||||||
<a href="https://status.bitwarden.com/">status page</a> or
|
© Copyright 2021 Bitwarden, Inc.
|
||||||
<a href="https://bitwarden.com/contact/">contact us</a>.
|
</div>
|
||||||
</p>
|
</body>
|
||||||
</div>
|
|
||||||
<div class="container footer text-muted content">© Copyright 2022 Bitwarden, Inc.</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
2
src/404/bootstrap.min.css
vendored
2
src/404/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
4
src/404/font-awesome.min.css
vendored
Normal file
4
src/404/font-awesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,151 +1,119 @@
|
|||||||
@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,
|
html, body, .row {
|
||||||
body,
|
height: 100%;
|
||||||
.row {
|
-webkit-font-smoothing: antialiased;
|
||||||
height: 100%;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 25px;
|
font-size: 25px;
|
||||||
margin-bottom: 12.5px;
|
margin-bottom: 12.5px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand {
|
.brand {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
padding-left: 15px;
|
padding-left: 15px;
|
||||||
padding-right: 15px;
|
padding-right: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
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,9 +0,0 @@
|
|||||||
import { StateService as BaseStateService } from "jslib-common/abstractions/state.service";
|
|
||||||
import { StorageOptions } from "jslib-common/models/domain/storageOptions";
|
|
||||||
|
|
||||||
import { Account } from "src/models/account";
|
|
||||||
|
|
||||||
export abstract class StateService extends BaseStateService<Account> {
|
|
||||||
getRememberEmail: (options?: StorageOptions) => Promise<boolean>;
|
|
||||||
setRememberEmail: (value: boolean, options?: StorageOptions) => Promise<void>;
|
|
||||||
}
|
|
||||||
@@ -1,41 +1,34 @@
|
|||||||
<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
|
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
|
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||||
title="{{ 'loading' | i18n }}"
|
</p>
|
||||||
aria-hidden="true"
|
</div>
|
||||||
></i>
|
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
|
||||||
</p>
|
|
||||||
</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
|
<a routerLink="/register" [queryParams]="{email: email}"
|
||||||
routerLink="/register"
|
class="btn btn-primary btn-block ml-2 mt-0">
|
||||||
[queryParams]="{ email: email }"
|
{{'createAccount' | i18n}}
|
||||||
class="btn btn-primary btn-block ml-2 mt-0"
|
</a>
|
||||||
>
|
</div>
|
||||||
{{ "createAccount" | i18n }}
|
</div>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,55 +1,60 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import {
|
||||||
|
ActivatedRoute,
|
||||||
|
Router,
|
||||||
|
} from '@angular/router';
|
||||||
|
|
||||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
import {
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
Toast,
|
||||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
ToasterService,
|
||||||
import { StateService } from "jslib-common/abstractions/state.service";
|
} from 'angular2-toaster';
|
||||||
import { EmergencyAccessAcceptRequest } from "jslib-common/models/request/emergencyAccessAcceptRequest";
|
|
||||||
|
|
||||||
import { BaseAcceptComponent } from "../common/base.accept.component";
|
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||||
|
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||||
|
import { StateService } from 'jslib-common/abstractions/state.service';
|
||||||
|
import { UserService } from 'jslib-common/abstractions/user.service';
|
||||||
|
import { 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;
|
|
||||||
|
|
||||||
protected requiredParameters: string[] = ["id", "name", "email", "token"];
|
name: string;
|
||||||
protected failedShortMessage = "emergencyInviteAcceptFailedShort";
|
|
||||||
protected failedMessage = "emergencyInviteAcceptFailed";
|
|
||||||
|
|
||||||
constructor(
|
protected requiredParameters: string[] = ['id', 'name', 'email', 'token'];
|
||||||
router: Router,
|
protected failedShortMessage = 'emergencyInviteAcceptFailedShort';
|
||||||
platformUtilsService: PlatformUtilsService,
|
protected failedMessage = 'emergencyInviteAcceptFailed';
|
||||||
i18nService: I18nService,
|
|
||||||
route: ActivatedRoute,
|
|
||||||
private apiService: ApiService,
|
|
||||||
stateService: StateService
|
|
||||||
) {
|
|
||||||
super(router, platformUtilsService, i18nService, route, stateService);
|
|
||||||
}
|
|
||||||
|
|
||||||
async authedHandler(qParams: any): Promise<void> {
|
constructor(router: Router, toasterService: ToasterService,
|
||||||
const request = new EmergencyAccessAcceptRequest();
|
i18nService: I18nService, route: ActivatedRoute,
|
||||||
request.token = qParams.token;
|
private apiService: ApiService, userService: UserService,
|
||||||
this.actionPromise = this.apiService.postEmergencyAccessAccept(qParams.id, request);
|
stateService: StateService) {
|
||||||
await this.actionPromise;
|
super(router, toasterService, i18nService, route, userService, stateService);
|
||||||
this.platformUtilService.showToast(
|
}
|
||||||
"success",
|
|
||||||
this.i18nService.t("inviteAccepted"),
|
async authedHandler(qParams: any): Promise<void> {
|
||||||
this.i18nService.t("emergencyInviteAcceptedDesc"),
|
const request = new EmergencyAccessAcceptRequest();
|
||||||
{ timeout: 10000 }
|
request.token = qParams.token;
|
||||||
);
|
this.actionPromise = this.apiService.postEmergencyAccessAccept(qParams.id, request);
|
||||||
this.router.navigate(["/vault"]);
|
await this.actionPromise;
|
||||||
}
|
const toast: Toast = {
|
||||||
|
type: 'success',
|
||||||
async unauthedHandler(qParams: any): Promise<void> {
|
title: this.i18nService.t('inviteAccepted'),
|
||||||
this.name = qParams.name;
|
body: this.i18nService.t('emergencyInviteAcceptedDesc'),
|
||||||
if (this.name != null) {
|
timeout: 10000,
|
||||||
// Fix URL encoding of space issue with Angular
|
};
|
||||||
this.name = this.name.replace(/\+/g, " ");
|
this.toasterService.popAsync(toast);
|
||||||
|
this.router.navigate(['/vault']);
|
||||||
|
}
|
||||||
|
|
||||||
|
async unauthedHandler(qParams: any): Promise<void> {
|
||||||
|
this.name = qParams.name;
|
||||||
|
if (this.name != null) {
|
||||||
|
// Fix URL encoding of space issue with Angular
|
||||||
|
this.name = this.name.replace(/\+/g, ' ');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,35 @@
|
|||||||
<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
|
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
|
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||||
title="{{ 'loading' | i18n }}"
|
</p>
|
||||||
aria-hidden="true"
|
</div>
|
||||||
></i>
|
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
|
||||||
</p>
|
|
||||||
</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
|
<a routerLink="/register" [queryParams]="{email: email}"
|
||||||
routerLink="/register"
|
class="btn btn-primary btn-block ml-2 mt-0">
|
||||||
[queryParams]="{ email: email }"
|
{{'createAccount' | i18n}}
|
||||||
class="btn btn-primary btn-block ml-2 mt-0"
|
</a>
|
||||||
>
|
</div>
|
||||||
{{ "createAccount" | i18n }}
|
</div>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,126 +1,117 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import {
|
||||||
|
ActivatedRoute,
|
||||||
|
Router,
|
||||||
|
} from '@angular/router';
|
||||||
|
|
||||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
import {
|
||||||
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
Toast,
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
ToasterService,
|
||||||
import { LogService } from "jslib-common/abstractions/log.service";
|
} from 'angular2-toaster';
|
||||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
|
||||||
import { PolicyService } from "jslib-common/abstractions/policy.service";
|
|
||||||
import { StateService } from "jslib-common/abstractions/state.service";
|
|
||||||
import { Utils } from "jslib-common/misc/utils";
|
|
||||||
import { Policy } from "jslib-common/models/domain/policy";
|
|
||||||
import { OrganizationUserAcceptRequest } from "jslib-common/models/request/organizationUserAcceptRequest";
|
|
||||||
import { OrganizationUserResetPasswordEnrollmentRequest } from "jslib-common/models/request/organizationUserResetPasswordEnrollmentRequest";
|
|
||||||
|
|
||||||
import { BaseAcceptComponent } from "../common/base.accept.component";
|
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||||
|
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
|
||||||
|
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||||
|
import { LogService } from 'jslib-common/abstractions/log.service';
|
||||||
|
import { PolicyService } from 'jslib-common/abstractions/policy.service';
|
||||||
|
import { StateService } from 'jslib-common/abstractions/state.service';
|
||||||
|
import { UserService } from 'jslib-common/abstractions/user.service';
|
||||||
|
|
||||||
|
import { OrganizationUserAcceptRequest } from 'jslib-common/models/request/organizationUserAcceptRequest';
|
||||||
|
import { OrganizationUserResetPasswordEnrollmentRequest } from 'jslib-common/models/request/organizationUserResetPasswordEnrollmentRequest';
|
||||||
|
|
||||||
|
import { Utils } from 'jslib-common/misc/utils';
|
||||||
|
import { Policy } from 'jslib-common/models/domain/policy';
|
||||||
|
import { BaseAcceptComponent } from '../common/base.accept.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-accept-organization",
|
selector: 'app-accept-organization',
|
||||||
templateUrl: "accept-organization.component.html",
|
templateUrl: 'accept-organization.component.html',
|
||||||
})
|
})
|
||||||
export class AcceptOrganizationComponent extends BaseAcceptComponent {
|
export class AcceptOrganizationComponent extends BaseAcceptComponent {
|
||||||
orgName: string;
|
orgName: string;
|
||||||
|
|
||||||
protected requiredParameters: string[] = ["organizationId", "organizationUserId", "token"];
|
protected requiredParameters: string[] = ['organizationId', 'organizationUserId', 'token'];
|
||||||
|
|
||||||
constructor(
|
constructor(router: Router, toasterService: ToasterService,
|
||||||
router: Router,
|
i18nService: I18nService, route: ActivatedRoute,
|
||||||
platformUtilsService: PlatformUtilsService,
|
private apiService: ApiService, userService: UserService,
|
||||||
i18nService: I18nService,
|
stateService: StateService, private cryptoService: CryptoService,
|
||||||
route: ActivatedRoute,
|
private policyService: PolicyService, private logService: LogService) {
|
||||||
private apiService: ApiService,
|
super(router, toasterService, i18nService, route, userService, stateService);
|
||||||
stateService: StateService,
|
|
||||||
private cryptoService: CryptoService,
|
|
||||||
private policyService: PolicyService,
|
|
||||||
private logService: LogService
|
|
||||||
) {
|
|
||||||
super(router, platformUtilsService, i18nService, route, stateService);
|
|
||||||
}
|
|
||||||
|
|
||||||
async authedHandler(qParams: any): Promise<void> {
|
|
||||||
const request = new OrganizationUserAcceptRequest();
|
|
||||||
request.token = qParams.token;
|
|
||||||
if (await this.performResetPasswordAutoEnroll(qParams)) {
|
|
||||||
this.actionPromise = this.apiService
|
|
||||||
.postOrganizationUserAccept(qParams.organizationId, qParams.organizationUserId, request)
|
|
||||||
.then(() => {
|
|
||||||
// Retrieve Public Key
|
|
||||||
return this.apiService.getOrganizationKeys(qParams.organizationId);
|
|
||||||
})
|
|
||||||
.then(async (response) => {
|
|
||||||
if (response == null) {
|
|
||||||
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const publicKey = Utils.fromB64ToArray(response.publicKey);
|
|
||||||
|
|
||||||
// RSA Encrypt user's encKey.key with organization public key
|
|
||||||
const encKey = await this.cryptoService.getEncKey();
|
|
||||||
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey.buffer);
|
|
||||||
|
|
||||||
// Create request and execute enrollment
|
|
||||||
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
|
||||||
resetRequest.resetPasswordKey = encryptedKey.encryptedString;
|
|
||||||
|
|
||||||
return this.apiService.putOrganizationUserResetPasswordEnrollment(
|
|
||||||
qParams.organizationId,
|
|
||||||
await this.stateService.getUserId(),
|
|
||||||
resetRequest
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.actionPromise = this.apiService.postOrganizationUserAccept(
|
|
||||||
qParams.organizationId,
|
|
||||||
qParams.organizationUserId,
|
|
||||||
request
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.actionPromise;
|
async authedHandler(qParams: any): Promise<void> {
|
||||||
this.platformUtilService.showToast(
|
const request = new OrganizationUserAcceptRequest();
|
||||||
"success",
|
request.token = qParams.token;
|
||||||
this.i18nService.t("inviteAccepted"),
|
if (await this.performResetPasswordAutoEnroll(qParams)) {
|
||||||
this.i18nService.t("inviteAcceptedDesc"),
|
this.actionPromise = this.apiService.postOrganizationUserAccept(qParams.organizationId,
|
||||||
{ timeout: 10000 }
|
qParams.organizationUserId, request).then(() => {
|
||||||
);
|
// Retrieve Public Key
|
||||||
|
return this.apiService.getOrganizationKeys(qParams.organizationId);
|
||||||
|
}).then(async response => {
|
||||||
|
if (response == null) {
|
||||||
|
throw new Error(this.i18nService.t('resetPasswordOrgKeysError'));
|
||||||
|
}
|
||||||
|
|
||||||
await this.stateService.setOrganizationInvitation(null);
|
const publicKey = Utils.fromB64ToArray(response.publicKey);
|
||||||
this.router.navigate(["/vault"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async unauthedHandler(qParams: any): Promise<void> {
|
// RSA Encrypt user's encKey.key with organization public key
|
||||||
this.orgName = qParams.organizationName;
|
const encKey = await this.cryptoService.getEncKey();
|
||||||
if (this.orgName != null) {
|
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey.buffer);
|
||||||
// Fix URL encoding of space issue with Angular
|
|
||||||
this.orgName = this.orgName.replace(/\+/g, " ");
|
|
||||||
}
|
|
||||||
await this.stateService.setOrganizationInvitation(qParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async performResetPasswordAutoEnroll(qParams: any): Promise<boolean> {
|
// Create request and execute enrollment
|
||||||
let policyList: Policy[] = null;
|
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||||
try {
|
resetRequest.resetPasswordKey = encryptedKey.encryptedString;
|
||||||
const policies = await this.apiService.getPoliciesByToken(
|
|
||||||
qParams.organizationId,
|
// Get User Id
|
||||||
qParams.token,
|
const userId = await this.userService.getUserId();
|
||||||
qParams.email,
|
|
||||||
qParams.organizationUserId
|
return this.apiService.putOrganizationUserResetPasswordEnrollment(qParams.organizationId, userId, resetRequest);
|
||||||
);
|
});
|
||||||
policyList = this.policyService.mapPoliciesFromToken(policies);
|
} else {
|
||||||
} catch (e) {
|
this.actionPromise = this.apiService.postOrganizationUserAccept(qParams.organizationId,
|
||||||
this.logService.error(e);
|
qParams.organizationUserId, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.actionPromise;
|
||||||
|
const toast: Toast = {
|
||||||
|
type: 'success',
|
||||||
|
title: this.i18nService.t('inviteAccepted'),
|
||||||
|
body: this.i18nService.t('inviteAcceptedDesc'),
|
||||||
|
timeout: 10000,
|
||||||
|
};
|
||||||
|
this.toasterService.popAsync(toast);
|
||||||
|
|
||||||
|
await this.stateService.remove('orgInvitation');
|
||||||
|
this.router.navigate(['/vault']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (policyList != null) {
|
async unauthedHandler(qParams: any): Promise<void> {
|
||||||
const result = this.policyService.getResetPasswordPolicyOptions(
|
this.orgName = qParams.organizationName;
|
||||||
policyList,
|
if (this.orgName != null) {
|
||||||
qParams.organizationId
|
// Fix URL encoding of space issue with Angular
|
||||||
);
|
this.orgName = this.orgName.replace(/\+/g, ' ');
|
||||||
// Return true if policy enabled and auto-enroll enabled
|
}
|
||||||
return result[1] && result[0].autoEnrollEnabled;
|
await this.stateService.save('orgInvitation', qParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
private async performResetPasswordAutoEnroll(qParams: any): Promise<boolean> {
|
||||||
}
|
let policyList: Policy[] = null;
|
||||||
|
try {
|
||||||
|
const policies = await this.apiService.getPoliciesByToken(qParams.organizationId, qParams.token,
|
||||||
|
qParams.email, qParams.organizationUserId);
|
||||||
|
policyList = this.policyService.mapPoliciesFromToken(policies);
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (policyList != null) {
|
||||||
|
const result = this.policyService.getResetPasswordPolicyOptions(policyList, qParams.organizationId);
|
||||||
|
// Return true if policy enabled and auto-enroll enabled
|
||||||
|
return result[1] && result[0].autoEnrollEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,27 @@
|
|||||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
|
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
|
||||||
<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">{{ "passwordHint" | i18n }}</p>
|
<p class="lead text-center mb-4">{{'passwordHint' | i18n}}</p>
|
||||||
<div class="card d-block">
|
<div class="card d-block">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="email">{{ "emailAddress" | i18n }}</label>
|
<label for="email">{{'emailAddress' | i18n}}</label>
|
||||||
<input
|
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required
|
||||||
id="email"
|
appAutofocus inputmode="email" appInputVerbatim="false">
|
||||||
class="form-control"
|
<small class="form-text text-muted">{{'enterEmailToGetHint' | i18n}}</small>
|
||||||
type="text"
|
</div>
|
||||||
name="Email"
|
<hr>
|
||||||
[(ngModel)]="email"
|
<div class="d-flex">
|
||||||
required
|
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
|
||||||
appAutofocus
|
<span [hidden]="form.loading">{{'submit' | i18n}}</span>
|
||||||
inputmode="email"
|
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
appInputVerbatim="false"
|
</button>
|
||||||
/>
|
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
||||||
<small class="form-text text-muted">{{ "enterEmailToGetHint" | i18n }}</small>
|
{{'cancel' | i18n}}
|
||||||
</div>
|
</a>
|
||||||
<hr />
|
</div>
|
||||||
<div class="d-flex">
|
</div>
|
||||||
<button
|
</div>
|
||||||
type="submit"
|
|
||||||
class="btn btn-primary btn-block btn-submit"
|
|
||||||
[disabled]="form.loading"
|
|
||||||
>
|
|
||||||
<span [hidden]="form.loading">{{ "submit" | i18n }}</span>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-spinner bwi-spin"
|
|
||||||
title="{{ 'loading' | i18n }}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
|
||||||
{{ "cancel" | i18n }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from '@angular/core';
|
||||||
import { Router } from "@angular/router";
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
import { HintComponent as BaseHintComponent } from "jslib-angular/components/hint.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 { 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 { HintComponent as BaseHintComponent } from 'jslib-angular/components/hint.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-hint",
|
selector: 'app-hint',
|
||||||
templateUrl: "hint.component.html",
|
templateUrl: 'hint.component.html',
|
||||||
})
|
})
|
||||||
export class HintComponent extends BaseHintComponent {
|
export class HintComponent extends BaseHintComponent {
|
||||||
constructor(
|
constructor(router: Router, i18nService: I18nService,
|
||||||
router: Router,
|
apiService: ApiService, platformUtilsService: PlatformUtilsService,
|
||||||
i18nService: I18nService,
|
logService: LogService) {
|
||||||
apiService: ApiService,
|
super(router, i18nService, apiService, platformUtilsService, logService);
|
||||||
platformUtilsService: PlatformUtilsService,
|
}
|
||||||
logService: LogService
|
|
||||||
) {
|
|
||||||
super(router, i18nService, apiService, platformUtilsService, logService);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user