mirror of
https://github.com/bitwarden/web
synced 2025-12-06 00:03:28 +00:00
Compare commits
357 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c5ce23d35 | ||
|
|
45c31aa089 | ||
|
|
34be07c220 | ||
|
|
968a255269 | ||
|
|
a27be135da | ||
|
|
bb95eb84ea | ||
|
|
54cd5a68b3 | ||
|
|
9abdefa947 | ||
|
|
d9322c1307 | ||
|
|
a8d614628a | ||
|
|
7f9f6d3d0e | ||
|
|
4c1e36462c | ||
|
|
32d04106a1 | ||
|
|
a3506e833a | ||
|
|
51f3fee75d | ||
|
|
3ac2ce079a | ||
|
|
97e1c7a2ea | ||
|
|
29f741316c | ||
|
|
293ae12e33 | ||
|
|
49d1c135db | ||
|
|
d900d2d3f8 | ||
|
|
4a61f0ac04 | ||
|
|
b1635debcc | ||
|
|
b3a4f833a1 | ||
|
|
dd56c9bc87 | ||
|
|
19f92e74f5 | ||
|
|
d71d0d9af6 | ||
|
|
2392d34ed8 | ||
|
|
f6eec08b70 | ||
|
|
9547b72566 | ||
|
|
38097c40d8 | ||
|
|
66b7f4d344 | ||
|
|
a1b77dc9ef | ||
|
|
9b38095aba | ||
|
|
714a574028 | ||
|
|
3e8194a3f7 | ||
|
|
6e4782784c | ||
|
|
ad40c38ca3 | ||
|
|
68f2de171e | ||
|
|
a9ef011cf3 | ||
|
|
53bd9a3b14 | ||
|
|
1466933e2c | ||
|
|
be515dc6a6 | ||
|
|
83859230cd | ||
|
|
ec7a40df0b | ||
|
|
aba98ba944 | ||
|
|
f6e8c7152e | ||
|
|
3a1fd5ba83 | ||
|
|
e43f816a8d | ||
|
|
9e61dbd512 | ||
|
|
8734d028d3 | ||
|
|
58850821ba | ||
|
|
f81ad479dd | ||
|
|
133d30ba97 | ||
|
|
09fba343fc | ||
|
|
ba3d4a2390 | ||
|
|
b1c59f3dc1 | ||
|
|
89dc3b70e1 | ||
|
|
769c247832 | ||
|
|
12e4b614f5 | ||
|
|
b28eaa1aae | ||
|
|
cd20b1c102 | ||
|
|
d6f80378eb | ||
|
|
32e9124b9c | ||
|
|
0aee3b7370 | ||
|
|
6bb6a674ec | ||
|
|
29d7a5e37e | ||
|
|
6067c1610c | ||
|
|
1b74d22b46 | ||
|
|
85a973afd4 | ||
|
|
1ea8762eeb | ||
|
|
35ecbcc11a | ||
|
|
3e988a741b | ||
|
|
db8f13d92f | ||
|
|
1a5885d6b4 | ||
|
|
31d2a09416 | ||
|
|
8ae96a6f88 | ||
|
|
d8aae1358b | ||
|
|
ed53c3b8f6 | ||
|
|
79ffafcc17 | ||
|
|
bdf6dcd8cd | ||
|
|
ec3154ea46 | ||
|
|
08fc18192d | ||
|
|
b01c71f579 | ||
|
|
a6c98f462a | ||
|
|
473dd8739a | ||
|
|
722bcfc31b | ||
|
|
929c3d7662 | ||
|
|
e25a8e051a | ||
|
|
4a1b46dd41 | ||
|
|
16877521e7 | ||
|
|
a16abb94cd | ||
|
|
5c8e9a990c | ||
|
|
c2515ed3ae | ||
|
|
227f457409 | ||
|
|
2e4a3501a2 | ||
|
|
fade7f1713 | ||
|
|
de84468ad8 | ||
|
|
2e20978cee | ||
|
|
2cc24335ef | ||
|
|
721a9f5f69 | ||
|
|
4ebbefa181 | ||
|
|
6ad930c609 | ||
|
|
85856d8390 | ||
|
|
a975f6df2b | ||
|
|
d2f1e39a9b | ||
|
|
8ef7944077 | ||
|
|
2a19189f04 | ||
|
|
cb4f318419 | ||
|
|
f239b0cd34 | ||
|
|
9d1b2b9f60 | ||
|
|
168f9a5525 | ||
|
|
13a04976fd | ||
|
|
84d03158b5 | ||
|
|
af7e2edbf0 | ||
|
|
2e7b88f149 | ||
|
|
5010736ca3 | ||
|
|
986f27294a | ||
|
|
1b8cddede8 | ||
|
|
66c814296b | ||
|
|
b14cdfcc72 | ||
|
|
aba2c70ad7 | ||
|
|
46e9158323 | ||
|
|
8449cdca75 | ||
|
|
8c0bc023b7 | ||
|
|
bcd488bb87 | ||
|
|
a7b7c716d4 | ||
|
|
6b29bb8468 | ||
|
|
3ffc035db3 | ||
|
|
d93392ba8b | ||
|
|
137b3b3490 | ||
|
|
99c8082866 | ||
|
|
24af5aca55 | ||
|
|
1429cb3f76 | ||
|
|
13cbba3e99 | ||
|
|
73d24162a0 | ||
|
|
4964ebd31e | ||
|
|
5a8198a878 | ||
|
|
03aa806af6 | ||
|
|
023bf0474c | ||
|
|
2e22ca9216 | ||
|
|
5a540bba9e | ||
|
|
2047a6378b | ||
|
|
dc87510a7a | ||
|
|
c3f4c6c03b | ||
|
|
e8b72477c9 | ||
|
|
862874c2ae | ||
|
|
4d2d686078 | ||
|
|
6d458646fa | ||
|
|
a1345488d0 | ||
|
|
c43012a5f2 | ||
|
|
577cab24c4 | ||
|
|
5c0a77aec8 | ||
|
|
a0904b14ed | ||
|
|
6774ae0ef3 | ||
|
|
5a76ca4676 | ||
|
|
3c5a972bc9 | ||
|
|
54b68ac543 | ||
|
|
ff378f05fe | ||
|
|
f207aa3a9d | ||
|
|
7b43dcb6a1 | ||
|
|
c487cf3284 | ||
|
|
c2e1d325f2 | ||
|
|
f090e8febf | ||
|
|
087c84bcfb | ||
|
|
a457c83242 | ||
|
|
bcd8963e8b | ||
|
|
1464e0fbe8 | ||
|
|
ec2b048289 | ||
|
|
04811c934f | ||
|
|
f84ee30b9d | ||
|
|
218caa28b0 | ||
|
|
a8af807650 | ||
|
|
826170507e | ||
|
|
c37979e48d | ||
|
|
7ebb046cd8 | ||
|
|
7c4d0a15dd | ||
|
|
512b9e0a92 | ||
|
|
5e95a8565c | ||
|
|
e83d0f2a9d | ||
|
|
eaebbcf6c8 | ||
|
|
7df5ed9b35 | ||
|
|
6b66f14319 | ||
|
|
2db1684b3c | ||
|
|
4625b44703 | ||
|
|
0356ecc17b | ||
|
|
1e7c27fba1 | ||
|
|
03f575f66f | ||
|
|
82b36c1b70 | ||
|
|
6878ab51fb | ||
|
|
8662033979 | ||
|
|
ef61652fba | ||
|
|
933a66b24c | ||
|
|
e2c6a5f8cd | ||
|
|
a818e7dd40 | ||
|
|
759dc647e5 | ||
|
|
37cf46d581 | ||
|
|
407032114e | ||
|
|
94aece134c | ||
|
|
7532bf9825 | ||
|
|
0f4f541b11 | ||
|
|
07a3d38bef | ||
|
|
e9273ff79a | ||
|
|
1aa708aed4 | ||
|
|
ebe5a6030e | ||
|
|
f6946085d8 | ||
|
|
beebe7c98b | ||
|
|
a51331d6b2 | ||
|
|
b7b970e654 | ||
|
|
d823e8522c | ||
|
|
6bc5ac46b7 | ||
|
|
1193a93f86 | ||
|
|
4cd052e009 | ||
|
|
949f61f1a4 | ||
|
|
2145c3f88c | ||
|
|
bb71d5dc0a | ||
|
|
41856ff6af | ||
|
|
a1388ddab7 | ||
|
|
b2d13f586d | ||
|
|
9f0cd586ee | ||
|
|
ce67497d3a | ||
|
|
0dc26e589a | ||
|
|
e14a676eea | ||
|
|
11cf89493d | ||
|
|
5be121ec71 | ||
|
|
95e58b5e69 | ||
|
|
506fd22280 | ||
|
|
d79b12dedc | ||
|
|
599cd7299c | ||
|
|
18d26b79af | ||
|
|
1f81b81a58 | ||
|
|
cc5e420484 | ||
|
|
b4eaa48765 | ||
|
|
76354741be | ||
|
|
1b466609f0 | ||
|
|
7e11b8bb5a | ||
|
|
b251e1f73c | ||
|
|
fa11382c08 | ||
|
|
e17a49acd5 | ||
|
|
bc71ffa6f2 | ||
|
|
95dc3c92c5 | ||
|
|
2135accaf4 | ||
|
|
429c38fc66 | ||
|
|
56e92b1695 | ||
|
|
b2685d455b | ||
|
|
abfd1fa254 | ||
|
|
24a5717e27 | ||
|
|
9d9503b00e | ||
|
|
7b0579ccf3 | ||
|
|
df84dff54f | ||
|
|
367c09f7e6 | ||
|
|
46967dc126 | ||
|
|
e0ede7ba74 | ||
|
|
1fe7554818 | ||
|
|
eff3332fef | ||
|
|
caea4775b3 | ||
|
|
5f04950358 | ||
|
|
c46af91240 | ||
|
|
f5034effd2 | ||
|
|
20408347fb | ||
|
|
49d5bfd3e7 | ||
|
|
e99d1a74fd | ||
|
|
43d1cede98 | ||
|
|
091fc93645 | ||
|
|
dfe2771ba7 | ||
|
|
d3664321fd | ||
|
|
2e01ff7826 | ||
|
|
59d5a7439d | ||
|
|
6e3edd75eb | ||
|
|
78992444bf | ||
|
|
f1dea8fb1a | ||
|
|
04e5ab0d01 | ||
|
|
22a1cef498 | ||
|
|
98eaeddbfd | ||
|
|
00e4df2dd3 | ||
|
|
cfb4133152 | ||
|
|
42361d17b5 | ||
|
|
02ee95506c | ||
|
|
a749946457 | ||
|
|
18fb86c243 | ||
|
|
7597e4006c | ||
|
|
50be5f4895 | ||
|
|
326fb47593 | ||
|
|
240c576bad | ||
|
|
88c8c8ae55 | ||
|
|
394a7e42fb | ||
|
|
869ee217eb | ||
|
|
03dbe272fc | ||
|
|
87973e9775 | ||
|
|
4450b1aa81 | ||
|
|
57575ea322 | ||
|
|
68d3d7abfd | ||
|
|
4502a966a1 | ||
|
|
e523733b2c | ||
|
|
3864f1d950 | ||
|
|
4bdb9c8632 | ||
|
|
b1c098614c | ||
|
|
4309064804 | ||
|
|
f91e67ad6b | ||
|
|
d63ec210c7 | ||
|
|
3d160ee1df | ||
|
|
51b482f57d | ||
|
|
b367c4b4ce | ||
|
|
7432ad310c | ||
|
|
5b02202efb | ||
|
|
23056bcd63 | ||
|
|
2b0c92a4ea | ||
|
|
d669d43fe4 | ||
|
|
426e0edfb5 | ||
|
|
2cc0aa6f3d | ||
|
|
f895916fbb | ||
|
|
fea3bba0df | ||
|
|
7ed7321219 | ||
|
|
b2bf192677 | ||
|
|
d323e775ca | ||
|
|
22a00b2341 | ||
|
|
f36bba6406 | ||
|
|
674c583881 | ||
|
|
eb5ad7c6dc | ||
|
|
ca771eb04c | ||
|
|
d705b8ab33 | ||
|
|
9454eda082 | ||
|
|
7d5329e186 | ||
|
|
18979a7f1a | ||
|
|
7301158e54 | ||
|
|
5b9c41f29a | ||
|
|
179884cf93 | ||
|
|
5bc01ea13e | ||
|
|
ca43db8d93 | ||
|
|
f4cb5e6632 | ||
|
|
da2e740e65 | ||
|
|
2f0d2bdf32 | ||
|
|
97eedb2034 | ||
|
|
3ac46e62cb | ||
|
|
97db3635af | ||
|
|
e3464da19a | ||
|
|
ec3ee8fbb3 | ||
|
|
96208d3760 | ||
|
|
5bb61c0730 | ||
|
|
858f86d9df | ||
|
|
aa1e5a11ad | ||
|
|
ded8865914 | ||
|
|
da1437a268 | ||
|
|
599f831a09 | ||
|
|
23b532e2bf | ||
|
|
9f1b8ae58f | ||
|
|
d62850f82d | ||
|
|
41a0cfd0a2 | ||
|
|
fb6e85c56b | ||
|
|
d58550c2b8 | ||
|
|
5bf3ca2708 | ||
|
|
3e4a7e7a56 | ||
|
|
5d17de227b | ||
|
|
0d985c0221 | ||
|
|
eaa6bc12ce | ||
|
|
b3337df774 | ||
|
|
fba2102518 |
@@ -1,4 +1,4 @@
|
||||
# EditorConfig is awesome: http://EditorConfig.org
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
@@ -13,3 +13,6 @@ insert_final_newline = true
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.{ts}]
|
||||
quote_type = single
|
||||
|
||||
201
.github/workflows/build.yml
vendored
Normal file
201
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,201 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'l10n_master'
|
||||
- 'gh-pages'
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
jobs:
|
||||
cloc:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
|
||||
- name: Set up cloc
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt -y install cloc
|
||||
|
||||
- name: Print lines of code
|
||||
run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
|
||||
|
||||
ubuntu:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||
with:
|
||||
node-version: '14'
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
whoami
|
||||
node --version
|
||||
npm --version
|
||||
gulp --version
|
||||
docker --version
|
||||
echo "GitHub ref: $GITHUB_REF"
|
||||
echo "GitHub event: $GITHUB_EVENT"
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
GITHUB_EVENT: ${{ github.event_name }}
|
||||
|
||||
- name: Login to Azure
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release' || github.ref == 'refs/heads/rc'
|
||||
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release' || 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.event_name == 'release' || 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.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
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
|
||||
- name: Restore
|
||||
run: dotnet tool restore
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
echo -e "# Building Web\n"
|
||||
echo "Building app"
|
||||
echo "npm version $(npm --version)"
|
||||
npm install
|
||||
npm run dist:selfhost
|
||||
|
||||
echo -e "\nBuilding docker image"
|
||||
docker --version
|
||||
docker build -t bitwarden/web .
|
||||
|
||||
- name: Tag rc branch
|
||||
if: github.ref == 'refs/heads/rc'
|
||||
run: docker tag bitwarden/web bitwarden/web:rc
|
||||
|
||||
- name: Tag dev
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
|
||||
run: docker tag bitwarden/web bitwarden/web:dev
|
||||
|
||||
- name: Tag beta
|
||||
if: github.event_name == 'release'
|
||||
run: docker tag bitwarden/web bitwarden/web:beta
|
||||
|
||||
- name: Tag version
|
||||
if: github.event_name == 'release'
|
||||
run: docker tag bitwarden/web bitwarden/web:$($env:RELEASE_TAG_NAME.trimStart('v'))
|
||||
shell: pwsh
|
||||
env:
|
||||
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
|
||||
- name: List docker images
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release' || github.ref == 'refs/heads/rc'
|
||||
run: docker images
|
||||
|
||||
- name: Push rc images
|
||||
if: github.ref == 'refs/heads/rc'
|
||||
run: docker push bitwarden/web:rc
|
||||
env:
|
||||
DOCKER_CONTENT_TRUST: 1
|
||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
|
||||
|
||||
- name: Push dev images
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
|
||||
run: docker push bitwarden/web:dev
|
||||
env:
|
||||
DOCKER_CONTENT_TRUST: 1
|
||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
|
||||
|
||||
- name: Push beta images
|
||||
if: github.event_name == 'release'
|
||||
run: docker push bitwarden/web:beta
|
||||
env:
|
||||
DOCKER_CONTENT_TRUST: 1
|
||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
|
||||
|
||||
- name: Push latest images
|
||||
if: github.event_name == 'release'
|
||||
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
|
||||
if: github.event_name == 'release'
|
||||
run: docker push bitwarden/web:$($env:RELEASE_TAG_NAME.trimStart('v'))
|
||||
shell: pwsh
|
||||
env:
|
||||
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
DOCKER_CONTENT_TRUST: 1
|
||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
|
||||
|
||||
- name: Log out of docker
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release' || github.ref == 'refs/heads/rc'
|
||||
run: docker logout
|
||||
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Set up NuGet
|
||||
uses: nuget/setup-nuget@04b0c2b8d1b97922f67eca497d7cf0bf17b8ffe1
|
||||
with:
|
||||
nuget-version: 'latest'
|
||||
|
||||
- name: Set up MSBuild
|
||||
uses: microsoft/setup-msbuild@c26a08ba26249b81327e26f6ef381897b6a8754d
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||
with:
|
||||
node-version: '14'
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
nuget help | grep Version
|
||||
msbuild -version
|
||||
dotnet --info
|
||||
node --version
|
||||
npm --version
|
||||
echo "GitHub ref: $GITHUB_REF"
|
||||
echo "GitHub event: $GITHUB_EVENT"
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
GITHUB_EVENT: ${{ github.event_name }}
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
|
||||
- name: npm install
|
||||
run: npm install
|
||||
|
||||
- name: npm build
|
||||
run: npm run build:prod
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@ dist/
|
||||
*.zip
|
||||
build/
|
||||
!dev-server.shared.pem
|
||||
config/development.json
|
||||
|
||||
@@ -1,4 +1,32 @@
|
||||
Code contributions are welcome! Please commit any pull requests against the `master` branch.
|
||||
# How to Contribute
|
||||
|
||||
Contributions of all kinds are welcome!
|
||||
|
||||
Please visit our [Community Forums](https://community.bitwarden.com/) for general community discussion and the development roadmap.
|
||||
|
||||
Here is how you can get involved:
|
||||
|
||||
* **Request a new feature:** Go to the [Feature Requests category](https://community.bitwarden.com/c/feature-requests/) of the Community Forums. Please search existing feature requests before making a new one
|
||||
|
||||
* **Write code for a new feature:** Make a new post in the [Github Contributions category](https://community.bitwarden.com/c/github-contributions/) of the Community Forums. Include a description of your proposed contribution, screeshots, and links to any relevant feature requests. This helps get feedback from the community and Bitwarden team members before you start writing code
|
||||
|
||||
* **Report a bug or submit a bugfix:** Use Github issues and pull requests
|
||||
|
||||
* **Write documentation:** Submit a pull request to the [Bitwarden help repository](https://github.com/bitwarden/help)
|
||||
|
||||
* **Help other users:** Go to the [User-to-User Support category](https://community.bitwarden.com/c/support/) on the Community Forums
|
||||
|
||||
* **Translate:** See the localization (l10n) section below
|
||||
|
||||
## Contributor Agreement
|
||||
|
||||
Please sign the [Contributor Agreement](https://cla-assistant.io/bitwarden/web) if you intend on contributing to any Github repository. Pull requests cannot be accepted and merged unless the author has signed the Contributor Agreement.
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
* use `npm run lint` and fix any linting suggestions before submitting a pull request
|
||||
* commit any pull requests against the `master` branch
|
||||
* include a link to your Community Forums post
|
||||
|
||||
# Localization (l10n)
|
||||
|
||||
|
||||
@@ -3,3 +3,50 @@ 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.
|
||||
-->
|
||||
|
||||
43
README.md
43
README.md
@@ -5,8 +5,8 @@
|
||||
The Bitwarden web project is an Angular application that powers the web vault (https://vault.bitwarden.com/).
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://ci.appveyor.com/project/bitwarden/web/branch/master" target="_blank">
|
||||
<img src="https://ci.appveyor.com/api/projects/status/github/bitwarden/web?branch=master&svg=true" alt="appveyor build" />
|
||||
<a href="https://github.com/bitwarden/web/actions?query=branch:master" target="_blank">
|
||||
<img src="https://github.com/bitwarden/web/actions/workflows/build.yml/badge.svg?branch=master" alt="Github Workflow build on master" />
|
||||
</a>
|
||||
<a href="https://crowdin.com/project/bitwarden-web" target="_blank">
|
||||
<img src="https://d322cqt584bo4o.cloudfront.net/bitwarden-web/localized.svg" alt="Crowdin" />
|
||||
@@ -23,38 +23,47 @@
|
||||
|
||||
### Requirements
|
||||
|
||||
- [Node.js](https://nodejs.org) v8.11 or greater
|
||||
- [Node.js](https://nodejs.org) v14 or greater
|
||||
|
||||
### Run the app
|
||||
|
||||
For local development, run the app with:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run build:watch
|
||||
```
|
||||
|
||||
You can now access the web vault in your browser at `https://localhost:8080`. You can adjust your API endpoint settings in `src/app/services/services.module.ts` by altering the `apiService.setUrls` call. For example:
|
||||
You can now access the web vault in your browser at `https://localhost:8080`.
|
||||
|
||||
```typescript
|
||||
await apiService.setUrls({
|
||||
base: isDev ? null : window.location.origin,
|
||||
api: isDev ? 'http://mylocalapi' : null,
|
||||
identity: isDev ? 'http://mylocalidentity' : null,
|
||||
});
|
||||
If you want to point the development web vault to the production APIs, you can run using:
|
||||
|
||||
```
|
||||
npm install
|
||||
ENV=production npm run build:watch
|
||||
```
|
||||
|
||||
If you want to point the development web vault to the production APIs, you can set:
|
||||
You can also manually adjusting your API endpoint settings by adding `config/development.js` overriding any of the values in `config/base.json`. For example:
|
||||
|
||||
```typescript
|
||||
await apiService.setUrls({
|
||||
base: null,
|
||||
api: 'https://api.bitwarden.com',
|
||||
identity: 'https://identity.bitwarden.com',
|
||||
});
|
||||
{
|
||||
"proxyApi": "http://your-api-url",
|
||||
"proxyIdentity": "http://your-identity-url",
|
||||
"proxyEvents": "http://your-events-url",
|
||||
"proxyNotifications": "http://your-notifications-url",
|
||||
"proxyPortal": "http://your-portal-url",
|
||||
"allowedHosts": ["hostnames-to-allow-in-webpack"]
|
||||
}
|
||||
```
|
||||
|
||||
To pick up the overrides in the newly created `config/development.js` file, run the app with:
|
||||
|
||||
```
|
||||
npm run build:dev:watch
|
||||
```
|
||||
|
||||
## Contribute
|
||||
|
||||
Code contributions are welcome! Please commit any pull requests against the `master` branch.
|
||||
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.
|
||||
|
||||
83
appveyor.yml
83
appveyor.yml
@@ -1,83 +0,0 @@
|
||||
image:
|
||||
- Visual Studio 2017
|
||||
- Ubuntu1804
|
||||
|
||||
branches:
|
||||
except:
|
||||
- l10n_master
|
||||
- gh-pages
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
stack: node 10
|
||||
|
||||
init:
|
||||
- ps: |
|
||||
if($isWindows) {
|
||||
Install-Product node 10
|
||||
}
|
||||
|
||||
install:
|
||||
- ps: |
|
||||
$env:PACKAGE_VERSION = (Get-Content -Raw -Path .\package.json | ConvertFrom-Json).version
|
||||
$env:PUSH_DOCKER = "false"
|
||||
$env:PROD_DEPLOY = "false"
|
||||
$env:TAG_NAME = ""
|
||||
if($env:APPVEYOR_REPO_TAG -eq "true" -and $env:APPVEYOR_RE_BUILD -eq "True") {
|
||||
$env:PROD_DEPLOY = "true"
|
||||
$env:TAG_NAME = $env:APPVEYOR_REPO_TAG_NAME.TrimStart("v")
|
||||
echo "This is a production deployment for ${env:TAG_NAME}."
|
||||
}
|
||||
if("${env:DOCKER_USERNAME}" -ne "" -and "${env:DOCKER_PASSWORD}" -ne "") {
|
||||
$env:PUSH_DOCKER = "true"
|
||||
}
|
||||
if($isWindows) {
|
||||
choco install cloc --no-progress
|
||||
cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
|
||||
}
|
||||
|
||||
before_build:
|
||||
- node --version
|
||||
- npm --version
|
||||
- sh: |
|
||||
if [ "${PUSH_DOCKER}" == "true" ]
|
||||
then
|
||||
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||
fi
|
||||
- cmd: set "GIT_PATH=C:\Program Files\Git\mingw64\libexec\git-core"
|
||||
- cmd: set "PATH=%GIT_PATH%;%PATH%"
|
||||
|
||||
build_script:
|
||||
- sh: chmod +x ./build.sh
|
||||
- ps: |
|
||||
if($isLinux) {
|
||||
./build.sh
|
||||
./build.sh tag dev
|
||||
|
||||
if($env:PROD_DEPLOY -eq "true") {
|
||||
./build.sh tag beta
|
||||
./build.sh tag $env:TAG_NAME
|
||||
}
|
||||
|
||||
docker images
|
||||
|
||||
if($env:PUSH_DOCKER -eq "true") {
|
||||
./build.sh push dev
|
||||
|
||||
if($env:PROD_DEPLOY -eq "true") {
|
||||
./build.sh push beta
|
||||
./build.sh push latest
|
||||
./build.sh push $env:TAG_NAME
|
||||
}
|
||||
}
|
||||
}
|
||||
- cmd: npm install
|
||||
- cmd: npm run build:prod
|
||||
|
||||
after_build:
|
||||
- sh: |
|
||||
if [ "${PUSH_DOCKER}" == "true" ]
|
||||
then
|
||||
docker logout
|
||||
fi
|
||||
33
build.sh
33
build.sh
@@ -1,33 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
echo ""
|
||||
|
||||
if [ $# -gt 1 -a "$1" == "push" ]
|
||||
then
|
||||
TAG=$2
|
||||
echo "# Pushing Web ($TAG)"
|
||||
echo ""
|
||||
docker push bitwarden/web:$TAG
|
||||
elif [ $# -gt 1 -a "$1" == "tag" ]
|
||||
then
|
||||
TAG=$2
|
||||
echo "Tagging Web as '$TAG'"
|
||||
docker tag bitwarden/web bitwarden/web:$TAG
|
||||
else
|
||||
echo "# Building Web"
|
||||
|
||||
echo ""
|
||||
echo "Building app"
|
||||
echo "npm version $(npm --version)"
|
||||
npm install
|
||||
npm run sub:update
|
||||
npm run dist:selfhost
|
||||
|
||||
echo ""
|
||||
echo "Building docker image"
|
||||
docker --version
|
||||
docker build -t bitwarden/web $DIR/.
|
||||
fi
|
||||
29
config.js
Normal file
29
config.js
Normal file
@@ -0,0 +1,29 @@
|
||||
function load(envName) {
|
||||
const envOverrides = {
|
||||
'production': () => require('./config/production.json'),
|
||||
'qa': () => require('./config/qa.json'),
|
||||
'development': () => require('./config/development.json'),
|
||||
};
|
||||
|
||||
const baseConfig = require('./config/base.json');
|
||||
const overrideConfig = envOverrides.hasOwnProperty(envName) ? envOverrides[envName]() : {};
|
||||
|
||||
return {
|
||||
...baseConfig,
|
||||
...overrideConfig
|
||||
};
|
||||
}
|
||||
|
||||
function log(configObj) {
|
||||
const repeatNum = 50
|
||||
console.log(`${"=".repeat(repeatNum)}\nenvConfig`)
|
||||
Object.entries(configObj).map(([key, value]) => {
|
||||
console.log(` ${key}: ${value}`)
|
||||
})
|
||||
console.log(`${"=".repeat(repeatNum)}`)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
load,
|
||||
log
|
||||
};
|
||||
8
config/base.json
Normal file
8
config/base.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"proxyApi": "http://localhost:4000",
|
||||
"proxyIdentity": "http://localhost:33656",
|
||||
"proxyEvents": "http://localhost:46273",
|
||||
"proxyNotifications": "http://localhost:61840",
|
||||
"proxyPortal": "http://localhost:52313",
|
||||
"allowedHosts": []
|
||||
}
|
||||
7
config/production.json
Normal file
7
config/production.json
Normal 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"
|
||||
}
|
||||
7
config/qa.json
Normal file
7
config/qa.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"proxyApi": "https://api.qa.bitwarden.com",
|
||||
"proxyIdentity": "https://identity.qa.bitwarden.com",
|
||||
"proxyEvents": "https://events.qa.bitwarden.com",
|
||||
"proxyNotifications": "https://notifications.qa.bitwarden.com",
|
||||
"proxyPortal": "https://portal.qa.bitwarden.com"
|
||||
}
|
||||
@@ -9,3 +9,4 @@ files:
|
||||
zh-CN: zh_CN
|
||||
zh-TW: zh_TW
|
||||
en-GB: en_GB
|
||||
en-IN: en_IN
|
||||
|
||||
2
jslib
2
jslib
Submodule jslib updated: 0a30c7eb1e...6b9246c272
13897
package-lock.json
generated
13897
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
154
package.json
154
package.json
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"name": "bitwarden-web",
|
||||
"version": "2.13.0",
|
||||
"version": "2.20.1",
|
||||
"license": "GPL-3.0",
|
||||
"repository": "https://github.com/bitwarden/web",
|
||||
"scripts": {
|
||||
"sub:init": "git submodule update --init --recursive",
|
||||
"sub:update": "git submodule update --remote",
|
||||
@@ -10,90 +12,100 @@
|
||||
"symlink:mac": "npm run symlink:lin",
|
||||
"symlink:lin": "rm -rf ./jslib && ln -s ../jslib ./jslib",
|
||||
"build": "gulp prebuild && webpack",
|
||||
"build:watch": "gulp prebuild && webpack-dev-server",
|
||||
"build:prod": "gulp prebuild && cross-env NODE_ENV=production webpack",
|
||||
"build:prod:watch": "gulp prebuild && cross-env NODE_ENV=production webpack-dev-server",
|
||||
"build:selfhost": "gulp prebuild && cross-env SELF_HOST=true webpack-dev-server",
|
||||
"build:selfhost:watch": "gulp prebuild && cross-env SELF_HOST=true webpack-dev-server",
|
||||
"build:watch": "gulp prebuild && webpack serve",
|
||||
"build:dev": "gulp prebuild && cross-env ENV=development webpack",
|
||||
"build:dev:watch": "gulp prebuild && cross-env ENV=development webpack serve",
|
||||
"build:qa": "gulp prebuild && cross-env NODE_ENV=production ENV=qa webpack",
|
||||
"build:qa:watch": "gulp prebuild && cross-env NODE_ENV=production ENV=qa webpack serve",
|
||||
"build:prod": "gulp prebuild && cross-env NODE_ENV=production ENV=production webpack",
|
||||
"build:prod:watch": "gulp prebuild && cross-env NODE_ENV=production ENV=production webpack serve",
|
||||
"build:selfhost": "gulp prebuild && cross-env SELF_HOST=true webpack serve",
|
||||
"build:selfhost:watch": "gulp prebuild && cross-env SELF_HOST=true webpack serve",
|
||||
"build:selfhost:prod": "gulp prebuild && cross-env SELF_HOST=true NODE_ENV=production webpack",
|
||||
"build:selfhost:prod:watch": "gulp prebuild && cross-env SELF_HOST=true NODE_ENV=production webpack-dev-server",
|
||||
"build:selfhost:prod:watch": "gulp prebuild && cross-env SELF_HOST=true NODE_ENV=production webpack serve",
|
||||
"clean:l10n": "git push origin --delete l10n_master",
|
||||
"dist": "npm run build:prod && gulp postdist",
|
||||
"dist:selfhost": "npm run build:selfhost:prod && gulp postdist",
|
||||
"deploy": "npm run dist && gh-pages -d build",
|
||||
"deploy:dev": "npm run dist && gh-pages -d build -r git@github.com:kspearrin/bitwarden-web-dev.git",
|
||||
"lint": "tslint src/**/*.ts || true",
|
||||
"lint:fix": "tslint src/**/*.ts --fix"
|
||||
"lint": "tslint 'src/**/*.ts' || true",
|
||||
"lint:fix": "tslint 'src/**/*.ts' --fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/compiler-cli": "^7.2.11",
|
||||
"@ngtools/webpack": "^7.2.2",
|
||||
"@types/jquery": "^3.3.6",
|
||||
"@types/lunr": "^2.1.6",
|
||||
"@types/node-forge": "^0.7.5",
|
||||
"@types/papaparse": "^4.5.3",
|
||||
"@angular/compiler-cli": "^11.2.11",
|
||||
"@ngtools/webpack": "^11.2.10",
|
||||
"@types/duo_web_sdk": "^2.7.0",
|
||||
"@types/jquery": "^3.5.5",
|
||||
"@types/lunr": "^2.3.3",
|
||||
"@types/node": "^14.14.43",
|
||||
"@types/node-forge": "^0.9.7",
|
||||
"@types/papaparse": "^5.2.5",
|
||||
"@types/webcrypto": "^0.0.28",
|
||||
"@types/webpack": "^4.4.11",
|
||||
"@types/zxcvbn": "^4.4.0",
|
||||
"angular2-template-loader": "^0.6.2",
|
||||
"clean-webpack-plugin": "^0.1.19",
|
||||
"copy-webpack-plugin": "^4.5.2",
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^1.0.0",
|
||||
"del": "^3.0.0",
|
||||
"extract-text-webpack-plugin": "next",
|
||||
"file-loader": "^2.0.0",
|
||||
"gh-pages": "^1.2.0",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-google-webfonts": "^2.0.0",
|
||||
"html-loader": "^0.5.5",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"node-sass": "^4.13.1",
|
||||
"sass-loader": "^7.1.0",
|
||||
"style-loader": "^0.23.0",
|
||||
"terser-webpack-plugin": "^1.2.3",
|
||||
"ts-loader": "^5.3.3",
|
||||
"tslint": "^5.12.1",
|
||||
"@types/webpack": "^4.4.27",
|
||||
"@types/zxcvbn": "^4.4.1",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"copy-webpack-plugin": "^6.4.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^5.2.3",
|
||||
"del": "^6.0.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"gh-pages": "^3.1.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-google-webfonts": "^4.0.0",
|
||||
"html-loader": "^1.3.2",
|
||||
"html-webpack-plugin": "^4.5.1",
|
||||
"mini-css-extract-plugin": "^1.5.0",
|
||||
"sass": "^1.32.10",
|
||||
"sass-loader": "^10.1.1",
|
||||
"style-loader": "^2.0.0",
|
||||
"tapable": "^1.1.3",
|
||||
"terser-webpack-plugin": "^4.2.3",
|
||||
"ts-loader": "^8.1.0",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint-loader": "^3.5.4",
|
||||
"typescript": "3.2.4",
|
||||
"webpack": "^4.29.0",
|
||||
"webpack-cli": "^3.2.1",
|
||||
"webpack-dev-server": "^3.1.14"
|
||||
"typescript": "4.1.5",
|
||||
"webpack": "^4.46.0",
|
||||
"webpack-cli": "^4.6.0",
|
||||
"webpack-dev-server": "^3.11.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "7.2.1",
|
||||
"@angular/cdk": "7.2.1",
|
||||
"@angular/common": "7.2.1",
|
||||
"@angular/compiler": "7.2.1",
|
||||
"@angular/core": "7.2.1",
|
||||
"@angular/forms": "7.2.1",
|
||||
"@angular/platform-browser": "7.2.1",
|
||||
"@angular/platform-browser-dynamic": "7.2.1",
|
||||
"@angular/router": "7.2.1",
|
||||
"@angular/upgrade": "7.2.1",
|
||||
"@microsoft/signalr": "3.1.0",
|
||||
"@microsoft/signalr-protocol-msgpack": "3.1.0",
|
||||
"angular2-toaster": "6.1.0",
|
||||
"angulartics2": "6.3.0",
|
||||
"big-integer": "1.6.36",
|
||||
"bootstrap": "4.3.1",
|
||||
"braintree-web-drop-in": "1.13.0",
|
||||
"core-js": "2.6.2",
|
||||
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git",
|
||||
"@angular/animations": "^11.2.11",
|
||||
"@angular/cdk": "^11.2.10",
|
||||
"@angular/common": "^11.2.11",
|
||||
"@angular/compiler": "^11.2.11",
|
||||
"@angular/core": "^11.2.11",
|
||||
"@angular/forms": "^11.2.11",
|
||||
"@angular/platform-browser": "^11.2.11",
|
||||
"@angular/platform-browser-dynamic": "^11.2.11",
|
||||
"@angular/router": "^11.2.11",
|
||||
"@microsoft/signalr": "3.1.13",
|
||||
"@microsoft/signalr-protocol-msgpack": "3.1.13",
|
||||
"angular2-toaster": "11.0.1",
|
||||
"big-integer": "1.6.48",
|
||||
"bootstrap": "4.6.0",
|
||||
"braintree-web-drop-in": "1.28.0",
|
||||
"browser-hrtime": "^1.1.8",
|
||||
"core-js": "^3.11.0",
|
||||
"date-input-polyfill": "^2.14.0",
|
||||
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git#5f9a1a8598d2cda494c4f5ee0e38b31474abfee9",
|
||||
"font-awesome": "4.7.0",
|
||||
"jquery": "3.4.1",
|
||||
"lunr": "2.3.3",
|
||||
"ngx-infinite-scroll": "7.0.1",
|
||||
"node-forge": "0.7.6",
|
||||
"papaparse": "4.6.0",
|
||||
"popper.js": "1.14.4",
|
||||
"jquery": "3.6.0",
|
||||
"lunr": "2.3.9",
|
||||
"ngx-infinite-scroll": "10.0.1",
|
||||
"node-forge": "0.10.0",
|
||||
"papaparse": "5.3.0",
|
||||
"popper.js": "1.16.1",
|
||||
"qrious": "4.0.2",
|
||||
"rxjs": "6.3.3",
|
||||
"sweetalert2": "9.8.1",
|
||||
"web-animations-js": "2.3.1",
|
||||
"webcrypto-shim": "0.1.4",
|
||||
"whatwg-fetch": "3.0.0",
|
||||
"zone.js": "0.8.28",
|
||||
"rxjs": "^6.6.7",
|
||||
"sweetalert2": "^10.16.6",
|
||||
"tslib": "^2.2.0",
|
||||
"webcrypto-shim": "0.1.7",
|
||||
"whatwg-fetch": "3.6.2",
|
||||
"zone.js": "0.11.4",
|
||||
"zxcvbn": "4.4.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "~14",
|
||||
"npm": "~6"
|
||||
}
|
||||
}
|
||||
|
||||
50
src/404.html
Normal file
50
src/404.html
Normal file
@@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="/404/bootstrap.min.css" rel="stylesheet" type="text/css"
|
||||
integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l">
|
||||
<link href="/404/font-awesome.min.css" rel="stylesheet" type="text/css"
|
||||
integrity="sha512-SfTiTlX6kk+qitfevl/7LibUOeJWlt9rbyDn92a1DqWOw9vWG2MFoays0sgObmWazO5BQPiFucnnEAjpAB+/Sw==">
|
||||
<link href="/404/styles.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/favicon-16x16.png">
|
||||
<link rel="mask-icon" href="/images/icons/safari-pinned-tab.svg" color="#175DDC">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
|
||||
<title>Page not found!</title>
|
||||
<meta name="description" content="404 Page Not Found">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="banner">
|
||||
<div class="container inner banner">
|
||||
<div class="row align-items-center">
|
||||
<div class="col brand">
|
||||
<i class="fa fa-shield"></i>
|
||||
<strong>bit</strong>warden</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container inner content">
|
||||
<h2>Page not found!</h2>
|
||||
<p>Sorry, but the page you were looking for could not be found.</p>
|
||||
<p>
|
||||
<a href="/">
|
||||
<img src="/images/404.png" class="img-fluid" alt="404 image" width="80%"/>
|
||||
</a>
|
||||
</p>
|
||||
<p>You can <a href="/">return to the web vault</a>, check our <a href="https://status.bitwarden.com/">status page</a>
|
||||
or <a href="https://bitwarden.com/contact/">contact us</a>.</p>
|
||||
</div>
|
||||
<div class="container footer text-muted content">
|
||||
© Copyright 2021 Bitwarden, Inc.
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
7
src/404/bootstrap.min.css
vendored
Normal file
7
src/404/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
src/404/font-awesome.min.css
vendored
Normal file
4
src/404/font-awesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
119
src/404/styles.css
Normal file
119
src/404/styles.css
Normal file
@@ -0,0 +1,119 @@
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: url(../fonts/Open_Sans-italic-300.woff) format('woff');
|
||||
unicode-range: U+0-10FFFF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url(../fonts/Open_Sans-italic-400.woff) format('woff');
|
||||
unicode-range: U+0-10FFFF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: url(../fonts/Open_Sans-italic-600.woff) format('woff');
|
||||
unicode-range: U+0-10FFFF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url(../fonts/Open_Sans-italic-700.woff) format('woff');
|
||||
unicode-range: U+0-10FFFF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
src: url(../fonts/Open_Sans-italic-800.woff) format('woff');
|
||||
unicode-range: U+0-10FFFF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url(../fonts/Open_Sans-normal-300.woff) format('woff');
|
||||
unicode-range: U+0-10FFFF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(../fonts/Open_Sans-normal-400.woff) format('woff');
|
||||
unicode-range: U+0-10FFFF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url(../fonts/Open_Sans-normal-600.woff) format('woff');
|
||||
unicode-range: U+0-10FFFF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(../fonts/Open_Sans-normal-700.woff) format('woff');
|
||||
unicode-range: U+0-10FFFF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: url(../fonts/Open_Sans-normal-800.woff) format('woff');
|
||||
unicode-range: U+0-10FFFF;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Open Sans';
|
||||
}
|
||||
|
||||
html, body, .row {
|
||||
height: 100%;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 25px;
|
||||
margin-bottom: 12.5px;
|
||||
font-weight: 500;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.brand {
|
||||
font-size: 23px;
|
||||
line-height: 25px;
|
||||
color: #fff;
|
||||
font-family: "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||
}
|
||||
|
||||
.banner {
|
||||
background-color: #175DDC;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 40px 0 40px 0;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
34
src/app/accounts/accept-emergency.component.html
Normal file
34
src/app/accounts/accept-emergency.component.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<div class="mt-5 d-flex justify-content-center" *ngIf="loading">
|
||||
<div>
|
||||
<img src="../../images/logo-dark@2x.png" class="mb-4 logo" alt="Bitwarden">
|
||||
<p class="text-center">
|
||||
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container" *ngIf="!loading && !authed">
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="lead text-center mb-4">{{'emergencyAccess' | i18n}}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body">
|
||||
<p class="text-center">
|
||||
{{name}}
|
||||
</p>
|
||||
<p>{{'acceptEmergencyAccess' | i18n}}</p>
|
||||
<hr>
|
||||
<div class="d-flex">
|
||||
<a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
|
||||
{{'logIn' | i18n}}
|
||||
</a>
|
||||
<a routerLink="/register" [queryParams]="{email: email}"
|
||||
class="btn btn-primary btn-block ml-2 mt-0">
|
||||
{{'createAccount' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
93
src/app/accounts/accept-emergency.component.ts
Normal file
93
src/app/accounts/accept-emergency.component.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import {
|
||||
Toast,
|
||||
ToasterService,
|
||||
} from 'angular2-toaster';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
import { EmergencyAccessAcceptRequest } from 'jslib/models/request/emergencyAccessAcceptRequest';
|
||||
|
||||
@Component({
|
||||
selector: 'app-accept-emergency',
|
||||
templateUrl: 'accept-emergency.component.html',
|
||||
})
|
||||
export class AcceptEmergencyComponent implements OnInit {
|
||||
loading = true;
|
||||
authed = false;
|
||||
name: string;
|
||||
email: string;
|
||||
actionPromise: Promise<any>;
|
||||
|
||||
constructor(private router: Router, private toasterService: ToasterService,
|
||||
private i18nService: I18nService, private route: ActivatedRoute,
|
||||
private apiService: ApiService, private userService: UserService,
|
||||
private stateService: StateService) { }
|
||||
|
||||
ngOnInit() {
|
||||
let fired = false;
|
||||
this.route.queryParams.subscribe(async qParams => {
|
||||
if (fired) {
|
||||
return;
|
||||
}
|
||||
fired = true;
|
||||
await this.stateService.remove('emergencyInvitation');
|
||||
let error = qParams.id == null || qParams.name == null || qParams.email == null || qParams.token == null;
|
||||
let errorMessage: string = null;
|
||||
if (!error) {
|
||||
this.authed = await this.userService.isAuthenticated();
|
||||
if (this.authed) {
|
||||
const request = new EmergencyAccessAcceptRequest();
|
||||
request.token = qParams.token;
|
||||
try {
|
||||
this.actionPromise = this.apiService.postEmergencyAccessAccept(qParams.id, request);
|
||||
await this.actionPromise;
|
||||
const toast: Toast = {
|
||||
type: 'success',
|
||||
title: this.i18nService.t('inviteAccepted'),
|
||||
body: this.i18nService.t('emergencyInviteAcceptedDesc'),
|
||||
timeout: 10000,
|
||||
};
|
||||
this.toasterService.popAsync(toast);
|
||||
this.router.navigate(['/vault']);
|
||||
} catch (e) {
|
||||
error = true;
|
||||
errorMessage = e.message;
|
||||
}
|
||||
} else {
|
||||
await this.stateService.save('emergencyInvitation', qParams);
|
||||
this.email = qParams.email;
|
||||
this.name = qParams.name;
|
||||
if (this.name != null) {
|
||||
// Fix URL encoding of space issue with Angular
|
||||
this.name = this.name.replace(/\+/g, ' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
const toast: Toast = {
|
||||
type: 'error',
|
||||
title: null,
|
||||
body: errorMessage != null ? this.i18nService.t('emergencyInviteAcceptFailedShort', errorMessage) :
|
||||
this.i18nService.t('emergencyInviteAcceptFailed'),
|
||||
timeout: 10000,
|
||||
};
|
||||
this.toasterService.popAsync(toast);
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export class AcceptOrganizationComponent implements OnInit {
|
||||
|
||||
ngOnInit() {
|
||||
let fired = false;
|
||||
this.route.queryParams.subscribe(async (qParams) => {
|
||||
this.route.queryParams.subscribe(async qParams => {
|
||||
if (fired) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<form (ngSubmit)="submit()" class="container" ngNativeValidate>
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="text-center mb-4">
|
||||
@@ -25,9 +25,11 @@
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex">
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
<i class="fa fa-unlock-alt" aria-hidden="true"></i>
|
||||
{{'unlock' | i18n}}
|
||||
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
|
||||
<span>
|
||||
<i class="fa fa-unlock-alt" aria-hidden="true"></i> {{'unlock' | i18n}}
|
||||
</span>
|
||||
<i class="fa fa-spinner fa-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}}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { LockService } from 'jslib/abstractions/lock.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
import { VaultTimeoutService } from 'jslib/abstractions/vaultTimeout.service';
|
||||
|
||||
import { RouterService } from '../services/router.service';
|
||||
|
||||
@@ -23,22 +24,15 @@ export class LockComponent extends BaseLockComponent {
|
||||
constructor(router: Router, i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService, messagingService: MessagingService,
|
||||
userService: UserService, cryptoService: CryptoService,
|
||||
storageService: StorageService, lockService: LockService,
|
||||
storageService: StorageService, vaultTimeoutService: VaultTimeoutService,
|
||||
environmentService: EnvironmentService, private routerService: RouterService,
|
||||
stateService: StateService) {
|
||||
stateService: StateService, apiService: ApiService) {
|
||||
super(router, i18nService, platformUtilsService, messagingService, userService, cryptoService,
|
||||
storageService, lockService, environmentService, stateService);
|
||||
storageService, vaultTimeoutService, environmentService, stateService, apiService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await super.ngOnInit();
|
||||
const authed = await this.userService.isAuthenticated();
|
||||
if (!authed) {
|
||||
this.router.navigate(['/']);
|
||||
} else if (await this.cryptoService.hasKey()) {
|
||||
this.router.navigate(['vault']);
|
||||
}
|
||||
|
||||
this.onSuccessfulSubmit = () => {
|
||||
const previousUrl = this.routerService.getPreviousUrl();
|
||||
if (previousUrl !== '/' && previousUrl.indexOf('lock') === -1) {
|
||||
|
||||
@@ -44,6 +44,11 @@
|
||||
<i class="fa fa-pencil-square-o" aria-hidden="true"></i> {{'createAccount' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<a routerLink="/sso" class="btn btn-outline-secondary btn-block mt-2">
|
||||
<i class="fa fa-bank" aria-hidden="true"></i> {{'enterpriseSingleSignOn' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,10 @@ import {
|
||||
} from '@angular/router';
|
||||
|
||||
import { AuthService } from 'jslib/abstractions/auth.service';
|
||||
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
|
||||
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
@@ -20,13 +23,18 @@ export class LoginComponent extends BaseLoginComponent {
|
||||
constructor(authService: AuthService, router: Router,
|
||||
i18nService: I18nService, private route: ActivatedRoute,
|
||||
storageService: StorageService, stateService: StateService,
|
||||
platformUtilsService: PlatformUtilsService) {
|
||||
super(authService, router, platformUtilsService, i18nService, storageService, stateService);
|
||||
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
|
||||
passwordGenerationService: PasswordGenerationService, cryptoFunctionService: CryptoFunctionService) {
|
||||
super(authService, router,
|
||||
platformUtilsService, i18nService,
|
||||
stateService, environmentService,
|
||||
passwordGenerationService, cryptoFunctionService,
|
||||
storageService);
|
||||
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
|
||||
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
|
||||
if (qParams.email != null && qParams.email.indexOf('@') > -1) {
|
||||
this.email = qParams.email;
|
||||
}
|
||||
@@ -44,9 +52,12 @@ export class LoginComponent extends BaseLoginComponent {
|
||||
}
|
||||
|
||||
async goAfterLogIn() {
|
||||
const invite = await this.stateService.get<any>('orgInvitation');
|
||||
if (invite != null) {
|
||||
this.router.navigate(['accept-organization'], { queryParams: invite });
|
||||
const orgInvite = await this.stateService.get<any>('orgInvitation');
|
||||
const emergencyInvite = await this.stateService.get<any>('emergencyInvitation');
|
||||
if (orgInvite != null) {
|
||||
this.router.navigate(['accept-organization'], { queryParams: orgInvite });
|
||||
} else if (emergencyInvite != null) {
|
||||
this.router.navigate(['accept-emergency'], { queryParams: emergencyInvite });
|
||||
} else {
|
||||
const loginRedirect = await this.stateService.get<any>('loginRedirect');
|
||||
if (loginRedirect != null) {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
@@ -18,8 +17,7 @@ export class RecoverDeleteComponent {
|
||||
formPromise: Promise<any>;
|
||||
|
||||
constructor(private router: Router, private apiService: ApiService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private i18nService: I18nService) {
|
||||
private toasterService: ToasterService, private i18nService: I18nService) {
|
||||
}
|
||||
|
||||
async submit() {
|
||||
@@ -28,7 +26,6 @@ export class RecoverDeleteComponent {
|
||||
request.email = this.email.trim().toLowerCase();
|
||||
this.formPromise = this.apiService.postAccountRecoverDelete(request);
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Started Delete Recovery' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('deleteRecoverEmailSent'));
|
||||
this.router.navigate(['/']);
|
||||
} catch { }
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { AuthService } from 'jslib/abstractions/auth.service';
|
||||
@@ -22,9 +21,8 @@ export class RecoverTwoFactorComponent {
|
||||
formPromise: Promise<any>;
|
||||
|
||||
constructor(private router: Router, private apiService: ApiService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private i18nService: I18nService, private cryptoService: CryptoService,
|
||||
private authService: AuthService) { }
|
||||
private toasterService: ToasterService, private i18nService: I18nService,
|
||||
private cryptoService: CryptoService, private authService: AuthService) { }
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
@@ -35,7 +33,6 @@ export class RecoverTwoFactorComponent {
|
||||
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key);
|
||||
this.formPromise = this.apiService.postTwoFactorRecover(request);
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Recovered 2FA' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('twoStepRecoverDisabled'));
|
||||
this.router.navigate(['/']);
|
||||
} catch { }
|
||||
|
||||
@@ -1,103 +1,156 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="lead text-center mb-4">{{'createAccount' | i18n}}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body">
|
||||
<app-callout title="{{'createOrganizationStep1' | i18n}}" type="info" icon="fa-thumb-tack"
|
||||
*ngIf="showCreateOrgMessage">
|
||||
{{'createOrganizationCreatePersonalAccount' | i18n}}
|
||||
</app-callout>
|
||||
<div class="form-group">
|
||||
<label for="email">{{'emailAddress' | i18n}}</label>
|
||||
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required
|
||||
[appAutofocus]="email === ''" inputmode="email" appInputVerbatim="false">
|
||||
<small class="form-text text-muted">{{'emailAddressDesc' | i18n}}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="name">{{'yourName' | i18n}}</label>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name"
|
||||
[appAutofocus]="email !== ''">
|
||||
<small class="form-text text-muted">{{'yourNameDesc' | i18n}}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<app-callout type="info" *ngIf="enforcedPolicyOptions">
|
||||
{{'masterPasswordPolicyInEffect' | i18n}}
|
||||
<ul class="mb-0">
|
||||
<li *ngIf="enforcedPolicyOptions?.minComplexity > 0">
|
||||
{{'policyInEffectMinComplexity' | i18n : getPasswordScoreAlertDisplay()}}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.minLength > 0">
|
||||
{{'policyInEffectMinLength' | i18n : enforcedPolicyOptions?.minLength.toString()}}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireUpper">
|
||||
{{'policyInEffectUppercase' | i18n}}</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireLower">
|
||||
{{'policyInEffectLowercase' | i18n}}</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireNumbers">
|
||||
{{'policyInEffectNumbers' | i18n}}</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireSpecial">
|
||||
{{'policyInEffectSpecial' | i18n : '!@#$%^&*'}}</li>
|
||||
</ul>
|
||||
</app-callout>
|
||||
<label for="masterPassword">{{'masterPass' | i18n}}</label>
|
||||
<div class="d-flex">
|
||||
<div class="w-100">
|
||||
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
|
||||
name="MasterPassword" class="text-monospace form-control mb-1"
|
||||
[(ngModel)]="masterPassword" (input)="updatePasswordStrength()" required
|
||||
appInputVerbatim>
|
||||
<app-password-strength [score]="masterPasswordScore" [showText]="true">
|
||||
</app-password-strength>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="ml-1 btn btn-link"
|
||||
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword(false)">
|
||||
<i class="fa fa-lg" aria-hidden="true"
|
||||
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
</button>
|
||||
<div class="progress-bar invisible"></div>
|
||||
</div>
|
||||
</div>
|
||||
<small class="form-text text-muted">{{'masterPassDesc' | i18n}}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="masterPasswordRetype">{{'reTypeMasterPass' | i18n}}</label>
|
||||
<div class="d-flex">
|
||||
<input id="masterPasswordRetype" type="{{showPassword ? 'text' : 'password'}}"
|
||||
name="MasterPasswordRetype" class="text-monospace form-control"
|
||||
[(ngModel)]="confirmMasterPassword" required appInputVerbatim>
|
||||
<button type="button" class="ml-1 btn btn-link" appA11yTitle="{{'toggleVisibility' | i18n}}"
|
||||
(click)="togglePassword(true)">
|
||||
<i class="fa fa-lg" aria-hidden="true"
|
||||
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="hint">{{'masterPassHint' | i18n}}</label>
|
||||
<input id="hint" class="form-control" type="text" name="Hint" [(ngModel)]="hint">
|
||||
<small class="form-text text-muted">{{'masterPassHintDesc' | i18n}}</small>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex mb-2">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
|
||||
<span>{{'submit' | i18n}}</span>
|
||||
<i class="fa fa-spinner fa-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>
|
||||
<small class="text-muted" *ngIf="showTerms">
|
||||
{{'submitAgreePolicies' | i18n}}
|
||||
<a href="https://bitwarden.com/terms/" target="_blank"
|
||||
rel="noopener">{{'termsOfService' | i18n}}</a>,
|
||||
<a href="https://bitwarden.com/privacy/" target="_blank"
|
||||
rel="noopener">{{'privacyPolicy' | i18n}}</a>
|
||||
</small>
|
||||
<div class="layout" [ngClass]="['layout', layout]">
|
||||
<header class="header" *ngIf="layout === 'enterprise2'">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-7">
|
||||
<img alt="Bitwarden" class="logo mb-2" src="../../images/register-layout/logo-horizontal-white.png">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</header>
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
|
||||
<div class="row">
|
||||
<div class="col-7" *ngIf="layout">
|
||||
<div class="mt-5">
|
||||
<div *ngIf="layout === 'enterprise2'">
|
||||
<h2>Companies globally trust Bitwarden for password management.</h2>
|
||||
<p>Start your 7-day free trial!</p>
|
||||
<p class="highlight">Quickly deploy your <b>organization</b></p>
|
||||
<p>Use Bitwarden across all platforms</p>
|
||||
<p>Collaborate and share securely</p>
|
||||
<figure>
|
||||
<figcaption>
|
||||
<cite>
|
||||
<img src="../../images/register-layout/wired-logo.png" alt="Wired">
|
||||
</cite>
|
||||
</figcaption>
|
||||
<blockquote>
|
||||
"Bitwarden has become a popular choice among open-source software advocates. After using
|
||||
it for a few months, I can see why." - February 2020
|
||||
</blockquote>
|
||||
</figure>
|
||||
</div>
|
||||
<div *ngIf="layout === 'enterprise3'">
|
||||
<p>Enterprise 3 layout</p>
|
||||
</div>
|
||||
<div *ngIf="layout === 'enterprise4'">
|
||||
<p>Enterprise 4 layout</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div [ngClass]="{'col-5': layout, 'col-12': !layout}">
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div [ngClass]="{'col-5': !layout, 'col-12': layout}">
|
||||
<p class="lead text-center mb-4" *ngIf="!layout">{{'createAccount' | i18n}}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body">
|
||||
<app-callout title="{{'createOrganizationStep1' | i18n}}" type="info"
|
||||
icon="fa-thumb-tack" *ngIf="showCreateOrgMessage">
|
||||
{{'createOrganizationCreatePersonalAccount' | i18n}}
|
||||
</app-callout>
|
||||
<div class="form-group">
|
||||
<label for="email">{{'emailAddress' | i18n}}</label>
|
||||
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email"
|
||||
required [appAutofocus]="email === ''" inputmode="email"
|
||||
appInputVerbatim="false">
|
||||
<small class="form-text text-muted">{{'emailAddressDesc' | i18n}}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="name">{{'yourName' | i18n}}</label>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name"
|
||||
[appAutofocus]="email !== ''">
|
||||
<small class="form-text text-muted">{{'yourNameDesc' | i18n}}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<app-callout type="info" *ngIf="enforcedPolicyOptions">
|
||||
{{'masterPasswordPolicyInEffect' | i18n}}
|
||||
<ul class="mb-0">
|
||||
<li *ngIf="enforcedPolicyOptions?.minComplexity > 0">
|
||||
{{'policyInEffectMinComplexity' | i18n : getPasswordScoreAlertDisplay()}}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.minLength > 0">
|
||||
{{'policyInEffectMinLength' | i18n : enforcedPolicyOptions?.minLength.toString()}}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireUpper">
|
||||
{{'policyInEffectUppercase' | i18n}}</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireLower">
|
||||
{{'policyInEffectLowercase' | i18n}}</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireNumbers">
|
||||
{{'policyInEffectNumbers' | i18n}}</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireSpecial">
|
||||
{{'policyInEffectSpecial' | i18n : '!@#$%^&*'}}</li>
|
||||
</ul>
|
||||
</app-callout>
|
||||
<label for="masterPassword">{{'masterPass' | i18n}}</label>
|
||||
<div class="d-flex">
|
||||
<div class="w-100">
|
||||
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
|
||||
name="MasterPassword" class="text-monospace form-control mb-1"
|
||||
[(ngModel)]="masterPassword" (input)="updatePasswordStrength()" required
|
||||
appInputVerbatim>
|
||||
<app-password-strength [score]="masterPasswordScore" [showText]="true">
|
||||
</app-password-strength>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="ml-1 btn btn-link"
|
||||
appA11yTitle="{{'toggleVisibility' | i18n}}"
|
||||
(click)="togglePassword(false)">
|
||||
<i class="fa fa-lg" aria-hidden="true"
|
||||
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
</button>
|
||||
<div class="progress-bar invisible"></div>
|
||||
</div>
|
||||
</div>
|
||||
<small class="form-text text-muted">{{'masterPassDesc' | i18n}}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="masterPasswordRetype">{{'reTypeMasterPass' | i18n}}</label>
|
||||
<div class="d-flex">
|
||||
<input id="masterPasswordRetype" type="{{showPassword ? 'text' : 'password'}}"
|
||||
name="MasterPasswordRetype" class="text-monospace form-control"
|
||||
[(ngModel)]="confirmMasterPassword" required appInputVerbatim>
|
||||
<button type="button" class="ml-1 btn btn-link"
|
||||
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword(true)">
|
||||
<i class="fa fa-lg" aria-hidden="true"
|
||||
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="hint">{{'masterPassHint' | i18n}}</label>
|
||||
<input id="hint" class="form-control" type="text" name="Hint" [(ngModel)]="hint">
|
||||
<small class="form-text text-muted">{{'masterPassHintDesc' | i18n}}</small>
|
||||
</div>
|
||||
<div class="form-group" *ngIf="showTerms">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="acceptPolicies"
|
||||
[(ngModel)]="acceptPolicies" name="AcceptPolicies">
|
||||
<label class="form-check-label small text-muted" for="acceptPolicies">
|
||||
{{'acceptPolicies' | i18n}}<br>
|
||||
<a href="https://bitwarden.com/terms/" target="_blank"
|
||||
rel="noopener">{{'termsOfService' | i18n}}</a>,
|
||||
<a href="https://bitwarden.com/privacy/" target="_blank"
|
||||
rel="noopener">{{'privacyPolicy' | i18n}}</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex mb-2">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-submit"
|
||||
[disabled]="form.loading">
|
||||
<span>{{'submit' | i18n}}</span>
|
||||
<i class="fa fa-spinner fa-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>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { MasterPasswordPolicyOptions } from 'jslib/models/domain/masterPasswordP
|
||||
import { Policy } from 'jslib/models/domain/policy';
|
||||
|
||||
import { PolicyData } from 'jslib/models/data/policyData';
|
||||
import { ReferenceEventRequest } from 'jslib/models/request/referenceEventRequest';
|
||||
|
||||
@Component({
|
||||
selector: 'app-register',
|
||||
@@ -26,7 +27,7 @@ import { PolicyData } from 'jslib/models/data/policyData';
|
||||
})
|
||||
export class RegisterComponent extends BaseRegisterComponent {
|
||||
showCreateOrgMessage = false;
|
||||
showTerms = true;
|
||||
layout = '';
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
|
||||
private policies: Policy[];
|
||||
@@ -38,7 +39,6 @@ export class RegisterComponent extends BaseRegisterComponent {
|
||||
passwordGenerationService: PasswordGenerationService, private policyService: PolicyService) {
|
||||
super(authService, router, i18nService, cryptoService, apiService, stateService, platformUtilsService,
|
||||
passwordGenerationService);
|
||||
this.showTerms = !platformUtilsService.isSelfHost();
|
||||
}
|
||||
|
||||
getPasswordScoreAlertDisplay() {
|
||||
@@ -62,7 +62,8 @@ export class RegisterComponent extends BaseRegisterComponent {
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
const queryParamsSub = this.route.queryParams.subscribe((qParams) => {
|
||||
const queryParamsSub = this.route.queryParams.subscribe(qParams => {
|
||||
this.referenceData = new ReferenceEventRequest();
|
||||
if (qParams.email != null && qParams.email.indexOf('@') > -1) {
|
||||
this.email = qParams.email;
|
||||
}
|
||||
@@ -70,9 +71,21 @@ export class RegisterComponent extends BaseRegisterComponent {
|
||||
this.stateService.save('loginRedirect', { route: '/settings/premium' });
|
||||
} else if (qParams.org != null) {
|
||||
this.showCreateOrgMessage = true;
|
||||
this.referenceData.flow = qParams.org;
|
||||
this.stateService.save('loginRedirect',
|
||||
{ route: '/settings/create-organization', qParams: { plan: qParams.org } });
|
||||
}
|
||||
if (qParams.layout != null) {
|
||||
this.layout = this.referenceData.layout = qParams.layout;
|
||||
}
|
||||
if (qParams.reference != null) {
|
||||
this.referenceData.id = qParams.reference;
|
||||
} else {
|
||||
this.referenceData.id = ('; ' + document.cookie).split('; reference=').pop().split(';').shift();
|
||||
}
|
||||
if (this.referenceData.id === '') {
|
||||
this.referenceData.id = null;
|
||||
}
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
@@ -83,8 +96,8 @@ export class RegisterComponent extends BaseRegisterComponent {
|
||||
const policies = await this.apiService.getPoliciesByToken(invite.organizationId, invite.token,
|
||||
invite.email, invite.organizationUserId);
|
||||
if (policies.data != null) {
|
||||
const policiesData = policies.data.map((p) => new PolicyData(p));
|
||||
this.policies = policiesData.map((p) => new Policy(p));
|
||||
const policiesData = policies.data.map(p => new PolicyData(p));
|
||||
this.policies = policiesData.map(p => new Policy(p));
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
85
src/app/accounts/set-password.component.html
Normal file
85
src/app/accounts/set-password.component.html
Normal file
@@ -0,0 +1,85 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate autocomplete="off">
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="lead text-center mb-4">{{'setMasterPassword' | i18n}}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body text-center" *ngIf="syncLoading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
{{'loading' | i18n}}
|
||||
</div>
|
||||
<div class="card-body" *ngIf="!syncLoading">
|
||||
<app-callout type="info">{{'ssoCompleteRegistration' | i18n}}</app-callout>
|
||||
<div class="form-group">
|
||||
<app-callout type="info" *ngIf="enforcedPolicyOptions">
|
||||
{{'masterPasswordPolicyInEffect' | i18n}}
|
||||
<ul class="mb-0">
|
||||
<li *ngIf="enforcedPolicyOptions?.minComplexity > 0">
|
||||
{{'policyInEffectMinComplexity' | i18n : getPasswordScoreAlertDisplay()}}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.minLength > 0">
|
||||
{{'policyInEffectMinLength' | i18n : enforcedPolicyOptions?.minLength.toString()}}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireUpper">
|
||||
{{'policyInEffectUppercase' | i18n}}</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireLower">
|
||||
{{'policyInEffectLowercase' | i18n}}</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireNumbers">
|
||||
{{'policyInEffectNumbers' | i18n}}</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireSpecial">
|
||||
{{'policyInEffectSpecial' | i18n : '!@#$%^&*'}}</li>
|
||||
</ul>
|
||||
</app-callout>
|
||||
<label for="masterPassword">{{'masterPass' | i18n}}</label>
|
||||
<div class="d-flex">
|
||||
<div class="w-100">
|
||||
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
|
||||
name="MasterPasswordHash" class="text-monospace form-control mb-1"
|
||||
[(ngModel)]="masterPassword" (input)="updatePasswordStrength()" required
|
||||
appInputVerbatim>
|
||||
<app-password-strength [score]="masterPasswordScore" [showText]="true">
|
||||
</app-password-strength>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="ml-1 btn btn-link"
|
||||
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword(false)">
|
||||
<i class="fa fa-lg" aria-hidden="true"
|
||||
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
</button>
|
||||
<div class="progress-bar invisible"></div>
|
||||
</div>
|
||||
</div>
|
||||
<small class="form-text text-muted">{{'masterPassDesc' | i18n}}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="masterPasswordRetype">{{'reTypeMasterPass' | i18n}}</label>
|
||||
<div class="d-flex">
|
||||
<input id="masterPasswordRetype" type="{{showPassword ? 'text' : 'password'}}"
|
||||
name="MasterPasswordRetype" class="text-monospace form-control"
|
||||
[(ngModel)]="masterPasswordRetype" required appInputVerbatim>
|
||||
<button type="button" class="ml-1 btn btn-link" appA11yTitle="{{'toggleVisibility' | i18n}}"
|
||||
(click)="togglePassword(true)">
|
||||
<i class="fa fa-lg" aria-hidden="true"
|
||||
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="hint">{{'masterPassHint' | i18n}}</label>
|
||||
<input id="hint" class="form-control" type="text" name="Hint" [(ngModel)]="hint">
|
||||
<small class="form-text text-muted">{{'masterPassHintDesc' | i18n}}</small>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex">
|
||||
<button type="submit" class="btn btn-primary btn-block 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 btn-block ml-2 mt-0" (click)="logOut()">
|
||||
{{'logOut' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
34
src/app/accounts/set-password.component.ts
Normal file
34
src/app/accounts/set-password.component.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { PolicyService } from 'jslib/abstractions/policy.service';
|
||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import {
|
||||
SetPasswordComponent as BaseSetPasswordComponent,
|
||||
} from 'jslib/angular/components/set-password.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-set-password',
|
||||
templateUrl: 'set-password.component.html',
|
||||
})
|
||||
export class SetPasswordComponent extends BaseSetPasswordComponent {
|
||||
constructor(apiService: ApiService, i18nService: I18nService,
|
||||
cryptoService: CryptoService, messagingService: MessagingService,
|
||||
userService: UserService, passwordGenerationService: PasswordGenerationService,
|
||||
platformUtilsService: PlatformUtilsService, policyService: PolicyService, router: Router,
|
||||
syncService: SyncService, route: ActivatedRoute) {
|
||||
super(i18nService, cryptoService, messagingService, userService, passwordGenerationService,
|
||||
platformUtilsService, policyService, router, apiService, syncService, route);
|
||||
}
|
||||
}
|
||||
33
src/app/accounts/sso.component.html
Normal file
33
src/app/accounts/sso.component.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<form #form (ngSubmit)="submit()" class="container" [appApiAction]="initiateSsoFormPromise" ngNativeValidate>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<img src="../../images/logo-dark@2x.png" class="logo mb-2" alt="Bitwarden">
|
||||
<div class="card d-block mt-4">
|
||||
<div class="card-body" *ngIf="loggingIn">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
{{'loading' | i18n}}
|
||||
</div>
|
||||
<div class="card-body" *ngIf="!loggingIn">
|
||||
<p>{{'ssoLogInWithOrgIdentifier' | i18n}}</p>
|
||||
<div class="form-group">
|
||||
<label for="identifier">{{'organizationIdentifier' | i18n}}</label>
|
||||
<input id="identifier" class="form-control" type="text" name="Identifier"
|
||||
[(ngModel)]="identifier" required appAutofocus>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
|
||||
<span>
|
||||
<i class="fa fa-sign-in" aria-hidden="true"></i> {{'logIn' | i18n}}
|
||||
</span>
|
||||
<i class="fa fa-spinner fa-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>
|
||||
</form>
|
||||
61
src/app/accounts/sso.component.ts
Normal file
61
src/app/accounts/sso.component.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { AuthService } from 'jslib/abstractions/auth.service';
|
||||
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
|
||||
import { SsoComponent as BaseSsoComponent } from 'jslib/angular/components/sso.component';
|
||||
|
||||
const IdentifierStorageKey = 'ssoOrgIdentifier';
|
||||
|
||||
@Component({
|
||||
selector: 'app-sso',
|
||||
templateUrl: 'sso.component.html',
|
||||
})
|
||||
export class SsoComponent extends BaseSsoComponent {
|
||||
constructor(authService: AuthService, router: Router,
|
||||
i18nService: I18nService, route: ActivatedRoute,
|
||||
storageService: StorageService, stateService: StateService,
|
||||
platformUtilsService: PlatformUtilsService, apiService: ApiService,
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
passwordGenerationService: PasswordGenerationService) {
|
||||
super(authService, router, i18nService, route, storageService, stateService, platformUtilsService,
|
||||
apiService, cryptoFunctionService, passwordGenerationService);
|
||||
this.redirectUri = window.location.origin + '/sso-connector.html';
|
||||
this.clientId = 'web';
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
super.ngOnInit();
|
||||
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
|
||||
if (qParams.identifier != null) {
|
||||
this.identifier = qParams.identifier;
|
||||
} else {
|
||||
const storedIdentifier = await this.storageService.get<string>(IdentifierStorageKey);
|
||||
if (storedIdentifier != null) {
|
||||
this.identifier = storedIdentifier;
|
||||
}
|
||||
}
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async submit() {
|
||||
await this.storageService.save(IdentifierStorageKey, this.identifier);
|
||||
if (this.clientId === 'browser') {
|
||||
document.cookie = `ssoHandOffMessage=${this.i18nService.t('ssoHandOff')};SameSite=strict`;
|
||||
}
|
||||
super.submit();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="twoStepOptionsTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="twoStepOptionsTitle">{{'twoStepOptions' | i18n}}</h2>
|
||||
@@ -7,17 +7,19 @@
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a href="#" appStopClick *ngFor="let p of providers" (click)="choose(p)"
|
||||
class="list-group-item list-group-item-action">
|
||||
<img [src]="'images/two-factor/' + p.type + '.png'" alt="" class="pull-right">
|
||||
<h3>{{p.name}}</h3>
|
||||
{{p.description}}
|
||||
</a>
|
||||
<a href="#" appStopClick class="list-group-item list-group-item-action" (click)="recover()">
|
||||
<h3>{{'recoveryCodeTitle' | i18n}}</h3>
|
||||
{{'recoveryCodeDesc' | i18n}}
|
||||
</a>
|
||||
<div class="modal-body">
|
||||
<div class="list-group list-group-flush">
|
||||
<a href="#" appStopClick *ngFor="let p of providers" (click)="choose(p)"
|
||||
class="list-group-item list-group-item-action">
|
||||
<img [src]="'images/two-factor/' + p.type + '.png'" alt="" class="pull-right">
|
||||
<h3>{{p.name}}</h3>
|
||||
{{p.description}}
|
||||
</a>
|
||||
<a href="#" appStopClick class="list-group-item list-group-item-action" (click)="recover()">
|
||||
<h3>{{'recoveryCodeTitle' | i18n}}</h3>
|
||||
{{'recoveryCodeDesc' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
|
||||
|
||||
@@ -33,16 +33,10 @@
|
||||
required appAutofocus appInputVerbatim autocomplete="new-password">
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectedProviderType === providerType.U2f">
|
||||
<p class="text-center" *ngIf="!u2fReady">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"
|
||||
aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</p>
|
||||
<ng-container *ngIf="u2fReady">
|
||||
<p class="text-center">{{'insertU2f' | i18n}}</p>
|
||||
<img src="../../images/u2fkey.jpg" alt="" class="rounded img-fluid mb-3">
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectedProviderType === providerType.WebAuthn">
|
||||
<div id="web-authn-frame" class="mb-3">
|
||||
<iframe id="webauthn_iframe"></iframe>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectedProviderType === providerType.Duo ||
|
||||
selectedProviderType === providerType.OrganizationDuo">
|
||||
@@ -51,7 +45,7 @@
|
||||
</div>
|
||||
</ng-container>
|
||||
<i class="fa fa-spinner text-muted fa-spin pull-right" title="{{'loading' | i18n}}"
|
||||
*ngIf="form.loading && selectedProviderType === providerType.U2f" aria-hidden="true"></i>
|
||||
*ngIf="form.loading && selectedProviderType === providerType.WebAuthn" aria-hidden="true"></i>
|
||||
<div class="form-check" *ngIf="selectedProviderType != null">
|
||||
<input id="remember" type="checkbox" name="Remember" class="form-check-input"
|
||||
[(ngModel)]="remember">
|
||||
@@ -65,7 +59,7 @@
|
||||
<div class="d-flex mb-3">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading"
|
||||
*ngIf="selectedProviderType != null && selectedProviderType !== providerType.Duo &&
|
||||
selectedProviderType !== providerType.OrganizationDuo && selectedProviderType !== providerType.U2f">
|
||||
selectedProviderType !== providerType.OrganizationDuo && selectedProviderType !== providerType.WebAuthn">
|
||||
<span>
|
||||
<i class="fa fa-sign-in" aria-hidden="true"></i> {{'continue' | i18n}}
|
||||
</span>
|
||||
@@ -84,4 +78,3 @@
|
||||
</div>
|
||||
</form>
|
||||
<ng-template #twoFactorOptions></ng-template>
|
||||
<iframe id="u2f_iframe" hidden></iframe>
|
||||
|
||||
@@ -5,7 +5,10 @@ import {
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
|
||||
import { Router } from '@angular/router';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { TwoFactorOptionsComponent } from './two-factor-options.component';
|
||||
|
||||
@@ -28,15 +31,15 @@ import { TwoFactorComponent as BaseTwoFactorComponent } from 'jslib/angular/comp
|
||||
templateUrl: 'two-factor.component.html',
|
||||
})
|
||||
export class TwoFactorComponent extends BaseTwoFactorComponent {
|
||||
@ViewChild('twoFactorOptions', { read: ViewContainerRef }) twoFactorOptionsModal: ViewContainerRef;
|
||||
@ViewChild('twoFactorOptions', { read: ViewContainerRef, static: true }) twoFactorOptionsModal: ViewContainerRef;
|
||||
|
||||
constructor(authService: AuthService, router: Router,
|
||||
i18nService: I18nService, apiService: ApiService,
|
||||
platformUtilsService: PlatformUtilsService, stateService: StateService,
|
||||
environmentService: EnvironmentService, private componentFactoryResolver: ComponentFactoryResolver,
|
||||
storageService: StorageService) {
|
||||
storageService: StorageService, route: ActivatedRoute) {
|
||||
super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService,
|
||||
stateService, storageService);
|
||||
stateService, storageService, route);
|
||||
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||
}
|
||||
|
||||
@@ -57,16 +60,23 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
||||
}
|
||||
|
||||
async goAfterLogIn() {
|
||||
const invite = await this.stateService.get<any>('orgInvitation');
|
||||
if (invite != null) {
|
||||
this.router.navigate(['accept-organization'], { queryParams: invite });
|
||||
const orgInvite = await this.stateService.get<any>('orgInvitation');
|
||||
const emergencyInvite = await this.stateService.get<any>('emergencyInvitation');
|
||||
if (orgInvite != null) {
|
||||
this.router.navigate(['accept-organization'], { queryParams: orgInvite });
|
||||
} else if (emergencyInvite != null) {
|
||||
this.router.navigate(['accept-emergency'], { queryParams: emergencyInvite });
|
||||
} else {
|
||||
const loginRedirect = await this.stateService.get<any>('loginRedirect');
|
||||
if (loginRedirect != null) {
|
||||
this.router.navigate([loginRedirect.route], { queryParams: loginRedirect.qParams });
|
||||
await this.stateService.remove('loginRedirect');
|
||||
} else {
|
||||
this.router.navigate([this.successRoute]);
|
||||
this.router.navigate([this.successRoute], {
|
||||
queryParams: {
|
||||
identifier: this.identifier,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export class VerifyEmailTokenComponent implements OnInit {
|
||||
|
||||
ngOnInit() {
|
||||
let fired = false;
|
||||
this.route.queryParams.subscribe(async (qParams) => {
|
||||
this.route.queryParams.subscribe(async qParams => {
|
||||
if (fired) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
} from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
@@ -27,13 +26,13 @@ export class VerifyRecoverDeleteComponent implements OnInit {
|
||||
private token: string;
|
||||
|
||||
constructor(private router: Router, private apiService: ApiService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private i18nService: I18nService, private route: ActivatedRoute) {
|
||||
private toasterService: ToasterService, private i18nService: I18nService,
|
||||
private route: ActivatedRoute) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
let fired = false;
|
||||
this.route.queryParams.subscribe(async (qParams) => {
|
||||
this.route.queryParams.subscribe(async qParams => {
|
||||
if (fired) {
|
||||
return;
|
||||
}
|
||||
@@ -53,7 +52,6 @@ export class VerifyRecoverDeleteComponent implements OnInit {
|
||||
const request = new VerifyDeleteRecoverRequest(this.userId, this.token);
|
||||
this.formPromise = this.apiService.postAccountRecoverDeleteToken(request);
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Recovered Delete' });
|
||||
this.toasterService.popAsync('success', this.i18nService.t('accountDeleted'),
|
||||
this.i18nService.t('accountDeletedDesc'));
|
||||
this.router.navigate(['/']);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { FrontendLayoutComponent } from './layouts/frontend-layout.component';
|
||||
import { OrganizationLayoutComponent } from './layouts/organization-layout.component';
|
||||
import { UserLayoutComponent } from './layouts/user-layout.component';
|
||||
|
||||
import { AcceptEmergencyComponent } from './accounts/accept-emergency.component';
|
||||
import { AcceptOrganizationComponent } from './accounts/accept-organization.component';
|
||||
import { HintComponent } from './accounts/hint.component';
|
||||
import { LockComponent } from './accounts/lock.component';
|
||||
@@ -15,6 +16,8 @@ import { LoginComponent } from './accounts/login.component';
|
||||
import { RecoverDeleteComponent } from './accounts/recover-delete.component';
|
||||
import { RecoverTwoFactorComponent } from './accounts/recover-two-factor.component';
|
||||
import { RegisterComponent } from './accounts/register.component';
|
||||
import { SetPasswordComponent } from './accounts/set-password.component';
|
||||
import { SsoComponent } from './accounts/sso.component';
|
||||
import { TwoFactorComponent } from './accounts/two-factor.component';
|
||||
import { VerifyEmailTokenComponent } from './accounts/verify-email-token.component';
|
||||
import { VerifyRecoverDeleteComponent } from './accounts/verify-recover-delete.component';
|
||||
@@ -55,6 +58,9 @@ import {
|
||||
|
||||
import { VaultComponent as OrgVaultComponent } from './organizations/vault/vault.component';
|
||||
|
||||
import { AccessComponent } from './send/access.component';
|
||||
import { SendComponent } from './send/send.component';
|
||||
|
||||
import { AccountComponent } from './settings/account.component';
|
||||
import { CreateOrganizationComponent } from './settings/create-organization.component';
|
||||
import { DomainRulesComponent } from './settings/domain-rules.component';
|
||||
@@ -81,11 +87,15 @@ import { VaultComponent } from './vault/vault.component';
|
||||
|
||||
import { OrganizationGuardService } from './services/organization-guard.service';
|
||||
import { OrganizationTypeGuardService } from './services/organization-type-guard.service';
|
||||
import { UnauthGuardService } from './services/unauth-guard.service';
|
||||
|
||||
import { AuthGuardService } from 'jslib/angular/services/auth-guard.service';
|
||||
import { LockGuardService } from 'jslib/angular/services/lock-guard.service';
|
||||
import { UnauthGuardService } from 'jslib/angular/services/unauth-guard.service';
|
||||
|
||||
import { OrganizationUserType } from 'jslib/enums/organizationUserType';
|
||||
import { Permissions } from 'jslib/enums/permissions';
|
||||
|
||||
import { EmergencyAccessViewComponent } from './settings/emergency-access-view.component';
|
||||
import { EmergencyAccessComponent } from './settings/emergency-access.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -99,18 +109,36 @@ const routes: Routes = [
|
||||
canActivate: [UnauthGuardService],
|
||||
data: { titleId: 'createAccount' },
|
||||
},
|
||||
{
|
||||
path: 'sso', component: SsoComponent,
|
||||
canActivate: [UnauthGuardService],
|
||||
data: { titleId: 'enterpriseSingleSignOn' },
|
||||
},
|
||||
{
|
||||
path: 'set-password', component: SetPasswordComponent,
|
||||
data: { titleId: 'setMasterPassword' },
|
||||
},
|
||||
{
|
||||
path: 'hint', component: HintComponent,
|
||||
canActivate: [UnauthGuardService],
|
||||
data: { titleId: 'passwordHint' },
|
||||
},
|
||||
{ path: 'lock', component: LockComponent },
|
||||
{
|
||||
path: 'lock',
|
||||
component: LockComponent,
|
||||
canActivate: [LockGuardService],
|
||||
},
|
||||
{ path: 'verify-email', component: VerifyEmailTokenComponent },
|
||||
{
|
||||
path: 'accept-organization',
|
||||
component: AcceptOrganizationComponent,
|
||||
data: { titleId: 'joinOrganization' },
|
||||
},
|
||||
{
|
||||
path: 'accept-emergency',
|
||||
component: AcceptEmergencyComponent,
|
||||
data: { titleId: 'acceptEmergency' },
|
||||
},
|
||||
{ path: 'recover', pathMatch: 'full', redirectTo: 'recover-2fa' },
|
||||
{
|
||||
path: 'recover-2fa',
|
||||
@@ -130,6 +158,11 @@ const routes: Routes = [
|
||||
canActivate: [UnauthGuardService],
|
||||
data: { titleId: 'deleteAccount' },
|
||||
},
|
||||
{
|
||||
path: 'send/:sendId/:key',
|
||||
component: AccessComponent,
|
||||
data: { title: 'Bitwarden Send' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -138,6 +171,7 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuardService],
|
||||
children: [
|
||||
{ path: 'vault', component: VaultComponent, data: { titleId: 'myVault' } },
|
||||
{ path: 'sends', component: SendComponent, data: { title: 'Send' } },
|
||||
{
|
||||
path: 'settings',
|
||||
component: SettingsComponent,
|
||||
@@ -160,6 +194,21 @@ const routes: Routes = [
|
||||
component: CreateOrganizationComponent,
|
||||
data: { titleId: 'newOrganization' },
|
||||
},
|
||||
{
|
||||
path: 'emergency-access',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: EmergencyAccessComponent,
|
||||
data: { titleId: 'emergencyAccess'},
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
component: EmergencyAccessViewComponent,
|
||||
data: { titleId: 'emergencyAccess'},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -216,35 +265,75 @@ const routes: Routes = [
|
||||
path: 'tools',
|
||||
component: OrgToolsComponent,
|
||||
canActivate: [OrganizationTypeGuardService],
|
||||
data: { allowedTypes: [OrganizationUserType.Owner, OrganizationUserType.Admin] },
|
||||
data: { permissions: [Permissions.AccessImportExport, Permissions.AccessReports] },
|
||||
children: [
|
||||
{ path: '', pathMatch: 'full', redirectTo: 'import' },
|
||||
{ path: 'import', component: OrgImportComponent, data: { titleId: 'importData' } },
|
||||
{ path: 'export', component: OrgExportComponent, data: { titleId: 'exportVault' } },
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
redirectTo: 'import',
|
||||
},
|
||||
{
|
||||
path: 'import',
|
||||
component: OrgImportComponent,
|
||||
canActivate: [OrganizationTypeGuardService],
|
||||
data: {
|
||||
titleId: 'importData',
|
||||
permissions: [Permissions.AccessImportExport],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'export',
|
||||
component: OrgExportComponent,
|
||||
canActivate: [OrganizationTypeGuardService],
|
||||
data: {
|
||||
titleId: 'exportVault',
|
||||
permissions: [Permissions.AccessImportExport],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'exposed-passwords-report',
|
||||
component: OrgExposedPasswordsReportComponent,
|
||||
data: { titleId: 'exposedPasswordsReport' },
|
||||
canActivate: [OrganizationTypeGuardService],
|
||||
data: {
|
||||
titleId: 'exposedPasswordsReport',
|
||||
permissions: [Permissions.AccessReports],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'inactive-two-factor-report',
|
||||
component: OrgInactiveTwoFactorReportComponent,
|
||||
data: { titleId: 'inactive2faReport' },
|
||||
canActivate: [OrganizationTypeGuardService],
|
||||
data: {
|
||||
titleId: 'inactive2faReport',
|
||||
permissions: [Permissions.AccessReports],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'reused-passwords-report',
|
||||
component: OrgReusedPasswordsReportComponent,
|
||||
data: { titleId: 'reusedPasswordsReport' },
|
||||
canActivate: [OrganizationTypeGuardService],
|
||||
data: {
|
||||
titleId: 'reusedPasswordsReport',
|
||||
permissions: [Permissions.AccessReports],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'unsecured-websites-report',
|
||||
component: OrgUnsecuredWebsitesReportComponent,
|
||||
data: { titleId: 'unsecuredWebsitesReport' },
|
||||
canActivate: [OrganizationTypeGuardService],
|
||||
data: {
|
||||
titleId: 'unsecuredWebsitesReport',
|
||||
permissions: [Permissions.AccessReports],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'weak-passwords-report',
|
||||
component: OrgWeakPasswordsReportComponent,
|
||||
data: { titleId: 'weakPasswordsReport' },
|
||||
canActivate: [OrganizationTypeGuardService],
|
||||
data: {
|
||||
titleId: 'weakPasswordsReport',
|
||||
permissions: [Permissions.AccessReports],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -253,26 +342,73 @@ const routes: Routes = [
|
||||
component: OrgManageComponent,
|
||||
canActivate: [OrganizationTypeGuardService],
|
||||
data: {
|
||||
allowedTypes: [
|
||||
OrganizationUserType.Owner,
|
||||
OrganizationUserType.Admin,
|
||||
OrganizationUserType.Manager,
|
||||
permissions: [
|
||||
Permissions.ManageAssignedCollections,
|
||||
Permissions.ManageAllCollections,
|
||||
Permissions.AccessEventLogs,
|
||||
Permissions.ManageGroups,
|
||||
Permissions.ManageUsers,
|
||||
Permissions.ManagePolicies,
|
||||
],
|
||||
},
|
||||
children: [
|
||||
{ path: '', pathMatch: 'full', redirectTo: 'people' },
|
||||
{ path: 'collections', component: OrgManageCollectionsComponent, data: { titleId: 'collections' } },
|
||||
{ path: 'events', component: OrgEventsComponent, data: { titleId: 'eventLogs' } },
|
||||
{ path: 'groups', component: OrgGroupsComponent, data: { titleId: 'groups' } },
|
||||
{ path: 'people', component: OrgPeopleComponent, data: { titleId: 'people' } },
|
||||
{ path: 'policies', component: OrgPoliciesComponent, data: { titleId: 'policies' } },
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
redirectTo: 'people',
|
||||
},
|
||||
{
|
||||
path: 'collections',
|
||||
component: OrgManageCollectionsComponent,
|
||||
canActivate: [OrganizationTypeGuardService],
|
||||
data: {
|
||||
titleId: 'collections',
|
||||
permissions: [Permissions.ManageAssignedCollections, Permissions.ManageAllCollections],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'events',
|
||||
component: OrgEventsComponent,
|
||||
canActivate: [OrganizationTypeGuardService],
|
||||
data: {
|
||||
titleId: 'eventLogs',
|
||||
permissions: [Permissions.AccessEventLogs],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'groups',
|
||||
component: OrgGroupsComponent,
|
||||
canActivate: [OrganizationTypeGuardService],
|
||||
data: {
|
||||
titleId: 'groups',
|
||||
permissions: [Permissions.ManageGroups],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'people',
|
||||
component: OrgPeopleComponent,
|
||||
canActivate: [OrganizationTypeGuardService],
|
||||
data: {
|
||||
titleId: 'people',
|
||||
permissions: [Permissions.ManageUsers],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'policies',
|
||||
component: OrgPoliciesComponent,
|
||||
canActivate: [OrganizationTypeGuardService],
|
||||
data: {
|
||||
titleId: 'policies',
|
||||
permissions: [Permissions.ManagePolicies],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
component: OrgSettingsComponent,
|
||||
canActivate: [OrganizationTypeGuardService],
|
||||
data: { allowedTypes: [OrganizationUserType.Owner] },
|
||||
data: { permissions: [Permissions.ManageOrganization] },
|
||||
children: [
|
||||
{ path: '', pathMatch: 'full', redirectTo: 'account' },
|
||||
{ path: 'account', component: OrgAccountComponent, data: { titleId: 'myOrganization' } },
|
||||
@@ -297,6 +433,7 @@ const routes: Routes = [
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes, {
|
||||
useHash: true,
|
||||
paramsInheritanceStrategy: 'always',
|
||||
/*enableTracing: true,*/
|
||||
})],
|
||||
exports: [RouterModule],
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import * as jq from 'jquery';
|
||||
import Swal from 'sweetalert2/src/sweetalert2.js';
|
||||
import Swal from 'sweetalert2';
|
||||
|
||||
import {
|
||||
BodyOutputType,
|
||||
Toast,
|
||||
ToasterConfig,
|
||||
ToasterContainerComponent,
|
||||
ToasterService,
|
||||
} from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||
|
||||
import {
|
||||
Component,
|
||||
@@ -35,7 +32,6 @@ import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { EventService } from 'jslib/abstractions/event.service';
|
||||
import { FolderService } from 'jslib/abstractions/folder.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { LockService } from 'jslib/abstractions/lock.service';
|
||||
import { NotificationsService } from 'jslib/abstractions/notifications.service';
|
||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
@@ -46,6 +42,7 @@ import { StateService } from 'jslib/abstractions/state.service';
|
||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||
import { TokenService } from 'jslib/abstractions/token.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
import { VaultTimeoutService } from 'jslib/abstractions/vaultTimeout.service';
|
||||
|
||||
import { ConstantsService } from 'jslib/services/constants.service';
|
||||
|
||||
@@ -70,15 +67,15 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
private idleTimer: number = null;
|
||||
private isIdle = false;
|
||||
|
||||
constructor(private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics,
|
||||
constructor(
|
||||
private broadcasterService: BroadcasterService, private userService: UserService,
|
||||
private tokenService: TokenService, private folderService: FolderService,
|
||||
private settingsService: SettingsService, private syncService: SyncService,
|
||||
private passwordGenerationService: PasswordGenerationService, private cipherService: CipherService,
|
||||
private authService: AuthService, private router: Router, private analytics: Angulartics2,
|
||||
private authService: AuthService, private router: Router,
|
||||
private toasterService: ToasterService, private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService, private ngZone: NgZone,
|
||||
private lockService: LockService, private storageService: StorageService,
|
||||
private vaultTimeoutService: VaultTimeoutService, private storageService: StorageService,
|
||||
private cryptoService: CryptoService, private collectionService: CollectionService,
|
||||
private sanitizer: DomSanitizer, private searchService: SearchService,
|
||||
private notificationsService: NotificationsService, private routerService: RouterService,
|
||||
@@ -110,7 +107,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
this.logOut(!!message.expired);
|
||||
break;
|
||||
case 'lockVault':
|
||||
await this.lockService.lock();
|
||||
await this.vaultTimeoutService.lock();
|
||||
break;
|
||||
case 'locked':
|
||||
this.notificationsService.updateConnection(false);
|
||||
@@ -139,14 +136,20 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
this.router.navigate(['settings/premium']);
|
||||
}
|
||||
break;
|
||||
case 'emailVerificationRequired':
|
||||
const emailVerificationConfirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('emailVerificationRequiredDesc'),
|
||||
this.i18nService.t('emailVerificationRequired'),
|
||||
this.i18nService.t('learnMore'), this.i18nService.t('cancel'));
|
||||
if (emailVerificationConfirmed) {
|
||||
this.platformUtilsService.launchUri('https://bitwarden.com/help/article/create-bitwarden-account/');
|
||||
}
|
||||
break;
|
||||
case 'showToast':
|
||||
this.showToast(message);
|
||||
break;
|
||||
case 'analyticsEventTrack':
|
||||
this.analytics.eventTrack.next({
|
||||
action: message.action,
|
||||
properties: { label: message.label },
|
||||
});
|
||||
case 'setFullWidth':
|
||||
this.setFullWidth();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -154,7 +157,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
});
|
||||
|
||||
this.router.events.subscribe((event) => {
|
||||
this.router.events.subscribe(event => {
|
||||
if (event instanceof NavigationEnd) {
|
||||
const modals = Array.from(document.querySelectorAll('.modal'));
|
||||
for (const modal of modals) {
|
||||
@@ -166,6 +169,8 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.setFullWidth();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -193,11 +198,12 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
|
||||
this.searchService.clearIndex();
|
||||
this.authService.logOut(async () => {
|
||||
this.analytics.eventTrack.next({ action: 'Logged Out' });
|
||||
if (expired) {
|
||||
this.toasterService.popAsync('warning', this.i18nService.t('loggedOut'),
|
||||
this.i18nService.t('loginExpired'));
|
||||
}
|
||||
|
||||
Swal.close();
|
||||
this.router.navigate(['/']);
|
||||
});
|
||||
}
|
||||
@@ -262,4 +268,13 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
this.notificationsService.reconnectFromActivity();
|
||||
}
|
||||
}
|
||||
|
||||
private async setFullWidth() {
|
||||
const enableFullWidth = await this.storageService.get<boolean>('enableFullWidth');
|
||||
if (enableFullWidth) {
|
||||
document.body.classList.add('full-width');
|
||||
} else {
|
||||
document.body.classList.remove('full-width');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import 'core-js';
|
||||
|
||||
import { ToasterModule } from 'angular2-toaster';
|
||||
import { Angulartics2Module } from 'angulartics2';
|
||||
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
@@ -27,6 +23,7 @@ import { NavbarComponent } from './layouts/navbar.component';
|
||||
import { OrganizationLayoutComponent } from './layouts/organization-layout.component';
|
||||
import { UserLayoutComponent } from './layouts/user-layout.component';
|
||||
|
||||
import { AcceptEmergencyComponent } from './accounts/accept-emergency.component';
|
||||
import { AcceptOrganizationComponent } from './accounts/accept-organization.component';
|
||||
import { HintComponent } from './accounts/hint.component';
|
||||
import { LockComponent } from './accounts/lock.component';
|
||||
@@ -34,6 +31,8 @@ import { LoginComponent } from './accounts/login.component';
|
||||
import { RecoverDeleteComponent } from './accounts/recover-delete.component';
|
||||
import { RecoverTwoFactorComponent } from './accounts/recover-two-factor.component';
|
||||
import { RegisterComponent } from './accounts/register.component';
|
||||
import { SetPasswordComponent } from './accounts/set-password.component';
|
||||
import { SsoComponent } from './accounts/sso.component';
|
||||
import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component';
|
||||
import { TwoFactorComponent } from './accounts/two-factor.component';
|
||||
import { VerifyEmailTokenComponent } from './accounts/verify-email-token.component';
|
||||
@@ -58,13 +57,11 @@ import { UserGroupsComponent as OrgUserGroupsComponent } from './organizations/m
|
||||
|
||||
import { AccountComponent as OrgAccountComponent } from './organizations/settings/account.component';
|
||||
import { AdjustSeatsComponent } from './organizations/settings/adjust-seats.component';
|
||||
import { ApiKeyComponent as OrgApiKeyComponent } from './organizations/settings/api-key.component';
|
||||
import { ChangePlanComponent } from './organizations/settings/change-plan.component';
|
||||
import { DeleteOrganizationComponent } from './organizations/settings/delete-organization.component';
|
||||
import { DownloadLicenseComponent } from './organizations/settings/download-license.component';
|
||||
import { OrganizationBillingComponent } from './organizations/settings/organization-billing.component';
|
||||
import { OrganizationSubscriptionComponent } from './organizations/settings/organization-subscription.component';
|
||||
import { RotateApiKeyComponent as OrgRotateApiKeyComponent } from './organizations/settings/rotate-api-key.component';
|
||||
import { SettingsComponent as OrgSettingComponent } from './organizations/settings/settings.component';
|
||||
import {
|
||||
TwoFactorSetupComponent as OrgTwoFactorSetupComponent,
|
||||
@@ -96,10 +93,15 @@ import { CollectionsComponent as OrgCollectionsComponent } from './organizations
|
||||
import { GroupingsComponent as OrgGroupingsComponent } from './organizations/vault/groupings.component';
|
||||
import { VaultComponent as OrgVaultComponent } from './organizations/vault/vault.component';
|
||||
|
||||
import { AccessComponent } from './send/access.component';
|
||||
import { AddEditComponent as SendAddEditComponent } from './send/add-edit.component';
|
||||
import { SendComponent } from './send/send.component';
|
||||
|
||||
import { AccountComponent } from './settings/account.component';
|
||||
import { AddCreditComponent } from './settings/add-credit.component';
|
||||
import { AdjustPaymentComponent } from './settings/adjust-payment.component';
|
||||
import { AdjustStorageComponent } from './settings/adjust-storage.component';
|
||||
import { ApiKeyComponent } from './settings/api-key.component';
|
||||
import { ChangeEmailComponent } from './settings/change-email.component';
|
||||
import { ChangeKdfComponent } from './settings/change-kdf.component';
|
||||
import { ChangePasswordComponent } from './settings/change-password.component';
|
||||
@@ -107,6 +109,14 @@ import { CreateOrganizationComponent } from './settings/create-organization.comp
|
||||
import { DeauthorizeSessionsComponent } from './settings/deauthorize-sessions.component';
|
||||
import { DeleteAccountComponent } from './settings/delete-account.component';
|
||||
import { DomainRulesComponent } from './settings/domain-rules.component';
|
||||
import { EmergencyAccessAddEditComponent } from './settings/emergency-access-add-edit.component';
|
||||
import { EmergencyAccessAttachmentsComponent } from './settings/emergency-access-attachments.component';
|
||||
import { EmergencyAccessConfirmComponent } from './settings/emergency-access-confirm.component';
|
||||
import { EmergencyAccessTakeoverComponent } from './settings/emergency-access-takeover.component';
|
||||
import { EmergencyAccessViewComponent } from './settings/emergency-access-view.component';
|
||||
import { EmergencyAccessComponent } from './settings/emergency-access.component';
|
||||
import { EmergencyAddEditComponent } from './settings/emergency-add-edit.component';
|
||||
import { LinkSsoComponent } from './settings/link-sso.component';
|
||||
import { OptionsComponent } from './settings/options.component';
|
||||
import { OrganizationPlansComponent } from './settings/organization-plans.component';
|
||||
import { OrganizationsComponent } from './settings/organizations.component';
|
||||
@@ -115,13 +125,14 @@ import { PremiumComponent } from './settings/premium.component';
|
||||
import { ProfileComponent } from './settings/profile.component';
|
||||
import { PurgeVaultComponent } from './settings/purge-vault.component';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
import { TaxInfoComponent } from './settings/tax-info.component';
|
||||
import { TwoFactorAuthenticatorComponent } from './settings/two-factor-authenticator.component';
|
||||
import { TwoFactorDuoComponent } from './settings/two-factor-duo.component';
|
||||
import { TwoFactorEmailComponent } from './settings/two-factor-email.component';
|
||||
import { TwoFactorRecoveryComponent } from './settings/two-factor-recovery.component';
|
||||
import { TwoFactorSetupComponent } from './settings/two-factor-setup.component';
|
||||
import { TwoFactorU2fComponent } from './settings/two-factor-u2f.component';
|
||||
import { TwoFactorVerifyComponent } from './settings/two-factor-verify.component';
|
||||
import { TwoFactorWebAuthnComponent } from './settings/two-factor-webauthn.component';
|
||||
import { TwoFactorYubiKeyComponent } from './settings/two-factor-yubikey.component';
|
||||
import { UpdateKeyComponent } from './settings/update-key.component';
|
||||
import { UpdateLicenseComponent } from './settings/update-license.component';
|
||||
@@ -143,13 +154,16 @@ import { WeakPasswordsReportComponent } from './tools/weak-passwords-report.comp
|
||||
|
||||
import { AddEditComponent } from './vault/add-edit.component';
|
||||
import { AttachmentsComponent } from './vault/attachments.component';
|
||||
import { BulkActionsComponent } from './vault/bulk-actions.component';
|
||||
import { BulkDeleteComponent } from './vault/bulk-delete.component';
|
||||
import { BulkMoveComponent } from './vault/bulk-move.component';
|
||||
import { BulkRestoreComponent } from './vault/bulk-restore.component';
|
||||
import { BulkShareComponent } from './vault/bulk-share.component';
|
||||
import { CiphersComponent } from './vault/ciphers.component';
|
||||
import { CollectionsComponent } from './vault/collections.component';
|
||||
import { FolderAddEditComponent } from './vault/folder-add-edit.component';
|
||||
import { GroupingsComponent } from './vault/groupings.component';
|
||||
import { SendInfoComponent } from './vault/send-info.component';
|
||||
import { ShareComponent } from './vault/share.component';
|
||||
import { VaultComponent } from './vault/vault.component';
|
||||
|
||||
@@ -173,51 +187,80 @@ import { I18nPipe } from 'jslib/angular/pipes/i18n.pipe';
|
||||
import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
|
||||
import { SearchPipe } from 'jslib/angular/pipes/search.pipe';
|
||||
|
||||
import { registerLocaleData } from '@angular/common';
|
||||
import {
|
||||
DatePipe,
|
||||
registerLocaleData,
|
||||
} from '@angular/common';
|
||||
import localeBg from '@angular/common/locales/bg';
|
||||
import localeCa from '@angular/common/locales/ca';
|
||||
import localeCs from '@angular/common/locales/cs';
|
||||
import localeDa from '@angular/common/locales/da';
|
||||
import localeDe from '@angular/common/locales/de';
|
||||
import localeEl from '@angular/common/locales/el';
|
||||
import localeEnGb from '@angular/common/locales/en-GB';
|
||||
import localeEnIn from '@angular/common/locales/en-IN';
|
||||
import localeEo from '@angular/common/locales/eo';
|
||||
import localeEs from '@angular/common/locales/es';
|
||||
import localeEt from '@angular/common/locales/et';
|
||||
import localeFi from '@angular/common/locales/fi';
|
||||
import localeFr from '@angular/common/locales/fr';
|
||||
import localeHe from '@angular/common/locales/he';
|
||||
import localeHr from '@angular/common/locales/hr';
|
||||
import localeHu from '@angular/common/locales/hu';
|
||||
import localeId from '@angular/common/locales/id';
|
||||
import localeIt from '@angular/common/locales/it';
|
||||
import localeJa from '@angular/common/locales/ja';
|
||||
import localeKo from '@angular/common/locales/ko';
|
||||
import localeLv from '@angular/common/locales/lv';
|
||||
import localeMl from '@angular/common/locales/ml';
|
||||
import localeNb from '@angular/common/locales/nb';
|
||||
import localeNl from '@angular/common/locales/nl';
|
||||
import localePl from '@angular/common/locales/pl';
|
||||
import localePtBr from '@angular/common/locales/pt';
|
||||
import localePtPt from '@angular/common/locales/pt-PT';
|
||||
import localeRo from '@angular/common/locales/ro';
|
||||
import localeRu from '@angular/common/locales/ru';
|
||||
import localeSk from '@angular/common/locales/sk';
|
||||
import localeSr from '@angular/common/locales/sr';
|
||||
import localeSv from '@angular/common/locales/sv';
|
||||
import localeTr from '@angular/common/locales/tr';
|
||||
import localeUk from '@angular/common/locales/uk';
|
||||
import localeZhCn from '@angular/common/locales/zh-Hans';
|
||||
import localeZhTw from '@angular/common/locales/zh-Hant';
|
||||
|
||||
registerLocaleData(localeCa, 'ca');
|
||||
registerLocaleData(localeCs, 'cs');
|
||||
registerLocaleData(localeBg, 'bg');
|
||||
registerLocaleData(localeDa, 'da');
|
||||
registerLocaleData(localeDe, 'de');
|
||||
registerLocaleData(localeEl, 'el');
|
||||
registerLocaleData(localeEnGb, 'en-GB');
|
||||
registerLocaleData(localeEnIn, 'en-IN');
|
||||
registerLocaleData(localeEs, 'es');
|
||||
registerLocaleData(localeEt, 'et');
|
||||
registerLocaleData(localeEo, 'eo');
|
||||
registerLocaleData(localeFi, 'fi');
|
||||
registerLocaleData(localeFr, 'fr');
|
||||
registerLocaleData(localeHe, 'he');
|
||||
registerLocaleData(localeHr, 'hr');
|
||||
registerLocaleData(localeHu, 'hu');
|
||||
registerLocaleData(localeId, 'id');
|
||||
registerLocaleData(localeIt, 'it');
|
||||
registerLocaleData(localeJa, 'ja');
|
||||
registerLocaleData(localeKo, 'ko');
|
||||
registerLocaleData(localeLv, 'lv');
|
||||
registerLocaleData(localeMl, 'ml');
|
||||
registerLocaleData(localeNb, 'nb');
|
||||
registerLocaleData(localeNl, 'nl');
|
||||
registerLocaleData(localePl, 'pl');
|
||||
registerLocaleData(localePtBr, 'pt-BR');
|
||||
registerLocaleData(localePtPt, 'pt-PT');
|
||||
registerLocaleData(localeRo, 'ro');
|
||||
registerLocaleData(localeRu, 'ru');
|
||||
registerLocaleData(localeSk, 'sk');
|
||||
registerLocaleData(localeSr, 'sr');
|
||||
registerLocaleData(localeSv, 'sv');
|
||||
registerLocaleData(localeTr, 'tr');
|
||||
registerLocaleData(localeUk, 'uk');
|
||||
registerLocaleData(localeZhCn, 'zh-CN');
|
||||
registerLocaleData(localeZhTw, 'zh-TW');
|
||||
@@ -229,25 +272,24 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
FormsModule,
|
||||
AppRoutingModule,
|
||||
ServicesModule,
|
||||
Angulartics2Module.forRoot([Angulartics2GoogleAnalytics], {
|
||||
pageTracking: {
|
||||
clearQueryParams: true,
|
||||
},
|
||||
}),
|
||||
ToasterModule.forRoot(),
|
||||
InfiniteScrollModule,
|
||||
DragDropModule,
|
||||
],
|
||||
declarations: [
|
||||
A11yTitleDirective,
|
||||
AcceptEmergencyComponent,
|
||||
AccessComponent,
|
||||
AcceptOrganizationComponent,
|
||||
AccountComponent,
|
||||
SetPasswordComponent,
|
||||
AddCreditComponent,
|
||||
AddEditComponent,
|
||||
AdjustPaymentComponent,
|
||||
AdjustSeatsComponent,
|
||||
AdjustStorageComponent,
|
||||
ApiActionDirective,
|
||||
ApiKeyComponent,
|
||||
AppComponent,
|
||||
AttachmentsComponent,
|
||||
AutofocusDirective,
|
||||
@@ -255,8 +297,10 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
BlurClickDirective,
|
||||
BoxRowDirective,
|
||||
BreachReportComponent,
|
||||
BulkActionsComponent,
|
||||
BulkDeleteComponent,
|
||||
BulkMoveComponent,
|
||||
BulkRestoreComponent,
|
||||
BulkShareComponent,
|
||||
CalloutComponent,
|
||||
ChangeEmailComponent,
|
||||
@@ -272,6 +316,13 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
DeleteOrganizationComponent,
|
||||
DomainRulesComponent,
|
||||
DownloadLicenseComponent,
|
||||
EmergencyAccessAddEditComponent,
|
||||
EmergencyAccessAttachmentsComponent,
|
||||
EmergencyAccessComponent,
|
||||
EmergencyAccessConfirmComponent,
|
||||
EmergencyAccessTakeoverComponent,
|
||||
EmergencyAccessViewComponent,
|
||||
EmergencyAddEditComponent,
|
||||
ExportComponent,
|
||||
ExposedPasswordsReportComponent,
|
||||
FallbackSrcDirective,
|
||||
@@ -285,6 +336,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
ImportComponent,
|
||||
InactiveTwoFactorReportComponent,
|
||||
InputVerbatimDirective,
|
||||
LinkSsoComponent,
|
||||
LockComponent,
|
||||
LoginComponent,
|
||||
ModalComponent,
|
||||
@@ -292,7 +344,6 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
OptionsComponent,
|
||||
OrgAccountComponent,
|
||||
OrgAddEditComponent,
|
||||
OrgApiKeyComponent,
|
||||
OrganizationBillingComponent,
|
||||
OrganizationPlansComponent,
|
||||
OrganizationSubscriptionComponent,
|
||||
@@ -316,7 +367,6 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
OrgPolicyEditComponent,
|
||||
OrgPoliciesComponent,
|
||||
OrgReusedPasswordsReportComponent,
|
||||
OrgRotateApiKeyComponent,
|
||||
OrgSettingComponent,
|
||||
OrgToolsComponent,
|
||||
OrgTwoFactorSetupComponent,
|
||||
@@ -342,10 +392,15 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
SearchCiphersPipe,
|
||||
SearchPipe,
|
||||
SelectCopyDirective,
|
||||
SendAddEditComponent,
|
||||
SendComponent,
|
||||
SendInfoComponent,
|
||||
SettingsComponent,
|
||||
ShareComponent,
|
||||
SsoComponent,
|
||||
StopClickDirective,
|
||||
StopPropDirective,
|
||||
TaxInfoComponent,
|
||||
ToolsComponent,
|
||||
TrueFalseValueDirective,
|
||||
TwoFactorAuthenticatorComponent,
|
||||
@@ -355,8 +410,8 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
TwoFactorOptionsComponent,
|
||||
TwoFactorRecoveryComponent,
|
||||
TwoFactorSetupComponent,
|
||||
TwoFactorU2fComponent,
|
||||
TwoFactorVerifyComponent,
|
||||
TwoFactorWebAuthnComponent,
|
||||
TwoFactorYubiKeyComponent,
|
||||
UnsecuredWebsitesReportComponent,
|
||||
UpdateKeyComponent,
|
||||
@@ -372,18 +427,25 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
],
|
||||
entryComponents: [
|
||||
AddEditComponent,
|
||||
ApiKeyComponent,
|
||||
AttachmentsComponent,
|
||||
BulkActionsComponent,
|
||||
BulkDeleteComponent,
|
||||
BulkMoveComponent,
|
||||
BulkRestoreComponent,
|
||||
BulkShareComponent,
|
||||
CollectionsComponent,
|
||||
DeauthorizeSessionsComponent,
|
||||
DeleteAccountComponent,
|
||||
DeleteOrganizationComponent,
|
||||
EmergencyAccessAddEditComponent,
|
||||
EmergencyAccessAttachmentsComponent,
|
||||
EmergencyAccessConfirmComponent,
|
||||
EmergencyAccessTakeoverComponent,
|
||||
EmergencyAddEditComponent,
|
||||
FolderAddEditComponent,
|
||||
ModalComponent,
|
||||
OrgAddEditComponent,
|
||||
OrgApiKeyComponent,
|
||||
OrgAttachmentsComponent,
|
||||
OrgCollectionAddEditComponent,
|
||||
OrgCollectionsComponent,
|
||||
@@ -391,23 +453,23 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
OrgEntityUsersComponent,
|
||||
OrgGroupAddEditComponent,
|
||||
OrgPolicyEditComponent,
|
||||
OrgRotateApiKeyComponent,
|
||||
OrgUserAddEditComponent,
|
||||
OrgUserConfirmComponent,
|
||||
OrgUserGroupsComponent,
|
||||
PasswordGeneratorHistoryComponent,
|
||||
PurgeVaultComponent,
|
||||
SendAddEditComponent,
|
||||
ShareComponent,
|
||||
TwoFactorAuthenticatorComponent,
|
||||
TwoFactorDuoComponent,
|
||||
TwoFactorEmailComponent,
|
||||
TwoFactorOptionsComponent,
|
||||
TwoFactorRecoveryComponent,
|
||||
TwoFactorU2fComponent,
|
||||
TwoFactorWebAuthnComponent,
|
||||
TwoFactorYubiKeyComponent,
|
||||
UpdateKeyComponent,
|
||||
],
|
||||
providers: [],
|
||||
providers: [DatePipe],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule { }
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { ModalComponent } from 'jslib/angular/components/modal.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [],
|
||||
declarations: [
|
||||
ModalComponent,
|
||||
],
|
||||
})
|
||||
export class DummyModule {
|
||||
}
|
||||
@@ -15,8 +15,8 @@ export class FooterComponent implements OnInit {
|
||||
|
||||
constructor(private platformUtilsService: PlatformUtilsService) { }
|
||||
|
||||
ngOnInit() {
|
||||
async ngOnInit() {
|
||||
this.year = new Date().getFullYear().toString();
|
||||
this.version = this.platformUtilsService.getApplicationVersion();
|
||||
this.version = await this.platformUtilsService.getApplicationVersion();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@ export class FrontendLayoutComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(private platformUtilsService: PlatformUtilsService) { }
|
||||
|
||||
ngOnInit() {
|
||||
async ngOnInit() {
|
||||
this.year = new Date().getFullYear().toString();
|
||||
this.version = this.platformUtilsService.getApplicationVersion();
|
||||
this.version = await this.platformUtilsService.getApplicationVersion();
|
||||
document.body.classList.add('layout_frontend');
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
<li class="nav-item" routerLinkActive="active">
|
||||
<a class="nav-link" routerLink="/vault">{{'myVault' | i18n}}</a>
|
||||
</li>
|
||||
<li class="nav-item" routerLinkActive="active">
|
||||
<a class="nav-link" routerLink="/sends">{{'send' | i18n}}</a>
|
||||
</li>
|
||||
<li class="nav-item" routerLinkActive="active">
|
||||
<a class="nav-link" routerLink="/tools">{{'tools' | i18n}}</a>
|
||||
</li>
|
||||
@@ -39,7 +42,7 @@
|
||||
<i class="fa fa-fw fa-question-circle" aria-hidden="true"></i>
|
||||
{{'getHelp' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="https://bitwarden.com#download" target="_blank" rel="noopener">
|
||||
<a class="dropdown-item" href="https://bitwarden.com/download/" target="_blank" rel="noopener">
|
||||
<i class="fa fa-fw fa-download" aria-hidden="true"></i>
|
||||
{{'getApps' | i18n}}
|
||||
</a>
|
||||
|
||||
@@ -1,45 +1,56 @@
|
||||
<app-navbar></app-navbar>
|
||||
<div class="org-nav" *ngIf="organization">
|
||||
<div class="container d-flex flex-column">
|
||||
<div class="my-auto d-flex align-items-center pl-1">
|
||||
<app-avatar [data]="organization.name" size="45" [circle]="true"></app-avatar>
|
||||
<div class="org-name ml-3">
|
||||
<span>{{organization.name}}</span>
|
||||
<small class="text-muted">{{'organization' | i18n}}</small>
|
||||
</div>
|
||||
<div class="ml-auto card border-danger text-danger bg-transparent" *ngIf="!organization.enabled">
|
||||
<div class="card-body py-2">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
{{'organizationIsDisabled' | i18n}}
|
||||
<div class="container d-flex">
|
||||
<div class="d-flex flex-column">
|
||||
<div class="my-auto d-flex align-items-center pl-1">
|
||||
<app-avatar [data]="organization.name" size="45" [circle]="true"></app-avatar>
|
||||
<div class="org-name ml-3">
|
||||
<span>{{organization.name}}</span>
|
||||
<small class="text-muted">{{'organization' | i18n}}</small>
|
||||
</div>
|
||||
<div class="ml-3 card border-danger text-danger bg-transparent" *ngIf="!organization.enabled">
|
||||
<div class="card-body py-2">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
{{'organizationIsDisabled' | i18n}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="nav nav-tabs" *ngIf="showMenuBar">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="vault" routerLinkActive="active">
|
||||
<i class="fa fa-lock" aria-hidden="true"></i>
|
||||
{{'vault' | 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="showToolsTab">
|
||||
<a class="nav-link" [routerLink]="toolsRoute" routerLinkActive="active">
|
||||
<i class="fa fa-wrench" aria-hidden="true"></i>
|
||||
{{'tools' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="organization.isOwner">
|
||||
<a class="nav-link" routerLink="settings" routerLinkActive="active">
|
||||
<i class="fa fa-cogs" aria-hidden="true"></i>
|
||||
{{'settings' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ml-auto d-flex align-items-center">
|
||||
<button class="btn btn-primary" (click)="goToBusinessPortal()" #businessBtn
|
||||
[appApiAction]="businessTokenPromise" *ngIf="showBusinessPortalButton">
|
||||
<i class="fa fa-bank fa-fw" [hidden]="businessBtn.loading" aria-hidden="true"></i>
|
||||
<i class="fa fa-spinner fa-spin fa-fw" [hidden]="!businessBtn.loading" title="{{'loading' | i18n}}"
|
||||
aria-hidden="true"></i>
|
||||
{{'businessPortal' | i18n}} →
|
||||
</button>
|
||||
</div>
|
||||
<ul class="nav nav-tabs" *ngIf="organization.isManager">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="vault" routerLinkActive="active">
|
||||
<i class="fa fa-lock" aria-hidden="true"></i>
|
||||
{{'vault' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="manage" routerLinkActive="active">
|
||||
<i class="fa fa-sliders" aria-hidden="true"></i>
|
||||
{{'manage' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="organization.isAdmin">
|
||||
<a class="nav-link" routerLink="tools" routerLinkActive="active">
|
||||
<i class="fa fa-wrench" aria-hidden="true"></i>
|
||||
{{'tools' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="organization.isOwner">
|
||||
<a class="nav-link" routerLink="settings" routerLinkActive="active">
|
||||
<i class="fa fa-cogs" aria-hidden="true"></i>
|
||||
{{'settings' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
@@ -4,10 +4,14 @@ import {
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { Organization } from 'jslib/models/domain/organization';
|
||||
@@ -20,19 +24,28 @@ const BroadcasterSubscriptionId = 'OrganizationLayoutComponent';
|
||||
})
|
||||
export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
||||
organization: Organization;
|
||||
|
||||
businessTokenPromise: Promise<any>;
|
||||
private organizationId: string;
|
||||
private businessUrl: string;
|
||||
|
||||
constructor(private route: ActivatedRoute, private userService: UserService,
|
||||
private broadcasterService: BroadcasterService, private ngZone: NgZone) { }
|
||||
private broadcasterService: BroadcasterService, private ngZone: NgZone,
|
||||
private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
|
||||
private environmentService: EnvironmentService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.businessUrl = 'https://portal.bitwarden.com';
|
||||
if (this.environmentService.enterpriseUrl != null) {
|
||||
this.businessUrl = this.environmentService.enterpriseUrl;
|
||||
} else if (this.environmentService.baseUrl != null) {
|
||||
this.businessUrl = this.environmentService.baseUrl + '/portal';
|
||||
}
|
||||
|
||||
document.body.classList.remove('layout_frontend');
|
||||
this.route.params.subscribe(async (params) => {
|
||||
this.route.params.subscribe(async params => {
|
||||
this.organizationId = params.organizationId;
|
||||
await this.load();
|
||||
});
|
||||
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
@@ -51,4 +64,69 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
||||
async load() {
|
||||
this.organization = await this.userService.getOrganization(this.organizationId);
|
||||
}
|
||||
|
||||
async goToBusinessPortal() {
|
||||
if (this.businessTokenPromise != null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.businessTokenPromise = this.apiService.getEnterprisePortalSignInToken();
|
||||
const token = await this.businessTokenPromise;
|
||||
if (token != null) {
|
||||
const userId = await this.userService.getUserId();
|
||||
this.platformUtilsService.launchUri(this.businessUrl + '/login?userId=' + userId +
|
||||
'&token=' + (window as any).encodeURIComponent(token) + '&organizationId=' + this.organization.id);
|
||||
}
|
||||
} catch { }
|
||||
this.businessTokenPromise = null;
|
||||
}
|
||||
|
||||
get showMenuBar() {
|
||||
return this.showManageTab || this.showToolsTab || this.organization.isOwner;
|
||||
}
|
||||
|
||||
get showManageTab(): boolean {
|
||||
return this.organization.canManageUsers ||
|
||||
this.organization.canManageAssignedCollections ||
|
||||
this.organization.canManageAllCollections ||
|
||||
this.organization.canManageGroups ||
|
||||
this.organization.canManagePolicies ||
|
||||
this.organization.canAccessEventLogs;
|
||||
}
|
||||
|
||||
get showToolsTab(): boolean {
|
||||
return this.organization.canAccessImportExport || this.organization.canAccessReports;
|
||||
}
|
||||
|
||||
get showBusinessPortalButton(): boolean {
|
||||
return this.organization.useBusinessPortal && this.organization.canAccessBusinessPortal;
|
||||
}
|
||||
|
||||
get toolsRoute(): string {
|
||||
return this.organization.canAccessImportExport ?
|
||||
'tools/import' :
|
||||
'tools/exposed-passwords-report';
|
||||
}
|
||||
|
||||
get manageRoute(): string {
|
||||
let route: string;
|
||||
switch (true) {
|
||||
case this.organization.canManageUsers:
|
||||
route = 'manage/people';
|
||||
break;
|
||||
case this.organization.canManageAssignedCollections || this.organization.canManageAllCollections:
|
||||
route = 'manage/collections';
|
||||
break;
|
||||
case this.organization.canManageGroups:
|
||||
route = 'manage/groups';
|
||||
break;
|
||||
case this.organization.canManagePolicies:
|
||||
route = 'manage/policies';
|
||||
break;
|
||||
case this.organization.canAccessEventLogs:
|
||||
route = 'manage/events';
|
||||
break;
|
||||
}
|
||||
return route;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="collectionAddEditTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="collectionAddEditTitle">{{title}}</h2>
|
||||
@@ -14,7 +14,8 @@
|
||||
<div class="modal-body" *ngIf="!loading">
|
||||
<div class="form-group">
|
||||
<label for="name">{{'name' | i18n}}</label>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required
|
||||
appAutofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="externalId">{{'externalId' | i18n}}</label>
|
||||
@@ -41,6 +42,7 @@
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th>{{'name' | i18n}}</th>
|
||||
<th width="100" class="text-center">{{'hidePasswords' | i18n}}</th>
|
||||
<th width="100" class="text-center">{{'readOnly' | i18n}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -58,6 +60,10 @@
|
||||
<span class="sr-only">{{'groupAccessAllItems' | i18n}}</span>
|
||||
</ng-container>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" [(ngModel)]="g.hidePasswords"
|
||||
name="Groups[{{i}}].HidePasswords" [disabled]="!g.checked || g.accessAll">
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" [(ngModel)]="g.readOnly" name="Groups[{{i}}].ReadOnly"
|
||||
[disabled]="!g.checked || g.accessAll">
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
} from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
@@ -15,7 +14,7 @@ import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { CipherString } from 'jslib/models/domain/cipherString';
|
||||
import { EncString } from 'jslib/models/domain/encString';
|
||||
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
|
||||
import { CollectionRequest } from 'jslib/models/request/collectionRequest';
|
||||
import { SelectionReadOnlyRequest } from 'jslib/models/request/selectionReadOnlyRequest';
|
||||
@@ -46,9 +45,8 @@ export class CollectionAddEditComponent implements OnInit {
|
||||
private orgKey: SymmetricCryptoKey;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private platformUtilsService: PlatformUtilsService, private cryptoService: CryptoService,
|
||||
private userService: UserService) { }
|
||||
private toasterService: ToasterService, private platformUtilsService: PlatformUtilsService,
|
||||
private cryptoService: CryptoService, private userService: UserService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
const organization = await this.userService.getOrganization(this.organizationId);
|
||||
@@ -56,7 +54,7 @@ export class CollectionAddEditComponent implements OnInit {
|
||||
this.editMode = this.loading = this.collectionId != null;
|
||||
if (this.accessGroups) {
|
||||
const groupsResponse = await this.apiService.getGroups(this.organizationId);
|
||||
this.groups = groupsResponse.data.map((r) => r).sort(Utils.getSortFunction(this.i18nService, 'name'));
|
||||
this.groups = groupsResponse.data.map(r => r).sort(Utils.getSortFunction(this.i18nService, 'name'));
|
||||
}
|
||||
this.orgKey = await this.cryptoService.getOrgKey(this.organizationId);
|
||||
|
||||
@@ -65,14 +63,15 @@ export class CollectionAddEditComponent implements OnInit {
|
||||
this.title = this.i18nService.t('editCollection');
|
||||
try {
|
||||
const collection = await this.apiService.getCollectionDetails(this.organizationId, this.collectionId);
|
||||
this.name = await this.cryptoService.decryptToUtf8(new CipherString(collection.name), this.orgKey);
|
||||
this.name = await this.cryptoService.decryptToUtf8(new EncString(collection.name), this.orgKey);
|
||||
this.externalId = collection.externalId;
|
||||
if (collection.groups != null && this.groups.length > 0) {
|
||||
collection.groups.forEach((s) => {
|
||||
const group = this.groups.filter((g) => !g.accessAll && g.id === s.id);
|
||||
collection.groups.forEach(s => {
|
||||
const group = this.groups.filter(g => !g.accessAll && g.id === s.id);
|
||||
if (group != null && group.length > 0) {
|
||||
(group[0] as any).checked = true;
|
||||
(group[0] as any).readOnly = s.readOnly;
|
||||
(group[0] as any).hidePasswords = s.hidePasswords;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -81,7 +80,7 @@ export class CollectionAddEditComponent implements OnInit {
|
||||
this.title = this.i18nService.t('addCollection');
|
||||
}
|
||||
|
||||
this.groups.forEach((g) => {
|
||||
this.groups.forEach(g => {
|
||||
if (g.accessAll) {
|
||||
(g as any).checked = true;
|
||||
}
|
||||
@@ -97,11 +96,12 @@ export class CollectionAddEditComponent implements OnInit {
|
||||
(g as any).checked = select == null ? !(g as any).checked : select;
|
||||
if (!(g as any).checked) {
|
||||
(g as any).readOnly = false;
|
||||
(g as any).hidePasswords = false;
|
||||
}
|
||||
}
|
||||
|
||||
selectAll(select: boolean) {
|
||||
this.groups.forEach((g) => this.check(g, select));
|
||||
this.groups.forEach(g => this.check(g, select));
|
||||
}
|
||||
|
||||
async submit() {
|
||||
@@ -112,8 +112,8 @@ export class CollectionAddEditComponent implements OnInit {
|
||||
const request = new CollectionRequest();
|
||||
request.name = (await this.cryptoService.encrypt(this.name, this.orgKey)).encryptedString;
|
||||
request.externalId = this.externalId;
|
||||
request.groups = this.groups.filter((g) => (g as any).checked && !g.accessAll)
|
||||
.map((g) => new SelectionReadOnlyRequest(g.id, !!(g as any).readOnly));
|
||||
request.groups = this.groups.filter(g => (g as any).checked && !g.accessAll)
|
||||
.map(g => new SelectionReadOnlyRequest(g.id, !!(g as any).readOnly, !!(g as any).hidePasswords));
|
||||
|
||||
try {
|
||||
if (this.editMode) {
|
||||
@@ -122,7 +122,6 @@ export class CollectionAddEditComponent implements OnInit {
|
||||
this.formPromise = this.apiService.postCollection(this.organizationId, request);
|
||||
}
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({ action: this.editMode ? 'Edited Collection' : 'Created Collection' });
|
||||
this.toasterService.popAsync('success', null,
|
||||
this.i18nService.t(this.editMode ? 'editedCollectionId' : 'createdCollectionId', this.name));
|
||||
this.onSavedCollection.emit();
|
||||
@@ -144,7 +143,6 @@ export class CollectionAddEditComponent implements OnInit {
|
||||
try {
|
||||
this.deletePromise = this.apiService.deleteCollection(this.organizationId, this.collectionId);
|
||||
await this.deletePromise;
|
||||
this.analytics.eventTrack.next({ action: 'Deleted Collection' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('deletedCollectionId', this.name));
|
||||
this.onDeletedCollection.emit();
|
||||
} catch { }
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!loading && (collections | search:searchText:'name':'id') as searchedCollections">
|
||||
<ng-container
|
||||
*ngIf="!loading && (isPaging() ? pagedCollections : collections | search:searchText:'name':'id') as searchedCollections">
|
||||
<p *ngIf="!searchedCollections.length">{{'noCollectionsInList' | i18n}}</p>
|
||||
<table class="table table-hover table-list" *ngIf="searchedCollections.length">
|
||||
<table class="table table-hover table-list" *ngIf="searchedCollections.length" infiniteScroll
|
||||
[infiniteScrollDistance]="1" [infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
|
||||
<tbody>
|
||||
<tr *ngFor="let c of searchedCollections">
|
||||
<td>
|
||||
|
||||
@@ -8,12 +8,12 @@ import {
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CollectionService } from 'jslib/abstractions/collection.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { SearchService } from 'jslib/abstractions/search.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { CollectionData } from 'jslib/models/data/collectionData';
|
||||
@@ -34,27 +34,32 @@ import { EntityUsersComponent } from './entity-users.component';
|
||||
templateUrl: 'collections.component.html',
|
||||
})
|
||||
export class CollectionsComponent implements OnInit {
|
||||
@ViewChild('addEdit', { read: ViewContainerRef }) addEditModalRef: ViewContainerRef;
|
||||
@ViewChild('usersTemplate', { read: ViewContainerRef }) usersModalRef: ViewContainerRef;
|
||||
@ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
||||
@ViewChild('usersTemplate', { read: ViewContainerRef, static: true }) usersModalRef: ViewContainerRef;
|
||||
|
||||
loading = true;
|
||||
organizationId: string;
|
||||
collections: CollectionView[];
|
||||
pagedCollections: CollectionView[];
|
||||
searchText: string;
|
||||
|
||||
protected didScroll = false;
|
||||
protected pageSize = 100;
|
||||
|
||||
private pagedCollectionsCount = 0;
|
||||
private modal: ModalComponent = null;
|
||||
|
||||
constructor(private apiService: ApiService, private route: ActivatedRoute,
|
||||
private collectionService: CollectionService, private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private i18nService: I18nService, private platformUtilsService: PlatformUtilsService,
|
||||
private userService: UserService) { }
|
||||
private toasterService: ToasterService, private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService, private userService: UserService,
|
||||
private searchService: SearchService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.route.parent.parent.params.subscribe(async params => {
|
||||
this.organizationId = params.organizationId;
|
||||
await this.load();
|
||||
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
|
||||
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
|
||||
this.searchText = qParams.search;
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
@@ -66,17 +71,35 @@ export class CollectionsComponent implements OnInit {
|
||||
async load() {
|
||||
const organization = await this.userService.getOrganization(this.organizationId);
|
||||
let response: ListResponse<CollectionResponse>;
|
||||
if (organization.isAdmin) {
|
||||
if (organization.canManageAllCollections) {
|
||||
response = await this.apiService.getCollections(this.organizationId);
|
||||
} else {
|
||||
response = await this.apiService.getUserCollections();
|
||||
}
|
||||
const collections = response.data.filter((c) => c.organizationId === this.organizationId).map((r) =>
|
||||
const collections = response.data.filter(c => c.organizationId === this.organizationId).map(r =>
|
||||
new Collection(new CollectionData(r as CollectionDetailsResponse)));
|
||||
this.collections = await this.collectionService.decryptMany(collections);
|
||||
this.resetPaging();
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
if (!this.collections || this.collections.length <= this.pageSize) {
|
||||
return;
|
||||
}
|
||||
const pagedLength = this.pagedCollections.length;
|
||||
let pagedSize = this.pageSize;
|
||||
if (pagedLength === 0 && this.pagedCollectionsCount > this.pageSize) {
|
||||
pagedSize = this.pagedCollectionsCount;
|
||||
}
|
||||
if (this.collections.length > pagedLength) {
|
||||
this.pagedCollections =
|
||||
this.pagedCollections.concat(this.collections.slice(pagedLength, pagedLength + pagedSize));
|
||||
}
|
||||
this.pagedCollectionsCount = this.pagedCollections.length;
|
||||
this.didScroll = this.pagedCollections.length > this.pageSize;
|
||||
}
|
||||
|
||||
edit(collection: CollectionView) {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
@@ -117,7 +140,6 @@ export class CollectionsComponent implements OnInit {
|
||||
|
||||
try {
|
||||
await this.apiService.deleteCollection(this.organizationId, collection.id);
|
||||
this.analytics.eventTrack.next({ action: 'Deleted Collection' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('deletedCollectionId', collection.name));
|
||||
this.removeCollection(collection);
|
||||
} catch { }
|
||||
@@ -147,10 +169,28 @@ export class CollectionsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
async resetPaging() {
|
||||
this.pagedCollections = [];
|
||||
this.loadMore();
|
||||
}
|
||||
|
||||
isSearching() {
|
||||
return this.searchService.isSearchable(this.searchText);
|
||||
}
|
||||
|
||||
isPaging() {
|
||||
const searching = this.isSearching();
|
||||
if (searching && this.didScroll) {
|
||||
this.resetPaging();
|
||||
}
|
||||
return !searching && this.collections && this.collections.length > this.pageSize;
|
||||
}
|
||||
|
||||
private removeCollection(collection: CollectionView) {
|
||||
const index = this.collections.indexOf(collection);
|
||||
if (index > -1) {
|
||||
this.collections.splice(index, 1);
|
||||
this.resetPaging();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="eventLogsTitle">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="eventLogsTitle">
|
||||
|
||||
@@ -50,7 +50,7 @@ export class EntityEventsComponent implements OnInit {
|
||||
async load() {
|
||||
if (this.showUser) {
|
||||
const response = await this.apiService.getOrganizationUsers(this.organizationId);
|
||||
response.data.forEach((u) => {
|
||||
response.data.forEach(u => {
|
||||
const name = u.name == null || u.name.trim() === '' ? u.email : u.name;
|
||||
this.orgUsersIdMap.set(u.id, { name: name, email: u.email });
|
||||
this.orgUsersUserIdMap.set(u.userId, { name: name, email: u.email });
|
||||
@@ -94,9 +94,9 @@ export class EntityEventsComponent implements OnInit {
|
||||
} catch { }
|
||||
|
||||
this.continuationToken = response.continuationToken;
|
||||
const events = response.data.map((r) => {
|
||||
const events = await Promise.all(response.data.map(async r => {
|
||||
const userId = r.actingUserId == null ? r.userId : r.actingUserId;
|
||||
const eventInfo = this.eventService.getEventInfo(r);
|
||||
const eventInfo = await this.eventService.getEventInfo(r);
|
||||
const user = this.showUser && userId != null && this.orgUsersUserIdMap.has(userId) ?
|
||||
this.orgUsersUserIdMap.get(userId) : null;
|
||||
return {
|
||||
@@ -110,7 +110,7 @@ export class EntityEventsComponent implements OnInit {
|
||||
ip: r.ipAddress,
|
||||
type: r.type,
|
||||
};
|
||||
});
|
||||
}));
|
||||
|
||||
if (!clearExisting && this.events != null && this.events.length > 0) {
|
||||
this.events = this.events.concat(events);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="userAccessTitle">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="userAccessTitle">
|
||||
@@ -47,6 +47,8 @@
|
||||
<th>{{'name' | i18n}}</th>
|
||||
<th *ngIf="entity === 'collection'"> </th>
|
||||
<th>{{'userType' | i18n}}</th>
|
||||
<th width="100" class="text-center" *ngIf="entity === 'collection'">{{'hidePasswords' |
|
||||
i18n}}</th>
|
||||
<th width="100" class="text-center" *ngIf="entity === 'collection'">{{'readOnly' |
|
||||
i18n}}</th>
|
||||
</tr>
|
||||
@@ -84,6 +86,12 @@
|
||||
<span *ngIf="u.type === organizationUserType.Admin">{{'admin' | i18n}}</span>
|
||||
<span *ngIf="u.type === organizationUserType.Manager">{{'manager' | i18n}}</span>
|
||||
<span *ngIf="u.type === organizationUserType.User">{{'user' | i18n}}</span>
|
||||
<span *ngIf="u.type === organizationUserType.Custom">{{'custom' | i18n}}</span>
|
||||
</td>
|
||||
<td class="text-center" *ngIf="entity === 'collection'">
|
||||
<input type="checkbox" [(ngModel)]="u.hidePasswords"
|
||||
name="{{u.id.substr(0,8)}}_HidePasswords"
|
||||
[disabled]="u.accessAll || !u.checked">
|
||||
</td>
|
||||
<td class="text-center" *ngIf="entity === 'collection'">
|
||||
<input type="checkbox" [(ngModel)]="u.readOnly" name="{{u.id.substr(0,8)}}_ReadOnly"
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
} from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
@@ -42,7 +41,7 @@ export class EntityUsersComponent implements OnInit {
|
||||
private allUsers: OrganizationUserUserDetailsResponse[] = [];
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService) { }
|
||||
private toasterService: ToasterService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
await this.loadUsers();
|
||||
@@ -51,7 +50,7 @@ export class EntityUsersComponent implements OnInit {
|
||||
|
||||
get users() {
|
||||
if (this.showSelected) {
|
||||
return this.allUsers.filter((u) => (u as any).checked);
|
||||
return this.allUsers.filter(u => (u as any).checked);
|
||||
} else {
|
||||
return this.allUsers;
|
||||
}
|
||||
@@ -59,12 +58,12 @@ export class EntityUsersComponent implements OnInit {
|
||||
|
||||
async loadUsers() {
|
||||
const users = await this.apiService.getOrganizationUsers(this.organizationId);
|
||||
this.allUsers = users.data.map((r) => r).sort(Utils.getSortFunction(this.i18nService, 'email'));
|
||||
this.allUsers = users.data.map(r => r).sort(Utils.getSortFunction(this.i18nService, 'email'));
|
||||
if (this.entity === 'group') {
|
||||
const response = await this.apiService.getGroupUsers(this.organizationId, this.entityId);
|
||||
if (response != null && users.data.length > 0) {
|
||||
response.forEach((s) => {
|
||||
const user = users.data.filter((u) => u.id === s);
|
||||
response.forEach(s => {
|
||||
const user = users.data.filter(u => u.id === s);
|
||||
if (user != null && user.length > 0) {
|
||||
(user[0] as any).checked = true;
|
||||
}
|
||||
@@ -73,17 +72,18 @@ export class EntityUsersComponent implements OnInit {
|
||||
} else if (this.entity === 'collection') {
|
||||
const response = await this.apiService.getCollectionUsers(this.organizationId, this.entityId);
|
||||
if (response != null && users.data.length > 0) {
|
||||
response.forEach((s) => {
|
||||
const user = users.data.filter((u) => !u.accessAll && u.id === s.id);
|
||||
response.forEach(s => {
|
||||
const user = users.data.filter(u => !u.accessAll && u.id === s.id);
|
||||
if (user != null && user.length > 0) {
|
||||
(user[0] as any).checked = true;
|
||||
(user[0] as any).readOnly = s.readOnly;
|
||||
(user[0] as any).hidePasswords = s.hidePasswords;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.allUsers.forEach((u) => {
|
||||
this.allUsers.forEach(u => {
|
||||
if (this.entity === 'collection' && u.accessAll) {
|
||||
(u as any).checked = true;
|
||||
}
|
||||
@@ -107,6 +107,7 @@ export class EntityUsersComponent implements OnInit {
|
||||
} else {
|
||||
if (this.entity === 'collection') {
|
||||
(u as any).readOnly = false;
|
||||
(u as any).hidePasswords = false;
|
||||
}
|
||||
this.selectedCount--;
|
||||
}
|
||||
@@ -119,17 +120,14 @@ export class EntityUsersComponent implements OnInit {
|
||||
async submit() {
|
||||
try {
|
||||
if (this.entity === 'group') {
|
||||
const selections = this.users.filter((u) => (u as any).checked).map((u) => u.id);
|
||||
const selections = this.users.filter(u => (u as any).checked).map(u => u.id);
|
||||
this.formPromise = this.apiService.putGroupUsers(this.organizationId, this.entityId, selections);
|
||||
} else {
|
||||
const selections = this.users.filter((u) => (u as any).checked && !u.accessAll)
|
||||
.map((u) => new SelectionReadOnlyRequest(u.id, !!(u as any).readOnly));
|
||||
const selections = this.users.filter(u => (u as any).checked && !u.accessAll)
|
||||
.map(u => new SelectionReadOnlyRequest(u.id, !!(u as any).readOnly, !!(u as any).hidePasswords));
|
||||
this.formPromise = this.apiService.putCollectionUsers(this.organizationId, this.entityId, selections);
|
||||
}
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({
|
||||
action: this.entity === 'group' ? 'Edited Group Users' : 'Edited Collection Users',
|
||||
});
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('updatedUsers'));
|
||||
this.onEditedUsers.emit();
|
||||
} catch { }
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
<i class="fa fa-refresh fa-fw" aria-hidden="true" [ngClass]="{'fa-spin': loaded && refreshBtn.loading}"></i>
|
||||
{{'refresh' | i18n}}
|
||||
</button>
|
||||
<button #exportBtn [appApiAction]="exportPromise" type="button" class="btn btn-sm btn-outline-primary ml-3"
|
||||
(click)="exportEvents()" [disabled]="loaded && refreshBtn.loading">
|
||||
{{'export' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="!loaded">
|
||||
|
||||
@@ -7,13 +7,16 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { ExportService } from 'jslib/abstractions/export.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { EventService } from '../../services/event.service';
|
||||
|
||||
import { EventResponse } from 'jslib/models/response/eventResponse';
|
||||
import { ListResponse } from 'jslib/models/response/listResponse';
|
||||
import { EventView } from 'jslib/models/view/eventView';
|
||||
|
||||
@Component({
|
||||
selector: 'app-org-events',
|
||||
@@ -23,23 +26,24 @@ export class EventsComponent implements OnInit {
|
||||
loading = true;
|
||||
loaded = false;
|
||||
organizationId: string;
|
||||
events: any[];
|
||||
events: EventView[];
|
||||
start: string;
|
||||
end: string;
|
||||
continuationToken: string;
|
||||
refreshPromise: Promise<any>;
|
||||
exportPromise: Promise<any>;
|
||||
morePromise: Promise<any>;
|
||||
|
||||
private orgUsersUserIdMap = new Map<string, any>();
|
||||
private orgUsersIdMap = new Map<string, any>();
|
||||
|
||||
constructor(private apiService: ApiService, private route: ActivatedRoute,
|
||||
private eventService: EventService, private i18nService: I18nService,
|
||||
private toasterService: ToasterService, private userService: UserService,
|
||||
constructor(private apiService: ApiService, private route: ActivatedRoute, private eventService: EventService,
|
||||
private i18nService: I18nService, private toasterService: ToasterService, private userService: UserService,
|
||||
private exportService: ExportService, private platformUtilsService: PlatformUtilsService,
|
||||
private router: Router) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.route.parent.parent.params.subscribe(async params => {
|
||||
this.organizationId = params.organizationId;
|
||||
const organization = await this.userService.getOrganization(this.organizationId);
|
||||
if (organization == null || !organization.useEvents) {
|
||||
@@ -55,7 +59,7 @@ export class EventsComponent implements OnInit {
|
||||
|
||||
async load() {
|
||||
const response = await this.apiService.getOrganizationUsers(this.organizationId);
|
||||
response.data.forEach((u) => {
|
||||
response.data.forEach(u => {
|
||||
const name = u.name == null || u.name.trim() === '' ? u.email : u.name;
|
||||
this.orgUsersIdMap.set(u.id, { name: name, email: u.email });
|
||||
this.orgUsersUserIdMap.set(u.userId, { name: name, email: u.email });
|
||||
@@ -64,8 +68,26 @@ export class EventsComponent implements OnInit {
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
async exportEvents() {
|
||||
if (this.appApiPromiseUnfulfilled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.exportPromise = this.exportService.getEventExport(this.events).then(data => {
|
||||
const fileName = this.exportService.getFileName('org-events', 'csv');
|
||||
this.platformUtilsService.saveFile(window, data, { type: 'text/plain' }, fileName);
|
||||
});
|
||||
try {
|
||||
await this.exportPromise;
|
||||
} catch { }
|
||||
|
||||
this.exportPromise = null;
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async loadEvents(clearExisting: boolean) {
|
||||
if (this.refreshPromise != null || this.morePromise != null) {
|
||||
if (this.appApiPromiseUnfulfilled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -92,13 +114,14 @@ export class EventsComponent implements OnInit {
|
||||
} catch { }
|
||||
|
||||
this.continuationToken = response.continuationToken;
|
||||
const events = response.data.map((r) => {
|
||||
const events = await Promise.all(response.data.map(async r => {
|
||||
const userId = r.actingUserId == null ? r.userId : r.actingUserId;
|
||||
const eventInfo = this.eventService.getEventInfo(r);
|
||||
const eventInfo = await this.eventService.getEventInfo(r);
|
||||
const user = userId != null && this.orgUsersUserIdMap.has(userId) ?
|
||||
this.orgUsersUserIdMap.get(userId) : null;
|
||||
return {
|
||||
return new EventView({
|
||||
message: eventInfo.message,
|
||||
humanReadableMessage: eventInfo.humanReadableMessage,
|
||||
appIcon: eventInfo.appIcon,
|
||||
appName: eventInfo.appName,
|
||||
userId: userId,
|
||||
@@ -107,8 +130,8 @@ export class EventsComponent implements OnInit {
|
||||
date: r.date,
|
||||
ip: r.ipAddress,
|
||||
type: r.type,
|
||||
};
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
if (!clearExisting && this.events != null && this.events.length > 0) {
|
||||
this.events = this.events.concat(events);
|
||||
@@ -120,4 +143,8 @@ export class EventsComponent implements OnInit {
|
||||
this.morePromise = null;
|
||||
this.refreshPromise = null;
|
||||
}
|
||||
|
||||
private appApiPromiseUnfulfilled() {
|
||||
return this.refreshPromise != null || this.morePromise != null || this.exportPromise != null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="groupAddEditTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="groupAddEditTitle">{{title}}</h2>
|
||||
@@ -24,6 +24,10 @@
|
||||
<h3 class="mt-4 d-flex">
|
||||
<div class="mb-2">
|
||||
{{'accessControl' | i18n}}
|
||||
<a target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}"
|
||||
href="https://bitwarden.com/help/article/user-types-access-control/#access-control">
|
||||
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="ml-auto" *ngIf="access === 'selected' && collections && collections.length">
|
||||
<button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
|
||||
@@ -59,6 +63,7 @@
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th>{{'name' | i18n}}</th>
|
||||
<th width="100" class="text-center">{{'hidePasswords' | i18n}}</th>
|
||||
<th width="100" class="text-center">{{'readOnly' | i18n}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -71,6 +76,10 @@
|
||||
<td (click)="check(c)">
|
||||
{{c.name}}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" [(ngModel)]="c.hidePasswords"
|
||||
name="Collection[{{i}}].HidePasswords" [disabled]="!c.checked">
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" [(ngModel)]="c.readOnly" name="Collection[{{i}}].ReadOnly"
|
||||
[disabled]="!c.checked">
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
} from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CollectionService } from 'jslib/abstractions/collection.service';
|
||||
@@ -42,8 +41,8 @@ export class GroupAddEditComponent implements OnInit {
|
||||
deletePromise: Promise<any>;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private collectionService: CollectionService, private platformUtilsService: PlatformUtilsService) { }
|
||||
private toasterService: ToasterService, private collectionService: CollectionService,
|
||||
private platformUtilsService: PlatformUtilsService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.editMode = this.loading = this.groupId != null;
|
||||
@@ -58,11 +57,12 @@ export class GroupAddEditComponent implements OnInit {
|
||||
this.name = group.name;
|
||||
this.externalId = group.externalId;
|
||||
if (group.collections != null && this.collections != null) {
|
||||
group.collections.forEach((s) => {
|
||||
const collection = this.collections.filter((c) => c.id === s.id);
|
||||
group.collections.forEach(s => {
|
||||
const collection = this.collections.filter(c => c.id === s.id);
|
||||
if (collection != null && collection.length > 0) {
|
||||
(collection[0] as any).checked = true;
|
||||
collection[0].readOnly = s.readOnly;
|
||||
collection[0].hidePasswords = s.hidePasswords;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -76,7 +76,7 @@ export class GroupAddEditComponent implements OnInit {
|
||||
|
||||
async loadCollections() {
|
||||
const response = await this.apiService.getCollections(this.organizationId);
|
||||
const collections = response.data.map((r) =>
|
||||
const collections = response.data.map(r =>
|
||||
new Collection(new CollectionData(r as CollectionDetailsResponse)));
|
||||
this.collections = await this.collectionService.decryptMany(collections);
|
||||
}
|
||||
@@ -89,7 +89,7 @@ export class GroupAddEditComponent implements OnInit {
|
||||
}
|
||||
|
||||
selectAll(select: boolean) {
|
||||
this.collections.forEach((c) => this.check(c, select));
|
||||
this.collections.forEach(c => this.check(c, select));
|
||||
}
|
||||
|
||||
async submit() {
|
||||
@@ -98,8 +98,8 @@ export class GroupAddEditComponent implements OnInit {
|
||||
request.externalId = this.externalId;
|
||||
request.accessAll = this.access === 'all';
|
||||
if (!request.accessAll) {
|
||||
request.collections = this.collections.filter((c) => (c as any).checked)
|
||||
.map((c) => new SelectionReadOnlyRequest(c.id, !!c.readOnly));
|
||||
request.collections = this.collections.filter(c => (c as any).checked)
|
||||
.map(c => new SelectionReadOnlyRequest(c.id, !!c.readOnly, !!c.hidePasswords));
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -109,7 +109,6 @@ export class GroupAddEditComponent implements OnInit {
|
||||
this.formPromise = this.apiService.postGroup(this.organizationId, request);
|
||||
}
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({ action: this.editMode ? 'Edited Group' : 'Created Group' });
|
||||
this.toasterService.popAsync('success', null,
|
||||
this.i18nService.t(this.editMode ? 'editedGroupId' : 'createdGroupId', this.name));
|
||||
this.onSavedGroup.emit();
|
||||
@@ -131,7 +130,6 @@ export class GroupAddEditComponent implements OnInit {
|
||||
try {
|
||||
this.deletePromise = this.apiService.deleteGroup(this.organizationId, this.groupId);
|
||||
await this.deletePromise;
|
||||
this.analytics.eventTrack.next({ action: 'Deleted Group' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('deletedGroupId', this.name));
|
||||
this.onDeletedGroup.emit();
|
||||
} catch { }
|
||||
|
||||
@@ -16,9 +16,10 @@
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!loading && (groups | search:searchText:'name':'id') as searchedGroups">
|
||||
<ng-container *ngIf="!loading && (isPaging() ? pagedGroups : groups | search:searchText:'name':'id') as searchedGroups">
|
||||
<p *ngIf="!searchedGroups.length">{{'noGroupsInList' | i18n}}</p>
|
||||
<table class="table table-hover table-list" *ngIf="searchedGroups.length">
|
||||
<table class="table table-hover table-list" *ngIf="searchedGroups.length" infiniteScroll
|
||||
[infiniteScrollDistance]="1" [infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
|
||||
<tbody>
|
||||
<tr *ngFor="let g of searchedGroups">
|
||||
<td>
|
||||
|
||||
@@ -11,11 +11,11 @@ import {
|
||||
} from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { SearchService } from 'jslib/abstractions/search.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { GroupResponse } from 'jslib/models/response/groupResponse';
|
||||
@@ -31,24 +31,29 @@ import { GroupAddEditComponent } from './group-add-edit.component';
|
||||
templateUrl: 'groups.component.html',
|
||||
})
|
||||
export class GroupsComponent implements OnInit {
|
||||
@ViewChild('addEdit', { read: ViewContainerRef }) addEditModalRef: ViewContainerRef;
|
||||
@ViewChild('usersTemplate', { read: ViewContainerRef }) usersModalRef: ViewContainerRef;
|
||||
@ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
||||
@ViewChild('usersTemplate', { read: ViewContainerRef, static: true }) usersModalRef: ViewContainerRef;
|
||||
|
||||
loading = true;
|
||||
organizationId: string;
|
||||
groups: GroupResponse[];
|
||||
pagedGroups: GroupResponse[];
|
||||
searchText: string;
|
||||
|
||||
protected didScroll = false;
|
||||
protected pageSize = 100;
|
||||
|
||||
private pagedGroupsCount = 0;
|
||||
private modal: ModalComponent = null;
|
||||
|
||||
constructor(private apiService: ApiService, private route: ActivatedRoute,
|
||||
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private platformUtilsService: PlatformUtilsService, private userService: UserService,
|
||||
private router: Router) { }
|
||||
private toasterService: ToasterService, private platformUtilsService: PlatformUtilsService,
|
||||
private userService: UserService, private router: Router,
|
||||
private searchService: SearchService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.route.parent.parent.params.subscribe(async params => {
|
||||
this.organizationId = params.organizationId;
|
||||
const organization = await this.userService.getOrganization(this.organizationId);
|
||||
if (organization == null || !organization.useGroups) {
|
||||
@@ -56,7 +61,7 @@ export class GroupsComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
await this.load();
|
||||
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
|
||||
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
|
||||
this.searchText = qParams.search;
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
@@ -70,9 +75,26 @@ export class GroupsComponent implements OnInit {
|
||||
const groups = response.data != null && response.data.length > 0 ? response.data : [];
|
||||
groups.sort(Utils.getSortFunction(this.i18nService, 'name'));
|
||||
this.groups = groups;
|
||||
this.resetPaging();
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
if (!this.groups || this.groups.length <= this.pageSize) {
|
||||
return;
|
||||
}
|
||||
const pagedLength = this.pagedGroups.length;
|
||||
let pagedSize = this.pageSize;
|
||||
if (pagedLength === 0 && this.pagedGroupsCount > this.pageSize) {
|
||||
pagedSize = this.pagedGroupsCount;
|
||||
}
|
||||
if (this.groups.length > pagedLength) {
|
||||
this.pagedGroups = this.pagedGroups.concat(this.groups.slice(pagedLength, pagedLength + pagedSize));
|
||||
}
|
||||
this.pagedGroupsCount = this.pagedGroups.length;
|
||||
this.didScroll = this.pagedGroups.length > this.pageSize;
|
||||
}
|
||||
|
||||
edit(group: GroupResponse) {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
@@ -113,7 +135,6 @@ export class GroupsComponent implements OnInit {
|
||||
|
||||
try {
|
||||
await this.apiService.deleteGroup(this.organizationId, group.id);
|
||||
this.analytics.eventTrack.next({ action: 'Deleted Group' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('deletedGroupId', group.name));
|
||||
this.removeGroup(group);
|
||||
} catch { }
|
||||
@@ -142,10 +163,28 @@ export class GroupsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
async resetPaging() {
|
||||
this.pagedGroups = [];
|
||||
this.loadMore();
|
||||
}
|
||||
|
||||
isSearching() {
|
||||
return this.searchService.isSearchable(this.searchText);
|
||||
}
|
||||
|
||||
isPaging() {
|
||||
const searching = this.isSearching();
|
||||
if (searching && this.didScroll) {
|
||||
this.resetPaging();
|
||||
}
|
||||
return !searching && this.groups && this.groups.length > this.pageSize;
|
||||
}
|
||||
|
||||
private removeGroup(group: GroupResponse) {
|
||||
const index = this.groups.indexOf(group);
|
||||
if (index > -1) {
|
||||
this.groups.splice(index, 1);
|
||||
this.resetPaging();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,22 +5,23 @@
|
||||
<div class="card-header">{{'manage' | i18n}}</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a routerLink="people" class="list-group-item" routerLinkActive="active"
|
||||
*ngIf="organization.isAdmin">
|
||||
*ngIf="organization.canManageUsers">
|
||||
{{'people' | i18n}}
|
||||
</a>
|
||||
<a routerLink="collections" class="list-group-item" routerLinkActive="active">
|
||||
<a routerLink="collections" class="list-group-item" routerLinkActive="active"
|
||||
*ngIf="organization.canManageAssignedCollections || organization.canManageAllCollections">
|
||||
{{'collections' | i18n}}
|
||||
</a>
|
||||
<a routerLink="groups" class="list-group-item" routerLinkActive="active"
|
||||
*ngIf="organization.isAdmin && accessGroups">
|
||||
*ngIf="organization.canManageGroups && accessGroups">
|
||||
{{'groups' | i18n}}
|
||||
</a>
|
||||
<a routerLink="policies" class="list-group-item" routerLinkActive="active"
|
||||
*ngIf="organization.isAdmin && accessPolicies">
|
||||
*ngIf="organization.canManagePolicies && accessPolicies">
|
||||
{{'policies' | i18n}}
|
||||
</a>
|
||||
<a routerLink="events" class="list-group-item" routerLinkActive="active"
|
||||
*ngIf="organization.isAdmin && accessEvents">
|
||||
*ngIf="organization.canAccessEventLogs && accessEvents">
|
||||
{{'eventLogs' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@ export class ManageComponent implements OnInit {
|
||||
constructor(private route: ActivatedRoute, private userService: UserService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.route.parent.params.subscribe(async (params) => {
|
||||
this.route.parent.params.subscribe(async params => {
|
||||
this.organization = await this.userService.getOrganization(params.organizationId);
|
||||
this.accessPolicies = this.organization.usePolicies;
|
||||
this.accessEvents = this.organization.useEvents;
|
||||
|
||||
@@ -25,6 +25,31 @@
|
||||
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
|
||||
[(ngModel)]="searchText">
|
||||
</div>
|
||||
<div class="dropdown ml-3" appListDropdown>
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="bulkActionsButton"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" appA11yTitle="{{'options' | i18n}}">
|
||||
<i class="fa fa-cog" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="bulkActionsButton">
|
||||
<button class="dropdown-item" appStopClick (click)="bulkReinvite()">
|
||||
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
|
||||
{{'reinviteSelected' | i18n}}
|
||||
</button>
|
||||
<button class="dropdown-item text-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}}
|
||||
@@ -35,15 +60,20 @@
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!loading && (users | search:searchText:'name':'email':'id') as searchedUsers">
|
||||
<ng-container
|
||||
*ngIf="!loading && (isPaging() ? pagedUsers : users | search:searchText:'name':'email':'id') as searchedUsers">
|
||||
<p *ngIf="!searchedUsers.length">{{'noUsersInList' | i18n}}</p>
|
||||
<ng-container *ngIf="searchedUsers.length">
|
||||
<app-callout type="info" title="{{'confirmUsers' | i18n}}" icon="fa-check-circle" *ngIf="showConfirmUsers">
|
||||
{{'usersNeedConfirmed' | i18n}}
|
||||
</app-callout>
|
||||
<table class="table table-hover table-list">
|
||||
<table class="table table-hover table-list" infiniteScroll [infiniteScrollDistance]="1"
|
||||
[infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
|
||||
<tbody>
|
||||
<tr *ngFor="let u of searchedUsers">
|
||||
<td (click)="checkUser(u)" class="table-list-checkbox">
|
||||
<input type="checkbox" [(ngModel)]="u.checked" appStopProp>
|
||||
</td>
|
||||
<td width="30">
|
||||
<app-avatar [data]="u.name || u.email" [email]="u.email" size="25" [circle]="true"
|
||||
[fontSize]="14"></app-avatar>
|
||||
@@ -67,6 +97,7 @@
|
||||
<span *ngIf="u.type === organizationUserType.Admin">{{'admin' | i18n}}</span>
|
||||
<span *ngIf="u.type === organizationUserType.Manager">{{'manager' | i18n}}</span>
|
||||
<span *ngIf="u.type === organizationUserType.User">{{'user' | i18n}}</span>
|
||||
<span *ngIf="u.type === organizationUserType.Custom">{{'custom' | i18n}}</span>
|
||||
</td>
|
||||
<td class="table-list-options">
|
||||
<div class="dropdown" appListDropdown>
|
||||
|
||||
@@ -11,19 +11,21 @@ import {
|
||||
} from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ValidationService } from 'jslib/angular/services/validation.service';
|
||||
import { ConstantsService } from 'jslib/services/constants.service';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { SearchService } from 'jslib/abstractions/search.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { OrganizationUserConfirmRequest } from 'jslib/models/request/organizationUserConfirmRequest';
|
||||
|
||||
import { OrganizationUserBulkRequest } from 'jslib/models/request/organizationUserBulkRequest';
|
||||
import { OrganizationUserUserDetailsResponse } from 'jslib/models/response/organizationUserResponse';
|
||||
|
||||
import { OrganizationUserStatusType } from 'jslib/enums/organizationUserStatusType';
|
||||
@@ -37,19 +39,22 @@ import { UserAddEditComponent } from './user-add-edit.component';
|
||||
import { UserConfirmComponent } from './user-confirm.component';
|
||||
import { UserGroupsComponent } from './user-groups.component';
|
||||
|
||||
const MaxCheckedCount = 500;
|
||||
|
||||
@Component({
|
||||
selector: 'app-org-people',
|
||||
templateUrl: 'people.component.html',
|
||||
})
|
||||
export class PeopleComponent implements OnInit {
|
||||
@ViewChild('addEdit', { read: ViewContainerRef }) addEditModalRef: ViewContainerRef;
|
||||
@ViewChild('groupsTemplate', { read: ViewContainerRef }) groupsModalRef: ViewContainerRef;
|
||||
@ViewChild('eventsTemplate', { read: ViewContainerRef }) eventsModalRef: ViewContainerRef;
|
||||
@ViewChild('confirmTemplate', { read: ViewContainerRef }) confirmModalRef: ViewContainerRef;
|
||||
@ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
||||
@ViewChild('groupsTemplate', { read: ViewContainerRef, static: true }) groupsModalRef: ViewContainerRef;
|
||||
@ViewChild('eventsTemplate', { read: ViewContainerRef, static: true }) eventsModalRef: ViewContainerRef;
|
||||
@ViewChild('confirmTemplate', { read: ViewContainerRef, static: true }) confirmModalRef: ViewContainerRef;
|
||||
|
||||
loading = true;
|
||||
organizationId: string;
|
||||
users: OrganizationUserUserDetailsResponse[];
|
||||
pagedUsers: OrganizationUserUserDetailsResponse[];
|
||||
searchText: string;
|
||||
status: OrganizationUserStatusType = null;
|
||||
statusMap = new Map<OrganizationUserStatusType, OrganizationUserUserDetailsResponse[]>();
|
||||
@@ -59,21 +64,25 @@ export class PeopleComponent implements OnInit {
|
||||
accessEvents = false;
|
||||
accessGroups = false;
|
||||
|
||||
protected didScroll = false;
|
||||
protected pageSize = 100;
|
||||
|
||||
private pagedUsersCount = 0;
|
||||
private modal: ModalComponent = null;
|
||||
private allUsers: OrganizationUserUserDetailsResponse[];
|
||||
|
||||
constructor(private apiService: ApiService, private route: ActivatedRoute,
|
||||
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private platformUtilsService: PlatformUtilsService, private analytics: Angulartics2,
|
||||
private toasterService: ToasterService, private cryptoService: CryptoService,
|
||||
private userService: UserService, private router: Router,
|
||||
private storageService: StorageService) { }
|
||||
private platformUtilsService: PlatformUtilsService, private toasterService: ToasterService,
|
||||
private cryptoService: CryptoService, private userService: UserService, private router: Router,
|
||||
private storageService: StorageService, private searchService: SearchService,
|
||||
private validationService: ValidationService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.route.parent.parent.params.subscribe(async params => {
|
||||
this.organizationId = params.organizationId;
|
||||
const organization = await this.userService.getOrganization(this.organizationId);
|
||||
if (!organization.isAdmin) {
|
||||
if (!organization.canManageUsers) {
|
||||
this.router.navigate(['../collections'], { relativeTo: this.route });
|
||||
return;
|
||||
}
|
||||
@@ -81,10 +90,10 @@ export class PeopleComponent implements OnInit {
|
||||
this.accessGroups = organization.useGroups;
|
||||
await this.load();
|
||||
|
||||
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
|
||||
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
|
||||
this.searchText = qParams.search;
|
||||
if (qParams.viewEvents != null) {
|
||||
const user = this.users.filter((u) => u.id === qParams.viewEvents);
|
||||
const user = this.users.filter(u => u.id === qParams.viewEvents);
|
||||
if (user.length > 0 && user[0].status === OrganizationUserStatusType.Confirmed) {
|
||||
this.events(user[0]);
|
||||
}
|
||||
@@ -101,7 +110,7 @@ export class PeopleComponent implements OnInit {
|
||||
this.statusMap.clear();
|
||||
this.allUsers = response.data != null && response.data.length > 0 ? response.data : [];
|
||||
this.allUsers.sort(Utils.getSortFunction(this.i18nService, 'email'));
|
||||
this.allUsers.forEach((u) => {
|
||||
this.allUsers.forEach(u => {
|
||||
if (!this.statusMap.has(u.status)) {
|
||||
this.statusMap.set(u.status, [u]);
|
||||
} else {
|
||||
@@ -119,10 +128,29 @@ export class PeopleComponent implements OnInit {
|
||||
} else {
|
||||
this.users = this.allUsers;
|
||||
}
|
||||
// Reset checkbox selecton
|
||||
this.selectAll(false);
|
||||
this.resetPaging();
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
if (!this.users || this.users.length <= this.pageSize) {
|
||||
return;
|
||||
}
|
||||
const pagedLength = this.pagedUsers.length;
|
||||
let pagedSize = this.pageSize;
|
||||
if (pagedLength === 0 && this.pagedUsersCount > this.pageSize) {
|
||||
pagedSize = this.pagedUsersCount;
|
||||
}
|
||||
if (this.users.length > pagedLength) {
|
||||
this.pagedUsers = this.pagedUsers.concat(this.users.slice(pagedLength, pagedLength + pagedSize));
|
||||
}
|
||||
this.pagedUsersCount = this.pagedUsers.length;
|
||||
this.didScroll = this.pagedUsers.length > this.pageSize;
|
||||
}
|
||||
|
||||
get allCount() {
|
||||
return this.allUsers.length;
|
||||
return this.allUsers != null ? this.allUsers.length : 0;
|
||||
}
|
||||
|
||||
get invitedCount() {
|
||||
@@ -206,22 +234,84 @@ export class PeopleComponent implements OnInit {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.actionPromise = this.apiService.deleteOrganizationUser(this.organizationId, user.id);
|
||||
try {
|
||||
await this.apiService.deleteOrganizationUser(this.organizationId, user.id);
|
||||
this.analytics.eventTrack.next({ action: 'Deleted User' });
|
||||
await this.actionPromise;
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('removedUserId', user.name || user.email));
|
||||
this.removeUser(user);
|
||||
} catch { }
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = null;
|
||||
}
|
||||
|
||||
async reinvite(user: OrganizationUserUserDetailsResponse) {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionPromise = this.apiService.postOrganizationUserReinvite(this.organizationId, user.id);
|
||||
await this.actionPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Reinvited User' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenReinvited', user.name || user.email));
|
||||
try {
|
||||
await this.actionPromise;
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenReinvited', user.name || user.email));
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = null;
|
||||
}
|
||||
|
||||
async bulkRemove() {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const users = this.getCheckedUsers();
|
||||
if (users.length <= 0) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('noSelectedUsersApplicable'));
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('removeSelectedUsersConfirmation'), this.i18nService.t('remove'),
|
||||
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const request = new OrganizationUserBulkRequest(users.map(user => user.id));
|
||||
this.actionPromise = this.apiService.deleteManyOrganizationUsers(this.organizationId, request);
|
||||
try {
|
||||
await this.actionPromise;
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('usersHasBeenRemoved'));
|
||||
await this.load();
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = null;
|
||||
}
|
||||
|
||||
async bulkReinvite() {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const users = this.getCheckedUsers().filter(u => u.status === OrganizationUserStatusType.Invited);
|
||||
|
||||
if (users.length <= 0) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('noSelectedUsersApplicable'));
|
||||
return;
|
||||
}
|
||||
|
||||
const request = new OrganizationUserBulkRequest(users.map(user => user.id));
|
||||
this.actionPromise = this.apiService.postManyOrganizationUserReinvite(this.organizationId, request);
|
||||
try {
|
||||
await this.actionPromise;
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('usersHasBeenReinvited'));
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = null;
|
||||
}
|
||||
|
||||
@@ -235,6 +325,20 @@ export class PeopleComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
const confirmUser = async (publicKey: Uint8Array) => {
|
||||
try {
|
||||
this.actionPromise = this.doConfirmation(user, publicKey);
|
||||
await this.actionPromise;
|
||||
updateUser(this);
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenConfirmed', user.name || user.email));
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
throw e;
|
||||
} finally {
|
||||
this.actionPromise = null;
|
||||
}
|
||||
};
|
||||
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
@@ -254,9 +358,14 @@ export class PeopleComponent implements OnInit {
|
||||
childComponent.organizationId = this.organizationId;
|
||||
childComponent.organizationUserId = user != null ? user.id : null;
|
||||
childComponent.userId = user != null ? user.userId : null;
|
||||
childComponent.onConfirmedUser.subscribe(() => {
|
||||
this.modal.close();
|
||||
updateUser(this);
|
||||
childComponent.onConfirmedUser.subscribe(async (publicKey: Uint8Array) => {
|
||||
try {
|
||||
await confirmUser(publicKey);
|
||||
this.modal.close();
|
||||
} catch (e) {
|
||||
// tslint:disable-next-line
|
||||
console.error('Handled exception:', e);
|
||||
}
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
@@ -265,12 +374,19 @@ export class PeopleComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionPromise = this.doConfirmation(user);
|
||||
await this.actionPromise;
|
||||
updateUser(this);
|
||||
this.analytics.eventTrack.next({ action: 'Confirmed User' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenConfirmed', user.name || user.email));
|
||||
this.actionPromise = null;
|
||||
try {
|
||||
const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId);
|
||||
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
|
||||
try {
|
||||
// tslint:disable-next-line
|
||||
console.log('User\'s fingerprint: ' +
|
||||
(await this.cryptoService.getFingerprint(user.userId, publicKey.buffer)).join('-'));
|
||||
} catch { }
|
||||
await confirmUser(publicKey);
|
||||
} catch (e) {
|
||||
// tslint:disable-next-line
|
||||
console.error('Handled exception:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async events(user: OrganizationUserUserDetailsResponse) {
|
||||
@@ -294,15 +410,41 @@ export class PeopleComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
private async doConfirmation(user: OrganizationUserUserDetailsResponse) {
|
||||
async resetPaging() {
|
||||
this.pagedUsers = [];
|
||||
this.loadMore();
|
||||
}
|
||||
|
||||
isSearching() {
|
||||
return this.searchService.isSearchable(this.searchText);
|
||||
}
|
||||
|
||||
isPaging() {
|
||||
const searching = this.isSearching();
|
||||
if (searching && this.didScroll) {
|
||||
this.resetPaging();
|
||||
}
|
||||
return !searching && this.users && this.users.length > this.pageSize;
|
||||
}
|
||||
|
||||
checkUser(user: OrganizationUserUserDetailsResponse, select?: boolean) {
|
||||
(user as any).checked = select == null ? !(user as any).checked : select;
|
||||
}
|
||||
|
||||
selectAll(select: boolean) {
|
||||
if (select) {
|
||||
this.selectAll(false);
|
||||
}
|
||||
const selectCount = select && this.users.length > MaxCheckedCount
|
||||
? MaxCheckedCount
|
||||
: this.users.length;
|
||||
for (let i = 0; i < selectCount; i++) {
|
||||
this.checkUser(this.users[i], select);
|
||||
}
|
||||
}
|
||||
|
||||
private async doConfirmation(user: OrganizationUserUserDetailsResponse, publicKey: Uint8Array) {
|
||||
const orgKey = await this.cryptoService.getOrgKey(this.organizationId);
|
||||
const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId);
|
||||
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
|
||||
try {
|
||||
// tslint:disable-next-line
|
||||
console.log('User\'s fingerprint: ' +
|
||||
(await this.cryptoService.getFingerprint(user.userId, publicKey.buffer)).join('-'));
|
||||
} catch { }
|
||||
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer);
|
||||
const request = new OrganizationUserConfirmRequest();
|
||||
request.key = key.encryptedString;
|
||||
@@ -313,6 +455,7 @@ export class PeopleComponent implements OnInit {
|
||||
let index = this.users.indexOf(user);
|
||||
if (index > -1) {
|
||||
this.users.splice(index, 1);
|
||||
this.resetPaging();
|
||||
}
|
||||
if (this.statusMap.has(OrganizationUserStatusType.Accepted)) {
|
||||
index = this.statusMap.get(OrganizationUserStatusType.Accepted).indexOf(user);
|
||||
@@ -333,4 +476,8 @@ export class PeopleComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getCheckedUsers() {
|
||||
return this.users.filter(u => (u as any).checked);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
<app-callout *ngIf="userCanAccessBusinessPortal" [type]="'warning'">
|
||||
<p>{{'webPoliciesDeprecationWarning' | i18n}}</p>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
(click)="goToEnterprisePortal()">{{'businessPortal' | i18n}}</button>
|
||||
</app-callout>
|
||||
<div class="page-header d-flex">
|
||||
<h1>{{'policies' | i18n}}</h1>
|
||||
</div>
|
||||
@@ -8,7 +13,7 @@
|
||||
<table class="table table-hover table-list" *ngIf="!loading">
|
||||
<tbody>
|
||||
<tr *ngFor="let p of policies">
|
||||
<td>
|
||||
<td *ngIf="p.display">
|
||||
<a href="#" appStopClick (click)="edit(p)">{{p.name}}</a>
|
||||
<span class="badge badge-success" *ngIf="p.enabled">{{'enabled' | i18n}}</span>
|
||||
<small class="text-muted d-block">{{p.description}}</small>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
|
||||
import { PolicyType } from 'jslib/enums/policyType';
|
||||
|
||||
import { EnvironmentService } from 'jslib/abstractions';
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
@@ -28,12 +29,18 @@ import { PolicyEditComponent } from './policy-edit.component';
|
||||
templateUrl: 'policies.component.html',
|
||||
})
|
||||
export class PoliciesComponent implements OnInit {
|
||||
@ViewChild('editTemplate', { read: ViewContainerRef }) editModalRef: ViewContainerRef;
|
||||
@ViewChild('editTemplate', { read: ViewContainerRef, static: true }) editModalRef: ViewContainerRef;
|
||||
|
||||
loading = true;
|
||||
organizationId: string;
|
||||
policies: any[];
|
||||
|
||||
// Remove when removing deprecation warning
|
||||
enterpriseTokenPromise: Promise<any>;
|
||||
userCanAccessBusinessPortal = false;
|
||||
|
||||
private enterpriseUrl: string;
|
||||
|
||||
private modal: ModalComponent = null;
|
||||
private orgPolicies: PolicyResponse[];
|
||||
private policiesEnabledMap: Map<PolicyType, boolean> = new Map<PolicyType, boolean>();
|
||||
@@ -41,48 +48,116 @@ export class PoliciesComponent implements OnInit {
|
||||
constructor(private apiService: ApiService, private route: ActivatedRoute,
|
||||
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private platformUtilsService: PlatformUtilsService, private userService: UserService,
|
||||
private router: Router) {
|
||||
this.policies = [
|
||||
{
|
||||
name: i18nService.t('twoStepLogin'),
|
||||
description: i18nService.t('twoStepLoginPolicyDesc'),
|
||||
type: PolicyType.TwoFactorAuthentication,
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
name: i18nService.t('masterPass'),
|
||||
description: i18nService.t('masterPassPolicyDesc'),
|
||||
type: PolicyType.MasterPassword,
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
name: i18nService.t('passwordGenerator'),
|
||||
description: i18nService.t('passwordGeneratorPolicyDesc'),
|
||||
type: PolicyType.PasswordGenerator,
|
||||
enabled: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
private router: Router, private environmentService: EnvironmentService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.route.parent.parent.params.subscribe(async params => {
|
||||
this.organizationId = params.organizationId;
|
||||
const organization = await this.userService.getOrganization(this.organizationId);
|
||||
if (organization == null || !organization.usePolicies) {
|
||||
this.router.navigate(['/organizations', this.organizationId]);
|
||||
return;
|
||||
}
|
||||
this.userCanAccessBusinessPortal = organization.canAccessBusinessPortal;
|
||||
this.policies = [
|
||||
{
|
||||
name: this.i18nService.t('twoStepLogin'),
|
||||
description: this.i18nService.t('twoStepLoginPolicyDesc'),
|
||||
type: PolicyType.TwoFactorAuthentication,
|
||||
enabled: false,
|
||||
display: true,
|
||||
},
|
||||
{
|
||||
name: this.i18nService.t('masterPass'),
|
||||
description: this.i18nService.t('masterPassPolicyDesc'),
|
||||
type: PolicyType.MasterPassword,
|
||||
enabled: false,
|
||||
display: true,
|
||||
},
|
||||
{
|
||||
name: this.i18nService.t('passwordGenerator'),
|
||||
description: this.i18nService.t('passwordGeneratorPolicyDesc'),
|
||||
type: PolicyType.PasswordGenerator,
|
||||
enabled: false,
|
||||
display: true,
|
||||
},
|
||||
{
|
||||
name: this.i18nService.t('singleOrg'),
|
||||
description: this.i18nService.t('singleOrgDesc'),
|
||||
type: PolicyType.SingleOrg,
|
||||
enabled: false,
|
||||
display: true,
|
||||
},
|
||||
{
|
||||
name: this.i18nService.t('requireSso'),
|
||||
description: this.i18nService.t('requireSsoPolicyDesc'),
|
||||
type: PolicyType.RequireSso,
|
||||
enabled: false,
|
||||
display: organization.useSso,
|
||||
},
|
||||
{
|
||||
name: this.i18nService.t('personalOwnership'),
|
||||
description: this.i18nService.t('personalOwnershipPolicyDesc'),
|
||||
type: PolicyType.PersonalOwnership,
|
||||
enabled: false,
|
||||
display: true,
|
||||
},
|
||||
{
|
||||
name: this.i18nService.t('disableSend'),
|
||||
description: this.i18nService.t('disableSendPolicyDesc'),
|
||||
type: PolicyType.DisableSend,
|
||||
enabled: false,
|
||||
display: true,
|
||||
},
|
||||
{
|
||||
name: this.i18nService.t('sendOptions'),
|
||||
description: this.i18nService.t('sendOptionsPolicyDesc'),
|
||||
type: PolicyType.SendOptions,
|
||||
enabled: false,
|
||||
display: true,
|
||||
},
|
||||
];
|
||||
await this.load();
|
||||
|
||||
// Handle policies component launch from Event message
|
||||
const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
|
||||
if (qParams.policyId != null) {
|
||||
const policyIdFromEvents: string = qParams.policyId;
|
||||
for (const orgPolicy of this.orgPolicies) {
|
||||
if (orgPolicy.id === policyIdFromEvents) {
|
||||
for (let i = 0; i < this.policies.length; i++) {
|
||||
if (this.policies[i].type === orgPolicy.type) {
|
||||
this.edit(this.policies[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Remove when removing deprecation warning
|
||||
this.enterpriseUrl = 'https://portal.bitwarden.com';
|
||||
if (this.environmentService.enterpriseUrl != null) {
|
||||
this.enterpriseUrl = this.environmentService.enterpriseUrl;
|
||||
} else if (this.environmentService.baseUrl != null) {
|
||||
this.enterpriseUrl = this.environmentService.baseUrl + '/portal';
|
||||
}
|
||||
}
|
||||
|
||||
async load() {
|
||||
const response = await this.apiService.getPolicies(this.organizationId);
|
||||
this.orgPolicies = response.data != null && response.data.length > 0 ? response.data : [];
|
||||
this.orgPolicies.forEach((op) => {
|
||||
this.orgPolicies.forEach(op => {
|
||||
this.policiesEnabledMap.set(op.type, op.enabled);
|
||||
});
|
||||
this.policies.forEach((p) => {
|
||||
this.policies.forEach(p => {
|
||||
p.enabled = this.policiesEnabledMap.has(p.type) && this.policiesEnabledMap.get(p.type);
|
||||
});
|
||||
this.loading = false;
|
||||
@@ -102,6 +177,7 @@ export class PoliciesComponent implements OnInit {
|
||||
childComponent.description = p.description;
|
||||
childComponent.type = p.type;
|
||||
childComponent.organizationId = this.organizationId;
|
||||
childComponent.policiesEnabledMap = this.policiesEnabledMap;
|
||||
childComponent.onSavedPolicy.subscribe(() => {
|
||||
this.modal.close();
|
||||
this.load();
|
||||
@@ -111,4 +187,22 @@ export class PoliciesComponent implements OnInit {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Remove when removing deprecation warning
|
||||
async goToEnterprisePortal() {
|
||||
if (this.enterpriseTokenPromise != null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.enterpriseTokenPromise = this.apiService.getEnterprisePortalSignInToken();
|
||||
const token = await this.enterpriseTokenPromise;
|
||||
if (token != null) {
|
||||
const userId = await this.userService.getUserId();
|
||||
this.platformUtilsService.launchUri(this.enterpriseUrl + '/login?userId=' + userId +
|
||||
'&token=' + (window as any).encodeURIComponent(token) + '&organizationId=' + this.organizationId);
|
||||
}
|
||||
} catch { }
|
||||
this.enterpriseTokenPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="policiesEditTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="policiesEditTitle">{{'editPolicy' | i18n}} - {{name}}</h2>
|
||||
@@ -17,11 +17,32 @@
|
||||
title="{{'warning' | i18n}}" icon="fa-warning">
|
||||
{{'twoStepLoginPolicyWarning' | i18n}}
|
||||
</app-callout>
|
||||
<app-callout type="warning" *ngIf="type === policyType.SingleOrg" title="{{'warning' | i18n}}"
|
||||
icon="fa-warning">
|
||||
{{'singleOrgPolicyWarning' | i18n}}
|
||||
</app-callout>
|
||||
<ng-container *ngIf="type === policyType.RequireSso">
|
||||
<app-callout type="tip" title="{{'prerequisite' | i18n}}">
|
||||
{{'requireSsoPolicyReq' | i18n}}
|
||||
</app-callout>
|
||||
<app-callout type="warning">
|
||||
{{'requireSsoExemption' | i18n}}
|
||||
</app-callout>
|
||||
</ng-container>
|
||||
<app-callout type="warning" *ngIf="type === policyType.PersonalOwnership">
|
||||
{{'personalOwnershipExemption' | i18n}}
|
||||
</app-callout>
|
||||
<app-callout type="warning" *ngIf="type === policyType.DisableSend">
|
||||
{{'disableSendExemption' | i18n}}
|
||||
</app-callout>
|
||||
<app-callout type="warning" *ngIf="type === policyType.SendOptions">
|
||||
{{'sendOptionsExemption' | i18n}}
|
||||
</app-callout>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="enabled" [(ngModel)]="enabled"
|
||||
name="Enabled">
|
||||
<label class="form-check-label" for="enabled">{{'enabled' | i18n}}</label>
|
||||
<label class="form-check-label" for="enabled">{{checkboxDesc}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="type === policyType.MasterPassword">
|
||||
@@ -129,6 +150,14 @@
|
||||
<label class="form-check-label" for="passGenIncludeNumber">{{'includeNumber' | i18n}}</label>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="type === policyType.SendOptions">
|
||||
<h3 class="mt-4">{{'options' | i18n}}</h3>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="sendDisableHideEmail" [(ngModel)]="sendDisableHideEmail"
|
||||
name="SendDisableHideEmail">
|
||||
<label class="form-check-label" for="sendDisableHideEmail">{{'disableHideEmail' | i18n}}</label>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
} from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
@@ -27,6 +26,7 @@ export class PolicyEditComponent implements OnInit {
|
||||
@Input() description: string;
|
||||
@Input() type: PolicyType;
|
||||
@Input() organizationId: string;
|
||||
@Input() policiesEnabledMap: Map<PolicyType, boolean> = new Map<PolicyType, boolean>();
|
||||
@Output() onSavedPolicy = new EventEmitter();
|
||||
|
||||
policyType = PolicyType;
|
||||
@@ -37,7 +37,6 @@ export class PolicyEditComponent implements OnInit {
|
||||
defaultTypes: any[];
|
||||
|
||||
// Master password
|
||||
|
||||
masterPassMinComplexity?: number = null;
|
||||
masterPassMinLength?: number;
|
||||
masterPassRequireUpper?: number;
|
||||
@@ -46,7 +45,6 @@ export class PolicyEditComponent implements OnInit {
|
||||
masterPassRequireSpecial?: number;
|
||||
|
||||
// Password generator
|
||||
|
||||
passGenDefaultType?: string;
|
||||
passGenMinLength?: number;
|
||||
passGenUseUpper?: boolean;
|
||||
@@ -59,10 +57,13 @@ export class PolicyEditComponent implements OnInit {
|
||||
passGenCapitalize?: boolean;
|
||||
passGenIncludeNumber?: boolean;
|
||||
|
||||
// Send options
|
||||
sendDisableHideEmail?: boolean;
|
||||
|
||||
private policy: PolicyResponse;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService) {
|
||||
private toasterService: ToasterService) {
|
||||
this.passwordScores = [
|
||||
{ name: '-- ' + i18nService.t('select') + ' --', value: null },
|
||||
{ name: i18nService.t('weak') + ' (0)', value: 0 },
|
||||
@@ -112,6 +113,9 @@ export class PolicyEditComponent implements OnInit {
|
||||
this.masterPassRequireNumbers = this.policy.data.requireNumbers;
|
||||
this.masterPassRequireSpecial = this.policy.data.requireSpecial;
|
||||
break;
|
||||
case PolicyType.SendOptions:
|
||||
this.sendDisableHideEmail = this.policy.data.disableHideEmail;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -127,45 +131,89 @@ export class PolicyEditComponent implements OnInit {
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const request = new PolicyRequest();
|
||||
request.enabled = this.enabled;
|
||||
request.type = this.type;
|
||||
request.data = null;
|
||||
switch (this.type) {
|
||||
case PolicyType.PasswordGenerator:
|
||||
request.data = {
|
||||
defaultType: this.passGenDefaultType,
|
||||
minLength: this.passGenMinLength || null,
|
||||
useUpper: this.passGenUseUpper,
|
||||
useLower: this.passGenUseLower,
|
||||
useNumbers: this.passGenUseNumbers,
|
||||
useSpecial: this.passGenUseSpecial,
|
||||
minNumbers: this.passGenMinNumbers || null,
|
||||
minSpecial: this.passGenMinSpecial || null,
|
||||
minNumberWords: this.passGenMinNumberWords || null,
|
||||
capitalize: this.passGenCapitalize,
|
||||
includeNumber: this.passGenIncludeNumber,
|
||||
};
|
||||
break;
|
||||
case PolicyType.MasterPassword:
|
||||
request.data = {
|
||||
minComplexity: this.masterPassMinComplexity || null,
|
||||
minLength: this.masterPassMinLength || null,
|
||||
requireUpper: this.masterPassRequireUpper,
|
||||
requireLower: this.masterPassRequireLower,
|
||||
requireNumbers: this.masterPassRequireNumbers,
|
||||
requireSpecial: this.masterPassRequireSpecial,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
if (this.preValidate()) {
|
||||
const request = new PolicyRequest();
|
||||
request.enabled = this.enabled;
|
||||
request.type = this.type;
|
||||
request.data = null;
|
||||
switch (this.type) {
|
||||
case PolicyType.PasswordGenerator:
|
||||
request.data = {
|
||||
defaultType: this.passGenDefaultType,
|
||||
minLength: this.passGenMinLength || null,
|
||||
useUpper: this.passGenUseUpper,
|
||||
useLower: this.passGenUseLower,
|
||||
useNumbers: this.passGenUseNumbers,
|
||||
useSpecial: this.passGenUseSpecial,
|
||||
minNumbers: this.passGenMinNumbers || null,
|
||||
minSpecial: this.passGenMinSpecial || null,
|
||||
minNumberWords: this.passGenMinNumberWords || null,
|
||||
capitalize: this.passGenCapitalize,
|
||||
includeNumber: this.passGenIncludeNumber,
|
||||
};
|
||||
break;
|
||||
case PolicyType.MasterPassword:
|
||||
request.data = {
|
||||
minComplexity: this.masterPassMinComplexity || null,
|
||||
minLength: this.masterPassMinLength || null,
|
||||
requireUpper: this.masterPassRequireUpper,
|
||||
requireLower: this.masterPassRequireLower,
|
||||
requireNumbers: this.masterPassRequireNumbers,
|
||||
requireSpecial: this.masterPassRequireSpecial,
|
||||
};
|
||||
break;
|
||||
case PolicyType.SendOptions:
|
||||
request.data = {
|
||||
disableHideEmail: this.sendDisableHideEmail,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
try {
|
||||
this.formPromise = this.apiService.putPolicy(this.organizationId, this.type, request);
|
||||
await this.formPromise;
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('editedPolicyId', this.name));
|
||||
this.onSavedPolicy.emit();
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
|
||||
get checkboxDesc(): string {
|
||||
return this.type === PolicyType.PersonalOwnership ? this.i18nService.t('personalOwnershipCheckboxDesc') :
|
||||
this.i18nService.t('enabled');
|
||||
}
|
||||
|
||||
private preValidate(): boolean {
|
||||
switch (this.type) {
|
||||
case PolicyType.RequireSso:
|
||||
// Don't need prevalidation checks if submitting to disable
|
||||
if (!this.enabled) {
|
||||
return true;
|
||||
}
|
||||
// Have SingleOrg policy enabled?
|
||||
if (!(this.policiesEnabledMap.has(PolicyType.SingleOrg)
|
||||
&& this.policiesEnabledMap.get(PolicyType.SingleOrg))) {
|
||||
this.toasterService.popAsync('error', null, this.i18nService.t('requireSsoPolicyReqError'));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
case PolicyType.SingleOrg:
|
||||
// Don't need prevalidation checks if submitting to enable
|
||||
if (this.enabled) {
|
||||
return true;
|
||||
}
|
||||
// If RequireSso Policy is enabled prevent submittal
|
||||
if (this.policiesEnabledMap.has(PolicyType.RequireSso)
|
||||
&& this.policiesEnabledMap.get(PolicyType.RequireSso)) {
|
||||
this.toasterService.popAsync('error', null, this.i18nService.t('disableRequireSsoError'));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
this.formPromise = this.apiService.putPolicy(this.organizationId, this.type, request);
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Edited Policy' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('editedPolicyId', this.name));
|
||||
this.onSavedPolicy.emit();
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
|
||||
<div class="modal-dialog" [ngClass]="{'modal-lg': !editMode}" role="document">
|
||||
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="userAddEditTitle">
|
||||
@@ -19,11 +19,18 @@
|
||||
<p>{{'inviteUserDesc' | i18n}}</p>
|
||||
<div class="form-group mb-4">
|
||||
<label for="emails">{{'email' | i18n}}</label>
|
||||
<input id="emails" class="form-control" type="text" name="Emails" [(ngModel)]="emails" required>
|
||||
<input id="emails" class="form-control" type="text" name="Emails" [(ngModel)]="emails" required
|
||||
appAutoFocus>
|
||||
<small class="text-muted">{{'inviteMultipleEmailDesc' | i18n : '20'}}</small>
|
||||
</div>
|
||||
</ng-container>
|
||||
<h3>{{'userType' | i18n}}</h3>
|
||||
<h3>
|
||||
{{'userType' | i18n}}
|
||||
<a target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}"
|
||||
href="https://bitwarden.com/help/article/user-types-access-control/#user-types">
|
||||
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
|
||||
</a>
|
||||
</h3>
|
||||
<div class="form-check mt-2 form-check-block">
|
||||
<input class="form-check-input" type="radio" name="userType" id="userTypeUser"
|
||||
[value]="organizationUserType.User" [(ngModel)]="type">
|
||||
@@ -56,9 +63,141 @@
|
||||
<small>{{'ownerDesc' | i18n}}</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mt-2 form-check-block">
|
||||
<input class="form-check-input" type="radio" name="userType" id="userTypeCustom"
|
||||
[value]="organizationUserType.Custom" [(ngModel)]="type">
|
||||
<label class="form-check-label" for="userTypeCustom">
|
||||
{{'custom' | i18n}}
|
||||
<small>{{'customDesc' | i18n}}</small>
|
||||
</label>
|
||||
</div>
|
||||
<ng-container *ngIf="customUserTypeSelected">
|
||||
<h3 class="mt-4 d-flex">
|
||||
{{'permissions' | i18n}}
|
||||
</h3>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="mb-3">
|
||||
<label class="font-weight-bold mb-0">Manager Permissions</label>
|
||||
<hr class="my-0 mr-2" />
|
||||
<div class="form-group mb-0">
|
||||
<div class="form-check mt-1 form-check-block">
|
||||
<input class="form-check-input" type="checkbox" name="manageAssignedCollections"
|
||||
id="manageAssignedCollections"
|
||||
[(ngModel)]="permissions.manageAssignedCollections">
|
||||
<label class="form-check-label font-weight-normal"
|
||||
for="manageAssignedCollections">
|
||||
{{'manageAssignedCollections' | i18n}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="mb-3">
|
||||
<label class="font-weight-bold mb-0">Admin Permissions</label>
|
||||
<hr class="my-0 mr-2" />
|
||||
<div class="form-group mb-0">
|
||||
<div class="form-check mt-1 form-check-block">
|
||||
<input class="form-check-input" type="checkbox" name="accessBusinessPortal"
|
||||
id="accessBusinessPortal" [(ngModel)]="permissions.accessBusinessPortal">
|
||||
<label class="form-check-label font-weight-normal" for="accessBusinessPortal">
|
||||
{{'accessBusinessPortal' | i18n}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<div class="form-check mt-1 form-check-block">
|
||||
<input class="form-check-input" type="checkbox" name="accessEventLogs"
|
||||
id="accessEventLogs" [(ngModel)]="permissions.accessEventLogs">
|
||||
<label class="form-check-label font-weight-normal" for="accessEventLogs">
|
||||
{{'accessEventLogs' | i18n}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<div class="form-check mt-1 form-check-block">
|
||||
<input class="form-check-input" type="checkbox" name="accessImportExport"
|
||||
id="accessImportExport" [(ngModel)]="permissions.accessImportExport">
|
||||
<label class="form-check-label font-weight-normal" for="accessImportExport">
|
||||
{{'accessImportExport' | i18n}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<div class="form-check mt-1 form-check-block">
|
||||
<input class="form-check-input" type="checkbox" name="accessReports"
|
||||
id="accessReports" [(ngModel)]="permissions.accessReports">
|
||||
<label class="form-check-label font-weight-normal" for="accessReports">
|
||||
{{'accessReports' | i18n}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<div class="form-check mt-1 form-check-block">
|
||||
<input class="form-check-input" type="checkbox" name="manageAllCollections"
|
||||
id="manageAllCollections" [(ngModel)]="permissions.manageAllCollections">
|
||||
<label class="form-check-label font-weight-normal" for="manageAllCollections">
|
||||
{{'manageAllCollections' | i18n}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<div class="form-check mt-1 form-check-block">
|
||||
<input class="form-check-input" type="checkbox" name="manageGroups"
|
||||
id="manageGroups" [(ngModel)]="permissions.manageGroups">
|
||||
<label class="form-check-label font-weight-normal" for="manageGroups">
|
||||
{{'manageGroups' | i18n}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<div class="form-check mt-1 form-check-block">
|
||||
<input class="form-check-input" type="checkbox" name="manageSso"
|
||||
id="managePolicies" [(ngModel)]="permissions.manageSso">
|
||||
<label class="form-check-label font-weight-normal" for="manageSso">
|
||||
{{'manageSso' | i18n}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<div class="form-check mt-1 form-check-block">
|
||||
<input class="form-check-input" type="checkbox" name="managePolicies"
|
||||
id="managePolicies" [(ngModel)]="permissions.managePolicies">
|
||||
<label class="form-check-label font-weight-normal" for="managePolicies">
|
||||
{{'managePolicies' | i18n}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<div class="form-check mt-1 form-check-block">
|
||||
<input class="form-check-input" type="checkbox" name="manageUsers"
|
||||
id="manageUsers" [(ngModel)]="permissions.manageUsers">
|
||||
<label class="form-check-label font-weight-normal" for="manageUsers">
|
||||
{{'manageUsers' | i18n}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<div class="form-check mt-1 form-check-block">
|
||||
<input class="form-check-input" type="checkbox" name="manageResetPassword"
|
||||
id="manageResetPassword" [(ngModel)]="permissions.manageResetPassword">
|
||||
<label class="form-check-label font-weight-normal" for="manageResetPassword">
|
||||
{{'manageResetPassword' | i18n}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<h3 class="mt-4 d-flex">
|
||||
<div class="mb-2">
|
||||
<div class="mb-3">
|
||||
{{'accessControl' | i18n}}
|
||||
<a target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}"
|
||||
href="https://bitwarden.com/help/article/user-types-access-control/#access-control">
|
||||
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="ml-auto" *ngIf="access === 'selected' && collections && collections.length">
|
||||
<button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
|
||||
@@ -94,6 +233,7 @@
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th>{{'name' | i18n}}</th>
|
||||
<th width="100" class="text-center">{{'hidePasswords' | i18n}}</th>
|
||||
<th width="100" class="text-center">{{'readOnly' | i18n}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -106,6 +246,10 @@
|
||||
<td (click)="check(c)">
|
||||
{{c.name}}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" [(ngModel)]="c.hidePasswords"
|
||||
name="Collection[{{i}}].HidePasswords" [disabled]="!c.checked">
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" [(ngModel)]="c.readOnly" name="Collection[{{i}}].ReadOnly"
|
||||
[disabled]="!c.checked">
|
||||
@@ -120,8 +264,9 @@
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'save' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
data-dismiss="modal">{{'cancel' | i18n}}</button>
|
||||
<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"
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
} from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CollectionService } from 'jslib/abstractions/collection.service';
|
||||
@@ -23,6 +22,7 @@ import { CollectionDetailsResponse } from 'jslib/models/response/collectionRespo
|
||||
import { CollectionView } from 'jslib/models/view/collectionView';
|
||||
|
||||
import { OrganizationUserType } from 'jslib/enums/organizationUserType';
|
||||
import { PermissionsApi } from 'jslib/models/api/permissionsApi';
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-add-edit',
|
||||
@@ -40,15 +40,21 @@ export class UserAddEditComponent implements OnInit {
|
||||
title: string;
|
||||
emails: string;
|
||||
type: OrganizationUserType = OrganizationUserType.User;
|
||||
permissions = new PermissionsApi();
|
||||
showCustom = false;
|
||||
access: 'all' | 'selected' = 'selected';
|
||||
collections: CollectionView[] = [];
|
||||
formPromise: Promise<any>;
|
||||
deletePromise: Promise<any>;
|
||||
organizationUserType = OrganizationUserType;
|
||||
|
||||
get customUserTypeSelected(): boolean {
|
||||
return this.type === OrganizationUserType.Custom;
|
||||
}
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private collectionService: CollectionService, private platformUtilsService: PlatformUtilsService) { }
|
||||
private toasterService: ToasterService, private collectionService: CollectionService,
|
||||
private platformUtilsService: PlatformUtilsService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.editMode = this.loading = this.organizationUserId != null;
|
||||
@@ -61,12 +67,16 @@ export class UserAddEditComponent implements OnInit {
|
||||
const user = await this.apiService.getOrganizationUser(this.organizationId, this.organizationUserId);
|
||||
this.access = user.accessAll ? 'all' : 'selected';
|
||||
this.type = user.type;
|
||||
if (user.type === OrganizationUserType.Custom) {
|
||||
this.permissions = user.permissions;
|
||||
}
|
||||
if (user.collections != null && this.collections != null) {
|
||||
user.collections.forEach((s) => {
|
||||
const collection = this.collections.filter((c) => c.id === s.id);
|
||||
user.collections.forEach(s => {
|
||||
const collection = this.collections.filter(c => c.id === s.id);
|
||||
if (collection != null && collection.length > 0) {
|
||||
(collection[0] as any).checked = true;
|
||||
collection[0].readOnly = s.readOnly;
|
||||
collection[0].hidePasswords = s.hidePasswords;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -80,7 +90,7 @@ export class UserAddEditComponent implements OnInit {
|
||||
|
||||
async loadCollections() {
|
||||
const response = await this.apiService.getCollections(this.organizationId);
|
||||
const collections = response.data.map((r) =>
|
||||
const collections = response.data.map(r =>
|
||||
new Collection(new CollectionData(r as CollectionDetailsResponse)));
|
||||
this.collections = await this.collectionService.decryptMany(collections);
|
||||
}
|
||||
@@ -93,14 +103,51 @@ export class UserAddEditComponent implements OnInit {
|
||||
}
|
||||
|
||||
selectAll(select: boolean) {
|
||||
this.collections.forEach((c) => this.check(c, select));
|
||||
this.collections.forEach(c => this.check(c, select));
|
||||
}
|
||||
|
||||
setRequestPermissions(p: PermissionsApi, clearPermissions: boolean) {
|
||||
p.accessBusinessPortal = clearPermissions ?
|
||||
false :
|
||||
this.permissions.accessBusinessPortal;
|
||||
p.accessEventLogs = this.permissions.accessEventLogs = clearPermissions ?
|
||||
false :
|
||||
this.permissions.accessEventLogs;
|
||||
p.accessImportExport = clearPermissions ?
|
||||
false :
|
||||
this.permissions.accessImportExport;
|
||||
p.accessReports = clearPermissions ?
|
||||
false :
|
||||
this.permissions.accessReports;
|
||||
p.manageAllCollections = clearPermissions ?
|
||||
false :
|
||||
this.permissions.manageAllCollections;
|
||||
p.manageAssignedCollections = clearPermissions ?
|
||||
false :
|
||||
this.permissions.manageAssignedCollections;
|
||||
p.manageGroups = clearPermissions ?
|
||||
false :
|
||||
this.permissions.manageGroups;
|
||||
p.manageSso = clearPermissions ?
|
||||
false :
|
||||
this.permissions.manageSso;
|
||||
p.managePolicies = clearPermissions ?
|
||||
false :
|
||||
this.permissions.managePolicies;
|
||||
p.manageUsers = clearPermissions ?
|
||||
false :
|
||||
this.permissions.manageUsers;
|
||||
p.manageResetPassword = clearPermissions ?
|
||||
false :
|
||||
this.permissions.manageResetPassword;
|
||||
return p;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
let collections: SelectionReadOnlyRequest[] = null;
|
||||
if (this.access !== 'all') {
|
||||
collections = this.collections.filter((c) => (c as any).checked)
|
||||
.map((c) => new SelectionReadOnlyRequest(c.id, !!c.readOnly));
|
||||
collections = this.collections.filter(c => (c as any).checked)
|
||||
.map(c => new SelectionReadOnlyRequest(c.id, !!c.readOnly, !!c.hidePasswords));
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -109,6 +156,7 @@ export class UserAddEditComponent implements OnInit {
|
||||
request.accessAll = this.access === 'all';
|
||||
request.type = this.type;
|
||||
request.collections = collections;
|
||||
request.permissions = this.setRequestPermissions(request.permissions ?? new PermissionsApi(), request.type !== OrganizationUserType.Custom);
|
||||
this.formPromise = this.apiService.putOrganizationUser(this.organizationId, this.organizationUserId,
|
||||
request);
|
||||
} else {
|
||||
@@ -116,11 +164,11 @@ export class UserAddEditComponent implements OnInit {
|
||||
request.emails = this.emails.trim().split(/\s*,\s*/);
|
||||
request.accessAll = this.access === 'all';
|
||||
request.type = this.type;
|
||||
request.permissions = this.setRequestPermissions(request.permissions ?? new PermissionsApi(), request.type !== OrganizationUserType.Custom);
|
||||
request.collections = collections;
|
||||
this.formPromise = this.apiService.postOrganizationUserInvite(this.organizationId, request);
|
||||
}
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({ action: this.editMode ? 'Edited User' : 'Invited User' });
|
||||
this.toasterService.popAsync('success', null,
|
||||
this.i18nService.t(this.editMode ? 'editedUserId' : 'invitedUsers', this.name));
|
||||
this.onSavedUser.emit();
|
||||
@@ -142,9 +190,9 @@ export class UserAddEditComponent implements OnInit {
|
||||
try {
|
||||
this.deletePromise = this.apiService.deleteOrganizationUser(this.organizationId, this.organizationUserId);
|
||||
await this.deletePromise;
|
||||
this.analytics.eventTrack.next({ action: 'Deleted User' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('removedUserId', this.name));
|
||||
this.onDeletedUser.emit();
|
||||
} catch { }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="confirmUserTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="confirmUserTitle">
|
||||
{{'confirmUser' | i18n}}
|
||||
|
||||
@@ -6,18 +6,12 @@ import {
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ConstantsService } from 'jslib/services/constants.service';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
|
||||
import { OrganizationUserConfirmRequest } from 'jslib/models/request/organizationUserConfirmRequest';
|
||||
|
||||
import { Utils } from 'jslib/misc/utils';
|
||||
|
||||
@Component({
|
||||
@@ -34,13 +28,11 @@ export class UserConfirmComponent implements OnInit {
|
||||
dontAskAgain = false;
|
||||
loading = true;
|
||||
fingerprint: string;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
private publicKey: Uint8Array = null;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private cryptoService: CryptoService, private storageService: StorageService) { }
|
||||
constructor(private apiService: ApiService, private cryptoService: CryptoService,
|
||||
private storageService: StorageService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
@@ -65,20 +57,6 @@ export class UserConfirmComponent implements OnInit {
|
||||
await this.storageService.save(ConstantsService.autoConfirmFingerprints, true);
|
||||
}
|
||||
|
||||
try {
|
||||
this.formPromise = this.doConfirmation();
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Confirmed User' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenConfirmed', this.name));
|
||||
this.onConfirmedUser.emit();
|
||||
} catch { }
|
||||
}
|
||||
|
||||
private async doConfirmation() {
|
||||
const orgKey = await this.cryptoService.getOrgKey(this.organizationId);
|
||||
const key = await this.cryptoService.rsaEncrypt(orgKey.key, this.publicKey.buffer);
|
||||
const request = new OrganizationUserConfirmRequest();
|
||||
request.key = key.encryptedString;
|
||||
await this.apiService.postOrganizationUserConfirm(this.organizationId, this.organizationUserId, request);
|
||||
this.onConfirmedUser.emit(this.publicKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="groupAccessTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="groupAccessTitle">
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
} from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
@@ -32,11 +31,11 @@ export class UserGroupsComponent implements OnInit {
|
||||
formPromise: Promise<any>;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService) { }
|
||||
private toasterService: ToasterService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
const groupsResponse = await this.apiService.getGroups(this.organizationId);
|
||||
const groups = groupsResponse.data.map((r) => r);
|
||||
const groups = groupsResponse.data.map(r => r);
|
||||
groups.sort(Utils.getSortFunction(this.i18nService, 'name'));
|
||||
this.groups = groups;
|
||||
|
||||
@@ -44,8 +43,8 @@ export class UserGroupsComponent implements OnInit {
|
||||
const userGroups = await this.apiService.getOrganizationUserGroups(
|
||||
this.organizationId, this.organizationUserId);
|
||||
if (userGroups != null && this.groups != null) {
|
||||
userGroups.forEach((ug) => {
|
||||
const group = this.groups.filter((g) => g.id === ug);
|
||||
userGroups.forEach(ug => {
|
||||
const group = this.groups.filter(g => g.id === ug);
|
||||
if (group != null && group.length > 0) {
|
||||
(group[0] as any).checked = true;
|
||||
}
|
||||
@@ -64,18 +63,17 @@ export class UserGroupsComponent implements OnInit {
|
||||
}
|
||||
|
||||
selectAll(select: boolean) {
|
||||
this.groups.forEach((g) => this.check(g, select));
|
||||
this.groups.forEach(g => this.check(g, select));
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const request = new OrganizationUserUpdateGroupsRequest();
|
||||
request.groupIds = this.groups.filter((g) => (g as any).checked).map((g) => g.id);
|
||||
request.groupIds = this.groups.filter(g => (g as any).checked).map(g => g.id);
|
||||
|
||||
try {
|
||||
this.formPromise = this.apiService.putOrganizationUserGroups(this.organizationId, this.organizationUserId,
|
||||
request);
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Edited User Groups' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('editedGroupsForUser', this.name));
|
||||
this.onSavedUser.emit();
|
||||
} catch { }
|
||||
|
||||
@@ -10,17 +10,23 @@
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="name">{{'organizationName' | i18n}}</label>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="org.name">
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="org.name"
|
||||
[disabled]="selfHosted">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="billingEmail">{{'billingEmail' | i18n}}</label>
|
||||
<input id="billingEmail" class="form-control" type="text" name="BillingEmail"
|
||||
[(ngModel)]="org.billingEmail">
|
||||
[(ngModel)]="org.billingEmail" [disabled]="selfHosted">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="businessName">{{'businessName' | i18n}}</label>
|
||||
<input id="businessName" class="form-control" type="text" name="BusinessName"
|
||||
[(ngModel)]="org.businessName">
|
||||
[(ngModel)]="org.businessName" [disabled]="selfHosted">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="identifier">{{'identifier' | i18n}}</label>
|
||||
<input id="identifier" class="form-control" type="text" name="Identifier"
|
||||
[(ngModel)]="org.identifier">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
@@ -49,9 +55,17 @@
|
||||
<h1>{{'taxInformation' | i18n}}</h1>
|
||||
</div>
|
||||
<p>{{'taxInformationDesc' | i18n}}</p>
|
||||
<a href="https://bitwarden.com/contact/" target="_blank" rel="noopener" class="btn btn-outline-secondary">
|
||||
{{'contactSupport' | i18n}}
|
||||
</a>
|
||||
<div *ngIf="!org || loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</div>
|
||||
<form *ngIf="org && !loading" #formTax (ngSubmit)="submitTaxInfo()" [appApiAction]="taxFormPromise" ngNativeValidate>
|
||||
<app-tax-info></app-tax-info>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="formTax.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'save' | i18n}}</span>
|
||||
</button>
|
||||
</form>
|
||||
<div class="secondary-header text-danger border-0 mb-0">
|
||||
<h1>{{'dangerZone' | i18n}}</h1>
|
||||
</div>
|
||||
|
||||
@@ -7,46 +7,50 @@ import {
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||
|
||||
import { OrganizationUpdateRequest } from 'jslib/models/request/organizationUpdateRequest';
|
||||
import { OrganizationResponse } from 'jslib/models/response/organizationResponse';
|
||||
|
||||
import { ModalComponent } from '../../modal.component';
|
||||
import { ApiKeyComponent } from '../../settings/api-key.component';
|
||||
import { PurgeVaultComponent } from '../../settings/purge-vault.component';
|
||||
import { ApiKeyComponent } from './api-key.component';
|
||||
import { TaxInfoComponent } from '../../settings/tax-info.component';
|
||||
import { DeleteOrganizationComponent } from './delete-organization.component';
|
||||
import { RotateApiKeyComponent } from './rotate-api-key.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-org-account',
|
||||
templateUrl: 'account.component.html',
|
||||
})
|
||||
export class AccountComponent {
|
||||
@ViewChild('deleteOrganizationTemplate', { read: ViewContainerRef }) deleteModalRef: ViewContainerRef;
|
||||
@ViewChild('purgeOrganizationTemplate', { read: ViewContainerRef }) purgeModalRef: ViewContainerRef;
|
||||
@ViewChild('apiKeyTemplate', { read: ViewContainerRef }) apiKeyModalRef: ViewContainerRef;
|
||||
@ViewChild('rotateApiKeyTemplate', { read: ViewContainerRef }) rotateApiKeyModalRef: ViewContainerRef;
|
||||
@ViewChild('deleteOrganizationTemplate', { read: ViewContainerRef, static: true }) deleteModalRef: ViewContainerRef;
|
||||
@ViewChild('purgeOrganizationTemplate', { read: ViewContainerRef, static: true }) purgeModalRef: ViewContainerRef;
|
||||
@ViewChild('apiKeyTemplate', { read: ViewContainerRef, static: true }) apiKeyModalRef: ViewContainerRef;
|
||||
@ViewChild('rotateApiKeyTemplate', { read: ViewContainerRef, static: true }) rotateApiKeyModalRef: ViewContainerRef;
|
||||
@ViewChild(TaxInfoComponent) taxInfo: TaxInfoComponent;
|
||||
|
||||
selfHosted = false;
|
||||
loading = true;
|
||||
canUseApi = false;
|
||||
org: OrganizationResponse;
|
||||
formPromise: Promise<any>;
|
||||
taxFormPromise: Promise<any>;
|
||||
|
||||
private organizationId: string;
|
||||
private modal: ModalComponent = null;
|
||||
|
||||
constructor(private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private route: ActivatedRoute, private syncService: SyncService) { }
|
||||
private toasterService: ToasterService, private route: ActivatedRoute,
|
||||
private syncService: SyncService, private platformUtilsService: PlatformUtilsService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.selfHosted = this.platformUtilsService.isSelfHost();
|
||||
this.route.parent.parent.params.subscribe(async params => {
|
||||
this.organizationId = params.organizationId;
|
||||
try {
|
||||
this.org = await this.apiService.getOrganization(this.organizationId);
|
||||
@@ -62,15 +66,21 @@ export class AccountComponent {
|
||||
request.name = this.org.name;
|
||||
request.businessName = this.org.businessName;
|
||||
request.billingEmail = this.org.billingEmail;
|
||||
request.identifier = this.org.identifier;
|
||||
this.formPromise = this.apiService.putOrganization(this.organizationId, request).then(() => {
|
||||
return this.syncService.fullSync(true);
|
||||
});
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Updated Organization Settings' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('organizationUpdated'));
|
||||
} catch { }
|
||||
}
|
||||
|
||||
async submitTaxInfo() {
|
||||
this.taxFormPromise = this.taxInfo.submitTaxInfo();
|
||||
await this.taxFormPromise;
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('taxInfoUpdated'));
|
||||
}
|
||||
|
||||
deleteOrganization() {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
@@ -110,7 +120,14 @@ export class AccountComponent {
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.apiKeyModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<ApiKeyComponent>(ApiKeyComponent, this.apiKeyModalRef);
|
||||
childComponent.organizationId = this.organizationId;
|
||||
childComponent.keyType = 'organization';
|
||||
childComponent.entityId = this.organizationId;
|
||||
childComponent.postKey = this.apiService.postOrganizationApiKey.bind(this.apiService);
|
||||
childComponent.scope = 'api.organization';
|
||||
childComponent.grantType = 'client_credentials';
|
||||
childComponent.apiKeyTitle = 'apiKey';
|
||||
childComponent.apiKeyWarning = 'apiKeyWarning';
|
||||
childComponent.apiKeyDescription = 'apiKeyDesc';
|
||||
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
this.modal = null;
|
||||
@@ -124,8 +141,16 @@ export class AccountComponent {
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.rotateApiKeyModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<RotateApiKeyComponent>(RotateApiKeyComponent, this.rotateApiKeyModalRef);
|
||||
childComponent.organizationId = this.organizationId;
|
||||
const childComponent = this.modal.show<ApiKeyComponent>(ApiKeyComponent, this.rotateApiKeyModalRef);
|
||||
childComponent.keyType = 'organization';
|
||||
childComponent.isRotation = true;
|
||||
childComponent.entityId = this.organizationId;
|
||||
childComponent.postKey = this.apiService.postOrganizationRotateApiKey.bind(this.apiService);
|
||||
childComponent.scope = 'api.organization';
|
||||
childComponent.grantType = 'client_credentials';
|
||||
childComponent.apiKeyTitle = 'apiKey';
|
||||
childComponent.apiKeyWarning = 'apiKeyWarning';
|
||||
childComponent.apiKeyDescription = 'apiKeyRotateDesc';
|
||||
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
this.modal = null;
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
} from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
@@ -33,14 +32,14 @@ export class AdjustSeatsComponent {
|
||||
@Output() onAdjusted = new EventEmitter<number>();
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
|
||||
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
||||
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
|
||||
|
||||
seatAdjustment = 0;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private router: Router, private activatedRoute: ActivatedRoute) { }
|
||||
private toasterService: ToasterService, private router: Router,
|
||||
private activatedRoute: ActivatedRoute) { }
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
@@ -63,7 +62,6 @@ export class AdjustSeatsComponent {
|
||||
};
|
||||
this.formPromise = action();
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({ action: this.add ? 'Added Seats' : 'Removed Seats' });
|
||||
this.onAdjusted.emit(this.seatAdjustment);
|
||||
if (paymentFailed) {
|
||||
this.toasterService.popAsync({
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
aria-hidden="true">×</span></button>
|
||||
<h2 class="card-body-header">{{'changeBillingPlan' | i18n}}</h2>
|
||||
<p class="mb-0">{{'changeBillingPlanUpgrade' | i18n}}</p>
|
||||
<app-organization-plans [showFree]="false" [showCancel]="true" plan="families" [organizationId]="organizationId"
|
||||
(onCanceled)="cancel()">
|
||||
<app-organization-plans [showFree]="false" [showCancel]="true" [plan]="defaultUpgradePlan"
|
||||
[product]="defaultUpgradeProduct" [organizationId]="organizationId" (onCanceled)="cancel()">
|
||||
</app-organization-plans>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,9 @@ import {
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
|
||||
import { PlanType } from 'jslib/enums/planType';
|
||||
import { ProductType } from 'jslib/enums/productType';
|
||||
|
||||
@Component({
|
||||
selector: 'app-change-plan',
|
||||
templateUrl: 'change-plan.component.html',
|
||||
@@ -18,12 +21,13 @@ export class ChangePlanComponent {
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
|
||||
formPromise: Promise<any>;
|
||||
defaultUpgradePlan: PlanType = PlanType.FamiliesAnnually;
|
||||
defaultUpgradeProduct: ProductType = ProductType.Families;
|
||||
|
||||
constructor(private apiService: ApiService, private platformUtilsService: PlatformUtilsService) { }
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
this.platformUtilsService.eventTrack('Changed Plan');
|
||||
this.onChanged.emit();
|
||||
} catch { }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="deleteOrganizationTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="deleteOrganizationTitle">{{'deleteOrganization' | i18n}}</h2>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
@@ -21,8 +20,8 @@ export class DeleteOrganizationComponent {
|
||||
formPromise: Promise<any>;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private cryptoService: CryptoService, private router: Router) { }
|
||||
private toasterService: ToasterService, private cryptoService: CryptoService,
|
||||
private router: Router) { }
|
||||
|
||||
async submit() {
|
||||
if (this.masterPassword == null || this.masterPassword === '') {
|
||||
@@ -36,7 +35,6 @@ export class DeleteOrganizationComponent {
|
||||
try {
|
||||
this.formPromise = this.apiService.deleteOrganization(this.organizationId, request);
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Deleted Organization' });
|
||||
this.toasterService.popAsync('success', this.i18nService.t('organizationDeleted'),
|
||||
this.i18nService.t('organizationDeletedDesc'));
|
||||
this.router.navigate(['/']);
|
||||
|
||||
@@ -32,7 +32,6 @@ export class DownloadLicenseComponent {
|
||||
const license = await this.formPromise;
|
||||
const licenseString = JSON.stringify(license, null, 2);
|
||||
this.platformUtilsService.saveFile(window, licenseString, null, 'bitwarden_organization_license.json');
|
||||
this.platformUtilsService.eventTrack('Downloaded License');
|
||||
this.onDownloaded.emit();
|
||||
} catch { }
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
@@ -18,14 +17,13 @@ import { UserBillingComponent } from '../../settings/user-billing.component';
|
||||
templateUrl: '../../settings/user-billing.component.html',
|
||||
})
|
||||
export class OrganizationBillingComponent extends UserBillingComponent implements OnInit {
|
||||
constructor(apiService: ApiService, i18nService: I18nService,
|
||||
analytics: Angulartics2, toasterService: ToasterService,
|
||||
constructor(apiService: ApiService, i18nService: I18nService, toasterService: ToasterService,
|
||||
private route: ActivatedRoute, platformUtilsService: PlatformUtilsService) {
|
||||
super(apiService, i18nService, analytics, toasterService, platformUtilsService);
|
||||
super(apiService, i18nService, toasterService, platformUtilsService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.route.parent.parent.params.subscribe(async params => {
|
||||
this.organizationId = params.organizationId;
|
||||
await this.load();
|
||||
this.firstLoaded = true;
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</app-callout>
|
||||
<dl *ngIf="selfHosted">
|
||||
<dt>{{'billingPlan' | i18n}}</dt>
|
||||
<dd>{{sub.plan}}</dd>
|
||||
<dd>{{sub.plan.name}}</dd>
|
||||
<dt>{{'expiration' | i18n}}</dt>
|
||||
<dd *ngIf="sub.expiration">
|
||||
{{sub.expiration | date:'mediumDate'}}
|
||||
@@ -39,7 +39,7 @@
|
||||
<div class="col-4">
|
||||
<dl>
|
||||
<dt>{{'billingPlan' | i18n}}</dt>
|
||||
<dd>{{sub.plan}}</dd>
|
||||
<dd>{{sub.plan.name}}</dd>
|
||||
<ng-container *ngIf="subscription">
|
||||
<dt>{{'status' | i18n}}</dt>
|
||||
<dd>
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { OrganizationSubscriptionResponse } from 'jslib/models/response/organizationSubscriptionResponse';
|
||||
|
||||
@@ -13,7 +12,6 @@ import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { TokenService } from 'jslib/abstractions/token.service';
|
||||
|
||||
import { PlanType } from 'jslib/enums/planType';
|
||||
|
||||
@@ -38,15 +36,14 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
cancelPromise: Promise<any>;
|
||||
reinstatePromise: Promise<any>;
|
||||
|
||||
constructor(private tokenService: TokenService, private apiService: ApiService,
|
||||
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
constructor(private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService, private toasterService: ToasterService,
|
||||
private messagingService: MessagingService, private route: ActivatedRoute) {
|
||||
this.selfHosted = platformUtilsService.isSelfHost();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.route.parent.parent.params.subscribe(async params => {
|
||||
this.organizationId = params.organizationId;
|
||||
await this.load();
|
||||
this.firstLoaded = true;
|
||||
@@ -76,7 +73,6 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
try {
|
||||
this.reinstatePromise = this.apiService.postOrganizationReinstate(this.organizationId);
|
||||
await this.reinstatePromise;
|
||||
this.analytics.eventTrack.next({ action: 'Reinstated Plan' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('reinstated'));
|
||||
this.load();
|
||||
} catch { }
|
||||
@@ -96,7 +92,6 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
try {
|
||||
this.cancelPromise = this.apiService.postOrganizationCancel(this.organizationId);
|
||||
await this.cancelPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Canceled Plan' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('canceledSubscription'));
|
||||
this.load();
|
||||
} catch { }
|
||||
@@ -192,34 +187,20 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
}
|
||||
|
||||
get billingInterval() {
|
||||
const monthly = this.sub.planType === PlanType.EnterpriseMonthly ||
|
||||
this.sub.planType === PlanType.TeamsMonthly;
|
||||
const monthly = !this.sub.plan.isAnnual;
|
||||
return monthly ? 'month' : 'year';
|
||||
}
|
||||
|
||||
get storageGbPrice() {
|
||||
return this.billingInterval === 'month' ? 0.5 : 4;
|
||||
return this.sub.plan.additionalStoragePricePerGb;
|
||||
}
|
||||
|
||||
get seatPrice() {
|
||||
switch (this.sub.planType) {
|
||||
case PlanType.EnterpriseMonthly:
|
||||
return 4;
|
||||
case PlanType.EnterpriseAnnually:
|
||||
return 36;
|
||||
case PlanType.TeamsMonthly:
|
||||
return 2.5;
|
||||
case PlanType.TeamsAnnually:
|
||||
return 24;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
return this.sub.plan.seatPrice;
|
||||
}
|
||||
|
||||
get canAdjustSeats() {
|
||||
return this.sub.planType === PlanType.EnterpriseMonthly ||
|
||||
this.sub.planType === PlanType.EnterpriseAnnually ||
|
||||
this.sub.planType === PlanType.TeamsMonthly || this.sub.planType === PlanType.TeamsAnnually;
|
||||
return this.sub.plan.hasAdditionalSeatsOption;
|
||||
}
|
||||
|
||||
get canDownloadLicense() {
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="rotateKeyTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="rotateKeyTitle">{{'rotateApiKey' | i18n}}</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{'apiKeyRotateDesc' | i18n}}</p>
|
||||
<ng-container *ngIf="!clientSecret">
|
||||
<label for="masterPassword">{{'masterPass' | i18n}}</label>
|
||||
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control"
|
||||
[(ngModel)]="masterPassword" required appAutofocus appInputVerbatim>
|
||||
</ng-container>
|
||||
<app-callout type="warning" *ngIf="clientSecret">{{'apiKeyWarning' | i18n}}</app-callout>
|
||||
<app-callout type="info" title="{{'oauth2ClientCredentials' | i18n}}" icon="fa-key"
|
||||
*ngIf="clientSecret">
|
||||
<p class="mb-1">
|
||||
<strong>client_id:</strong><br>
|
||||
<code>{{clientId}}</code>
|
||||
</p>
|
||||
<p class="mb-1">
|
||||
<strong>client_secret:</strong><br>
|
||||
<code>{{clientSecret}}</code>
|
||||
</p>
|
||||
<p class="mb-1">
|
||||
<strong>scope:</strong><br>
|
||||
<code>{{scope}}</code>
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
<strong>grant_type:</strong><br>
|
||||
<code>client_credentials</code>
|
||||
</p>
|
||||
</app-callout>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"
|
||||
*ngIf="!clientSecret">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'rotateApiKey' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
|
||||
import { PasswordVerificationRequest } from 'jslib/models/request/passwordVerificationRequest';
|
||||
|
||||
import { ApiKeyResponse } from 'jslib/models/response/apiKeyResponse';
|
||||
|
||||
@Component({
|
||||
selector: 'app-rotate-api-key',
|
||||
templateUrl: 'rotate-api-key.component.html',
|
||||
})
|
||||
export class RotateApiKeyComponent {
|
||||
organizationId: string;
|
||||
|
||||
masterPassword: string;
|
||||
formPromise: Promise<ApiKeyResponse>;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
scope: string;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private cryptoService: CryptoService, private router: Router) { }
|
||||
|
||||
async submit() {
|
||||
if (this.masterPassword == null || this.masterPassword === '') {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('masterPassRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
const request = new PasswordVerificationRequest();
|
||||
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
|
||||
try {
|
||||
this.formPromise = this.apiService.postOrganizationRotateApiKey(this.organizationId, request);
|
||||
const response = await this.formPromise;
|
||||
this.clientSecret = response.apiKey;
|
||||
this.clientId = 'organization.' + this.organizationId;
|
||||
this.scope = 'api.organization';
|
||||
this.analytics.eventTrack.next({ action: 'Rotated Organization API Key' });
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export class SettingsComponent {
|
||||
private platformUtilsService: PlatformUtilsService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.route.parent.params.subscribe(async (params) => {
|
||||
this.route.parent.params.subscribe(async params => {
|
||||
this.selfHosted = await this.platformUtilsService.isSelfHost();
|
||||
const organization = await this.userService.getOrganization(params.organizationId);
|
||||
this.access2fa = organization.use2fa;
|
||||
|
||||
@@ -26,7 +26,7 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent {
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.route.parent.parent.params.subscribe(async params => {
|
||||
this.organizationId = params.organizationId;
|
||||
await super.ngOnInit();
|
||||
});
|
||||
|
||||
@@ -16,8 +16,6 @@ import { EventType } from 'jslib/enums/eventType';
|
||||
templateUrl: '../../tools/export.component.html',
|
||||
})
|
||||
export class ExportComponent extends BaseExportComponent {
|
||||
organizationId: string;
|
||||
|
||||
constructor(cryptoService: CryptoService, i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService, exportService: ExportService,
|
||||
eventService: EventService, private route: ActivatedRoute) {
|
||||
@@ -25,7 +23,7 @@ export class ExportComponent extends BaseExportComponent {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.route.parent.parent.params.subscribe(async params => {
|
||||
this.organizationId = params.organizationId;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
ExposedPasswordsReportComponent as BaseExposedPasswordsReportComponent,
|
||||
} from '../../tools/exposed-passwords-report.component';
|
||||
|
||||
import { Cipher } from 'jslib/models/domain/cipher';
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
|
||||
@Component({
|
||||
@@ -20,6 +21,8 @@ import { CipherView } from 'jslib/models/view/cipherView';
|
||||
templateUrl: '../../tools/exposed-passwords-report.component.html',
|
||||
})
|
||||
export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportComponent {
|
||||
manageableCiphers: Cipher[];
|
||||
|
||||
constructor(cipherService: CipherService, auditService: AuditService,
|
||||
componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService,
|
||||
userService: UserService, private route: ActivatedRoute) {
|
||||
@@ -27,8 +30,9 @@ export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportC
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.route.parent.parent.params.subscribe(async params => {
|
||||
this.organization = await this.userService.getOrganization(params.organizationId);
|
||||
this.manageableCiphers = await this.cipherService.getAll();
|
||||
super.ngOnInit();
|
||||
});
|
||||
}
|
||||
@@ -36,4 +40,8 @@ export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportC
|
||||
getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
|
||||
}
|
||||
|
||||
canManageCipher(c: CipherView): boolean {
|
||||
return this.manageableCiphers.some(x => x.id === c.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@ import {
|
||||
} from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { ImportService } from 'jslib/abstractions/import.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { ImportComponent as BaseImportComponent } from '../../tools/import.component';
|
||||
|
||||
@@ -17,17 +18,32 @@ import { ImportComponent as BaseImportComponent } from '../../tools/import.compo
|
||||
templateUrl: '../../tools/import.component.html',
|
||||
})
|
||||
export class ImportComponent extends BaseImportComponent {
|
||||
constructor(i18nService: I18nService, analytics: Angulartics2,
|
||||
toasterService: ToasterService, importService: ImportService,
|
||||
router: Router, private route: ActivatedRoute) {
|
||||
super(i18nService, analytics, toasterService, importService, router);
|
||||
organizationName: string;
|
||||
|
||||
constructor(i18nService: I18nService, toasterService: ToasterService,
|
||||
importService: ImportService, router: Router, private route: ActivatedRoute,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
private userService: UserService) {
|
||||
super(i18nService, toasterService, importService, router, platformUtilsService);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async params => {
|
||||
this.organizationId = params.organizationId;
|
||||
this.successNavigate = ['organizations', this.organizationId, 'vault'];
|
||||
super.ngOnInit();
|
||||
});
|
||||
const organization = await this.userService.getOrganization(this.organizationId);
|
||||
this.organizationName = organization.name;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('importWarning', this.organizationName),
|
||||
this.i18nService.t('warning'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
super.submit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export class InactiveTwoFactorReportComponent extends BaseInactiveTwoFactorRepor
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.route.parent.parent.params.subscribe(async params => {
|
||||
this.organization = await this.userService.getOrganization(params.organizationId);
|
||||
await super.ngOnInit();
|
||||
});
|
||||
|
||||
@@ -8,6 +8,8 @@ import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { Cipher } from 'jslib/models/domain/cipher';
|
||||
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
|
||||
import {
|
||||
@@ -19,6 +21,8 @@ import {
|
||||
templateUrl: '../../tools/reused-passwords-report.component.html',
|
||||
})
|
||||
export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportComponent {
|
||||
manageableCiphers: Cipher[];
|
||||
|
||||
constructor(cipherService: CipherService, componentFactoryResolver: ComponentFactoryResolver,
|
||||
messagingService: MessagingService, userService: UserService,
|
||||
private route: ActivatedRoute) {
|
||||
@@ -26,8 +30,9 @@ export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportCom
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.route.parent.parent.params.subscribe(async params => {
|
||||
this.organization = await this.userService.getOrganization(params.organizationId);
|
||||
this.manageableCiphers = await this.cipherService.getAll();
|
||||
await super.ngOnInit();
|
||||
});
|
||||
}
|
||||
@@ -35,4 +40,8 @@ export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportCom
|
||||
getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
|
||||
}
|
||||
|
||||
canManageCipher(c: CipherView): boolean {
|
||||
return this.manageableCiphers.some(x => x.id === c.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,54 @@
|
||||
<div class="container page-content">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">{{'tools' | i18n}}</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a routerLink="import" class="list-group-item" routerLinkActive="active">
|
||||
{{'importData' | i18n}}
|
||||
</a>
|
||||
<a routerLink="export" class="list-group-item" routerLinkActive="active">
|
||||
{{'exportVault' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header d-flex">
|
||||
{{'reports' | i18n}}
|
||||
<div class="ml-auto">
|
||||
<a href="#" appStopClick class="badge badge-primary" *ngIf="!accessReports"
|
||||
(click)="upgradeOrganization()">
|
||||
{{'upgrade' | i18n}}
|
||||
<ng-container *ngIf="loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!loading">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<div class="card mb-4" *ngIf="organization.canAccessImportExport">
|
||||
<div class="card-header">{{'tools' | i18n}}</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a routerLink="import" class="list-group-item" routerLinkActive="active">
|
||||
{{'importData' | i18n}}
|
||||
</a>
|
||||
<a routerLink="export" class="list-group-item" routerLinkActive="active">
|
||||
{{'exportVault' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a routerLink="exposed-passwords-report" class="list-group-item" routerLinkActive="active">
|
||||
{{'exposedPasswordsReport' | i18n}}
|
||||
</a>
|
||||
<a routerLink="reused-passwords-report" class="list-group-item" routerLinkActive="active">
|
||||
{{'reusedPasswordsReport' | i18n}}
|
||||
</a>
|
||||
<a routerLink="weak-passwords-report" class="list-group-item" routerLinkActive="active">
|
||||
{{'weakPasswordsReport' | i18n}}
|
||||
</a>
|
||||
<a routerLink="unsecured-websites-report" class="list-group-item" routerLinkActive="active">
|
||||
{{'unsecuredWebsitesReport' | i18n}}
|
||||
</a>
|
||||
<a routerLink="inactive-two-factor-report" class="list-group-item" routerLinkActive="active">
|
||||
{{'inactive2faReport' | i18n}}
|
||||
</a>
|
||||
<div class="card" *ngIf="organization.canAccessReports">
|
||||
<div class="card-header d-flex">
|
||||
{{'reports' | i18n}}
|
||||
<div class="ml-auto">
|
||||
<a href="#" appStopClick class="badge badge-primary" *ngIf="!accessReports"
|
||||
(click)="upgradeOrganization()">
|
||||
{{'upgrade' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a routerLink="exposed-passwords-report" class="list-group-item" routerLinkActive="active">
|
||||
{{'exposedPasswordsReport' | i18n}}
|
||||
</a>
|
||||
<a routerLink="reused-passwords-report" class="list-group-item" routerLinkActive="active">
|
||||
{{'reusedPasswordsReport' | i18n}}
|
||||
</a>
|
||||
<a routerLink="weak-passwords-report" class="list-group-item" routerLinkActive="active">
|
||||
{{'weakPasswordsReport' | i18n}}
|
||||
</a>
|
||||
<a routerLink="unsecured-websites-report" class="list-group-item" routerLinkActive="active">
|
||||
{{'unsecuredWebsitesReport' | i18n}}
|
||||
</a>
|
||||
<a routerLink="inactive-two-factor-report" class="list-group-item" routerLinkActive="active">
|
||||
{{'inactive2faReport' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
@@ -13,16 +13,18 @@ import { UserService } from 'jslib/abstractions/user.service';
|
||||
export class ToolsComponent {
|
||||
organization: Organization;
|
||||
accessReports = false;
|
||||
loading = true;
|
||||
|
||||
constructor(private route: ActivatedRoute, private userService: UserService,
|
||||
private messagingService: MessagingService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.route.parent.params.subscribe(async (params) => {
|
||||
this.route.parent.params.subscribe(async params => {
|
||||
this.organization = await this.userService.getOrganization(params.organizationId);
|
||||
// TODO: Maybe we want to just make sure they are not on a free plan? Just compare useTotp for now
|
||||
// since all paid plans include useTotp
|
||||
this.accessReports = this.organization.useTotp;
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export class UnsecuredWebsitesReportComponent extends BaseUnsecuredWebsitesRepor
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.route.parent.parent.params.subscribe(async params => {
|
||||
this.organization = await this.userService.getOrganization(params.organizationId);
|
||||
await super.ngOnInit();
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user