1
0
mirror of https://github.com/bitwarden/web synced 2025-12-06 00:03:28 +00:00

Compare commits

..

11 Commits

Author SHA1 Message Date
Joseph Flinn
2fe196df5e reverting the Chinese translations that got swapped (#1150) 2021-08-26 14:49:47 -07:00
Daniel James Smith
637f6e7b8a Fixed order of supportedLocales to have en as fallback again (#1140)
(cherry picked from commit c7de347cec)
2021-08-23 21:40:37 +02:00
Chad Scharf
3f54b7b687 Version bump 2.22.2 (#1142) 2021-08-20 16:14:12 -04:00
Oscar Hinton
d8115386a0 Fix role not being displayed in organization user table (#1141) 2021-08-20 15:39:06 -04:00
Oscar Hinton
5757e42bce Bump version to 2.22.1 (#1138)
(cherry picked from commit f2e591086e)
2021-08-19 13:27:49 +02:00
Oscar Hinton
6d2f0da489 Overwrite icon service url for prod (#1137)
(cherry picked from commit 361022fc26)
2021-08-19 10:14:29 +02:00
github-actions[bot]
36e64bfd48 Autosync Crowdin translations (#1134)
Co-authored-by: github-actions <>
2021-08-18 15:27:39 -07:00
Matt Gibson
fae0d44b29 2.22.0
(cherry picked from commit 424c9e7f95)
2021-08-17 15:02:55 -04:00
Matt Gibson
6f266c44d3 Update jslib 2021-08-13 10:49:34 -04:00
Matt Gibson
1b8cc3ae87 Set iframe allow on window load (#1125)
* Set webauthn allow on initial page load

* Update jslib

(cherry picked from commit b47835df68)
2021-08-13 10:49:08 -04:00
Matt Gibson
7f77a4ee76 Do not call parent if callback given (#1123)
(cherry picked from commit 919af717b9)
2021-08-12 20:20:44 -04:00
540 changed files with 45623 additions and 90428 deletions

View File

@@ -1,4 +1,3 @@
* *
!docker/*
!build/* !build/*
!entrypoint.sh !entrypoint.sh

View File

@@ -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

View File

@@ -1,2 +0,0 @@
# Apply Prettier https://github.com/bitwarden/web/pull/1347
56477eb39cfd8a73c9920577d24d75fed36e2cf5

4
.gitattributes vendored
View File

@@ -1 +1,3 @@
* text=auto eol=lf *.sh eol=lf
.dockerignore eol=lf
dockerfile eol=lf

View File

@@ -1,93 +0,0 @@
name: Bug Report
description: File a bug report
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
Please do not submit feature requests. The [Community Forums](https://community.bitwarden.com) has a section for submitting, voting for, and discussing product feature requests.
- type: textarea
id: reproduce
attributes:
label: Steps To Reproduce
description: How can we reproduce the behavior.
value: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. Click on '...'
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Result
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual Result
description: A clear and concise description of what is happening.
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots or Videos
description: If applicable, add screenshots and/or a short video to help explain your problem.
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Add any other context about the problem here.
- type: dropdown
id: os
attributes:
label: Operating System
description: What operating system are you seeing the problem on?
multiple: true
options:
- Windows
- macOS
- Linux
- Android
- iOS
validations:
required: true
- type: input
id: os-version
attributes:
label: Operating System Version
description: What version of the operating system(s) are you seeing the problem on?
- type: dropdown
id: browsers
attributes:
label: Web Browser
description: What browser(s) are you seeing the problem on?
multiple: true
options:
- Chrome
- Safari
- Microsoft Edge
- Firefox
- Opera
- Brave
- Vivaldi
validations:
required: true
- type: input
id: browser-version
attributes:
label: Browser Version
description: What version of the browser(s) are you seeing the problem on?
- type: input
id: version
attributes:
label: Build Version
description: What version of our software are you running? (Bottom of the page)
validations:
required: true

View File

@@ -1,14 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Feature Requests
url: https://community.bitwarden.com/c/feature-requests/
about: Request new features using the Community Forums. Please search existing feature requests before making a new one.
- name: Bitwarden Community Forums
url: https://community.bitwarden.com
about: Please visit the community forums for general community discussion, support and the development roadmap.
- name: Customer Support
url: https://bitwarden.com/contact/
about: Please contact our customer support for account issues and general customer support.
- name: Security Issues
url: https://hackerone.com/bitwarden
about: We use HackerOne to manage security disclosures.

View File

@@ -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)

View File

@@ -1,4 +1,3 @@
---
name: Build name: Build
on: on:
@@ -9,13 +8,13 @@ on:
required: false required: false
push: push:
branches-ignore: branches-ignore:
- "l10n_master" - 'l10n_master'
- "gh-pages" - 'gh-pages'
jobs: jobs:
cloc: cloc:
name: CLOC name: CLOC
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
@@ -28,131 +27,26 @@ jobs:
- name: Print lines of code - name: Print lines of code
run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
setup:
name: Setup
runs-on: ubuntu-20.04
outputs:
version: ${{ steps.version.outputs.value }}
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Get GitHub sha as version build-selfhost:
id: version
run: echo "::set-output name=value::${GITHUB_SHA:0:7}"
build-oss-selfhost:
name: Build OSS zip
runs-on: ubuntu-20.04
needs: setup
env:
_VERSION: ${{ needs.setup.outputs.version }}
steps:
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: "16"
- name: Cache npm
id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with:
path: "~/.npm"
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Print environment
run: |
whoami
node --version
npm --version
gulp --version
docker --version
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Install dependencies
run: npm ci
- name: Build OSS selfhost
run: |
npm run dist:oss:selfhost
zip -r web-$_VERSION-selfhosted-open-source.zip build
- name: Upload build artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: web-${{ env._VERSION }}-selfhosted-open-source.zip
path: ./web-${{ env._VERSION }}-selfhosted-open-source.zip
if-no-files-found: error
build-cloud:
name: Build Cloud zip
runs-on: ubuntu-20.04
needs: setup
env:
_VERSION: ${{ needs.setup.outputs.version }}
steps:
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: "16"
- name: Cache npm
id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with:
path: "~/.npm"
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Print environment
run: |
whoami
node --version
npm --version
gulp --version
docker --version
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Install dependencies
run: npm ci
- name: Build Cloud
run: |
npm run dist:bit:cloud
zip -r web-$_VERSION-cloud-COMMERCIAL.zip build
- name: Upload build artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: web-${{ env._VERSION }}-cloud-COMMERCIAL.zip
path: ./web-${{ env._VERSION }}-cloud-COMMERCIAL.zip
if-no-files-found: error
build-commercial-selfhost:
name: Build SelfHost Docker image name: Build SelfHost Docker image
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
needs: setup
env:
_VERSION: ${{ needs.setup.outputs.version }}
steps: steps:
- name: Set up Node - name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with: with:
node-version: "16" node-version: '14'
- name: Update NPM
run: |
npm install -g npm@7
- name: Cache npm - name: Cache npm
id: npm-cache id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6 uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with: with:
path: "~/.npm" path: '~/.npm'
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-${{ github.run_id }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Print environment - name: Print environment
run: | run: |
@@ -164,13 +58,39 @@ jobs:
echo "GitHub ref: $GITHUB_REF" echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT" echo "GitHub event: $GITHUB_EVENT"
- name: Setup DCT - name: Login to Azure
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix' if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
id: setup-dct uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff
with: with:
azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
azure-keyvault-name: "bitwarden-prod-kv"
- name: Retrieve secrets
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
with:
keyvault: "bitwarden-prod-kv"
secrets: "docker-password,
docker-username,
dct-delegate-2-repo-passphrase,
dct-delegate-2-key"
- name: Log into Docker
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
run: echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
env:
DOCKER_USERNAME: ${{ steps.retrieve-secrets.outputs.docker-username }}
DOCKER_PASSWORD: ${{ steps.retrieve-secrets.outputs.docker-password }}
- name: Setup Docker Trust
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
run: |
mkdir -p ~/.docker/trust/private
echo "$DCT_DELEGATE_KEY" > ~/.docker/trust/private/$DCT_DELEGATION_KEY_ID.key
env:
DCT_DELEGATION_KEY_ID: "c9bde8ec820701516491e5e03d3a6354e7bd66d05fa3df2b0062f68b116dc59c"
DCT_DELEGATE_KEY: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-key }}
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
@@ -179,26 +99,15 @@ jobs:
run: dotnet tool restore run: dotnet tool restore
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm install
- name: Build - name: Build
run: | run: |
echo -e "# Building Web\n" echo -e "# Building Web\n"
echo "Building app" echo "Building app"
echo "npm version $(npm --version)" echo "npm version $(npm --version)"
npm run dist:selfhost
npm run dist:bit:selfhost
zip -r web-$_VERSION-selfhosted-COMMERCIAL.zip build
- name: Upload build artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: web-${{ env._VERSION }}-selfhosted-COMMERCIAL.zip
path: ./web-${{ env._VERSION }}-selfhosted-COMMERCIAL.zip
if-no-files-found: error
- name: Build Docker image
run: |
echo -e "\nBuilding Docker image" echo -e "\nBuilding Docker image"
docker --version docker --version
docker build -t bitwarden/web . docker build -t bitwarden/web .
@@ -211,54 +120,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'
run: docker tag bitwarden/web bitwarden/web:hotfix
- name: List Docker images - name: List Docker images
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix' 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.retrieve-secrets.outputs.dct-delegate-2-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.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
- name: Push hotfix image
if: github.ref == 'refs/heads/hotfix'
run: docker push bitwarden/web:hotfix
env:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
- name: Log out of Docker - name: Log out of Docker
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix' if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
run: docker logout run: docker logout
build-qa: build-qa:
name: Build Docker images for QA environment name: Build QA Docker image
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Set up Node - name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with: with:
node-version: "16" node-version: '14'
- name: Update NPM
run: |
npm install -g npm@7
- name: Cache npm - name: Cache npm
id: npm-cache id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6 uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with: with:
path: "~/.npm" path: '~/.npm'
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-${{ github.run_id }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Print environment - name: Print environment
run: | run: |
@@ -292,32 +195,26 @@ jobs:
echo -e "# Building Web\n" echo -e "# Building Web\n"
echo "Building app" echo "Building app"
echo "npm version $(npm --version)" echo "npm version $(npm --version)"
VERSION=$( jq -r ".version" package.json) npm run build:qa
jq --arg version "$VERSION - ${GITHUB_SHA:0:7}" '.version = $version' package.json > package.json.tmp
mv package.json.tmp package.json
npm run build:bit:qa
echo "{\"commit_hash\": \"$GITHUB_SHA\", \"ref\": \"$GITHUB_REF\"}" | jq . > build/info.json
echo -e "\nBuilding Docker image" echo -e "\nBuilding Docker image"
docker --version docker --version
docker build -t bitwardenqa.azurecr.io/web . docker build -t bitwardenqa.azurecr.io/web .
- 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.events.inputs.custom_tag_extension }}
if [[ $TAG_EXTENSION ]]; then if [[ $TAG_EXTENSION ]]; then
IMAGE_TAG=$IMAGE_TAG-$TAG_EXTENSION IMAGE_TAG=$IMAGE_TAG-$TAG_EXTENSION
fi fi
echo "::set-output name=value::$IMAGE_TAG" echo "::set-output name=value::$IMAGE_TAG"
- name: Tag image - name: Tag image
env: env:
IMAGE_TAG: ${{ steps.image-tag.outputs.value }} IMAGE_TAG: ${{ steps.image_tag.outputs.value }}
run: docker tag bitwardenqa.azurecr.io/web "bitwardenqa.azurecr.io/web:$IMAGE_TAG" run: docker tag bitwardenqa.azurecr.io/web "bitwardenqa.azurecr.io/web:$IMAGE_TAG"
- name: Tag dev - name: Tag dev
@@ -329,7 +226,7 @@ jobs:
- name: Push image - name: Push image
env: env:
IMAGE_TAG: ${{ steps.image-tag.outputs.value }} IMAGE_TAG: ${{ steps.image_tag.outputs.value }}
run: docker push "bitwardenqa.azurecr.io/web:$IMAGE_TAG" run: docker push "bitwardenqa.azurecr.io/web:$IMAGE_TAG"
- name: Push dev images - name: Push dev images
@@ -339,29 +236,27 @@ jobs:
- name: Log out of Docker - name: Log out of Docker
run: docker logout run: docker logout
windows: windows:
name: Test code on Windows name: Test code on Windows
runs-on: windows-2019 runs-on: windows-latest
steps: steps:
- name: Set up NuGet - name: Set up NuGet
uses: nuget/setup-nuget@04b0c2b8d1b97922f67eca497d7cf0bf17b8ffe1 uses: nuget/setup-nuget@04b0c2b8d1b97922f67eca497d7cf0bf17b8ffe1
with: with:
nuget-version: "latest" nuget-version: 'latest'
- name: Set up MSBuild - name: Set up MSBuild
uses: microsoft/setup-msbuild@c26a08ba26249b81327e26f6ef381897b6a8754d uses: microsoft/setup-msbuild@c26a08ba26249b81327e26f6ef381897b6a8754d
- name: Cache npm
id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with:
path: "~/.npm"
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Set up Node - name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with: with:
node-version: "16" node-version: '14'
- name: Update NPM
run: |
npm install -g npm@7
- name: Print environment - name: Print environment
run: | run: |
@@ -379,115 +274,9 @@ jobs:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Install dependencies - name: npm install
run: npm ci run: npm install
- name: Run linter - name: npm build
run: npm run lint run: npm run build:prod
- name: NPM build
run: npm run build:bit:cloud
crowdin-push:
name: Crowdin Push
if: github.ref == 'refs/heads/master'
needs:
- build-oss-selfhost
- build-cloud
- build-commercial-selfhost
- build-qa
runs-on: ubuntu-20.04
env:
_CROWDIN_PROJECT_ID: "308189"
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Login to Azure
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
with:
keyvault: "bitwarden-prod-kv"
secrets: "crowdin-api-token"
- name: Upload Sources
uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea # v1.3.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
with:
config: crowdin.yml
crowdin_branch_name: master
upload_sources: true
upload_translations: false
check-failures:
name: Check for failures
if: always()
runs-on: ubuntu-20.04
needs:
- cloc
- setup
- build-oss-selfhost
- build-cloud
- build-commercial-selfhost
- build-qa
- crowdin-push
- windows
steps:
- name: Check if any job failed
if: ${{ (github.ref == 'refs/heads/master') || (github.ref == 'refs/heads/rc') }}
env:
CLOC_STATUS: ${{ needs.cloc.result }}
SETUP_STATUS: ${{ needs.setup.result }}
BUILD_OSS_SELFHOST_STATUS: ${{ needs.build-oss-selfhost.result }}
BUILD_CLOUD_STATUS: ${{ needs.build-cloud.result }}
BUILD_COMMERCIAL_SELFHOST_STATUS: ${{ needs.build-commercial-selfhost.result }}
BUILD_QA_STATUS: ${{ needs.build-qa.result }}
CROWDIN_PUSH_STATUS: ${{ needs.crowdin-push.result }}
WINDOWS_STATUS: ${{ needs.windows.result }}
run: |
if [ "$CLOC_STATUS" = "failure" ]; then
exit 1
elif [ "$SETUP_STATUS" = "failure" ]; then
exit 1
elif [ "$BUILD_OSS_SELFHOST_STATUS" = "failure" ]; then
exit 1
elif [ "$BUILD_CLOUD_STATUS" = "failure" ]; then
exit 1
elif [ "$BUILD_COMMERCIAL_SELFHOST_STATUS" = "failure" ]; then
exit 1
elif [ "$BUILD_QA_STATUS" = "failure" ]; then
exit 1
elif [ "$CROWDIN_PUSH_STATUS" = "failure" ]; then
exit 1
elif [ "$WINDOWS_STATUS" = "failure" ]; then
exit 1
fi
- name: Login to Azure - Prod Subscription
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
if: failure()
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
if: failure()
with:
keyvault: "bitwarden-prod-kv"
secrets: "devops-alerts-slack-webhook-url"
- name: Notify Slack on failure
uses: act10ns/slack@e4e71685b9b239384b0f676a63c32367f59c2522 # v1.2.2
if: failure()
env:
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
with:
status: ${{ job.status }}

View File

@@ -1,49 +0,0 @@
---
name: Crowdin Pull
on:
workflow_dispatch:
inputs: {}
schedule:
- cron: "0 0 * * 5"
jobs:
crowdin-pull:
name: Pull
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: Download translations
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: false
upload_translations: false
download_translations: true
github_user_name: "github-actions"
github_user_email: "<>"
commit_message: "Autosync the updated translations"
localization_branch_name: crowdin-auto-sync
create_pull_request: true
pull_request_title: "Autosync Crowdin Translations"
pull_request_body: "Autosync the updated translations"

133
.github/workflows/crowdin-sync.yml vendored Normal file
View File

@@ -0,0 +1,133 @@
name: Crowdin Sync
on:
workflow_dispatch:
inputs: {}
#schedule:
# - cron: '0 0 * * *'
jobs:
crowdin-sync:
name: Autosync
runs-on: ubuntu-20.04
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Setup git config
run: |
git config user.name = "GitHub Action Bot"
git config user.email = "<>"
- name: Get Crowndin Sync Branch
id: branch
run: |
BRANCH_NAME=crowdin-auto-sync
BRANCH_EXISTED=true
git fetch -a
git switch master
if [ $(git branch -a | egrep "remotes/origin/${BRANCH_NAME}$" | wc -l) -eq 0 ]; then
BRANCH_EXISTED=false
git switch -c $BRANCH_NAME
else
git switch $BRANCH_NAME
fi
git branch
echo "::set-output name=branch-existed::${BRANCH_EXISTED}"
echo "::set-output name=branch-name::${BRANCH_NAME}"
- 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: Get Crowdin updates
env:
CROWDIN_BASE_URL="https://api.crowdin.com/api/v2/projects"
CROWDIN_PROJECT_ID="308189"
run: |
# Step 1: GET master branchId
BRANCH_ID=$(
curl -s -H "Authorization: Bearer $CROWDIN_API_TOKEN" \
$CROWDIN_BASE_URL/$CROWDIN_PROJECT_ID/branches | jq -r '.data[0].data.id'
)
# Step 2: POST Build the translations and get store build id
BUILD_ID=$(
curl -X POST -s \
-H "Authorization: Bearer $CROWDIN_API_TOKEN" \
-H "Content-Type: application/json" \
$CROWDIN_BASE_URL/$CROWDIN_PROJECT_ID/translations/builds \
-d "{\"branchId\": $BRANCH_ID}" | jq -r '.data.id'
)
MAX_TRIES=12
for try in {1..$MAX_TRIES}; do
BRANCH_STATUS=$(
curl -s -H "Authorization: Bearer $CROWDIN_API_TOKEN" \
$CROWDIN_BASE_URL/$CROWDIN_PROJECT_ID/translations/builds/$BUILD_ID | jq -r '.data.status'
)
echo "[*] Build status: $BRANCH_STATUS"
if [[ "$BRANCH_STATUS" == "finished" ]]; then
break
fi
if [[ $try -eq $MAX_TRIES ]]; then
echo "[!] Exceeded tries: $try"
exit 1
else
sleep 5
fi
done
# Step 4: when build is finished, get download url
DOWNLOAD_URL=$(
curl -s -H "Authorization: Bearer $CROWDIN_API_TOKEN" \
$CROWDIN_BASE_URL/$CROWDIN_PROJECT_ID/translations/builds/$BUILD_ID/download | jq -r '.data.url'
)
# Step 5: download the translations via the download url
SAVE_FILE=translations.zip
curl -s $DOWNLOAD_URL --output $SAVE_FILE
echo "[*] Saved to: $SAVE_FILE"
# Step 6: Unzip and cleanup
unzip -o $SAVE_FILE
rm $SAVE_FILE
- name: Commit changes
env:
BRANCH_NAME: ${{ steps.branch.outputs.branch-name }}
run: |
echo "[*] Adding new translations"
git add .
echo "=====Translations Changed====="
git status
echo "=============================="
echo "[*] Committing"
git commit -m "Autosync Crowdin translations"
echo "[*] Pushing"
git push -u origin $BRANCH_NAME
- name: Create/Update PR
env:
BRANCH_NAME: ${{ steps.cherry-pick.outputs.branch-name }}
BRANCH_EXISTED: ${{ steps.cherry-pick.outputs.branch-existed }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ "$BRANCH_EXISTED" == "false" ]; then
echo "[*] Creating PR"
gh pr create --title "Autosync Crowdin Translations" \
--body "Autosync the updated translations"
else
echo "[*] Existing PR updated"
fi

73
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,73 @@
name: Deploy
on:
workflow_dispatch:
inputs:
release_version:
description: "Release Tag Version <vX.X.X>"
required: true
release:
types:
- published
jobs:
deploy:
name: Deploy Web Vault
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
with:
ref: gh-pages
- name: Get release version
id: release-version
run: |
if [[ "${{ github.event_name }}" == "release" ]]; then
echo "::set-output name=version::${{ github.event.release.tag_name }}"
else
echo "::set-output name=version::${{ github.event.inputs.release_version }}"
fi
- name: Create deploy branch
run: |
git switch -c deploy-${{ steps.release-version.outputs.version }}
git push -u origin deploy-${{ steps.release-version.outputs.version }}
- name: Checkout Repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
with:
ref: rc
- name: setup git config
run: |
git config user.name = "GitHub Action Bot"
git config user.email = "<>"
git config --global url."https://github.com/".insteadOf ssh://git@github.com/
git config --global url."https://".insteadOf ssh://
- name: Install and Build
run: |
npm run sub:init
npm ci
npm run dist
- name: Deploy GitHub Pages
uses: crazy-max/ghaction-github-pages@db4476a01402e1a7ce05f41832040eef16d14925 # v2.5.0
with:
target_branch: deploy-${{ steps.release-version.outputs.version }}
build_dir: build
keep_history: true
commit_message: "Staging deploy ${{ steps.release-version.outputs.version }}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create Deploy PR
run: |
gh pr create --title "Deploy $VERSION" --body "Deploying $VERSION" --base gh-pages --head "$PR_BRANCH"
env:
VERSION: ${{ steps.release-version.outputs.version }}
PR_BRANCH: deploy-${{ steps.release-version.outputs.version }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,7 +1,6 @@
---
name: QA Deploy name: QA Deploy
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
image_extension: image_extension:
@@ -9,21 +8,22 @@ 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"
jobs: jobs:
deploy: deploy:
name: Deploy QA Web name: Deploy QA Web
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Checkout Repo - name: Checkout Repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 uses: actions/checkout@v2
- 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,37 +35,36 @@ 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
echo "---az get-creds---" echo "---az get-creds---"
az aks get-credentials -n $_QA_CLUSTER_NAME -g $_QA_CLUSTER_RESOURCE_GROUP az aks get-credentials -n $QA_CLUSTER_NAME -g $QA_CLUSTER_RESOURCE_GROUP
- 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.events.inputs.image_extension }}
if [[ $TAG_EXTENSION ]]; then if [[ $TAG_EXTENSION ]]; then
IMAGE_TAG=$IMAGE_TAG-$TAG_EXTENSION IMAGE_TAG=$IMAGE_TAG-$TAG_EXTENSION
fi fi
echo "::set-output name=value::$IMAGE_TAG" echo "::set-output name=value::$IMAGE_TAG"
- name: Deploy Web image - name: Deploy Web image
env: env:
IMAGE_TAG: ${{ steps.image_tag.outputs.value }} IMAGE_TAG: ${{ steps.image_tag.outputs.value }}
run: | run: |
kubectl set image -n $_QA_K8S_NAMESPACE deployment/web web=bitwardenqa.azurecr.io/web:$IMAGE_TAG --record kubectl set image -n $QA_K8S_NAMESPACE deployment/web web=bitwardenqa.azurecr.io/web:$IMAGE_TAG --record
kubectl rollout restart -n $_QA_K8S_NAMESPACE deployment/web kubectl rollout status deployment/web -n $QA_K8S_NAMESPACE
kubectl rollout status deployment/web -n $_QA_K8S_NAMESPACE

View File

@@ -1,32 +1,25 @@
---
name: Release name: Release
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
release_type: release_tag_name_input:
description: 'Release Options' description: "Release Tag Name <X.X.X>"
required: true required: true
default: 'Initial Release'
type: choice
options:
- Initial Release
- Redeploy
jobs: jobs:
setup: setup:
name: Setup runs-on: ubuntu-latest
runs-on: ubuntu-20.04
outputs: outputs:
release_version: ${{ steps.version.outputs.package }} release_upload_url: ${{ steps.create_release.outputs.upload_url }}
tag_version: ${{ steps.version.outputs.tag }} release_version: ${{ steps.create_tags.outputs.package_version }}
branch-name: ${{ steps.branch.outputs.branch-name }} tag_version: ${{ steps.create_tags.outputs.tag_version }}
steps: steps:
- name: Branch check - name: Branch check
run: | run: |
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix" ]]; then if [[ "$GITHUB_REF" != "refs/heads/rc" ]]; then
echo "===================================" echo "==================================="
echo "[!] Can only release from the 'rc' or 'hotfix' branches" echo "[!] Can only release from rc branch"
echo "===================================" echo "==================================="
exit 1 exit 1
fi fi
@@ -34,168 +27,131 @@ jobs:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # 2.3.4 uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # 2.3.4
- name: Check Release Version - name: Create Release Vars
id: version id: create_tags
run: | run: |
version=$( jq -r ".version" package.json) case "${RELEASE_TAG_NAME_INPUT:0:1}" in
previous_release_tag_version=$( v)
curl -sL https://api.github.com/repos/$GITHUB_REPOSITORY/releases/latest | jq -r ".tag_name" echo "RELEASE_NAME=${RELEASE_TAG_NAME_INPUT:1}" >> $GITHUB_ENV
) echo "RELEASE_TAG_NAME=$RELEASE_TAG_NAME_INPUT" >> $GITHUB_ENV
echo "::set-output name=package_version::${RELEASE_TAG_NAME_INPUT:1}"
echo "::set-output name=tag_version::$RELEASE_TAG_NAME_INPUT"
;;
[0-9])
echo "RELEASE_NAME=$RELEASE_TAG_NAME_INPUT" >> $GITHUB_ENV
echo "RELEASE_TAG_NAME=v$RELEASE_TAG_NAME_INPUT" >> $GITHUB_ENV
echo "::set-output name=package_version::$RELEASE_TAG_NAME_INPUT"
echo "::set-output name=tag_version::v$RELEASE_TAG_NAME_INPUT"
;;
*)
exit 1
;;
esac
env:
RELEASE_TAG_NAME_INPUT: ${{ github.event.inputs.release_tag_name_input }}
if [ "v$version" == "$previous_release_tag_version" ] && \ - name: Create Draft Release
[ "${{ github.event.inputs.release_type }}" == "Initial Release" ]; then id: create_release
echo "[!] Already released v$version. Please bump version to continue" uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e # 1.1.4 - Repo Archived
exit 1 env:
fi GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ env.RELEASE_TAG_NAME }}
release_name: Version ${{ env.RELEASE_NAME }}
draft: true
prerelease: false
echo "::set-output name=package::$version" ubuntu:
echo "::set-output name=tag::v$version" runs-on: ubuntu-latest
- name: Get branch name
id: branch
run: |
BRANCH_NAME=$(basename ${{ github.ref }})
echo "::set-output name=branch-name::$BRANCH_NAME"
self-host:
name: Release self-host docker
runs-on: ubuntu-20.04
needs: setup needs: setup
env: env:
_BRANCH_NAME: ${{ needs.setup.outputs.branch-name }} RELEASE_VERSION: ${{ needs.setup.outputs.release_version }}
_RELEASE_VERSION: ${{ needs.setup.outputs.release_version }}
steps: steps:
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '14'
- name: Update NPM
run: |
npm install -g npm@7
- name: Print environment - name: Print environment
run: | run: |
whoami whoami
node --version
npm --version
gulp --version
docker --version docker --version
echo "GitHub ref: $GITHUB_REF" echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT" echo "GitHub event: $GITHUB_EVENT"
- name: Setup DCT - name: Login to Azure
id: setup-dct uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff
with: with:
azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
azure-keyvault-name: "bitwarden-prod-kv"
- name: Retrieve secrets
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
with:
keyvault: "bitwarden-prod-kv"
secrets: "docker-password,
docker-username,
dct-delegate-2-repo-passphrase,
dct-delegate-2-key"
- name: Log into Docker
run: echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
env:
DOCKER_USERNAME: ${{ steps.retrieve-secrets.outputs.docker-username }}
DOCKER_PASSWORD: ${{ steps.retrieve-secrets.outputs.docker-password }}
- name: Setup Docker Trust
if: github.ref == 'refs/heads/master' || github.event_name == 'release' || github.ref == 'refs/heads/rc'
run: |
mkdir -p ~/.docker/trust/private
echo "$DCT_DELEGATE_KEY" > ~/.docker/trust/private/$DCT_DELEGATION_KEY_ID.key
env:
DCT_DELEGATION_KEY_ID: "c9bde8ec820701516491e5e03d3a6354e7bd66d05fa3df2b0062f68b116dc59c"
DCT_DELEGATE_KEY: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-key }}
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Pull latest selfhost image - name: Restore
run: docker pull bitwarden/web:$_BRANCH_NAME run: dotnet tool restore
- name: Tag version and latest - name: Build
run: | run: |
docker tag bitwarden/web:$_BRANCH_NAME bitwarden/web:$_RELEASE_VERSION echo -e "# Building Web\n"
docker tag bitwarden/web:$_BRANCH_NAME bitwarden/web:latest echo "Building app"
echo "npm version $(npm --version)"
npm install
npm run dist:selfhost
echo -e "\nBuilding Docker image"
docker --version
docker build -t bitwarden/web .
- name: Tag version
run: docker tag bitwarden/web bitwarden/web:$RELEASE_VERSION
- name: List Docker images - name: List Docker images
run: docker images run: docker images
- name: Push version and latest image - name: Push latest images
run: docker push bitwarden/web:latest
env:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
- name: Push version images
run: docker push bitwarden/web:$RELEASE_VERSION
env: 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.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
run: |
docker push bitwarden/web:$_RELEASE_VERSION
docker push bitwarden/web:latest
- name: Log out of Docker - name: Log out of Docker
run: docker logout run: docker logout
ghpages-deploy:
name: Deploy Web Vault
runs-on: ubuntu-20.04
needs:
- setup
- self-host
env:
_RELEASE_VERSION: ${{ needs.setup.outputs.release_version }}
_TAG_VERSION: ${{ needs.setup.outputs.tag_version }}
steps:
- name: Checkout Repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
with:
ref: gh-pages
- name: Create deploy branch
run: |
git switch -c deploy-$_TAG_VERSION
git push -u origin deploy-$_TAG_VERSION
- name: Checkout Repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Setup git config
run: |
git config user.name = "GitHub Action Bot"
git config user.email = "<>"
git config --global url."https://github.com/".insteadOf ssh://git@github.com/
git config --global url."https://".insteadOf ssh://
- name: Download latest cloud asset
uses: bitwarden/gh-actions/download-artifacts@23433be15ed6fd046ce12b6889c5184a8d9c8783
with:
workflow: build.yml
workflow_conclusion: success
branch: ${{ needs.setup.outputs.branch-name }}
artifacts: web-*-cloud-COMMERCIAL.zip
# This should result in a build directory in the current working directory
- name: Unzip build asset
run: unzip web-*-cloud-COMMERCIAL.zip
- name: Deploy GitHub Pages
uses: crazy-max/ghaction-github-pages@db4476a01402e1a7ce05f41832040eef16d14925 # v2.5.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
target_branch: deploy-${{ needs.setup.outputs.tag_version }}
build_dir: build
keep_history: true
commit_message: "Staging deploy ${{ needs.setup.outputs.release_version }}"
- name: Create Deploy PR
env:
PR_BRANCH: deploy-${{ env._TAG_VERSION }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr create --title "Deploy $_RELEASE_VERSION" \
--body "Deploying $_RELEASE_VERSION" \
--base gh-pages \
--head "$PR_BRANCH"
release:
name: Create GitHub Release
runs-on: ubuntu-20.04
needs:
- setup
- self-host
- ghpages-deploy
steps:
- name: Download latest build artifacts
uses: bitwarden/gh-actions/download-artifacts@23433be15ed6fd046ce12b6889c5184a8d9c8783
with:
workflow: build.yml
workflow_conclusion: success
branch: ${{ needs.setup.outputs.branch-name }}
artifacts: "web-*-selfhosted-COMMERCIAL.zip,
web-*-selfhosted-open-source.zip"
- name: Rename assets
run: |
mv web-*-selfhosted-COMMERCIAL.zip web-${{ needs.setup.outputs.release_version }}-selfhosted-COMMERCIAL.zip
mv web-*-selfhosted-open-source.zip web-${{ needs.setup.outputs.release_version }}-selfhosted-open-source.zip
- name: Create release
uses: ncipollo/release-action@95215a3cb6e6a1908b3c44e00b4fdb15548b1e09
with:
name: "Version ${{ needs.setup.outputs.release_version }}"
commit: ${{ github.sha }}
tag: "${{ needs.setup.outputs.tag_version }}"
body: "<insert release notes here>"
artifacts: "web-${{ needs.setup.outputs.release_version }}-selfhosted-COMMERCIAL.zip,
web-${{ needs.setup.outputs.release_version }}-selfhosted-open-source.zip"
token: ${{ secrets.GITHUB_TOKEN }}
draft: true

View File

@@ -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 }}"

2
.gitignore vendored
View File

@@ -12,4 +12,4 @@ dist/
*.swp *.swp
build/ build/
!dev-server.shared.pem !dev-server.shared.pem
config/local.json config/development.json

View File

@@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged

View File

@@ -1,12 +0,0 @@
# Build directories
build
dist
jslib
# External libraries / auto synced locales
src/locales
src/404/*.min.css
# Github Workflows
.github/workflows

View File

@@ -1,3 +0,0 @@
{
"printWidth": 100
}

View File

@@ -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)

View File

@@ -1,4 +1,4 @@
FROM nginx:stable FROM bitwarden/server
LABEL com.bitwarden.product="bitwarden" LABEL com.bitwarden.product="bitwarden"
@@ -8,29 +8,13 @@ RUN apt-get update \
curl \ curl \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
COPY docker/nginx.conf /etc/nginx ENV ASPNETCORE_URLS http://+:5000
COPY docker/nginx-web.conf /etc/nginx
COPY docker/mime.types /etc/nginx
COPY docker/security-headers.conf /etc/nginx
WORKDIR /app WORKDIR /app
EXPOSE 5000
COPY ./build . COPY ./build .
COPY docker/entrypoint.sh / COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh
RUN bash /entrypoint.sh HEALTHCHECK CMD curl -f http://localhost:5000 || exit 1
RUN chown -R bitwarden:bitwarden /app && chmod -R 755 /app && \
chown -R bitwarden:bitwarden /var/cache/nginx && \
chown -R bitwarden:bitwarden /var/log/nginx && \
chown -R bitwarden:bitwarden /etc/nginx/conf.d
RUN touch /var/run/nginx.pid && \
chown -R bitwarden:bitwarden /var/run/nginx.pid
USER bitwarden ENTRYPOINT ["/entrypoint.sh"]
EXPOSE 8080
HEALTHCHECK CMD curl -f http://localhost:8080 || exit 1
#ENTRYPOINT ["/entrypoint.sh"]
#CMD ["tail", "-f", "/dev/null"]
CMD nginx -g 'daemon off;'

52
ISSUE_TEMPLATE.md Normal file
View File

@@ -0,0 +1,52 @@
<!--
Please do not submit feature requests. The [Community Forums][1] has a
section for submitting, voting for, and discussing product feature requests.
[1]: https://community.bitwarden.com
-->
## Describe the Bug
<!-- Comment:
A clear and concise description of what the bug is.
-->
## Steps To Reproduce
<!-- Comment:
How can we reproduce the behavior:
-->
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. Click on '...'
## Expected Result
<!-- Comment:
A clear and concise description of what you expected to happen.
-->
## Actual Result
<!-- Comment:
A clear and concise description of what is happening.
-->
## Screenshots or Videos
<!-- Comment:
If applicable, add screenshots and/or a short video to help explain your problem.
-->
## Environment
- Operating system: [e.g. Windows 10, Mac OS Catalina]
- Browser: [e.g. Firefox 73.0.1]
- Build Version (Bottom of the page): [2.13.0]
## Additional Context
<!-- Comment:
Add any other context about the problem here.
-->

View File

@@ -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
@@ -32,7 +32,7 @@ For local development, run the app with:
``` ```
npm install npm install
npm run build:oss:watch npm run build:watch
``` ```
You can now access the web vault in your browser at `https://localhost:8080`. You can now access the web vault in your browser at `https://localhost:8080`.
@@ -41,48 +41,30 @@ 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: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/development.json` overriding any of the values in `config/base.json`. For example:
```json ```typescript
{ {
"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",
"proxyPortal": "http://your-portal-url",
"allowedHosts": ["hostnames-to-allow-in-webpack"] "allowedHosts": ["hostnames-to-allow-in-webpack"]
},
"urls": {}
} }
``` ```
Where the `urls` object is defined by the [Urls type in jslib](https://github.com/bitwarden/jslib/blob/master/common/src/abstractions/environment.service.ts). To pick up the overrides in the newly created `config/development.json` file, run the app with:
```
npm run build:dev:watch
```
## 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
```

View File

@@ -7,7 +7,7 @@ notify us. We welcome working with you to resolve the issue promptly. Thanks in
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every - Let us know as soon as possible upon discovery of a potential security issue, and we'll make every
effort to quickly resolve the issue. effort to quickly resolve the issue.
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a - Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a
third-party. We may publicly disclose the issue before resolving it, if appropriate. third-party. We may publicly disclose the issue before resolving it, if appropriate.
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or - 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 degradation of our service. Only interact with accounts you own or with explicit permission of the
account holder. account holder.

View File

@@ -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 { }

View File

@@ -1,20 +0,0 @@
import { Component } from "@angular/core";
import { AppComponent as BaseAppComponent } from "src/app/app.component";
import { DisablePersonalVaultExportPolicy } from "./policies/disable-personal-vault-export.component";
import { MaximumVaultTimeoutPolicy } from "./policies/maximum-vault-timeout.component";
@Component({
selector: "app-root",
templateUrl: "../../../src/app/app.component.html",
})
export class AppComponent extends BaseAppComponent {
ngOnInit() {
super.ngOnInit();
this.policyListService.addPolicies([
new MaximumVaultTimeoutPolicy(),
new DisablePersonalVaultExportPolicy(),
]);
}
}

View File

@@ -1,48 +1,30 @@
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 { BitwardenToastModule } from "jslib-angular/components/toastr.component"; import { DragDropModule } from '@angular/cdk/drag-drop';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppRoutingModule } from "./app-routing.module"; import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from "./app.component";
import { OrganizationsModule } from "./organizations/organizations.module";
import { DisablePersonalVaultExportPolicyComponent } from "./policies/disable-personal-vault-export.component";
import { MaximumVaultTimeoutPolicyComponent } from "./policies/maximum-vault-timeout.component";
import { OssRoutingModule } from "src/app/oss-routing.module"; import { AppComponent } from 'src/app/app.component';
import { OssModule } from "src/app/oss.module"; import { OssRoutingModule } from 'src/app/oss-routing.module';
import { ServicesModule } from "src/app/services/services.module"; import { OssModule } from 'src/app/oss.module';
import { WildcardRoutingModule } from "src/app/wildcard-routing.module"; import { ServicesModule } from 'src/app/services/services.module';
@NgModule({ @NgModule({
imports: [ imports: [
OssModule, OssModule,
BrowserAnimationsModule, BrowserAnimationsModule,
FormsModule, FormsModule,
ReactiveFormsModule, ServicesModule,
ServicesModule, ToasterModule.forRoot(),
BitwardenToastModule.forRoot({ InfiniteScrollModule,
maxOpened: 5, DragDropModule,
autoDismiss: true, AppRoutingModule,
closeButton: true, OssRoutingModule,
}), ],
InfiniteScrollModule, bootstrap: [AppComponent],
DragDropModule,
AppRoutingModule,
OssRoutingModule,
OrganizationsModule,
RouterModule,
WildcardRoutingModule, // Needs to be last to catch all non-existing routes
],
declarations: [
AppComponent,
MaximumVaultTimeoutPolicyComponent,
DisablePersonalVaultExportPolicyComponent,
],
bootstrap: [AppComponent],
}) })
export class AppModule {} export class AppModule { }

View File

@@ -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';
// tslint:disable-next-line // tslint:disable-next-line
require("src/scss/styles.scss"); require('src/scss/styles.scss');
import { AppModule } from "./app.module"; import { AppModule } from './app.module';
if (process.env.NODE_ENV === "production") { if (process.env.NODE_ENV === 'production') {
enableProdMode(); enableProdMode();
} }
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true }); platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });

View File

@@ -1,492 +0,0 @@
<div class="page-header d-flex">
<h1>{{ "singleSignOn" | i18n }}</h1>
</div>
<ng-container *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<form
#form
(ngSubmit)="submit()"
[formGroup]="data"
[appApiAction]="formPromise"
*ngIf="!loading"
ngNativeValidate
>
<p>
{{ "ssoPolicyHelpStart" | i18n }}
<a routerLink="../policies">{{ "ssoPolicyHelpLink" | i18n }}</a>
{{ "ssoPolicyHelpEnd" | i18n }}
<br />
{{ "ssoPolicyHelpKeyConnector" | i18n }}
</p>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="enabled"
[formControl]="enabled"
name="Enabled"
/>
<label class="form-check-label" for="enabled">{{ "allowSso" | i18n }}</label>
</div>
<small class="form-text text-muted">{{ "allowSsoDesc" | i18n }}</small>
</div>
<div class="form-group">
<label>{{ "memberDecryptionOption" | i18n }}</label>
<div class="form-check form-check-block">
<input
class="form-check-input"
type="radio"
id="memberDecryptionPass"
[value]="false"
formControlName="keyConnectorEnabled"
/>
<label class="form-check-label" for="memberDecryptionPass">
{{ "masterPass" | i18n }}
<small>{{ "memberDecryptionPassDesc" | i18n }}</small>
</label>
</div>
<div class="form-check mt-2 form-check-block">
<input
class="form-check-input"
type="radio"
id="memberDecryptionKey"
[value]="true"
formControlName="keyConnectorEnabled"
[attr.disabled]="!organization.useKeyConnector || null"
/>
<label class="form-check-label" for="memberDecryptionKey">
{{ "keyConnector" | i18n }}
<a
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
href="https://bitwarden.com/help/article/about-key-connector/"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
<small>{{ "memberDecryptionKeyConnectorDesc" | i18n }}</small>
</label>
</div>
</div>
<ng-container *ngIf="data.value.keyConnectorEnabled">
<app-callout type="warning" [useAlertRole]="true">
{{ "keyConnectorWarning" | i18n }}
</app-callout>
<div class="form-group">
<label for="keyConnectorUrl">{{ "keyConnectorUrl" | i18n }}</label>
<div class="input-group">
<input
class="form-control"
formControlName="keyConnectorUrl"
id="keyConnectorUrl"
required
/>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
(click)="validateKeyConnectorUrl()"
[disabled]="!enableTestKeyConnector"
>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
*ngIf="keyConnectorUrl.pending"
></i>
<span *ngIf="!keyConnectorUrl.pending">
{{ "keyConnectorTest" | i18n }}
</span>
</button>
</div>
</div>
<ng-container *ngIf="keyConnectorUrl.pristine && !keyConnectorUrl.pending">
<div class="text-danger" *ngIf="keyConnectorUrl.hasError('invalidUrl')" role="alert">
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
{{ "keyConnectorTestFail" | i18n }}
</div>
<div class="text-success" *ngIf="!keyConnectorUrl.hasError('invalidUrl')" role="alert">
<i class="bwi bwi-check-circle" aria-hidden="true"></i>
{{ "keyConnectorTestSuccess" | i18n }}
</div>
</ng-container>
</div>
</ng-container>
<div class="form-group">
<label for="type">{{ "type" | i18n }}</label>
<select class="form-control" id="type" formControlName="configType">
<option [ngValue]="0" disabled>{{ "selectType" | i18n }}</option>
<option [ngValue]="1">OpenID Connect</option>
<option [ngValue]="2">SAML 2.0</option>
</select>
</div>
<!-- OIDC -->
<div *ngIf="data.value.configType == 1">
<div class="config-section">
<h2>{{ "openIdConnectConfig" | i18n }}</h2>
<div class="form-group">
<label>{{ "callbackPath" | i18n }}</label>
<div class="input-group">
<input class="form-control" readonly [value]="callbackPath" />
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyValue' | i18n }}"
(click)="copy(callbackPath)"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<div class="form-group">
<label>{{ "signedOutCallbackPath" | i18n }}</label>
<div class="input-group">
<input class="form-control" readonly [value]="signedOutCallbackPath" />
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyValue' | i18n }}"
(click)="copy(signedOutCallbackPath)"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<div class="form-group">
<label for="authority">{{ "authority" | i18n }}</label>
<input class="form-control" formControlName="authority" id="authority" />
</div>
<div class="form-group">
<label for="clientId">{{ "clientId" | i18n }}</label>
<input class="form-control" formControlName="clientId" id="clientId" />
</div>
<div class="form-group">
<label for="clientSecret">{{ "clientSecret" | i18n }}</label>
<input class="form-control" formControlName="clientSecret" id="clientSecret" />
</div>
<div class="form-group">
<label for="metadataAddress">{{ "metadataAddress" | i18n }}</label>
<input class="form-control" formControlName="metadataAddress" id="metadataAddress" />
</div>
<div class="form-group">
<label for="redirectBehavior">{{ "oidcRedirectBehavior" | i18n }}</label>
<select class="form-control" formControlName="redirectBehavior" id="redirectBehavior">
<option [ngValue]="0">Redirect GET</option>
<option [ngValue]="1">Form POST</option>
</select>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="getClaimsFromUserInfoEndpoint"
formControlName="getClaimsFromUserInfoEndpoint"
/>
<label class="form-check-label" for="getClaimsFromUserInfoEndpoint">
{{ "getClaimsFromUserInfoEndpoint" | i18n }}
</label>
</div>
</div>
<div class="form-group">
<label for="additionalScopes">{{ "additionalScopes" | i18n }}</label>
<input class="form-control" formControlName="additionalScopes" id="additionalScopes" />
</div>
<div class="form-group">
<label for="additionalUserIdClaimTypes">{{ "additionalUserIdClaimTypes" | i18n }}</label>
<input
class="form-control"
formControlName="additionalUserIdClaimTypes"
id="additionalUserIdClaimTypes"
/>
</div>
<div class="form-group">
<label for="additionalEmailClaimTypes">{{ "additionalEmailClaimTypes" | i18n }}</label>
<input
class="form-control"
formControlName="additionalEmailClaimTypes"
id="additionalEmailClaimTypes"
/>
</div>
<div class="form-group">
<label for="additionalNameClaimTypes">{{ "additionalNameClaimTypes" | i18n }}</label>
<input
class="form-control"
formControlName="additionalNameClaimTypes"
id="additionalNameClaimTypes"
/>
</div>
<div class="form-group">
<label for="acrValues">{{ "acrValues" | i18n }}</label>
<input class="form-control" formControlName="acrValues" id="acrValues" />
</div>
<div class="form-group">
<label for="expectedReturnAcrValue">{{ "expectedReturnAcrValue" | i18n }}</label>
<input
class="form-control"
formControlName="expectedReturnAcrValue"
id="expectedReturnAcrValue"
/>
</div>
</div>
</div>
<div *ngIf="data.value.configType == 2">
<!-- SAML2 SP -->
<div class="config-section">
<h2>{{ "samlSpConfig" | i18n }}</h2>
<div class="form-group">
<label>{{ "spEntityId" | i18n }}</label>
<div class="input-group">
<input class="form-control" readonly [value]="spEntityId" />
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyValue' | i18n }}"
(click)="copy(spEntityId)"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<div class="form-group">
<label>{{ "spMetadataUrl" | i18n }}</label>
<div class="input-group">
<input class="form-control" readonly [value]="spMetadataUrl" />
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'launch' | i18n }}"
(click)="launchUri(spMetadataUrl)"
>
<i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i>
</button>
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyValue' | i18n }}"
(click)="copy(spMetadataUrl)"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<div class="form-group">
<label>{{ "spAcsUrl" | i18n }}</label>
<div class="input-group">
<input class="form-control" readonly [value]="spAcsUrl" />
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyValue' | i18n }}"
(click)="copy(spAcsUrl)"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<div class="form-group">
<label for="spNameIdFormat">{{ "spNameIdFormat" | i18n }}</label>
<select class="form-control" formControlName="spNameIdFormat" id="spNameIdFormat">
<option value="0">Not Configured</option>
<option value="1">Unspecified</option>
<option value="2">Email Address</option>
<option value="3">X.509 Subject Name</option>
<option value="4">Windows Domain Qualified Name</option>
<option value="5">Kerberos Principal Name</option>
<option value="6">Entity Identifier</option>
<option value="7">Persistent</option>
<option value="8">Transient</option>
</select>
</div>
<div class="form-group">
<label for="spOutboundSigningAlgorithm">{{ "spOutboundSigningAlgorithm" | i18n }}</label>
<select
class="form-control"
formControlName="spOutboundSigningAlgorithm"
id="spOutboundSigningAlgorithm"
>
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{ o }}</option>
</select>
</div>
<div class="form-group">
<label for="spSigningBehavior">{{ "spSigningBehavior" | i18n }}</label>
<select class="form-control" formControlName="spSigningBehavior" id="spSigningBehavior">
<option value="0">If IdP Wants Authn Requests Signed</option>
<option value="1">Always</option>
<option value="3">Never</option>
</select>
</div>
<div class="form-group">
<label for="spMinIncomingSigningAlgorithm">{{
"spMinIncomingSigningAlgorithm" | i18n
}}</label>
<select
class="form-control"
formControlName="spMinIncomingSigningAlgorithm"
id="spMinIncomingSigningAlgorithm"
>
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{ o }}</option>
</select>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="spWantAssertionsSigned"
formControlName="spWantAssertionsSigned"
/>
<label class="form-check-label" for="spWantAssertionsSigned">
{{ "spWantAssertionsSigned" | i18n }}
</label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="spValidateCertificates"
formControlName="spValidateCertificates"
/>
<label class="form-check-label" for="spValidateCertificates">
{{ "spValidateCertificates" | i18n }}
</label>
</div>
</div>
</div>
<!-- SAML2 IDP -->
<div class="config-section">
<h2>{{ "samlIdpConfig" | i18n }}</h2>
<div class="form-group">
<label for="idpEntityId">{{ "idpEntityId" | i18n }}</label>
<input class="form-control" formControlName="idpEntityId" id="idpEntityId" />
</div>
<div class="form-group">
<label for="idpBindingType">{{ "idpBindingType" | i18n }}</label>
<select class="form-control" formControlName="idpBindingType" id="idpBindingType">
<option value="1">Redirect</option>
<option value="2">HTTP POST</option>
<option value="4">Artifact</option>
</select>
</div>
<div class="form-group">
<label for="idpSingleSignOnServiceUrl">{{ "idpSingleSignOnServiceUrl" | i18n }}</label>
<input
class="form-control"
formControlName="idpSingleSignOnServiceUrl"
id="idpSingleSignOnServiceUrl"
/>
</div>
<div class="form-group">
<label for="idpSingleLogoutServiceUrl">{{ "idpSingleLogoutServiceUrl" | i18n }}</label>
<input
class="form-control"
formControlName="idpSingleLogoutServiceUrl"
id="idpSingleLogoutServiceUrl"
/>
</div>
<div class="form-group">
<label for="idpArtifactResolutionServiceUrl">{{
"idpArtifactResolutionServiceUrl" | i18n
}}</label>
<input
class="form-control"
formControlName="idpArtifactResolutionServiceUrl"
id="idpArtifactResolutionServiceUrl"
/>
</div>
<div class="form-group">
<label for="idpX509PublicCert">{{ "idpX509PublicCert" | i18n }}</label>
<textarea
formControlName="idpX509PublicCert"
class="form-control form-control-sm text-monospace"
rows="6"
id="idpX509PublicCert"
></textarea>
</div>
<div class="form-group">
<label for="idpOutboundSigningAlgorithm">{{ "idpOutboundSigningAlgorithm" | i18n }}</label>
<select
class="form-control"
formControlName="idpOutboundSigningAlgorithm"
id="idpOutboundSigningAlgorithm"
>
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{ o }}</option>
</select>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="idpAllowUnsolicitedAuthnResponse"
formControlName="idpAllowUnsolicitedAuthnResponse"
/>
<label class="form-check-label" for="idpAllowUnsolicitedAuthnResponse">
{{ "idpAllowUnsolicitedAuthnResponse" | i18n }}
</label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="idpDisableOutboundLogoutRequests"
formControlName="idpDisableOutboundLogoutRequests"
/>
<label class="form-check-label" for="idpDisableOutboundLogoutRequests">
{{ "idpDisableOutboundLogoutRequests" | i18n }}
</label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="idpWantAuthnRequestsSigned"
formControlName="idpWantAuthnRequestsSigned"
/>
<label class="form-check-label" for="idpWantAuthnRequestsSigned">
{{ "idpWantAuthnRequestsSigned" | i18n }}
</label>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span>
</button>
</form>

View File

@@ -1,183 +0,0 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { Organization } from "jslib-common/models/domain/organization";
import { OrganizationSsoRequest } from "jslib-common/models/request/organization/organizationSsoRequest";
@Component({
selector: "app-org-manage-sso",
templateUrl: "sso.component.html",
})
export class SsoComponent implements OnInit {
samlSigningAlgorithms = [
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
"http://www.w3.org/2000/09/xmldsig#rsa-sha384",
"http://www.w3.org/2000/09/xmldsig#rsa-sha512",
"http://www.w3.org/2000/09/xmldsig#rsa-sha1",
];
loading = true;
organizationId: string;
organization: Organization;
formPromise: Promise<any>;
callbackPath: string;
signedOutCallbackPath: string;
spEntityId: string;
spMetadataUrl: string;
spAcsUrl: string;
enabled = this.formBuilder.control(false);
data = this.formBuilder.group({
configType: [],
keyConnectorEnabled: [],
keyConnectorUrl: [],
// OpenId
authority: [],
clientId: [],
clientSecret: [],
metadataAddress: [],
redirectBehavior: [],
getClaimsFromUserInfoEndpoint: [],
additionalScopes: [],
additionalUserIdClaimTypes: [],
additionalEmailClaimTypes: [],
additionalNameClaimTypes: [],
acrValues: [],
expectedReturnAcrValue: [],
// SAML
spNameIdFormat: [],
spOutboundSigningAlgorithm: [],
spSigningBehavior: [],
spMinIncomingSigningAlgorithm: [],
spWantAssertionsSigned: [],
spValidateCertificates: [],
idpEntityId: [],
idpBindingType: [],
idpSingleSignOnServiceUrl: [],
idpSingleLogoutServiceUrl: [],
idpArtifactResolutionServiceUrl: [],
idpX509PublicCert: [],
idpOutboundSigningAlgorithm: [],
idpAllowUnsolicitedAuthnResponse: [],
idpDisableOutboundLogoutRequests: [],
idpWantAuthnRequestsSigned: [],
});
constructor(
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private apiService: ApiService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private organizationService: OrganizationService
) {}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
await this.load();
});
}
async load() {
this.organization = await this.organizationService.get(this.organizationId);
const ssoSettings = await this.apiService.getOrganizationSso(this.organizationId);
this.data.patchValue(ssoSettings.data);
this.enabled.setValue(ssoSettings.enabled);
this.callbackPath = ssoSettings.urls.callbackPath;
this.signedOutCallbackPath = ssoSettings.urls.signedOutCallbackPath;
this.spEntityId = ssoSettings.urls.spEntityId;
this.spMetadataUrl = ssoSettings.urls.spMetadataUrl;
this.spAcsUrl = ssoSettings.urls.spAcsUrl;
this.keyConnectorUrl.markAsDirty();
this.loading = false;
}
copy(value: string) {
this.platformUtilsService.copyToClipboard(value);
}
launchUri(url: string) {
this.platformUtilsService.launchUri(url);
}
async submit() {
this.formPromise = this.postData();
try {
const response = await this.formPromise;
this.data.patchValue(response.data);
this.enabled.setValue(response.enabled);
this.platformUtilsService.showToast("success", null, this.i18nService.t("ssoSettingsSaved"));
} catch {
// Logged by appApiAction, do nothing
}
this.formPromise = null;
}
async postData() {
if (this.data.get("keyConnectorEnabled").value) {
await this.validateKeyConnectorUrl();
if (this.keyConnectorUrl.hasError("invalidUrl")) {
throw new Error(this.i18nService.t("keyConnectorTestFail"));
}
}
const request = new OrganizationSsoRequest();
request.enabled = this.enabled.value;
request.data = this.data.value;
return this.apiService.postOrganizationSso(this.organizationId, request);
}
async validateKeyConnectorUrl() {
if (this.keyConnectorUrl.pristine) {
return;
}
this.keyConnectorUrl.markAsPending();
try {
await this.apiService.getKeyConnectorAlive(this.keyConnectorUrl.value);
this.keyConnectorUrl.updateValueAndValidity();
} catch {
this.keyConnectorUrl.setErrors({
invalidUrl: true,
});
}
this.keyConnectorUrl.markAsPristine();
}
get enableTestKeyConnector() {
return (
this.data.get("keyConnectorEnabled").value &&
this.keyConnectorUrl != null &&
this.keyConnectorUrl.value !== ""
);
}
get keyConnectorUrl() {
return this.data.get("keyConnectorUrl");
}
}

View File

@@ -1,54 +0,0 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { AuthGuardService } from "jslib-angular/services/auth-guard.service";
import { Permissions } from "jslib-common/enums/permissions";
import { OrganizationLayoutComponent } from "src/app/layouts/organization-layout.component";
import { ManageComponent } from "src/app/organizations/manage/manage.component";
import { OrganizationGuardService } from "src/app/services/organization-guard.service";
import { OrganizationTypeGuardService } from "src/app/services/organization-type-guard.service";
import { SsoComponent } from "./manage/sso.component";
const routes: Routes = [
{
path: "organizations/:organizationId",
component: OrganizationLayoutComponent,
canActivate: [AuthGuardService, OrganizationGuardService],
children: [
{
path: "manage",
component: ManageComponent,
canActivate: [OrganizationTypeGuardService],
data: {
permissions: [
Permissions.CreateNewCollections,
Permissions.EditAnyCollection,
Permissions.DeleteAnyCollection,
Permissions.EditAssignedCollections,
Permissions.DeleteAssignedCollections,
Permissions.AccessEventLogs,
Permissions.ManageGroups,
Permissions.ManageUsers,
Permissions.ManagePolicies,
Permissions.ManageSso,
],
},
children: [
{
path: "sso",
component: SsoComponent,
},
],
},
],
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class OrganizationsRoutingModule {}

View File

@@ -1,14 +0,0 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { OssModule } from "src/app/oss.module";
import { SsoComponent } from "./manage/sso.component";
import { OrganizationsRoutingModule } from "./organizations-routing.module";
@NgModule({
imports: [CommonModule, FormsModule, ReactiveFormsModule, OssModule, OrganizationsRoutingModule],
declarations: [SsoComponent],
})
export class OrganizationsModule {}

View File

@@ -1,12 +0,0 @@
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="enabled"
[formControl]="enabled"
name="Enabled"
/>
<label class="form-check-label" for="enabled">{{ "enabled" | i18n }}</label>
</div>
</div>

View File

@@ -1,26 +0,0 @@
import { Component } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PolicyType } from "jslib-common/enums/policyType";
import { PolicyRequest } from "jslib-common/models/request/policyRequest";
import {
BasePolicy,
BasePolicyComponent,
} from "src/app/organizations/policies/base-policy.component";
export class DisablePersonalVaultExportPolicy extends BasePolicy {
name = "disablePersonalVaultExport";
description = "disablePersonalVaultExportDesc";
type = PolicyType.DisablePersonalVaultExport;
component = DisablePersonalVaultExportPolicyComponent;
}
@Component({
selector: "policy-disable-personal-vault-export",
templateUrl: "disable-personal-vault-export.component.html",
})
export class DisablePersonalVaultExportPolicyComponent extends BasePolicyComponent {}

View File

@@ -1,47 +0,0 @@
<app-callout type="tip" title="{{ 'prerequisite' | i18n }}">
{{ "requireSsoPolicyReq" | i18n }}
</app-callout>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="enabled"
[formControl]="enabled"
name="Enabled"
/>
<label class="form-check-label" for="enabled">{{ "enabled" | i18n }}</label>
</div>
</div>
<div [formGroup]="data">
<div class="form-group">
<label for="hours">{{ "maximumVaultTimeoutLabel" | i18n }}</label>
<div class="row">
<div class="col-6">
<input
id="hours"
class="form-control"
type="number"
min="0"
name="hours"
formControlName="hours"
/>
<small>{{ "hours" | i18n }}</small>
</div>
<div class="col-6">
<input
id="minutes"
class="form-control"
type="number"
min="0"
max="59"
name="minutes"
formControlName="minutes"
/>
<small>{{ "minutes" | i18n }}</small>
</div>
</div>
</div>
</div>

View File

@@ -1,72 +0,0 @@
import { Component } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PolicyType } from "jslib-common/enums/policyType";
import { PolicyRequest } from "jslib-common/models/request/policyRequest";
import {
BasePolicy,
BasePolicyComponent,
} from "src/app/organizations/policies/base-policy.component";
export class MaximumVaultTimeoutPolicy extends BasePolicy {
name = "maximumVaultTimeout";
description = "maximumVaultTimeoutDesc";
type = PolicyType.MaximumVaultTimeout;
component = MaximumVaultTimeoutPolicyComponent;
}
@Component({
selector: "policy-maximum-timeout",
templateUrl: "maximum-vault-timeout.component.html",
})
export class MaximumVaultTimeoutPolicyComponent extends BasePolicyComponent {
data = this.formBuilder.group({
hours: [null],
minutes: [null],
});
constructor(private formBuilder: FormBuilder, private i18nService: I18nService) {
super();
}
loadData() {
const minutes = this.policyResponse.data?.minutes;
if (minutes == null) {
return;
}
this.data.patchValue({
hours: Math.floor(minutes / 60),
minutes: minutes % 60,
});
}
buildRequestData() {
if (this.data.value.hours == null && this.data.value.minutes == null) {
return null;
}
return {
minutes: this.data.value.hours * 60 + this.data.value.minutes,
};
}
buildRequest(policiesEnabledMap: Map<PolicyType, boolean>): Promise<PolicyRequest> {
const singleOrgEnabled = policiesEnabledMap.get(PolicyType.SingleOrg) ?? false;
if (this.enabled.value && !singleOrgEnabled) {
throw new Error(this.i18nService.t("requireSsoPolicyReqError"));
}
const data = this.buildRequestData();
if (data?.minutes == null || data?.minutes <= 0) {
throw new Error(this.i18nService.t("invalidMaximumVaultTimeout"));
}
return super.buildRequest(policiesEnabledMap);
}
}

View File

@@ -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">&times;</span>
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</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>

View File

@@ -1,86 +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 { I18nService } from "jslib-common/abstractions/i18n.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 { ProviderService } from "jslib-common/abstractions/provider.service"; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { ValidationService } from "jslib-angular/services/validation.service"; import { ValidationService } from 'jslib-angular/services/validation.service';
import { WebProviderService } from "../services/webProvider.service"; import { ProviderService } from '../services/provider.service';
import { Organization } from "jslib-common/models/domain/organization"; import { Organization } from 'jslib-common/models/domain/organization';
import { Provider } from "jslib-common/models/domain/provider"; import { Provider } from 'jslib-common/models/domain/provider';
import { PlanType } from 'jslib-common/enums/planType';
@Component({ @Component({
selector: "provider-add-organization", selector: 'provider-add-organization',
templateUrl: "add-organization.component.html", templateUrl: 'add-organization.component.html',
}) })
export class AddOrganizationComponent implements OnInit { export class AddOrganizationComponent implements OnInit {
@Input() providerId: string;
@Input() 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();
}
} }

View File

@@ -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>

View File

@@ -1,183 +1,169 @@
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import {
import { ActivatedRoute } from "@angular/router"; Component,
ComponentFactoryResolver,
OnInit,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { first } from "rxjs/operators"; import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { SearchService } from 'jslib-common/abstractions/search.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { ApiService } from "jslib-common/abstractions/api.service"; import { PlanType } from 'jslib-common/enums/planType';
import { I18nService } from "jslib-common/abstractions/i18n.service"; import { ProviderUserType } from 'jslib-common/enums/providerUserType';
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 { ModalService } from "jslib-angular/services/modal.service"; import { ValidationService } from 'jslib-angular/services/validation.service';
import { ValidationService } from "jslib-angular/services/validation.service";
import { PlanType } from "jslib-common/enums/planType"; import {
import { ProviderUserType } from "jslib-common/enums/providerUserType"; ProviderOrganizationOrganizationDetailsResponse
} from 'jslib-common/models/response/provider/providerOrganizationResponse';
import { Organization } from 'jslib-common/models/domain/organization';
import { Organization } from "jslib-common/models/domain/organization"; import { ModalComponent } from 'src/app/modal.component';
import { ProviderOrganizationOrganizationDetailsResponse } from "jslib-common/models/response/provider/providerOrganizationResponse";
import { WebProviderService } from "../services/webProvider.service"; import { ProviderService } from '../services/provider.service';
import { AddOrganizationComponent } from "./add-organization.component"; import { AddOrganizationComponent } from './add-organization.component';
const DisallowedPlanTypes = [ const DisallowedPlanTypes = [PlanType.Free, PlanType.FamiliesAnnually2019, PlanType.FamiliesAnnually];
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>; modal: ModalComponent;
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 componentFactoryResolver: ComponentFactoryResolver,
private logService: LogService) { }
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) { const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
this.logService.error(`Handled exception: ${e}`); this.searchText = qParams.search;
} if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
}
});
}); });
}
);
}
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.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;
}
addExistingOrganization() {
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.addModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<AddOrganizationComponent>(AddOrganizationComponent, this.addModalRef);
childComponent.providerId = this.providerId;
childComponent.organizations = this.addableOrganizations;
childComponent.onAddedOrganization.subscribe(async () => {
try {
await this.load();
this.modal.close();
} catch (e) {
this.logService.error(`Handled exception: ${e}`);
}
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
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;
}
} }

View File

@@ -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>

View File

@@ -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;
}); });
} }
} }

View File

@@ -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 src="/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">{{ "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>

View File

@@ -1,56 +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 { BaseAcceptComponent } from "src/app/common/base.accept.component"; import { BaseAcceptComponent } from 'src/app/common/base.accept.component';
import { ApiService } from "jslib-common/abstractions/api.service"; import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from "jslib-common/abstractions/i18n.service"; import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; import { StateService } from 'jslib-common/abstractions/state.service';
import { StateService } from "jslib-common/abstractions/state.service"; import { UserService } from 'jslib-common/abstractions/user.service';
import { ProviderUserAcceptRequest } from "jslib-common/models/request/provider/providerUserAcceptRequest"; import { 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;
} }
} }

View File

@@ -1,34 +1,38 @@
import { Component, Input } from "@angular/core"; import {
Component,
Input,
} from '@angular/core';
import { ProviderUserBulkConfirmRequest } from "jslib-common/models/request/provider/providerUserBulkConfirmRequest"; import { ProviderUserBulkConfirmRequest } from 'jslib-common/models/request/provider/providerUserBulkConfirmRequest';
import { ProviderUserBulkRequest } from "jslib-common/models/request/provider/providerUserBulkRequest"; import { ProviderUserBulkRequest } from 'jslib-common/models/request/provider/providerUserBulkRequest';
import { ProviderUserStatusType } from "jslib-common/enums/providerUserStatusType"; import { ProviderUserStatusType } from 'jslib-common/enums/providerUserStatusType';
import { BulkConfirmComponent as OrganizationBulkConfirmComponent } from "src/app/organizations/manage/bulk/bulk-confirm.component"; import { BulkConfirmComponent as OrganizationBulkConfirmComponent } from 'src/app/organizations/manage/bulk/bulk-confirm.component';
import { BulkUserDetails } from "src/app/organizations/manage/bulk/bulk-status.component"; import { BulkUserDetails } from 'src/app/organizations/manage/bulk/bulk-status.component';
@Component({ @Component({
templateUrl: "../../../../../../src/app/organizations/manage/bulk/bulk-confirm.component.html", templateUrl: '/src/app/organizations/manage/bulk/bulk-confirm.component.html',
}) })
export class BulkConfirmComponent extends OrganizationBulkConfirmComponent { export class BulkConfirmComponent extends OrganizationBulkConfirmComponent {
@Input() providerId: string;
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);
}
} }

View File

@@ -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);
}
} }

View File

@@ -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>

View File

@@ -1,82 +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 { 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 { ProviderService } from "jslib-common/abstractions/provider.service"; import { UserService } from 'jslib-common/abstractions/user.service';
import { UserNamePipe } from "jslib-angular/pipes/user-name.pipe"; import { UserNamePipe } from 'jslib-angular/pipes/user-name.pipe';
import { EventResponse } from "jslib-common/models/response/eventResponse"; import { EventResponse } from 'jslib-common/models/response/eventResponse';
import { EventService } from "src/app/services/event.service"; import { EventService } from 'src/app/services/event.service';
import { BaseEventsComponent } from "src/app/common/base.events.component"; import { BaseEventsComponent } from 'src/app/common/base.events.component';
@Component({ @Component({
selector: "provider-events", selector: 'provider-events',
templateUrl: "events.component.html", templateUrl: 'events.component.html',
}) })
export class EventsComponent extends BaseEventsComponent implements OnInit { export class EventsComponent extends BaseEventsComponent implements OnInit {
exportFileName: string = "provider-events"; exportFileName: string = 'provider-events';
providerId: string; providerId: string;
private providerUsersUserIdMap = new Map<string, any>(); private providerUsersUserIdMap = new Map<string, any>();
private providerUsersIdMap = new Map<string, any>(); private providerUsersIdMap = new Map<string, any>();
constructor( 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;
}
} }

View File

@@ -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-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-9">
*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>

View File

@@ -1,24 +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;
}); });
} }
} }

View File

@@ -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-remove" 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>

View File

@@ -1,292 +1,286 @@
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import {
import { ActivatedRoute, Router } from "@angular/router"; Component,
ComponentFactoryResolver,
OnInit,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { first } from "rxjs/operators"; import { ApiService } from 'jslib-common/abstractions/api.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { SearchService } from 'jslib-common/abstractions/search.service';
import { StorageService } from 'jslib-common/abstractions/storage.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { ApiService } from "jslib-common/abstractions/api.service"; import { ValidationService } from 'jslib-angular/services/validation.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 { ModalService } from "jslib-angular/services/modal.service"; import { ProviderUserStatusType } from 'jslib-common/enums/providerUserStatusType';
import { ValidationService } from "jslib-angular/services/validation.service"; import { ProviderUserType } from 'jslib-common/enums/providerUserType';
import { ProviderUserStatusType } from "jslib-common/enums/providerUserStatusType"; import { SearchPipe } from 'jslib-angular/pipes/search.pipe';
import { ProviderUserType } from "jslib-common/enums/providerUserType"; import { UserNamePipe } from 'jslib-angular/pipes/user-name.pipe';
import { SearchPipe } from "jslib-angular/pipes/search.pipe"; import { ListResponse } from 'jslib-common/models/response/listResponse';
import { UserNamePipe } from "jslib-angular/pipes/user-name.pipe"; import { ProviderUserUserDetailsResponse } from 'jslib-common/models/response/provider/providerUserResponse';
import { ListResponse } from "jslib-common/models/response/listResponse"; import { ProviderUserBulkRequest } from 'jslib-common/models/request/provider/providerUserBulkRequest';
import { ProviderUserUserDetailsResponse } from "jslib-common/models/response/provider/providerUserResponse"; import { ProviderUserConfirmRequest } from 'jslib-common/models/request/provider/providerUserConfirmRequest';
import { ProviderUserBulkResponse } from 'jslib-common/models/response/provider/providerUserBulkResponse';
import { ProviderUserBulkRequest } from "jslib-common/models/request/provider/providerUserBulkRequest"; import { BasePeopleComponent } from 'src/app/common/base.people.component';
import { ProviderUserConfirmRequest } from "jslib-common/models/request/provider/providerUserConfirmRequest"; import { ModalComponent } from 'src/app/modal.component';
import { ProviderUserBulkResponse } from "jslib-common/models/response/provider/providerUserBulkResponse"; import { BulkStatusComponent } from 'src/app/organizations/manage/bulk/bulk-status.component';
import { EntityEventsComponent } from 'src/app/organizations/manage/entity-events.component';
import { BasePeopleComponent } from "src/app/common/base.people.component"; import { BulkConfirmComponent } from './bulk/bulk-confirm.component';
import { BulkStatusComponent } from "src/app/organizations/manage/bulk/bulk-status.component"; import { BulkRemoveComponent } from './bulk/bulk-remove.component';
import { EntityEventsComponent } from "src/app/organizations/manage/entity-events.component"; import { UserAddEditComponent } from './user-add-edit.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, componentFactoryResolver: ComponentFactoryResolver,
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, componentFactoryResolver, 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) { const queryParamsSub = this.route.queryParams.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]);
}
}
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
}
});
});
}
getUsers(): Promise<ListResponse<ProviderUserUserDetailsResponse>> {
return this.apiService.getProviderUsers(this.providerId);
}
deleteUser(id: string): Promise<any> {
return this.apiService.deleteProviderUser(this.providerId, id);
}
reinviteUser(id: string): Promise<any> {
return this.apiService.postProviderUserReinvite(this.providerId, id);
}
async confirmUser(user: ProviderUserUserDetailsResponse, publicKey: Uint8Array): Promise<any> {
const providerKey = await this.cryptoService.getProviderKey(this.providerId);
const key = await this.cryptoService.rsaEncrypt(providerKey.key, publicKey.buffer);
const request = new ProviderUserConfirmRequest();
request.key = key.encryptedString;
await this.apiService.postProviderUserConfirm(this.providerId, user.id, request);
}
edit(user: ProviderUserUserDetailsResponse) {
if (this.modal != null) {
this.modal.close();
} }
});
});
}
getUsers(): Promise<ListResponse<ProviderUserUserDetailsResponse>> { const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
return this.apiService.getProviderUsers(this.providerId); this.modal = this.addEditModalRef.createComponent(factory).instance;
} const childComponent = this.modal.show<UserAddEditComponent>(
UserAddEditComponent, this.addEditModalRef);
deleteUser(id: string): Promise<any> { childComponent.name = this.userNamePipe.transform(user);
return this.apiService.deleteProviderUser(this.providerId, id); childComponent.providerId = this.providerId;
} childComponent.providerUserId = user != null ? user.id : null;
childComponent.onSavedUser.subscribe(() => {
reinviteUser(id: string): Promise<any> { this.modal.close();
return this.apiService.postProviderUserReinvite(this.providerId, id); this.load();
}
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(() => { childComponent.onDeletedUser.subscribe(() => {
modal.close(); this.modal.close();
this.removeUser(user); this.removeUser(user);
}); });
}
);
}
async events(user: ProviderUserUserDetailsResponse) { this.modal.onClosed.subscribe(() => {
const [modal] = await this.modalService.openViewRef( this.modal = null;
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;
}
const [modal] = await this.modalService.openViewRef(
BulkRemoveComponent,
this.bulkRemoveModalRef,
(comp) => {
comp.providerId = this.providerId;
comp.users = this.getCheckedUsers();
}
);
await modal.onClosedPromise();
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.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;
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();
} }
}
async events(user: ProviderUserUserDetailsResponse) {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.eventsModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<EntityEventsComponent>(
EntityEventsComponent, this.eventsModalRef);
childComponent.name = this.userNamePipe.transform(user);
childComponent.providerId = this.providerId;
childComponent.entityId = user.id;
childComponent.showUser = false;
childComponent.entity = 'user';
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
async bulkRemove() {
if (this.actionPromise != null) {
return;
}
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.bulkRemoveModalRef.createComponent(factory).instance;
const childComponent = this.modal.show(BulkRemoveComponent, this.bulkRemoveModalRef);
childComponent.providerId = this.providerId;
childComponent.users = this.getCheckedUsers();
this.modal.onClosed.subscribe(async () => {
await this.load();
this.modal = null;
});
}
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;
}
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.bulkConfirmModalRef.createComponent(factory).instance;
const childComponent = this.modal.show(BulkConfirmComponent, this.bulkConfirmModalRef);
childComponent.providerId = this.providerId;
childComponent.users = this.getCheckedUsers();
this.modal.onClosed.subscribe(async () => {
await this.load();
this.modal = null;
});
}
private async showBulkStatus(users: ProviderUserUserDetailsResponse[], filteredUsers: ProviderUserUserDetailsResponse[],
request: Promise<ListResponse<ProviderUserBulkResponse>>, successfullMessage: string) {
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.bulkStatusModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<BulkStatusComponent>(
BulkStatusComponent, this.bulkStatusModalRef);
childComponent.loading = true;
// Workaround to handle closing the modal shortly after it has been opened
let close = false;
this.modal.onShown.subscribe(() => {
if (close) {
this.modal.close();
}
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
try {
const response = await request;
if (this.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;
if (this.modal) {
this.modal.close();
}
}
}
} }

View File

@@ -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">&times;</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">&times;</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/article/user-types-access-control/#user-types" </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>

View File

@@ -1,121 +1,104 @@
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 { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { ProviderUserInviteRequest } from "jslib-common/models/request/provider/providerUserInviteRequest"; 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 { PermissionsApi } from "jslib-common/models/api/permissionsApi"; import { ProviderUserInviteRequest } from 'jslib-common/models/request/provider/providerUserInviteRequest';
import { ProviderUserType } from "jslib-common/enums/providerUserType"; import { PermissionsApi } from 'jslib-common/models/api/permissionsApi';
import { ProviderUserUpdateRequest } from "jslib-common/models/request/provider/providerUserUpdateRequest";
import { ProviderUserType } from 'jslib-common/enums/providerUserType';
import { ProviderUserUpdateRequest } from 'jslib-common/models/request/provider/providerUserUpdateRequest';
@Component({ @Component({
selector: "provider-user-add-edit", selector: 'provider-user-add-edit',
templateUrl: "user-add-edit.component.html", templateUrl: 'user-add-edit.component.html',
}) })
export class UserAddEditComponent implements OnInit { export class UserAddEditComponent implements OnInit {
@Input() name: string; @Input() name: string;
@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: boolean = 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 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 { }
this.logService.error(e); } else {
} this.title = this.i18nService.t('inviteUser');
} else { }
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 { }
this.formPromise = this.apiService.postProviderUserInvite(this.providerId, request);
}
await this.formPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.name)
);
this.onSavedUser.emit();
} catch (e) {
this.logService.error(e);
}
}
async delete() {
if (!this.editMode) {
return;
} }
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 { }
} }
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);
}
}
} }

View File

@@ -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>

View File

@@ -1,50 +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';
}
} }
}
} }

View File

@@ -1,123 +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 { AddOrganizationComponent } from "./clients/add-organization.component"; import { AddOrganizationComponent } from './clients/add-organization.component';
import { ClientsComponent } from "./clients/clients.component"; import { ClientsComponent } from './clients/clients.component';
import { CreateOrganizationComponent } from "./clients/create-organization.component"; import { CreateOrganizationComponent } from './clients/create-organization.component';
import { AcceptProviderComponent } from "./manage/accept-provider.component"; import { AcceptProviderComponent } from './manage/accept-provider.component';
import { EventsComponent } from "./manage/events.component"; import { EventsComponent } from './manage/events.component';
import { ManageComponent } from "./manage/manage.component"; import { ManageComponent } from './manage/manage.component';
import { PeopleComponent } from "./manage/people.component"; import { PeopleComponent } from './manage/people.component';
import { ProvidersLayoutComponent } from "./providers-layout.component"; import { ProvidersLayoutComponent } from './providers-layout.component';
import { SettingsComponent } from "./settings/settings.component"; import { SettingsComponent } from './settings/settings.component';
import { SetupProviderComponent } from "./setup/setup-provider.component"; import { SetupProviderComponent } from './setup/setup-provider.component';
import { SetupComponent } from "./setup/setup.component"; import { SetupComponent } from './setup/setup.component';
import { FrontendLayoutComponent } from "src/app/layouts/frontend-layout.component"; import { FrontendLayoutComponent } from 'src/app/layouts/frontend-layout.component';
import { ProvidersComponent } from "src/app/providers/providers.component"; import { ProvidersComponent } from 'src/app/providers/providers.component';
import { ProviderGuardService } from "./services/provider-guard.service"; import { ProviderGuardService } from './services/provider-guard.service';
import { ProviderTypeGuardService } from "./services/provider-type-guard.service"; import { ProviderTypeGuardService } from './services/provider-type-guard.service';
import { AccountComponent } from "./settings/account.component"; import { AccountComponent } from './settings/account.component';
const routes: Routes = [ const routes: Routes = [
{ {
path: "", path: '',
canActivate: [AuthGuardService], canActivate: [AuthGuardService],
component: ProvidersComponent, component: ProvidersComponent,
}, },
{ {
path: "", path: '',
component: FrontendLayoutComponent, component: FrontendLayoutComponent,
children: [
{
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 { }

View File

@@ -1,63 +1,62 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from '@angular/common';
import { ComponentFactoryResolver } from "@angular/core"; import { NgModule } from '@angular/core';
import { NgModule } from "@angular/core"; import { FormsModule } from '@angular/forms';
import { FormsModule } from "@angular/forms";
import { ModalService } from "jslib-angular/services/modal.service"; import { ProviderGuardService } from './services/provider-guard.service';
import { ProviderTypeGuardService } from './services/provider-type-guard.service';
import { ProviderService } from './services/provider.service';
import { ProviderGuardService } from "./services/provider-guard.service"; import { ProvidersLayoutComponent } from './providers-layout.component';
import { ProviderTypeGuardService } from "./services/provider-type-guard.service"; import { ProvidersRoutingModule } from './providers-routing.module';
import { WebProviderService } from "./services/webProvider.service";
import { ProvidersLayoutComponent } from "./providers-layout.component"; import { AddOrganizationComponent } from './clients/add-organization.component';
import { ProvidersRoutingModule } from "./providers-routing.module"; import { ClientsComponent } from './clients/clients.component';
import { CreateOrganizationComponent } from './clients/create-organization.component';
import { AddOrganizationComponent } from "./clients/add-organization.component"; import { AcceptProviderComponent } from './manage/accept-provider.component';
import { ClientsComponent } from "./clients/clients.component"; import { BulkConfirmComponent } from './manage/bulk/bulk-confirm.component';
import { CreateOrganizationComponent } from "./clients/create-organization.component"; import { BulkRemoveComponent } from './manage/bulk/bulk-remove.component';
import { EventsComponent } from './manage/events.component';
import { ManageComponent } from './manage/manage.component';
import { PeopleComponent } from './manage/people.component';
import { UserAddEditComponent } from './manage/user-add-edit.component';
import { AcceptProviderComponent } from "./manage/accept-provider.component"; import { AccountComponent } from './settings/account.component';
import { BulkConfirmComponent } from "./manage/bulk/bulk-confirm.component"; import { SettingsComponent } from './settings/settings.component';
import { BulkRemoveComponent } from "./manage/bulk/bulk-remove.component";
import { EventsComponent } from "./manage/events.component";
import { ManageComponent } from "./manage/manage.component";
import { PeopleComponent } from "./manage/people.component";
import { UserAddEditComponent } from "./manage/user-add-edit.component";
import { AccountComponent } from "./settings/account.component"; import { SetupProviderComponent } from './setup/setup-provider.component';
import { SettingsComponent } from "./settings/settings.component"; import { SetupComponent } from './setup/setup.component';
import { SetupProviderComponent } from "./setup/setup-provider.component"; import { OssModule } from 'src/app/oss.module';
import { SetupComponent } from "./setup/setup.component";
import { OssModule } from "src/app/oss.module";
@NgModule({ @NgModule({
imports: [CommonModule, FormsModule, OssModule, 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) {
modalService.registerComponentFactoryResolver(
AddOrganizationComponent,
componentFactoryResolver
);
}
}

View File

@@ -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;
} }
} }

View File

@@ -1,27 +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;
}
} }

View File

@@ -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);
}
}

View File

@@ -1,36 +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);
}
}

View File

@@ -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-6">
(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-6">
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>

View File

@@ -1,65 +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 { ProviderUpdateRequest } from 'jslib-common/models/request/provider/providerUpdateRequest';
import { ProviderResponse } from "jslib-common/models/response/provider/providerResponse"; import { ProviderResponse } from 'jslib-common/models/response/provider/providerResponse';
@Component({ @Component({
selector: "provider-account", selector: 'provider-account',
templateUrl: "account.component.html", templateUrl: 'account.component.html',
}) })
export class AccountComponent { export class AccountComponent {
selfHosted = false; selfHosted = false;
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}`);
}
} }
}
} }

View File

@@ -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>

View File

@@ -1,23 +1,20 @@
import { Component } from "@angular/core"; import { Component } from '@angular/core';
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from '@angular/router';
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { ProviderService } from "jslib-common/abstractions/provider.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( constructor(private route: ActivatedRoute, private userService: UserService,
private route: ActivatedRoute, private platformUtilsService: PlatformUtilsService) { }
private providerService: ProviderService,
private platformUtilsService: PlatformUtilsService
) {}
ngOnInit() { ngOnInit() {
this.route.parent.params.subscribe(async (params) => { this.route.parent.params.subscribe(async params => {
const provider = await this.providerService.get(params.providerId); const provider = await this.userService.getProvider(params.providerId);
}); });
} }
} }

View File

@@ -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 src="/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">{{ "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>

View File

@@ -1,21 +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 });
}
// tslint:disable-next-line async authedHandler(qParams: any) {
async unauthedHandler(qParams: any) {} this.router.navigate(['/providers/setup'], {queryParams: qParams});
}
// tslint:disable-next-line
async unauthedHandler(qParams: any) {}
} }

View File

@@ -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-6">
</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-6">
</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>

View File

@@ -1,99 +1,94 @@
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 {
Toast,
ToasterService,
} from 'angular2-toaster';
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 { ValidationService } from 'jslib-angular/services/validation.service';
import { CryptoService } from "jslib-common/abstractions/crypto.service"; import { I18nService } from 'jslib-common/abstractions/i18n.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 { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { ProviderSetupRequest } from "jslib-common/models/request/provider/providerSetupRequest";
@Component({ @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) => { let fired = false;
const error = qParams.providerId == null || qParams.email == null || qParams.token == null; this.route.queryParams.subscribe(async qParams => {
if (fired) {
return;
}
fired = true;
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(['/']);
} } else {
this.providerId = qParams.providerId;
this.providerId = qParams.providerId; this.token = qParams.token;
this.token = qParams.token; }
});
// Check if provider exists, redirect if it does }
try {
const provider = await this.apiService.getProvider(this.providerId); async submit() {
if (provider.name != null) { this.formPromise = this.doSubmit();
this.router.navigate(["/providers", provider.id], { replaceUrl: true }); await this.formPromise;
} this.formPromise = null;
} catch (e) { }
this.validationService.showError(e);
this.router.navigate(["/"]); async doSubmit() {
} try {
}); const shareKey = await this.cryptoService.makeShareKey();
} const key = shareKey[0].encryptedString;
async submit() { const request = new ProviderSetupRequest();
this.formPromise = this.doSubmit(); request.name = this.name;
await this.formPromise; request.billingEmail = this.billingEmail;
this.formPromise = null; request.token = this.token;
} request.key = key;
async doSubmit() { const provider = await this.apiService.postProviderSetup(this.providerId, request);
try { this.toasterService.popAsync('success', null, this.i18nService.t('providerSetup'));
const shareKey = await this.cryptoService.makeShareKey(); await this.syncService.fullSync(true);
const key = shareKey[0].encryptedString;
this.router.navigate(['/providers', provider.id]);
const request = new ProviderSetupRequest(); } catch (e) {
request.name = this.name; this.validationService.showError(e);
request.billingEmail = this.billingEmail; }
request.token = this.token;
request.key = key;
const provider = await this.apiService.postProviderSetup(this.providerId, request);
this.platformUtilsService.showToast("success", null, this.i18nService.t("providerSetup"));
await this.syncService.fullSync(true);
this.router.navigate(["/providers", provider.id]);
} catch (e) {
this.validationService.showError(e);
} }
}
} }

View File

@@ -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;

View File

@@ -1,36 +1,29 @@
function load(envName) { function load(envName) {
return { const envOverrides = {
...require("./config/base.json"), 'production': () => require('./config/production.json'),
...loadConfig(envName), 'qa': () => require('./config/qa.json'),
...loadConfig("local"), 'development': () => require('./config/development.json'),
dev: { };
...require("./config/base.json").dev,
...loadConfig(envName).dev, const baseConfig = require('./config/base.json');
...loadConfig("local").dev, const overrideConfig = envOverrides.hasOwnProperty(envName) ? envOverrides[envName]() : {};
},
}; return {
...baseConfig,
...overrideConfig
};
} }
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)); Object.entries(configObj).map(([key, value]) => {
console.log(`${"=".repeat(repeatNum)}`); console.log(` ${key}: ${value}`)
} })
console.log(`${"=".repeat(repeatNum)}`)
function loadConfig(configName) {
try {
return require(`./config/${configName}.json`);
} catch (e) {
if (e instanceof Error && e.code === "MODULE_NOT_FOUND") {
return {};
} else {
throw e;
}
}
} }
module.exports = { module.exports = {
load, load,
log, log
}; };

View File

@@ -1,12 +1,8 @@
{ {
"urls": {}, "proxyApi": "http://localhost:4000",
"stripeKey": "pk_test_KPoCfZXu7mznb9uSCPZ2JpTD", "proxyIdentity": "http://localhost:33656",
"braintreeKey": "sandbox_r72q8jq6_9pnxkwm75f87sdc2", "proxyEvents": "http://localhost:46273",
"paypal": { "proxyNotifications": "http://localhost:61840",
"businessId": "AD3LAUZSNVPJY", "proxyPortal": "http://localhost:52313",
"buttonAction": "https://www.sandbox.paypal.com/cgi-bin/webscr" "allowedHosts": []
},
"dev": {
"allowedHosts": "auto"
}
} }

View File

@@ -1,17 +0,0 @@
{
"urls": {
"icons": "https://icons.bitwarden.net",
"notifications": "https://notifications.bitwarden.com"
},
"stripeKey": "pk_live_bpN0P37nMxrMQkcaHXtAybJk",
"braintreeKey": "production_qfbsv8kc_njj2zjtyngtjmbjd",
"paypal": {
"businessId": "4ZDA7DLUUJGMN",
"buttonAction": "https://www.paypal.com/cgi-bin/webscr"
},
"dev": {
"proxyApi": "https://api.bitwarden.com",
"proxyIdentity": "https://identity.bitwarden.com",
"proxyEvents": "https://events.bitwarden.com"
}
}

View File

@@ -1,11 +0,0 @@
{
"urls": {
"notifications": "http://localhost:61840"
},
"dev": {
"proxyApi": "http://localhost:4000",
"proxyIdentity": "http://localhost:33656",
"proxyEvents": "http://localhost:46273",
"proxyNotifications": "http://localhost:61840"
}
}

7
config/production.json Normal file
View File

@@ -0,0 +1,7 @@
{
"proxyApi": "https://api.bitwarden.com",
"proxyIdentity": "https://identity.bitwarden.com",
"proxyEvents": "https://events.bitwarden.com",
"proxyNotifications": "https://notifications.bitwarden.com",
"proxyPortal": "https://portal.bitwarden.com"
}

View File

@@ -1,11 +1,7 @@
{ {
"urls": { "proxyApi": "https://api.qa.bitwarden.com",
"icons": "https://icons.qa.bitwarden.pw", "proxyIdentity": "https://identity.qa.bitwarden.com",
"notifications": "https://notifications.qa.bitwarden.pw" "proxyEvents": "https://events.qa.bitwarden.com",
}, "proxyNotifications": "https://notifications.qa.bitwarden.com",
"dev": { "proxyPortal": "https://portal.qa.bitwarden.com"
"proxyApi": "https://api.qa.bitwarden.pw",
"proxyIdentity": "https://identity.qa.bitwarden.pw",
"proxyEvents": "https://events.qa.bitwarden.pw"
}
} }

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,9 +1,5 @@
project_id_env: _CROWDIN_PROJECT_ID
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:

View File

@@ -1,138 +0,0 @@
types {
# Data interchange
application/atom+xml atom;
application/json json map topojson;
application/ld+json jsonld;
application/rss+xml rss;
application/vnd.geo+json geojson;
application/xml rdf xml;
# JavaScript
# Normalize to standard type.
# https://tools.ietf.org/html/rfc4329#section-7.2
application/javascript js;
# Manifest files
application/manifest+json webmanifest;
application/x-web-app-manifest+json webapp;
text/cache-manifest appcache;
# Media files
audio/midi mid midi kar;
audio/mp4 aac f4a f4b m4a;
audio/mpeg mp3;
audio/ogg oga ogg opus;
audio/x-realaudio ra;
audio/x-wav wav;
image/bmp bmp;
image/gif gif;
image/jpeg jpeg jpg;
image/jxr jxr hdp wdp;
image/png png;
image/svg+xml svg svgz;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/webp webp;
image/x-jng jng;
video/3gpp 3gp 3gpp;
video/mp4 f4p f4v m4v mp4;
video/mpeg mpeg mpg;
video/ogg ogv;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-mng mng;
video/x-ms-asf asf asx;
video/x-ms-wmv wmv;
video/x-msvideo avi;
# Serving `.ico` image files with a different media type
# prevents Internet Explorer from displaying then as images:
# https://github.com/h5bp/html5-boilerplate/commit/37b5fec090d00f38de64b591bcddcb205aadf8ee
image/x-icon cur ico;
# Microsoft Office
application/msword doc;
application/vnd.ms-excel xls;
application/vnd.ms-powerpoint ppt;
application/vnd.openxmlformats-officedocument.wordprocessingml.document docx;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx;
application/vnd.openxmlformats-officedocument.presentationml.presentation pptx;
# Web fonts
application/font-woff woff;
application/font-woff2 woff2;
application/vnd.ms-fontobject eot;
# Browsers usually ignore the font media types and simply sniff
# the bytes to figure out the font type.
# https://mimesniff.spec.whatwg.org/#matching-a-font-type-pattern
#
# However, Blink and WebKit based browsers will show a warning
# in the console if the following font types are served with any
# other media types.
application/x-font-ttf ttc ttf;
font/opentype otf;
# Other
application/java-archive ear jar war;
application/mac-binhex40 hqx;
application/octet-stream bin deb dll dmg exe img iso msi msm msp safariextz;
application/pdf pdf;
application/postscript ai eps ps;
application/rtf rtf;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/vnd.wap.wmlc wmlc;
application/x-7z-compressed 7z;
application/x-bb-appworld bbaw;
application/x-bittorrent torrent;
application/x-chrome-extension crx;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-opera-extension oex;
application/x-perl pl pm;
application/x-pilot pdb prc;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert crt der pem;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/xslt+xml xsl;
application/zip zip;
text/css css;
text/csv csv;
text/html htm html shtml;
text/markdown md;
text/mathml mml;
text/plain txt;
text/vcard vcard vcf;
text/vnd.rim.location.xloc xloc;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/vtt vtt;
text/x-component htc;
}

View File

@@ -1,25 +0,0 @@
#######################################################################
# WARNING: This file is generated. Do not make changes to this file. #
# They will be overwritten on update. You can manage various settings #
# used in this file from the ./bwdata/config.yml file for your #
# installation. #
#######################################################################
server {
listen 8080 default_server;
listen [::]:8080 default_server;
include /etc/nginx/security-headers.conf;
location / {
root /app;
index index.html index.htm;
include /etc/nginx/security-headers.conf;
add_header X-Frame-Options SAMEORIGIN;
add_header X-Robots-Tag "noindex, nofollow";
}
location /alive {
return 200 'alive';
add_header Content-Type text/plain;
}
}

View File

@@ -1,147 +0,0 @@
# nginx Configuration File
# http://wiki.nginx.org/Configuration
# Run as a less privileged user for security reasons.
# user www www;
# How many worker threads to run;
# "auto" sets it to the number of CPU cores available in the system, and
# offers the best performance. Don't set it higher than the number of CPU
# cores if changing this parameter.
# The maximum number of connections for Nginx is calculated by:
# max_clients = worker_processes * worker_connections
worker_processes auto;
# Maximum open file descriptors per process;
# should be > worker_connections.
worker_rlimit_nofile 8192;
events {
# When you need > 8000 * cpu_cores connections, you start optimizing your OS,
# and this is probably the point at which you hire people who are smarter than
# you, as this is *a lot* of requests.
worker_connections 8000;
}
# Default error log file
# (this is only used when you don't override error_log on a server{} level)
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
http {
# Hide nginx version information.
server_tokens off;
# Define the MIME types for files.
include mime.types;
default_type application/octet-stream;
# Update charset_types to match updated mime.types.
# text/html is always included by charset module.
# Default: text/html text/xml text/plain text/vnd.wap.wml application/javascript application/rss+xml
charset_types
text/css
text/plain
text/vnd.wap.wml
application/javascript
application/json
application/rss+xml
application/xml;
# Format to use in log files
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
# Default log file
# (this is only used when you don't override access_log on a server{} level)
access_log /var/log/nginx/access.log main;
# How long to allow each connection to stay idle; longer values are better
# for each individual client, particularly for SSL, but means that worker
# connections are tied up longer. (Default: 65)
keepalive_timeout 20;
# Speed up file transfers by using sendfile() to copy directly
# between descriptors rather than using read()/write().
# For performance reasons, on FreeBSD systems w/ ZFS
# this option should be disabled as ZFS's ARC caches
# frequently used files in RAM by default.
sendfile on;
# Tell Nginx not to send out partial frames; this increases throughput
# since TCP frames are filled up before being sent out. (adds TCP_CORK)
tcp_nopush on;
# Compression
# Enable Gzip compressed.
gzip on;
# Compression level (1-9).
# 5 is a perfect compromise between size and cpu usage, offering about
# 75% reduction for most ascii files (almost identical to level 9).
gzip_comp_level 5;
# Don't compress anything that's already small and unlikely to shrink much
# if at all (the default is 20 bytes, which is bad as that usually leads to
# larger files after gzipping).
gzip_min_length 256;
# Compress data even for clients that are connecting to us via proxies,
# identified by the "Via" header (required for CloudFront).
gzip_proxied any;
# Tell proxies to cache both the gzipped and regular version of a resource
# whenever the client's Accept-Encoding capabilities header varies;
# Avoids the issue where a non-gzip capable client (which is extremely rare
# today) would display gibberish if their proxy gave them the gzipped version.
gzip_vary on;
# Compress all output labeled with one of the following MIME-types.
gzip_types
application/atom+xml
application/javascript
application/json
application/ld+json
application/manifest+json
application/rss+xml
application/vnd.geo+json
application/vnd.ms-fontobject
application/x-font-ttf
application/x-web-app-manifest+json
application/xhtml+xml
application/xml
font/opentype
image/bmp
image/svg+xml
image/x-icon
text/cache-manifest
text/css
text/plain
text/vcard
text/vnd.rim.location.xloc
text/vtt
text/x-component
text/x-cross-domain-policy;
# text/html is always compressed by HttpGzipModule
# This should be turned on if you are going to have pre-compressed copies (.gz) of
# static files available. If not it should be left off as it will cause extra I/O
# for the check. It is best if you enable this in a location{} block for
# a specific directory, or on an individual server{} level.
# gzip_static on;
# Content type for FIDO U2F facets
map $uri $fido_content_type {
default "application/fido.trusted-apps+json";
}
# Include files in the sites-enabled folder. server{} configuration files should be
# placed in the sites-available folder, and then the configuration should be enabled
# by creating a symlink to it in the sites-enabled folder.
# See doc/sites-enabled.md for more info.
include conf.d/*.conf;
}

View File

@@ -1,3 +0,0 @@
add_header Referrer-Policy same-origin;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";

View File

@@ -31,7 +31,9 @@ mkhomedir_helper $USERNAME
chown -R $USERNAME:$GROUPNAME /etc/bitwarden chown -R $USERNAME:$GROUPNAME /etc/bitwarden
cp /etc/bitwarden/web/app-id.json /app/app-id.json cp /etc/bitwarden/web/app-id.json /app/app-id.json
cp /etc/bitwarden/web/assetlinks.json /app/assetlinks.json
chown -R $USERNAME:$GROUPNAME /app chown -R $USERNAME:$GROUPNAME /app
#chown -R $USERNAME:$GROUPNAME /bitwarden_server chown -R $USERNAME:$GROUPNAME /bitwarden_server
#exec nginx -g 'daemon off;' exec gosu $USERNAME:$GROUPNAME dotnet /bitwarden_server/Server.dll \
/contentRoot=/app /webRoot=. /serveUnknown=false /webVault=true

37
gulpfile.js Normal file
View File

@@ -0,0 +1,37 @@
const gulp = require('gulp');
const googleWebFonts = require('gulp-google-webfonts');
const del = require('del');
const package = require('./package.json');
const fs = require('fs');
const paths = {
node_modules: './node_modules/',
src: './src/',
build: './build/',
cssDir: './src/css/',
};
function clean() {
return del([paths.cssDir]);
}
function webfonts() {
return gulp.src('./webfonts.list')
.pipe(googleWebFonts({
fontsDir: 'webfonts',
cssFilename: 'webfonts.css',
format: 'woff',
}))
.pipe(gulp.dest(paths.cssDir));
};
function version(cb) {
fs.writeFileSync(paths.build + 'version.json', '{"version":"' + package.version + '"}');
cb();
}
exports.clean = clean;
exports.webfonts = gulp.series(clean, webfonts);
exports.prebuild = gulp.series(clean, webfonts);
exports.version = version;
exports.postdist = version;

2
jslib

Submodule jslib updated: e372bf242b...0a2ff12bed

20139
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@bitwarden/web-vault", "name": "bitwarden-web",
"version": "2.25.1", "version": "2.22.2",
"license": "GPL-3.0", "license": "GPL-3.0",
"repository": "https://github.com/bitwarden/web", "repository": "https://github.com/bitwarden/web",
"scripts": { "scripts": {
@@ -11,75 +11,68 @@
"symlink:win": "rm -rf ./jslib && cmd /c mklink /J .\\jslib ..\\jslib", "symlink:win": "rm -rf ./jslib && cmd /c mklink /J .\\jslib ..\\jslib",
"symlink:mac": "npm run symlink:lin", "symlink:mac": "npm run symlink:lin",
"symlink:lin": "rm -rf ./jslib && ln -s ../jslib ./jslib", "symlink:lin": "rm -rf ./jslib && ln -s ../jslib ./jslib",
"build:oss": "webpack", "build": "gulp prebuild && webpack -c bitwarden_license/webpack.config.js",
"build:bit": "webpack -c bitwarden_license/webpack.config.js", "build:oss": "gulp prebuild && webpack",
"build:oss:watch": "webpack serve", "build:watch": "gulp prebuild && webpack serve -c bitwarden_license/webpack.config.js",
"build:bit:watch": "webpack serve -c bitwarden_license/webpack.config.js", "build:watch:oss": "gulp prebuild && webpack serve",
"build:bit:dev": "cross-env ENV=development npm run build:bit", "build:dev": "cross-env ENV=development npm run build",
"build:bit:dev:watch": "cross-env ENV=development npm run build:bit:watch", "build:dev:watch": "cross-env ENV=development npm run build:watch",
"build:bit:qa": "cross-env NODE_ENV=production ENV=qa npm run build:bit", "build:qa": "cross-env NODE_ENV=production ENV=qa npm run build",
"build:bit:cloud": "cross-env NODE_ENV=production ENV=cloud npm run build:bit", "build:qa:watch": "cross-env NODE_ENV=production ENV=qa npm run build:watch",
"build:oss:selfhost:watch": "cross-env ENV=selfhosted npm run build:oss:watch", "build:prod": "cross-env NODE_ENV=production ENV=production npm run build",
"build:bit:selfhost:watch": "cross-env ENV=selfhosted npm run build:bit:watch", "build:prod:oss": "cross-env NODE_ENV=production ENV=production npm run build:oss",
"build:oss:selfhost:prod": "cross-env ENV=selfhosted NODE_ENV=production npm run build:oss", "build:prod:watch": "cross-env NODE_ENV=production ENV=production npm run build:watch",
"build:bit:selfhost:prod": "cross-env ENV=selfhosted NODE_ENV=production npm run build:bit", "build:selfhost": "cross-env SELF_HOST=true npm run build:watch",
"build:selfhost:watch": "cross-env SELF_HOST=true npm run build:watch",
"build:selfhost:prod": "cross-env SELF_HOST=true NODE_ENV=production npm run build",
"build:selfhost:prod:oss": "cross-env SELF_HOST=true NODE_ENV=production npm run build:oss",
"build:selfhost:prod:watch": "cross-env SELF_HOST=true NODE_ENV=production npm run build:watch",
"clean:l10n": "git push origin --delete l10n_master", "clean:l10n": "git push origin --delete l10n_master",
"dist:bit:cloud": "npm run build:bit:cloud", "dist": "npm run build:prod && gulp postdist",
"dist:oss:selfhost": "npm run build:oss:selfhost:prod", "dist:oss": "npm run build:prod:oss && gulp postdist",
"dist:bit:selfhost": "npm run build:bit:selfhost:prod", "dist:selfhost": "npm run build:selfhost:prod && gulp postdist",
"deploy": "npm run dist:bit && gh-pages -d build", "dist:selfhost:oss": "npm run build:selfhost:prod:oss && gulp postdist",
"deploy:dev": "npm run dist:bit && gh-pages -d build -r git@github.com:kspearrin/bitwarden-web-dev.git", "deploy": "npm run dist && gh-pages -d build",
"lint": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' && prettier --check .", "deploy:dev": "npm run dist && gh-pages -d build -r git@github.com:kspearrin/bitwarden-web-dev.git",
"lint:fix": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' --fix", "lint": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' || true",
"prettier": "prettier --write .", "lint:fix": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' --fix"
"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",
"buffer": "^6.0.3", "clean-webpack-plugin": "^3.0.0",
"clean-webpack-plugin": "^4.0.0", "copy-webpack-plugin": "^6.4.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",
"del": "^6.0.0",
"file-loader": "^6.2.0",
"gh-pages": "^3.1.0", "gh-pages": "^3.1.0",
"html-loader": "^3.0.1", "gulp": "^4.0.2",
"html-webpack-injector": "1.1.4", "gulp-google-webfonts": "^4.0.0",
"html-webpack-plugin": "^5.5.0", "html-loader": "^1.3.2",
"husky": "^7.0.4", "html-webpack-plugin": "^4.5.1",
"lint-staged": "^12.1.2", "mini-css-extract-plugin": "^1.5.0",
"mini-css-extract-plugin": "^2.4.5",
"prettier": "2.5.1",
"process": "^0.11.10",
"sass": "^1.32.10", "sass": "^1.32.10",
"sass-loader": "^12.4.0", "sass-loader": "^10.1.1",
"style-loader": "^3.3.1", "style-loader": "^2.0.0",
"terser-webpack-plugin": "^5.2.5", "tapable": "^1.1.3",
"ts-loader": "^9.2.5", "terser-webpack-plugin": "^4.2.3",
"ts-loader": "^8.1.0",
"tslint": "^6.1.3", "tslint": "^6.1.3",
"tslint-loader": "^3.5.4", "tslint-loader": "^3.5.4",
"typescript": "4.3.5", "typescript": "4.1.5",
"util": "^0.12.4", "webpack": "^4.46.0",
"webpack": "^5.64.4", "webpack-cli": "^4.6.0",
"webpack-cli": "^4.9.1", "webpack-dev-server": "^3.11.2"
"webpack-dev-server": "^4.6.0"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "^12.2.13",
"@angular/cdk": "^12.2.13",
"@angular/common": "^12.2.13",
"@angular/compiler": "^12.2.13",
"@angular/core": "^12.2.13",
"@angular/forms": "^12.2.13",
"@angular/platform-browser": "^12.2.13",
"@angular/platform-browser-dynamic": "^12.2.13",
"@angular/router": "^12.2.13",
"@bitwarden/jslib-angular": "file:jslib/angular", "@bitwarden/jslib-angular": "file:jslib/angular",
"@bitwarden/jslib-common": "file:jslib/common", "@bitwarden/jslib-common": "file:jslib/common",
"angular2-toaster": "11.0.1",
"bootstrap": "4.6.0", "bootstrap": "4.6.0",
"braintree-web-drop-in": "1.30.1", "braintree-web-drop-in": "1.30.1",
"browser-hrtime": "^1.1.8", "browser-hrtime": "^1.1.8",
@@ -88,20 +81,14 @@
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"jquery": "3.6.0", "jquery": "3.6.0",
"ngx-infinite-scroll": "^10.0.1", "ngx-infinite-scroll": "^10.0.1",
"ngx-toastr": "14.1.4",
"popper.js": "1.16.1", "popper.js": "1.16.1",
"qrious": "4.0.2", "qrious": "4.0.2",
"rxjs": "^7.4.0",
"sweetalert2": "^10.16.6", "sweetalert2": "^10.16.6",
"webcrypto-shim": "0.1.7", "webcrypto-shim": "0.1.7",
"whatwg-fetch": "3.6.2" "whatwg-fetch": "3.6.2"
}, },
"engines": { "engines": {
"node": "~16", "node": "~14",
"npm": "~8" "npm": "~7"
},
"lint-staged": {
"*": "prettier --ignore-unknown --write",
"*.png": "node scripts/optimize.js"
} }
} }

View File

@@ -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;
}
});

View File

@@ -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>&nbsp; <strong>bit</strong>warden <i class="fa fa-shield"></i>&nbsp;
</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>

File diff suppressed because one or more lines are too long

4
src/404/font-awesome.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -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";
} }

View File

@@ -8,7 +8,8 @@
"ids": [ "ids": [
"https://vault.bitwarden.com", "https://vault.bitwarden.com",
"ios:bundle-id:com.8bit.bitwarden", "ios:bundle-id:com.8bit.bitwarden",
"android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI" "android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI",
"android:apk-key-hash:pSCbprJwYtwCZOPOpmU6YuPBs/g"
] ]
} }
] ]

View File

@@ -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 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">{{ "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>

View File

@@ -1,54 +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, ' ');
}
} }
}
} }

View File

@@ -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>

View File

@@ -1,127 +1,114 @@
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 { OrganizationUserAcceptRequest } from "jslib-common/models/request/organizationUserAcceptRequest"; import { ApiService } from 'jslib-common/abstractions/api.service';
import { OrganizationUserResetPasswordEnrollmentRequest } from "jslib-common/models/request/organizationUserResetPasswordEnrollmentRequest"; import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { I18nService } from 'jslib-common/abstractions/i18n.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 { Utils } from "jslib-common/misc/utils"; import { OrganizationUserAcceptRequest } from 'jslib-common/models/request/organizationUserAcceptRequest';
import { Policy } from "jslib-common/models/domain/policy"; import { OrganizationUserResetPasswordEnrollmentRequest } from 'jslib-common/models/request/organizationUserResetPasswordEnrollmentRequest';
import { BaseAcceptComponent } from "../common/base.accept.component";
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 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 { }
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;
}
} }

View File

@@ -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>

View File

@@ -1,25 +1,19 @@
import { Component } from "@angular/core"; import { Component } from '@angular/core';
import { Router } from "@angular/router"; import { Router } from '@angular/router';
import { ApiService } from "jslib-common/abstractions/api.service"; import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from "jslib-common/abstractions/i18n.service"; import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { 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"; 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, super(router, i18nService, apiService, platformUtilsService);
apiService: ApiService, }
platformUtilsService: PlatformUtilsService,
logService: LogService
) {
super(router, i18nService, apiService, platformUtilsService, logService);
}
} }

View File

@@ -1,66 +1,42 @@
<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="text-center mb-4"> <p class="text-center mb-4">
<i class="bwi bwi-lock bwi-4x text-muted" aria-hidden="true"></i> <i class="fa fa-lock fa-4x text-muted" aria-hidden="true"></i>
</p> </p>
<p class="lead text-center mx-4 mb-4">{{ "yourVaultIsLocked" | i18n }}</p> <p class="lead text-center mx-4 mb-4">{{'yourVaultIsLocked' | 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="masterPassword">{{ "masterPass" | i18n }}</label> <label for="masterPassword">{{'masterPass' | i18n}}</label>
<div class="d-flex"> <div class="d-flex">
<input <input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
id="masterPassword" name="MasterPassword" class="text-monospace form-control" [(ngModel)]="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}" required appAutofocus appInputVerbatim>
name="MasterPassword" <button type="button" class="ml-1 btn btn-link" appA11yTitle="{{'toggleVisibility' | i18n}}"
class="text-monospace form-control" (click)="togglePassword()">
[(ngModel)]="masterPassword" <i class="fa fa-lg" aria-hidden="true"
required [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
appAutofocus </button>
appInputVerbatim </div>
/> <small class="text-muted form-text">
<button {{'loggedInAsEmailOn' | i18n : email : webVaultHostname}}
type="button" </small>
class="ml-1 btn btn-link" </div>
appA11yTitle="{{ 'toggleVisibility' | i18n }}" <hr>
(click)="togglePassword()" <div class="d-flex">
> <button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
<i <span>
class="bwi bwi-lg" <i class="fa fa-unlock-alt" aria-hidden="true"></i> {{'unlock' | i18n}}
aria-hidden="true" </span>
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }" <i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
></i> </button>
</button> <button type="button" class="btn btn-outline-secondary btn-block ml-2 mt-0" (click)="logOut()">
{{'logOut' | i18n}}
</button>
</div>
</div>
</div> </div>
<small class="text-muted form-text">
{{ "loggedInAsEmailOn" | i18n: email:webVaultHostname }}
</small>
</div>
<hr />
<div class="d-flex">
<button
type="submit"
class="btn btn-primary btn-block btn-submit"
[disabled]="form.loading"
>
<span> <i class="bwi bwi-unlock" aria-hidden="true"></i> {{ "unlock" | i18n }} </span>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
<button
type="button"
class="btn btn-outline-secondary btn-block ml-2 mt-0"
(click)="logOut()"
>
{{ "logOut" | i18n }}
</button>
</div>
</div> </div>
</div>
</div> </div>
</div>
</form> </form>

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