mirror of
https://github.com/bitwarden/web
synced 2025-12-06 00:03:28 +00:00
Compare commits
439 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bf3ca2708 | ||
|
|
3e4a7e7a56 | ||
|
|
5d17de227b | ||
|
|
0d985c0221 | ||
|
|
eaa6bc12ce | ||
|
|
b3337df774 | ||
|
|
6c8c5bcde6 | ||
|
|
d255f6add4 | ||
|
|
84dde72990 | ||
|
|
5dfeee548d | ||
|
|
09516b4d4e | ||
|
|
b7b74d8f1f | ||
|
|
80d3cd3126 | ||
|
|
bbd416ba24 | ||
|
|
75563660f0 | ||
|
|
c2197bcc53 | ||
|
|
12114c786b | ||
|
|
73c192ad18 | ||
|
|
465564325e | ||
|
|
7c0d093be5 | ||
|
|
a1fbe6b970 | ||
|
|
305d86f765 | ||
|
|
e7e5816ded | ||
|
|
cd9b1b906c | ||
|
|
0b5a74aa9f | ||
|
|
c3407ac35a | ||
|
|
c9699647d7 | ||
|
|
aac011d3b3 | ||
|
|
e2108ff85b | ||
|
|
5c492f893b | ||
|
|
2877b3c63d | ||
|
|
1d94185078 | ||
|
|
a27eddae56 | ||
|
|
5ed830205d | ||
|
|
aeca6f04f9 | ||
|
|
c099ff7662 | ||
|
|
83ba366558 | ||
|
|
6129fdb6e5 | ||
|
|
8db66bf282 | ||
|
|
b7cd18b715 | ||
|
|
6ed991593a | ||
|
|
ccf3d49fc4 | ||
|
|
7e95e44f1d | ||
|
|
a5de11d002 | ||
|
|
756bd82a46 | ||
|
|
f9ce4a2f81 | ||
|
|
088301c4be | ||
|
|
f7f70408c9 | ||
|
|
292d713423 | ||
|
|
e02eadc9f7 | ||
|
|
6e66df59b7 | ||
|
|
00b9f4cab6 | ||
|
|
f6fb56229e | ||
|
|
5b770084c9 | ||
|
|
a2472e0cf5 | ||
|
|
4de7b52044 | ||
|
|
1e100d1bf1 | ||
|
|
d00fb9e0a5 | ||
|
|
f5d8673ad4 | ||
|
|
bd2cba1f31 | ||
|
|
45c07b7c39 | ||
|
|
36244d58aa | ||
|
|
e968d5a2a5 | ||
|
|
84df9cca87 | ||
|
|
e550989ce2 | ||
|
|
94edc1e284 | ||
|
|
b9f8cad578 | ||
|
|
02eb382ae7 | ||
|
|
1ecc092f08 | ||
|
|
191fa922d2 | ||
|
|
fb817f1ca7 | ||
|
|
9c2f128585 | ||
|
|
9ebd700317 | ||
|
|
9ab6cf31fd | ||
|
|
bb5c114b8d | ||
|
|
1f2a724d32 | ||
|
|
9b28203757 | ||
|
|
ac9f30f5f0 | ||
|
|
b13b0a66ce | ||
|
|
fcfdd5bc76 | ||
|
|
cdbbc37d59 | ||
|
|
4ba4af7cf9 | ||
|
|
89708d1fd6 | ||
|
|
6cb48c186e | ||
|
|
a1c9c47c89 | ||
|
|
85cc2865b6 | ||
|
|
2dc74b26f3 | ||
|
|
3d0ed43920 | ||
|
|
dc54943a19 | ||
|
|
c6ae5368fe | ||
|
|
c947354517 | ||
|
|
076f01b65f | ||
|
|
e37292a276 | ||
|
|
7d76473580 | ||
|
|
8bafbbd2ff | ||
|
|
80c5dff5ad | ||
|
|
a4571a2617 | ||
|
|
18608a8b63 | ||
|
|
c9116ad7ab | ||
|
|
d982902986 | ||
|
|
3ab6868460 | ||
|
|
8d5974d0f8 | ||
|
|
35a64afdf9 | ||
|
|
1ed850324d | ||
|
|
8f886df84f | ||
|
|
55481b255b | ||
|
|
b0b9d8445e | ||
|
|
3a2f04006f | ||
|
|
1aacd4ece1 | ||
|
|
f0e3e3b6f9 | ||
|
|
26533713ff | ||
|
|
b55d54eb5b | ||
|
|
01cb57c9fb | ||
|
|
eb85464f8d | ||
|
|
d30fcf8dca | ||
|
|
004d14eaf4 | ||
|
|
d1a7c3390a | ||
|
|
132c4139ad | ||
|
|
0aa664fb4f | ||
|
|
d25dc1a23f | ||
|
|
3d5f22b67d | ||
|
|
cf6ae951d2 | ||
|
|
cca9384cd7 | ||
|
|
e7b2557bcd | ||
|
|
dad084b309 | ||
|
|
e7fea1b138 | ||
|
|
b24d7df789 | ||
|
|
c2f801b6a9 | ||
|
|
20112688ab | ||
|
|
2a19bdd8d1 | ||
|
|
2d95806feb | ||
|
|
40da48a106 | ||
|
|
df81d9fd5f | ||
|
|
1060775cad | ||
|
|
96cc9c681c | ||
|
|
b4200fba60 | ||
|
|
2ded5228cb | ||
|
|
7be58fb884 | ||
|
|
a29e9e11f7 | ||
|
|
18c89e4fa5 | ||
|
|
84dd370cfb | ||
|
|
b45c79d65b | ||
|
|
52a5086f7e | ||
|
|
3980dc7e84 | ||
|
|
ffd0608dda | ||
|
|
322bc90920 | ||
|
|
9685f2c2b3 | ||
|
|
342871a216 | ||
|
|
1cd1ab07a2 | ||
|
|
137be678c0 | ||
|
|
bcf0aaab17 | ||
|
|
9c331e1777 | ||
|
|
789516e573 | ||
|
|
02f964c7d9 | ||
|
|
ea4d1de772 | ||
|
|
0f3d71a504 | ||
|
|
5dc00a8bc6 | ||
|
|
5690e3fe9e | ||
|
|
f6fcb280fc | ||
|
|
65a20815bf | ||
|
|
9a55202a9f | ||
|
|
06ec65fb10 | ||
|
|
371ecd9d3a | ||
|
|
ff3fce821c | ||
|
|
cc706a48da | ||
|
|
e4093209cc | ||
|
|
1f7e5632ac | ||
|
|
bda9e7b2b2 | ||
|
|
5427ddb8d6 | ||
|
|
b34d40252f | ||
|
|
f73d74dd73 | ||
|
|
eb48b8e65f | ||
|
|
ce0fe368ab | ||
|
|
34ef71707a | ||
|
|
ffeb9dbaa5 | ||
|
|
62a1d09f48 | ||
|
|
60039de67d | ||
|
|
526df6e41a | ||
|
|
cf8b451e35 | ||
|
|
059260d318 | ||
|
|
45134f903d | ||
|
|
fefe4edda1 | ||
|
|
aabb1bc264 | ||
|
|
02ba2d3b60 | ||
|
|
925c5aa389 | ||
|
|
2b6ce14a32 | ||
|
|
ed45e524b9 | ||
|
|
e645204e37 | ||
|
|
ff1429c6b3 | ||
|
|
abe17a02c4 | ||
|
|
7bde73102b | ||
|
|
3f27093f82 | ||
|
|
37ed53cb3c | ||
|
|
4b20d3ef0a | ||
|
|
12492b5749 | ||
|
|
af2b422730 | ||
|
|
0c63f65aa7 | ||
|
|
4b2d1e6745 | ||
|
|
25e6a03435 | ||
|
|
d681f91de9 | ||
|
|
12e2bcbbd9 | ||
|
|
ec3e438c99 | ||
|
|
2089237d23 | ||
|
|
7bcd0ac3e5 | ||
|
|
8e9ab12219 | ||
|
|
33b539858f | ||
|
|
cdfd828a8b | ||
|
|
22727b5abe | ||
|
|
041cf1268d | ||
|
|
fb3afbdc76 | ||
|
|
1f6632146b | ||
|
|
944187f276 | ||
|
|
81eb2189ca | ||
|
|
4fc90984d8 | ||
|
|
0b1abc9ab0 | ||
|
|
212d81b93c | ||
|
|
238ac22b85 | ||
|
|
773f0be84a | ||
|
|
e45c988637 | ||
|
|
8305b49046 | ||
|
|
92b2601ba2 | ||
|
|
af8ab752ad | ||
|
|
e45105ccb3 | ||
|
|
9114b68659 | ||
|
|
cd2e091580 | ||
|
|
38d8f83587 | ||
|
|
5f3b6501d7 | ||
|
|
f35efbdd5b | ||
|
|
961954364a | ||
|
|
259725882a | ||
|
|
fb2288c4bc | ||
|
|
0220f4519d | ||
|
|
3432243acb | ||
|
|
27a32463d9 | ||
|
|
b47f7e8cf1 | ||
|
|
459bc69032 | ||
|
|
378b4bb8c1 | ||
|
|
6a5712070f | ||
|
|
48e125881b | ||
|
|
e6cec93f2c | ||
|
|
b5726393f3 | ||
|
|
82010e4fa3 | ||
|
|
eb99fe58dd | ||
|
|
a18e7ab2da | ||
|
|
e1f78f519c | ||
|
|
50a57727fe | ||
|
|
4bbb7f82b4 | ||
|
|
1da4cf8907 | ||
|
|
978a58391b | ||
|
|
b2be44e372 | ||
|
|
650fc6aa27 | ||
|
|
47bda7d789 | ||
|
|
64f41f004d | ||
|
|
68f69074cb | ||
|
|
4d3fb52956 | ||
|
|
18a23d6844 | ||
|
|
bdf653bc70 | ||
|
|
0aaa351797 | ||
|
|
1f39761f8c | ||
|
|
c7914fa8e4 | ||
|
|
a48cc2a7f3 | ||
|
|
3942409c9a | ||
|
|
6b0719db45 | ||
|
|
9728116836 | ||
|
|
6cffabe259 | ||
|
|
28b20cc8ba | ||
|
|
62b012941e | ||
|
|
1b94ac383c | ||
|
|
9a96ef2623 | ||
|
|
6480750757 | ||
|
|
df313560c2 | ||
|
|
be0e832589 | ||
|
|
9f87f551fd | ||
|
|
5804c57236 | ||
|
|
7efd81191a | ||
|
|
84bea20891 | ||
|
|
c2b9b6e162 | ||
|
|
3720b9481f | ||
|
|
b565d40ec7 | ||
|
|
0e1f2e721f | ||
|
|
951a22b90e | ||
|
|
1dd88a690b | ||
|
|
55ba78c66a | ||
|
|
b0364041e2 | ||
|
|
6b9c90f99b | ||
|
|
4050bc1da8 | ||
|
|
4bb9051136 | ||
|
|
ceca4fbe53 | ||
|
|
9b7c0288d4 | ||
|
|
392a90c02c | ||
|
|
7a58f6d967 | ||
|
|
35d1e51f9b | ||
|
|
9729a4c724 | ||
|
|
f13713a055 | ||
|
|
31655f7832 | ||
|
|
fb4bb81595 | ||
|
|
31cb6916c6 | ||
|
|
61d37615af | ||
|
|
eaa7701696 | ||
|
|
d6cff8e0b0 | ||
|
|
8ba761b33c | ||
|
|
05e7e452df | ||
|
|
3f0fd4f771 | ||
|
|
a587c1d1da | ||
|
|
c3355f7fe4 | ||
|
|
c182d874af | ||
|
|
ab9ebfb667 | ||
|
|
cb953eda61 | ||
|
|
93c291dba1 | ||
|
|
603a1ef046 | ||
|
|
5a504b00fb | ||
|
|
dfa59dc93d | ||
|
|
534bcdd52c | ||
|
|
ea032bf551 | ||
|
|
b44eee8d81 | ||
|
|
8f57ada128 | ||
|
|
3963990831 | ||
|
|
dc1ffafdf3 | ||
|
|
4a0b4de322 | ||
|
|
0ede65e9ca | ||
|
|
0ebf30b8b6 | ||
|
|
4222b192c4 | ||
|
|
4a301aaec3 | ||
|
|
97a3a97a15 | ||
|
|
c526d73e23 | ||
|
|
58baf137aa | ||
|
|
066ab1500f | ||
|
|
224a468712 | ||
|
|
dd282383d7 | ||
|
|
9a99a95b15 | ||
|
|
e814494e37 | ||
|
|
90a0155be1 | ||
|
|
555d40408d | ||
|
|
0c3fbeb0b7 | ||
|
|
2f27decaa1 | ||
|
|
867115659f | ||
|
|
6282ab58db | ||
|
|
95c25c1bcd | ||
|
|
74a62de7d0 | ||
|
|
7fc021648d | ||
|
|
95914ad312 | ||
|
|
5ed00e037a | ||
|
|
6f3074536a | ||
|
|
21f5cb36bb | ||
|
|
7b7592822f | ||
|
|
9c7b7b0d75 | ||
|
|
d88b23c42d | ||
|
|
1602c0aca2 | ||
|
|
384978a511 | ||
|
|
afd7a0494f | ||
|
|
ac1f8a69e1 | ||
|
|
05cfa99ea0 | ||
|
|
9b43ccbbc0 | ||
|
|
6d8b156455 | ||
|
|
1b9943a4c8 | ||
|
|
8232a4c9c8 | ||
|
|
9d4d64c95a | ||
|
|
2d0acc7663 | ||
|
|
4231ed74ba | ||
|
|
912e1cf89f | ||
|
|
26d4fb8005 | ||
|
|
4a6c0b39a8 | ||
|
|
9d01bba170 | ||
|
|
85c0ddba10 | ||
|
|
2664059812 | ||
|
|
b7e4d9c806 | ||
|
|
95b91f0ce2 | ||
|
|
f0407e4327 | ||
|
|
a7555f56e7 | ||
|
|
b093ed33b2 | ||
|
|
ec1a45ba18 | ||
|
|
def5dc3b0f | ||
|
|
24ec89c220 | ||
|
|
303e70bb58 | ||
|
|
ec3e92fc19 | ||
|
|
5ae776309d | ||
|
|
76dd606a48 | ||
|
|
8998798fa4 | ||
|
|
60ee82ca47 | ||
|
|
e1284002a9 | ||
|
|
8252512784 | ||
|
|
1390d7eb1d | ||
|
|
8da1bb13ff | ||
|
|
340e377b37 | ||
|
|
171589fb3d | ||
|
|
bcd07cce0d | ||
|
|
68880114b4 | ||
|
|
eb2360ae24 | ||
|
|
62712a352b | ||
|
|
745e6c1715 | ||
|
|
e20a75eb0c | ||
|
|
a24c41ff25 | ||
|
|
69f0339bd5 | ||
|
|
5e7c9a7278 | ||
|
|
726c323fe1 | ||
|
|
e96cbe2710 | ||
|
|
323e54b4bd | ||
|
|
7ab132bbf6 | ||
|
|
6b09210a80 | ||
|
|
be80d62c01 | ||
|
|
30587d625a | ||
|
|
af43cd407e | ||
|
|
647388e475 | ||
|
|
329e06ac30 | ||
|
|
5d96138720 | ||
|
|
66b275605c | ||
|
|
9b7478c0c7 | ||
|
|
668271bb31 | ||
|
|
1aa93e7737 | ||
|
|
a0864f5f67 | ||
|
|
6e9f71f942 | ||
|
|
65211372df | ||
|
|
2ca8d8817a | ||
|
|
ec266ea657 | ||
|
|
d117aa5139 | ||
|
|
4534b7d4dc | ||
|
|
707fe01d77 | ||
|
|
0e09ba0dd5 | ||
|
|
989560f23c | ||
|
|
844a9f934f | ||
|
|
b5348c593a | ||
|
|
7f809ba541 | ||
|
|
f9058fcddc | ||
|
|
05c9957fd2 | ||
|
|
675739d24f | ||
|
|
10be0867ad | ||
|
|
d2a4b85bdd | ||
|
|
782061ac5e | ||
|
|
8d98e9e6f9 | ||
|
|
4aa75e9376 | ||
|
|
c6d6eecb43 | ||
|
|
1d6d7b8aa8 | ||
|
|
68ed8e51bd | ||
|
|
d4dd962193 | ||
|
|
7dfb70eb8e | ||
|
|
53675eeba7 | ||
|
|
6399973bfa | ||
|
|
f1384f5dc1 | ||
|
|
027cad9e52 |
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*.sh eol=lf
|
||||
.dockerignore eol=lf
|
||||
dockerfile eol=lf
|
||||
@@ -5,6 +5,7 @@ LABEL com.bitwarden.product="bitwarden"
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
gosu \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV ASPNETCORE_URLS http://+:5000
|
||||
@@ -14,4 +15,6 @@ COPY ./build .
|
||||
COPY entrypoint.sh /
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
HEALTHCHECK CMD curl -f http://localhost:5000 || exit 1
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
||||
83
appveyor.yml
Normal file
83
appveyor.yml
Normal file
@@ -0,0 +1,83 @@
|
||||
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
|
||||
@@ -8,3 +8,4 @@ files:
|
||||
pt-BR: pt_BR
|
||||
zh-CN: zh_CN
|
||||
zh-TW: zh_TW
|
||||
en-GB: en_GB
|
||||
|
||||
14
gulpfile.js
14
gulpfile.js
@@ -5,6 +5,7 @@ const package = require('./package.json');
|
||||
const fs = require('fs');
|
||||
|
||||
const paths = {
|
||||
node_modules: './node_modules/',
|
||||
src: './src/',
|
||||
build: './build/',
|
||||
cssDir: './src/css/',
|
||||
@@ -24,12 +25,13 @@ function webfonts() {
|
||||
.pipe(gulp.dest(paths.cssDir));
|
||||
};
|
||||
|
||||
function version() {
|
||||
function version(cb) {
|
||||
fs.writeFileSync(paths.build + 'version.json', '{"version":"' + package.version + '"}');
|
||||
cb();
|
||||
}
|
||||
|
||||
gulp.task('clean', clean);
|
||||
gulp.task('webfonts', ['clean'], webfonts);
|
||||
gulp.task('prebuild', ['webfonts']);
|
||||
gulp.task('version', version);
|
||||
gulp.task('postdist', ['version']);
|
||||
exports.clean = clean;
|
||||
exports.webfonts = gulp.series(clean, webfonts);
|
||||
exports.prebuild = gulp.series(clean, webfonts);
|
||||
exports.version = version;
|
||||
exports.postdist = version;
|
||||
2
jslib
2
jslib
Submodule jslib updated: 85587e0672...31a257407b
9464
package-lock.json
generated
9464
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
88
package.json
88
package.json
@@ -1,35 +1,40 @@
|
||||
{
|
||||
"name": "bitwarden-web",
|
||||
"version": "2.3.0",
|
||||
"version": "2.13.2",
|
||||
"scripts": {
|
||||
"sub:init": "git submodule update --init --recursive",
|
||||
"sub:update": "git submodule update --remote",
|
||||
"sub:pull": "git submodule foreach git pull",
|
||||
"sub:pull": "git submodule foreach git pull origin master",
|
||||
"postinstall": "npm run sub:init",
|
||||
"build": "gulp prebuild && webpack --config webpack.config.js",
|
||||
"build:watch": "gulp prebuild && webpack-serve --config webpack.config.js",
|
||||
"build:prod": "gulp prebuild && cross-env NODE_ENV=production webpack --config webpack.config.js",
|
||||
"build:prod:watch": "gulp prebuild && cross-env NODE_ENV=production webpack-serve --config webpack.config.js",
|
||||
"build:selfhost": "gulp prebuild && cross-env SELF_HOST=true webpack-serve --config webpack.config.js",
|
||||
"build:selfhost:watch": "gulp prebuild && cross-env SELF_HOST=true webpack-serve --config webpack.config.js",
|
||||
"build:selfhost:prod": "gulp prebuild && cross-env SELF_HOST=true NODE_ENV=production webpack --config webpack.config.js",
|
||||
"build:selfhost:prod:watch": "gulp prebuild && cross-env SELF_HOST=true NODE_ENV=production webpack-serve --config webpack.config.js",
|
||||
"symlink:win": "rm -rf ./jslib && cmd /c mklink /J .\\jslib ..\\jslib",
|
||||
"symlink:mac": "npm run symlink:lin",
|
||||
"symlink:lin": "rm -rf ./jslib && ln -s ../jslib ./jslib",
|
||||
"build": "gulp prebuild && webpack",
|
||||
"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: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",
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/compiler-cli": "^6.1.7",
|
||||
"@ngtools/webpack": "^6.2.1",
|
||||
"@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",
|
||||
"@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",
|
||||
@@ -39,53 +44,56 @@
|
||||
"extract-text-webpack-plugin": "next",
|
||||
"file-loader": "^2.0.0",
|
||||
"gh-pages": "^1.2.0",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-google-webfonts": "^2.0.0",
|
||||
"html-loader": "^0.5.5",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"node-sass": "^4.9.3",
|
||||
"node-sass": "^4.13.1",
|
||||
"sass-loader": "^7.1.0",
|
||||
"style-loader": "^0.23.0",
|
||||
"ts-loader": "^5.1.0",
|
||||
"tslint": "^5.11.0",
|
||||
"tslint-loader": "^3.6.0",
|
||||
"typescript": "^2.7.2",
|
||||
"webpack": "^4.18.0",
|
||||
"webpack-cli": "^3.1.0",
|
||||
"webpack-serve": "^2.0.2"
|
||||
"terser-webpack-plugin": "^1.2.3",
|
||||
"ts-loader": "^5.3.3",
|
||||
"tslint": "^5.12.1",
|
||||
"tslint-loader": "^3.5.4",
|
||||
"typescript": "3.2.4",
|
||||
"webpack": "^4.29.0",
|
||||
"webpack-cli": "^3.2.1",
|
||||
"webpack-dev-server": "^3.1.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "6.1.7",
|
||||
"@angular/common": "6.1.7",
|
||||
"@angular/compiler": "6.1.7",
|
||||
"@angular/core": "6.1.7",
|
||||
"@angular/forms": "6.1.7",
|
||||
"@angular/http": "6.1.7",
|
||||
"@angular/platform-browser": "6.1.7",
|
||||
"@angular/platform-browser-dynamic": "6.1.7",
|
||||
"@angular/router": "6.1.7",
|
||||
"@angular/upgrade": "6.1.7",
|
||||
"@aspnet/signalr": "1.0.3",
|
||||
"@aspnet/signalr-protocol-msgpack": "1.0.3",
|
||||
"@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",
|
||||
"bootstrap": "4.1.3",
|
||||
"big-integer": "1.6.36",
|
||||
"bootstrap": "4.3.1",
|
||||
"braintree-web-drop-in": "1.13.0",
|
||||
"core-js": "2.5.7",
|
||||
"core-js": "2.6.2",
|
||||
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git",
|
||||
"font-awesome": "4.7.0",
|
||||
"jquery": "3.3.1",
|
||||
"jquery": "3.4.1",
|
||||
"lunr": "2.3.3",
|
||||
"ngx-infinite-scroll": "6.0.1",
|
||||
"ngx-infinite-scroll": "7.0.1",
|
||||
"node-forge": "0.7.6",
|
||||
"papaparse": "4.6.0",
|
||||
"popper.js": "1.14.4",
|
||||
"qrious": "4.0.2",
|
||||
"rxjs": "6.3.2",
|
||||
"sweetalert": "2.1.0",
|
||||
"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.26"
|
||||
"zone.js": "0.8.28",
|
||||
"zxcvbn": "4.4.2"
|
||||
}
|
||||
}
|
||||
|
||||
0
src/.nojekyll
Normal file
0
src/.nojekyll
Normal file
@@ -2,7 +2,8 @@
|
||||
<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}}"></i>
|
||||
<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>
|
||||
@@ -22,7 +23,8 @@
|
||||
<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">
|
||||
<a routerLink="/register" [queryParams]="{email: email}"
|
||||
class="btn btn-primary btn-block ml-2 mt-0">
|
||||
{{'createAccount' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -44,6 +44,7 @@ export class AcceptOrganizationComponent implements OnInit {
|
||||
fired = true;
|
||||
await this.stateService.remove('orgInvitation');
|
||||
let error = qParams.organizationId == null || qParams.organizationUserId == null || qParams.token == null;
|
||||
let errorMessage: string = null;
|
||||
if (!error) {
|
||||
this.authed = await this.userService.isAuthenticated();
|
||||
if (this.authed) {
|
||||
@@ -61,8 +62,9 @@ export class AcceptOrganizationComponent implements OnInit {
|
||||
};
|
||||
this.toasterService.popAsync(toast);
|
||||
this.router.navigate(['/vault']);
|
||||
} catch {
|
||||
} catch (e) {
|
||||
error = true;
|
||||
errorMessage = e.message;
|
||||
}
|
||||
} else {
|
||||
await this.stateService.save('orgInvitation', qParams);
|
||||
@@ -76,7 +78,14 @@ export class AcceptOrganizationComponent implements OnInit {
|
||||
}
|
||||
|
||||
if (error) {
|
||||
this.toasterService.popAsync('error', null, this.i18nService.t('inviteAcceptFailed'));
|
||||
const toast: Toast = {
|
||||
type: 'error',
|
||||
title: null,
|
||||
body: errorMessage != null ? this.i18nService.t('inviteAcceptFailedShort', errorMessage) :
|
||||
this.i18nService.t('inviteAcceptFailed'),
|
||||
timeout: 10000,
|
||||
};
|
||||
this.toasterService.popAsync(toast);
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="email">{{'emailAddress' | i18n}}</label>
|
||||
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required appAutofocus inputmode="email"
|
||||
appInputVerbatim="false">
|
||||
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required
|
||||
appAutofocus inputmode="email" appInputVerbatim="false">
|
||||
<small class="form-text text-muted">{{'enterEmailToGetHint' | i18n}}</small>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
|
||||
<span [hidden]="form.loading">{{'submit' | i18n}}</span>
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<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}}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
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';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
|
||||
import { HintComponent as BaseHintComponent } from 'jslib/angular/components/hint.component';
|
||||
|
||||
@@ -14,9 +12,8 @@ import { HintComponent as BaseHintComponent } from 'jslib/angular/components/hin
|
||||
templateUrl: 'hint.component.html',
|
||||
})
|
||||
export class HintComponent extends BaseHintComponent {
|
||||
constructor(router: Router, analytics: Angulartics2,
|
||||
toasterService: ToasterService, i18nService: I18nService,
|
||||
apiService: ApiService) {
|
||||
super(router, analytics, toasterService, i18nService, apiService);
|
||||
constructor(router: Router, i18nService: I18nService,
|
||||
apiService: ApiService, platformUtilsService: PlatformUtilsService) {
|
||||
super(router, i18nService, apiService, platformUtilsService);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="text-center mb-4">
|
||||
<i class="fa fa-lock fa-4x text-muted"></i>
|
||||
<i class="fa fa-lock fa-4x text-muted" aria-hidden="true"></i>
|
||||
</p>
|
||||
<p class="lead text-center mx-4 mb-4">{{'yourVaultIsLocked' | i18n}}</p>
|
||||
<div class="card d-block">
|
||||
@@ -10,17 +10,23 @@
|
||||
<div class="form-group">
|
||||
<label for="masterPassword">{{'masterPass' | i18n}}</label>
|
||||
<div class="d-flex">
|
||||
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}" name="MasterPassword" class="text-monospace form-control"
|
||||
[(ngModel)]="masterPassword" required appAutofocus appInputVerbatim>
|
||||
<button type="button" class="ml-1 btn btn-link" title="{{'toggleVisibility' | i18n}}" (click)="togglePassword()">
|
||||
<i class="fa fa-lg" [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
|
||||
name="MasterPassword" class="text-monospace form-control" [(ngModel)]="masterPassword"
|
||||
required appAutofocus appInputVerbatim>
|
||||
<button type="button" class="ml-1 btn btn-link" appA11yTitle="{{'toggleVisibility' | i18n}}"
|
||||
(click)="togglePassword()">
|
||||
<i class="fa fa-lg" aria-hidden="true"
|
||||
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted form-text">
|
||||
{{'loggedInAsEmailOn' | i18n : email : webVaultHostname}}
|
||||
</small>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex">
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
<i class="fa fa-unlock-alt"></i>
|
||||
<i class="fa fa-unlock-alt" aria-hidden="true"></i>
|
||||
{{'unlock' | i18n}}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-block ml-2 mt-0" (click)="logOut()">
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
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 { RouterService } from '../services/router.service';
|
||||
@@ -21,17 +19,19 @@ import { LockComponent as BaseLockComponent } from 'jslib/angular/components/loc
|
||||
selector: 'app-lock',
|
||||
templateUrl: 'lock.component.html',
|
||||
})
|
||||
export class LockComponent extends BaseLockComponent implements OnInit {
|
||||
constructor(router: Router, analytics: Angulartics2,
|
||||
toasterService: ToasterService, i18nService: I18nService,
|
||||
export class LockComponent extends BaseLockComponent {
|
||||
constructor(router: Router, i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService, messagingService: MessagingService,
|
||||
userService: UserService, cryptoService: CryptoService,
|
||||
private routerService: RouterService) {
|
||||
super(router, analytics, toasterService, i18nService, platformUtilsService,
|
||||
messagingService, userService, cryptoService);
|
||||
storageService: StorageService, lockService: LockService,
|
||||
environmentService: EnvironmentService, private routerService: RouterService,
|
||||
stateService: StateService) {
|
||||
super(router, i18nService, platformUtilsService, messagingService, userService, cryptoService,
|
||||
storageService, lockService, environmentService, stateService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await super.ngOnInit();
|
||||
const authed = await this.userService.isAuthenticated();
|
||||
if (!authed) {
|
||||
this.router.navigate(['/']);
|
||||
@@ -39,9 +39,12 @@ export class LockComponent extends BaseLockComponent implements OnInit {
|
||||
this.router.navigate(['vault']);
|
||||
}
|
||||
|
||||
const previousUrl = this.routerService.getPreviousUrl();
|
||||
if (previousUrl !== '/' && previousUrl.indexOf('lock') === -1) {
|
||||
this.successRoute = previousUrl;
|
||||
}
|
||||
this.onSuccessfulSubmit = () => {
|
||||
const previousUrl = this.routerService.getPreviousUrl();
|
||||
if (previousUrl !== '/' && previousUrl.indexOf('lock') === -1) {
|
||||
this.successRoute = previousUrl;
|
||||
}
|
||||
this.router.navigate([this.successRoute]);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,19 @@
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="email">{{'emailAddress' | i18n}}</label>
|
||||
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required inputmode="email" appInputVerbatim="false">
|
||||
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required
|
||||
inputmode="email" appInputVerbatim="false">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="masterPassword">{{'masterPass' | i18n}}</label>
|
||||
<div class="d-flex">
|
||||
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}" name="MasterPassword" class="text-monospace form-control"
|
||||
[(ngModel)]="masterPassword" required appInputVerbatim>
|
||||
<button type="button" class="ml-1 btn btn-link" title="{{'toggleVisibility' | i18n}}" (click)="togglePassword()">
|
||||
<i class="fa fa-lg" [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
|
||||
name="MasterPassword" class="text-monospace form-control" [(ngModel)]="masterPassword"
|
||||
required appInputVerbatim>
|
||||
<button type="button" class="ml-1 btn btn-link" appA11yTitle="{{'toggleVisibility' | i18n}}"
|
||||
(click)="togglePassword()">
|
||||
<i class="fa fa-lg" aria-hidden="true"
|
||||
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-text">
|
||||
@@ -23,19 +27,21 @@
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="rememberEmail" name="RememberEmail" [(ngModel)]="rememberEmail">
|
||||
<input type="checkbox" class="form-check-input" id="rememberEmail" name="RememberEmail"
|
||||
[(ngModel)]="rememberEmail">
|
||||
<label class="form-check-label" for="rememberEmail">{{'rememberEmail' | i18n}}</label>
|
||||
</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"></i> {{'logIn' | i18n}}
|
||||
<i class="fa fa-sign-in" aria-hidden="true"></i> {{'logIn' | i18n}}
|
||||
</span>
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
</button>
|
||||
<a routerLink="/register" [queryParams]="{email: email}" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
||||
<i class="fa fa-pencil-square-o"></i> {{'createAccount' | i18n}}
|
||||
<a routerLink="/register" [queryParams]="{email: email}"
|
||||
class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
||||
<i class="fa fa-pencil-square-o" aria-hidden="true"></i> {{'createAccount' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,11 +4,9 @@ import {
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { AuthService } from 'jslib/abstractions/auth.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
|
||||
@@ -20,15 +18,15 @@ import { LoginComponent as BaseLoginComponent } from 'jslib/angular/components/l
|
||||
})
|
||||
export class LoginComponent extends BaseLoginComponent {
|
||||
constructor(authService: AuthService, router: Router,
|
||||
analytics: Angulartics2, toasterService: ToasterService,
|
||||
i18nService: I18nService, private route: ActivatedRoute,
|
||||
storageService: StorageService, private stateService: StateService) {
|
||||
super(authService, router, analytics, toasterService, i18nService, storageService);
|
||||
storageService: StorageService, stateService: StateService,
|
||||
platformUtilsService: PlatformUtilsService) {
|
||||
super(authService, router, platformUtilsService, i18nService, storageService, stateService);
|
||||
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
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;
|
||||
}
|
||||
@@ -39,6 +37,9 @@ export class LoginComponent extends BaseLoginComponent {
|
||||
{ route: '/settings/create-organization', qParams: { plan: qParams.org } });
|
||||
}
|
||||
await super.ngOnInit();
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
<p>{{'deleteRecoverDesc' | i18n}}</p>
|
||||
<div class="form-group">
|
||||
<label for="email">{{'emailAddress' | i18n}}</label>
|
||||
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required appAutofocus inputmode="email"
|
||||
appInputVerbatim="false">
|
||||
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required
|
||||
appAutofocus inputmode="email" appInputVerbatim="false">
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex">
|
||||
<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}}"></i>
|
||||
<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}}
|
||||
|
||||
@@ -5,28 +5,29 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p>{{'recoverAccountTwoStepDesc' | i18n}}
|
||||
<a href="https://help.bitwarden.com/article/lost-two-step-device/" target="_blank" rel="noopener">{{'learnMore' | i18n}}</a>
|
||||
<a href="https://help.bitwarden.com/article/lost-two-step-device/" target="_blank"
|
||||
rel="noopener">{{'learnMore' | i18n}}</a>
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label for="email">{{'emailAddress' | i18n}}</label>
|
||||
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required appAutofocus inputmode="email"
|
||||
appInputVerbatim="false">
|
||||
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required
|
||||
appAutofocus inputmode="email" appInputVerbatim="false">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="masterPassword">{{'masterPass' | i18n}}</label>
|
||||
<input id="masterPassword" type="password" name="MasterPassword" class="form-control" [(ngModel)]="masterPassword" required
|
||||
appInputVerbatim>
|
||||
<input id="masterPassword" type="password" name="MasterPassword" class="form-control"
|
||||
[(ngModel)]="masterPassword" required appInputVerbatim>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="recoveryCode">{{'recoveryCodeTitle' | i18n}}</label>
|
||||
<input id="recoveryCode" class="text-monospace form-control" type="text" name="RecoveryCode" [(ngModel)]="recoveryCode" required
|
||||
appInputVerbatim>
|
||||
<input id="recoveryCode" class="text-monospace form-control" type="text" name="RecoveryCode"
|
||||
[(ngModel)]="recoveryCode" required appInputVerbatim>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex">
|
||||
<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}}"></i>
|
||||
<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}}
|
||||
|
||||
@@ -4,38 +4,73 @@
|
||||
<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">
|
||||
<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">
|
||||
<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 !== ''">
|
||||
<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">
|
||||
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}" name="MasterPassword" class="text-monospace form-control"
|
||||
[(ngModel)]="masterPassword" required appInputVerbatim>
|
||||
<button type="button" class="ml-1 btn btn-link" title="{{'toggleVisibility' | i18n}}" (click)="togglePassword(false)">
|
||||
<i class="fa fa-lg" [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
</button>
|
||||
<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"
|
||||
<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" title="{{'toggleVisibility' | i18n}}" (click)="togglePassword(true)">
|
||||
<i class="fa fa-lg" [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
<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>
|
||||
@@ -48,7 +83,7 @@
|
||||
<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}}"></i>
|
||||
<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}}
|
||||
@@ -56,8 +91,10 @@
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -4,18 +4,22 @@ 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';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { PolicyService } from 'jslib/abstractions/policy.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
|
||||
import { RegisterComponent as BaseRegisterComponent } from 'jslib/angular/components/register.component';
|
||||
|
||||
import { MasterPasswordPolicyOptions } from 'jslib/models/domain/masterPasswordPolicyOptions';
|
||||
import { Policy } from 'jslib/models/domain/policy';
|
||||
|
||||
import { PolicyData } from 'jslib/models/data/policyData';
|
||||
|
||||
@Component({
|
||||
selector: 'app-register',
|
||||
templateUrl: 'register.component.html',
|
||||
@@ -23,18 +27,42 @@ import { RegisterComponent as BaseRegisterComponent } from 'jslib/angular/compon
|
||||
export class RegisterComponent extends BaseRegisterComponent {
|
||||
showCreateOrgMessage = false;
|
||||
showTerms = true;
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
|
||||
private policies: Policy[];
|
||||
|
||||
constructor(authService: AuthService, router: Router,
|
||||
analytics: Angulartics2, toasterService: ToasterService,
|
||||
i18nService: I18nService, cryptoService: CryptoService,
|
||||
apiService: ApiService, private route: ActivatedRoute,
|
||||
stateService: StateService, platformUtilsService: PlatformUtilsService) {
|
||||
super(authService, router, analytics, toasterService, i18nService, cryptoService, apiService, stateService);
|
||||
stateService: StateService, platformUtilsService: PlatformUtilsService,
|
||||
passwordGenerationService: PasswordGenerationService, private policyService: PolicyService) {
|
||||
super(authService, router, i18nService, cryptoService, apiService, stateService, platformUtilsService,
|
||||
passwordGenerationService);
|
||||
this.showTerms = !platformUtilsService.isSelfHost();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.queryParams.subscribe((qParams) => {
|
||||
getPasswordScoreAlertDisplay() {
|
||||
if (this.enforcedPolicyOptions == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let str: string;
|
||||
switch (this.enforcedPolicyOptions.minComplexity) {
|
||||
case 4:
|
||||
str = this.i18nService.t('strong');
|
||||
break;
|
||||
case 3:
|
||||
str = this.i18nService.t('good');
|
||||
break;
|
||||
default:
|
||||
str = this.i18nService.t('weak');
|
||||
break;
|
||||
}
|
||||
return str + ' (' + this.enforcedPolicyOptions.minComplexity + ')';
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
const queryParamsSub = this.route.queryParams.subscribe((qParams) => {
|
||||
if (qParams.email != null && qParams.email.indexOf('@') > -1) {
|
||||
this.email = qParams.email;
|
||||
}
|
||||
@@ -45,6 +73,36 @@ export class RegisterComponent extends BaseRegisterComponent {
|
||||
this.stateService.save('loginRedirect',
|
||||
{ route: '/settings/create-organization', qParams: { plan: qParams.org } });
|
||||
}
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
const invite = await this.stateService.get<any>('orgInvitation');
|
||||
if (invite != null) {
|
||||
try {
|
||||
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));
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
if (this.policies != null) {
|
||||
this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(this.policies);
|
||||
}
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.enforcedPolicyOptions != null &&
|
||||
!this.policyService.evaluateMasterPassword(this.masterPasswordScore, this.masterPassword,
|
||||
this.enforcedPolicyOptions)) {
|
||||
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('masterPasswordPolicyRequirementsNotMet'));
|
||||
return;
|
||||
}
|
||||
|
||||
await super.submit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<div class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="twoStepOptionsTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">{{'twoStepOptions' | i18n}}</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
|
||||
<h2 class="modal-title" id="twoStepOptionsTitle">{{'twoStepOptions' | i18n}}</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||
<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">
|
||||
<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}}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { AuthService } from 'jslib/abstractions/auth.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
@@ -18,8 +15,7 @@ import {
|
||||
})
|
||||
export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent {
|
||||
constructor(authService: AuthService, router: Router,
|
||||
analytics: Angulartics2, toasterService: ToasterService,
|
||||
i18nService: I18nService, platformUtilsService: PlatformUtilsService) {
|
||||
super(authService, router, analytics, toasterService, i18nService, platformUtilsService, window);
|
||||
super(authService, router, i18nService, platformUtilsService, window);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate autocomplete="off">
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5" [ngClass]="{'col-9': selectedProviderType === providerType.Duo || selectedProviderType === providerType.OrganizationDuo}">
|
||||
<div class="col-5"
|
||||
[ngClass]="{'col-9': selectedProviderType === providerType.Duo || selectedProviderType === providerType.OrganizationDuo}">
|
||||
<p class="lead text-center mb-4">{{title}}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body">
|
||||
<ng-container *ngIf="selectedProviderType === providerType.Email || selectedProviderType === providerType.Authenticator">
|
||||
<p *ngIf="selectedProviderType === providerType.Authenticator">{{'enterVerificationCodeApp' | i18n}}</p>
|
||||
<ng-container
|
||||
*ngIf="selectedProviderType === providerType.Email || selectedProviderType === providerType.Authenticator">
|
||||
<p *ngIf="selectedProviderType === providerType.Authenticator">
|
||||
{{'enterVerificationCodeApp' | i18n}}</p>
|
||||
<p *ngIf="selectedProviderType === providerType.Email">
|
||||
{{'enterVerificationCodeEmail' | i18n : twoFactorEmail}}
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label for="code" class="sr-only">{{'verificationCode' | i18n}}</label>
|
||||
<input id="code" type="text" name="Code" class="form-control" [(ngModel)]="token" required appAutofocus inputmode="tel" appInputVerbatim>
|
||||
<input id="code" type="text" name="Code" class="form-control" [(ngModel)]="token" required
|
||||
appAutofocus inputmode="tel" appInputVerbatim>
|
||||
<small class="form-text" *ngIf="selectedProviderType === providerType.Email">
|
||||
<a href="#" appStopClick (click)="sendEmail(true)" [appApiAction]="emailPromise" *ngIf="selectedProviderType === providerType.Email">
|
||||
<a href="#" appStopClick (click)="sendEmail(true)" [appApiAction]="emailPromise"
|
||||
*ngIf="selectedProviderType === providerType.Email">
|
||||
{{'sendVerificationCodeEmailAgain' | i18n}}
|
||||
</a>
|
||||
</small>
|
||||
@@ -24,13 +29,15 @@
|
||||
<img src="../../images/yubikey.jpg" class="rounded img-fluid mb-3" alt="">
|
||||
<div class="form-group">
|
||||
<label for="code" class="sr-only">{{'verificationCode' | i18n}}</label>
|
||||
<input id="code" type="password" name="Code" class="form-control" [(ngModel)]="token" required appAutofocus appInputVerbatim
|
||||
autocomplete="new-password">
|
||||
<input id="code" type="password" name="Code" class="form-control" [(ngModel)]="token"
|
||||
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}}"></i>
|
||||
<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>
|
||||
@@ -43,9 +50,11 @@
|
||||
<iframe id="duo_iframe"></iframe>
|
||||
</div>
|
||||
</ng-container>
|
||||
<i class="fa fa-spinner text-muted fa-spin pull-right" title="{{'loading' | i18n}}" *ngIf="form.loading && selectedProviderType === providerType.U2f"></i>
|
||||
<i class="fa fa-spinner text-muted fa-spin pull-right" title="{{'loading' | i18n}}"
|
||||
*ngIf="form.loading && selectedProviderType === providerType.U2f" aria-hidden="true"></i>
|
||||
<div class="form-check" *ngIf="selectedProviderType != null">
|
||||
<input id="remember" type="checkbox" name="Remember" class="form-check-input" [(ngModel)]="remember">
|
||||
<input id="remember" type="checkbox" name="Remember" class="form-check-input"
|
||||
[(ngModel)]="remember">
|
||||
<label for="remember" class="form-check-label">{{'rememberMe' | i18n}}</label>
|
||||
</div>
|
||||
<ng-container *ngIf="selectedProviderType == null">
|
||||
@@ -54,12 +63,13 @@
|
||||
</ng-container>
|
||||
<hr>
|
||||
<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 &&
|
||||
<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">
|
||||
<span>
|
||||
<i class="fa fa-sign-in"></i> {{'continue' | i18n}}
|
||||
<i class="fa fa-sign-in" aria-hidden="true"></i> {{'continue' | i18n}}
|
||||
</span>
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<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}}
|
||||
|
||||
@@ -7,9 +7,6 @@ import {
|
||||
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { TwoFactorOptionsComponent } from './two-factor-options.component';
|
||||
|
||||
import { ModalComponent } from '../modal.component';
|
||||
@@ -22,6 +19,7 @@ import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
|
||||
import { TwoFactorComponent as BaseTwoFactorComponent } from 'jslib/angular/components/two-factor.component';
|
||||
|
||||
@@ -33,12 +31,12 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
||||
@ViewChild('twoFactorOptions', { read: ViewContainerRef }) twoFactorOptionsModal: ViewContainerRef;
|
||||
|
||||
constructor(authService: AuthService, router: Router,
|
||||
analytics: Angulartics2, toasterService: ToasterService,
|
||||
i18nService: I18nService, apiService: ApiService,
|
||||
platformUtilsService: PlatformUtilsService, private stateService: StateService,
|
||||
environmentService: EnvironmentService, private componentFactoryResolver: ComponentFactoryResolver) {
|
||||
super(authService, router, analytics, toasterService, i18nService, apiService,
|
||||
platformUtilsService, window, environmentService);
|
||||
platformUtilsService: PlatformUtilsService, stateService: StateService,
|
||||
environmentService: EnvironmentService, private componentFactoryResolver: ComponentFactoryResolver,
|
||||
storageService: StorageService) {
|
||||
super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService,
|
||||
stateService, storageService);
|
||||
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
<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}}"></i>
|
||||
<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>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="d-flex">
|
||||
<button type="submit" class="btn btn-danger btn-block btn-submit" [disabled]="form.loading">
|
||||
<span>{{'deleteAccount' | i18n}}</span>
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<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}}
|
||||
|
||||
@@ -24,17 +24,34 @@ import { EventsComponent as OrgEventsComponent } from './organizations/manage/ev
|
||||
import { GroupsComponent as OrgGroupsComponent } from './organizations/manage/groups.component';
|
||||
import { ManageComponent as OrgManageComponent } from './organizations/manage/manage.component';
|
||||
import { PeopleComponent as OrgPeopleComponent } from './organizations/manage/people.component';
|
||||
import { PoliciesComponent as OrgPoliciesComponent } from './organizations/manage/policies.component';
|
||||
|
||||
import { AccountComponent as OrgAccountComponent } from './organizations/settings/account.component';
|
||||
import { OrganizationBillingComponent } from './organizations/settings/organization-billing.component';
|
||||
import { OrganizationSubscriptionComponent } from './organizations/settings/organization-subscription.component';
|
||||
import { SettingsComponent as OrgSettingsComponent } from './organizations/settings/settings.component';
|
||||
import {
|
||||
TwoFactorSetupComponent as OrgTwoFactorSetupComponent,
|
||||
} from './organizations/settings/two-factor-setup.component';
|
||||
|
||||
import { ExportComponent as OrgExportComponent } from './organizations/tools/export.component';
|
||||
import {
|
||||
ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent,
|
||||
} from './organizations/tools/exposed-passwords-report.component';
|
||||
import { ImportComponent as OrgImportComponent } from './organizations/tools/import.component';
|
||||
import {
|
||||
InactiveTwoFactorReportComponent as OrgInactiveTwoFactorReportComponent,
|
||||
} from './organizations/tools/inactive-two-factor-report.component';
|
||||
import {
|
||||
ReusedPasswordsReportComponent as OrgReusedPasswordsReportComponent,
|
||||
} from './organizations/tools/reused-passwords-report.component';
|
||||
import { ToolsComponent as OrgToolsComponent } from './organizations/tools/tools.component';
|
||||
import {
|
||||
UnsecuredWebsitesReportComponent as OrgUnsecuredWebsitesReportComponent,
|
||||
} from './organizations/tools/unsecured-websites-report.component';
|
||||
import {
|
||||
WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent,
|
||||
} from './organizations/tools/weak-passwords-report.component';
|
||||
|
||||
import { VaultComponent as OrgVaultComponent } from './organizations/vault/vault.component';
|
||||
|
||||
@@ -47,12 +64,18 @@ import { PremiumComponent } from './settings/premium.component';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
import { TwoFactorSetupComponent } from './settings/two-factor-setup.component';
|
||||
import { UserBillingComponent } from './settings/user-billing.component';
|
||||
import { UserSubscriptionComponent } from './settings/user-subscription.component';
|
||||
|
||||
import { BreachReportComponent } from './tools/breach-report.component';
|
||||
import { ExportComponent } from './tools/export.component';
|
||||
import { ExposedPasswordsReportComponent } from './tools/exposed-passwords-report.component';
|
||||
import { ImportComponent } from './tools/import.component';
|
||||
import { InactiveTwoFactorReportComponent } from './tools/inactive-two-factor-report.component';
|
||||
import { PasswordGeneratorComponent } from './tools/password-generator.component';
|
||||
import { ReusedPasswordsReportComponent } from './tools/reused-passwords-report.component';
|
||||
import { ToolsComponent } from './tools/tools.component';
|
||||
import { UnsecuredWebsitesReportComponent } from './tools/unsecured-websites-report.component';
|
||||
import { WeakPasswordsReportComponent } from './tools/weak-passwords-report.component';
|
||||
|
||||
import { VaultComponent } from './vault/vault.component';
|
||||
|
||||
@@ -125,7 +148,12 @@ const routes: Routes = [
|
||||
{ path: 'domain-rules', component: DomainRulesComponent, data: { titleId: 'domainRules' } },
|
||||
{ path: 'two-factor', component: TwoFactorSetupComponent, data: { titleId: 'twoStepLogin' } },
|
||||
{ path: 'premium', component: PremiumComponent, data: { titleId: 'goPremium' } },
|
||||
{ path: 'billing', component: UserBillingComponent, data: { titleId: 'billingAndLicensing' } },
|
||||
{ path: 'billing', component: UserBillingComponent, data: { titleId: 'billing' } },
|
||||
{
|
||||
path: 'subscription',
|
||||
component: UserSubscriptionComponent,
|
||||
data: { titleId: 'premiumMembership' },
|
||||
},
|
||||
{ path: 'organizations', component: OrganizationsComponent, data: { titleId: 'organizations' } },
|
||||
{
|
||||
path: 'create-organization',
|
||||
@@ -148,6 +176,31 @@ const routes: Routes = [
|
||||
data: { titleId: 'passwordGenerator' },
|
||||
},
|
||||
{ path: 'breach-report', component: BreachReportComponent, data: { titleId: 'dataBreachReport' } },
|
||||
{
|
||||
path: 'reused-passwords-report',
|
||||
component: ReusedPasswordsReportComponent,
|
||||
data: { titleId: 'reusedPasswordsReport' },
|
||||
},
|
||||
{
|
||||
path: 'unsecured-websites-report',
|
||||
component: UnsecuredWebsitesReportComponent,
|
||||
data: { titleId: 'unsecuredWebsitesReport' },
|
||||
},
|
||||
{
|
||||
path: 'weak-passwords-report',
|
||||
component: WeakPasswordsReportComponent,
|
||||
data: { titleId: 'weakPasswordsReport' },
|
||||
},
|
||||
{
|
||||
path: 'exposed-passwords-report',
|
||||
component: ExposedPasswordsReportComponent,
|
||||
data: { titleId: 'exposedPasswordsReport' },
|
||||
},
|
||||
{
|
||||
path: 'inactive-two-factor-report',
|
||||
component: InactiveTwoFactorReportComponent,
|
||||
data: { titleId: 'inactive2faReport' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -168,19 +221,51 @@ const routes: Routes = [
|
||||
{ path: '', pathMatch: 'full', redirectTo: 'import' },
|
||||
{ path: 'import', component: OrgImportComponent, data: { titleId: 'importData' } },
|
||||
{ path: 'export', component: OrgExportComponent, data: { titleId: 'exportVault' } },
|
||||
{
|
||||
path: 'exposed-passwords-report',
|
||||
component: OrgExposedPasswordsReportComponent,
|
||||
data: { titleId: 'exposedPasswordsReport' },
|
||||
},
|
||||
{
|
||||
path: 'inactive-two-factor-report',
|
||||
component: OrgInactiveTwoFactorReportComponent,
|
||||
data: { titleId: 'inactive2faReport' },
|
||||
},
|
||||
{
|
||||
path: 'reused-passwords-report',
|
||||
component: OrgReusedPasswordsReportComponent,
|
||||
data: { titleId: 'reusedPasswordsReport' },
|
||||
},
|
||||
{
|
||||
path: 'unsecured-websites-report',
|
||||
component: OrgUnsecuredWebsitesReportComponent,
|
||||
data: { titleId: 'unsecuredWebsitesReport' },
|
||||
},
|
||||
{
|
||||
path: 'weak-passwords-report',
|
||||
component: OrgWeakPasswordsReportComponent,
|
||||
data: { titleId: 'weakPasswordsReport' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'manage',
|
||||
component: OrgManageComponent,
|
||||
canActivate: [OrganizationTypeGuardService],
|
||||
data: { allowedTypes: [OrganizationUserType.Owner, OrganizationUserType.Admin] },
|
||||
data: {
|
||||
allowedTypes: [
|
||||
OrganizationUserType.Owner,
|
||||
OrganizationUserType.Admin,
|
||||
OrganizationUserType.Manager,
|
||||
],
|
||||
},
|
||||
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' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -195,7 +280,12 @@ const routes: Routes = [
|
||||
{
|
||||
path: 'billing',
|
||||
component: OrganizationBillingComponent,
|
||||
data: { titleId: 'billingAndLicensing' },
|
||||
data: { titleId: 'billing' },
|
||||
},
|
||||
{
|
||||
path: 'subscription',
|
||||
component: OrganizationSubscriptionComponent,
|
||||
data: { titleId: 'subscription' },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
<toaster-container [toasterconfig]="toasterConfig"></toaster-container>
|
||||
<toaster-container [toasterconfig]="toasterConfig" aria-live="polite"></toaster-container>
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import * as jq from 'jquery';
|
||||
import * as _swal from 'sweetalert';
|
||||
import { SweetAlert } from 'sweetalert/typings/core';
|
||||
import Swal from 'sweetalert2/src/sweetalert2.js';
|
||||
|
||||
import {
|
||||
BodyOutputType,
|
||||
Toast,
|
||||
ToasterConfig,
|
||||
ToasterContainerComponent,
|
||||
ToasterService,
|
||||
} from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
@@ -14,7 +16,9 @@ import {
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
SecurityContext,
|
||||
} from '@angular/core';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import {
|
||||
NavigationEnd,
|
||||
Router,
|
||||
@@ -28,14 +32,17 @@ import { AuthService } from 'jslib/abstractions/auth.service';
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { CollectionService } from 'jslib/abstractions/collection.service';
|
||||
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';
|
||||
import { PolicyService } from 'jslib/abstractions/policy.service';
|
||||
import { SearchService } from 'jslib/abstractions/search.service';
|
||||
import { SettingsService } from 'jslib/abstractions/settings.service';
|
||||
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';
|
||||
@@ -45,8 +52,6 @@ import { ConstantsService } from 'jslib/services/constants.service';
|
||||
import { RouterService } from './services/router.service';
|
||||
|
||||
const BroadcasterSubscriptionId = 'AppComponent';
|
||||
// Hack due to Angular 5.2 bug
|
||||
const swal: SweetAlert = _swal as any;
|
||||
const IdleTimeout = 60000 * 10; // 10 minutes
|
||||
|
||||
@Component({
|
||||
@@ -75,8 +80,10 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
private platformUtilsService: PlatformUtilsService, private ngZone: NgZone,
|
||||
private lockService: LockService, private storageService: StorageService,
|
||||
private cryptoService: CryptoService, private collectionService: CollectionService,
|
||||
private routerService: RouterService, private searchService: SearchService,
|
||||
private notificationsService: NotificationsService) { }
|
||||
private sanitizer: DomSanitizer, private searchService: SearchService,
|
||||
private notificationsService: NotificationsService, private routerService: RouterService,
|
||||
private stateService: StateService, private eventService: EventService,
|
||||
private policyService: PolicyService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.ngZone.runOutsideAngular(() => {
|
||||
@@ -96,6 +103,9 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
case 'unlocked':
|
||||
this.notificationsService.updateConnection(false);
|
||||
break;
|
||||
case 'authBlocked':
|
||||
this.router.navigate(['/']);
|
||||
break;
|
||||
case 'logout':
|
||||
this.logOut(!!message.expired);
|
||||
break;
|
||||
@@ -106,6 +116,9 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
this.notificationsService.updateConnection(false);
|
||||
this.router.navigate(['lock']);
|
||||
break;
|
||||
case 'lockedUrl':
|
||||
window.setTimeout(() => this.routerService.setPreviousUrl(message.url), 500);
|
||||
break;
|
||||
case 'syncStarted':
|
||||
break;
|
||||
case 'syncCompleted':
|
||||
@@ -126,6 +139,15 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
this.router.navigate(['settings/premium']);
|
||||
}
|
||||
break;
|
||||
case 'showToast':
|
||||
this.showToast(message);
|
||||
break;
|
||||
case 'analyticsEventTrack':
|
||||
this.analytics.eventTrack.next({
|
||||
action: message.action,
|
||||
properties: { label: message.label },
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -140,7 +162,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
if (document.querySelector('.swal-modal') != null) {
|
||||
swal.close(undefined);
|
||||
Swal.close(undefined);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -151,9 +173,11 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
private async logOut(expired: boolean) {
|
||||
await this.eventService.uploadEvents();
|
||||
const userId = await this.userService.getUserId();
|
||||
|
||||
await Promise.all([
|
||||
this.eventService.clearEvents(),
|
||||
this.syncService.setLastSync(new Date(0)),
|
||||
this.tokenService.clearToken(),
|
||||
this.cryptoService.clearKeys(),
|
||||
@@ -162,7 +186,9 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
this.cipherService.clear(userId),
|
||||
this.folderService.clear(userId),
|
||||
this.collectionService.clear(userId),
|
||||
this.policyService.clear(userId),
|
||||
this.passwordGenerationService.clear(),
|
||||
this.stateService.purge(),
|
||||
]);
|
||||
|
||||
this.searchService.clearIndex();
|
||||
@@ -202,6 +228,33 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
}, IdleTimeout);
|
||||
}
|
||||
|
||||
private showToast(msg: any) {
|
||||
const toast: Toast = {
|
||||
type: msg.type,
|
||||
title: msg.title,
|
||||
};
|
||||
if (typeof (msg.text) === 'string') {
|
||||
toast.body = msg.text;
|
||||
} else if (msg.text.length === 1) {
|
||||
toast.body = msg.text[0];
|
||||
} else {
|
||||
let message = '';
|
||||
msg.text.forEach((t: string) =>
|
||||
message += ('<p>' + this.sanitizer.sanitize(SecurityContext.HTML, t) + '</p>'));
|
||||
toast.body = message;
|
||||
toast.bodyOutputType = BodyOutputType.TrustedHtml;
|
||||
}
|
||||
if (msg.options != null) {
|
||||
if (msg.options.trustedHtml === true) {
|
||||
toast.bodyOutputType = BodyOutputType.TrustedHtml;
|
||||
}
|
||||
if (msg.options.timeout != null && msg.options.timeout > 0) {
|
||||
toast.timeout = msg.options.timeout;
|
||||
}
|
||||
}
|
||||
this.toasterService.popAsync(toast);
|
||||
}
|
||||
|
||||
private idleStateChanged() {
|
||||
if (this.isIdle) {
|
||||
this.notificationsService.disconnectFromInactivity();
|
||||
|
||||
@@ -3,9 +3,11 @@ 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';
|
||||
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
@@ -17,7 +19,7 @@ import { AppComponent } from './app.component';
|
||||
import { ModalComponent } from './modal.component';
|
||||
|
||||
import { AvatarComponent } from './components/avatar.component';
|
||||
import { CalloutComponent } from './components/callout.component';
|
||||
import { PasswordStrengthComponent } from './components/password-strength.component';
|
||||
|
||||
import { FooterComponent } from './layouts/footer.component';
|
||||
import { FrontendLayoutComponent } from './layouts/frontend-layout.component';
|
||||
@@ -48,21 +50,44 @@ import { GroupAddEditComponent as OrgGroupAddEditComponent } from './organizatio
|
||||
import { GroupsComponent as OrgGroupsComponent } from './organizations/manage/groups.component';
|
||||
import { ManageComponent as OrgManageComponent } from './organizations/manage/manage.component';
|
||||
import { PeopleComponent as OrgPeopleComponent } from './organizations/manage/people.component';
|
||||
import { PoliciesComponent as OrgPoliciesComponent } from './organizations/manage/policies.component';
|
||||
import { PolicyEditComponent as OrgPolicyEditComponent } from './organizations/manage/policy-edit.component';
|
||||
import { UserAddEditComponent as OrgUserAddEditComponent } from './organizations/manage/user-add-edit.component';
|
||||
import { UserConfirmComponent as OrgUserConfirmComponent } from './organizations/manage/user-confirm.component';
|
||||
import { UserGroupsComponent as OrgUserGroupsComponent } from './organizations/manage/user-groups.component';
|
||||
|
||||
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,
|
||||
} from './organizations/settings/two-factor-setup.component';
|
||||
|
||||
import { ExportComponent as OrgExportComponent } from './organizations/tools/export.component';
|
||||
import {
|
||||
ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent,
|
||||
} from './organizations/tools/exposed-passwords-report.component';
|
||||
import { ImportComponent as OrgImportComponent } from './organizations/tools/import.component';
|
||||
import {
|
||||
InactiveTwoFactorReportComponent as OrgInactiveTwoFactorReportComponent,
|
||||
} from './organizations/tools/inactive-two-factor-report.component';
|
||||
import {
|
||||
ReusedPasswordsReportComponent as OrgReusedPasswordsReportComponent,
|
||||
} from './organizations/tools/reused-passwords-report.component';
|
||||
import { ToolsComponent as OrgToolsComponent } from './organizations/tools/tools.component';
|
||||
import {
|
||||
UnsecuredWebsitesReportComponent as OrgUnsecuredWebsitesReportComponent,
|
||||
} from './organizations/tools/unsecured-websites-report.component';
|
||||
import {
|
||||
WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent,
|
||||
} from './organizations/tools/weak-passwords-report.component';
|
||||
|
||||
import { AddEditComponent as OrgAddEditComponent } from './organizations/vault/add-edit.component';
|
||||
import { AttachmentsComponent as OrgAttachmentsComponent } from './organizations/vault/attachments.component';
|
||||
@@ -72,6 +97,7 @@ import { GroupingsComponent as OrgGroupingsComponent } from './organizations/vau
|
||||
import { VaultComponent as OrgVaultComponent } from './organizations/vault/vault.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 { ChangeEmailComponent } from './settings/change-email.component';
|
||||
@@ -82,6 +108,7 @@ import { DeauthorizeSessionsComponent } from './settings/deauthorize-sessions.co
|
||||
import { DeleteAccountComponent } from './settings/delete-account.component';
|
||||
import { DomainRulesComponent } from './settings/domain-rules.component';
|
||||
import { OptionsComponent } from './settings/options.component';
|
||||
import { OrganizationPlansComponent } from './settings/organization-plans.component';
|
||||
import { OrganizationsComponent } from './settings/organizations.component';
|
||||
import { PaymentComponent } from './settings/payment.component';
|
||||
import { PremiumComponent } from './settings/premium.component';
|
||||
@@ -99,14 +126,20 @@ import { TwoFactorYubiKeyComponent } from './settings/two-factor-yubikey.compone
|
||||
import { UpdateKeyComponent } from './settings/update-key.component';
|
||||
import { UpdateLicenseComponent } from './settings/update-license.component';
|
||||
import { UserBillingComponent } from './settings/user-billing.component';
|
||||
import { UserSubscriptionComponent } from './settings/user-subscription.component';
|
||||
import { VerifyEmailComponent } from './settings/verify-email.component';
|
||||
|
||||
import { BreachReportComponent } from './tools/breach-report.component';
|
||||
import { ExportComponent } from './tools/export.component';
|
||||
import { ExposedPasswordsReportComponent } from './tools/exposed-passwords-report.component';
|
||||
import { ImportComponent } from './tools/import.component';
|
||||
import { InactiveTwoFactorReportComponent } from './tools/inactive-two-factor-report.component';
|
||||
import { PasswordGeneratorHistoryComponent } from './tools/password-generator-history.component';
|
||||
import { PasswordGeneratorComponent } from './tools/password-generator.component';
|
||||
import { ReusedPasswordsReportComponent } from './tools/reused-passwords-report.component';
|
||||
import { ToolsComponent } from './tools/tools.component';
|
||||
import { UnsecuredWebsitesReportComponent } from './tools/unsecured-websites-report.component';
|
||||
import { WeakPasswordsReportComponent } from './tools/weak-passwords-report.component';
|
||||
|
||||
import { AddEditComponent } from './vault/add-edit.component';
|
||||
import { AttachmentsComponent } from './vault/attachments.component';
|
||||
@@ -120,30 +153,39 @@ import { GroupingsComponent } from './vault/groupings.component';
|
||||
import { ShareComponent } from './vault/share.component';
|
||||
import { VaultComponent } from './vault/vault.component';
|
||||
|
||||
import { CalloutComponent } from 'jslib/angular/components/callout.component';
|
||||
import { IconComponent } from 'jslib/angular/components/icon.component';
|
||||
|
||||
import { A11yTitleDirective } from 'jslib/angular/directives/a11y-title.directive';
|
||||
import { ApiActionDirective } from 'jslib/angular/directives/api-action.directive';
|
||||
import { AutofocusDirective } from 'jslib/angular/directives/autofocus.directive';
|
||||
import { BlurClickDirective } from 'jslib/angular/directives/blur-click.directive';
|
||||
import { BoxRowDirective } from 'jslib/angular/directives/box-row.directive';
|
||||
import { FallbackSrcDirective } from 'jslib/angular/directives/fallback-src.directive';
|
||||
import { InputVerbatimDirective } from 'jslib/angular/directives/input-verbatim.directive';
|
||||
import { SelectCopyDirective } from 'jslib/angular/directives/select-copy.directive';
|
||||
import { StopClickDirective } from 'jslib/angular/directives/stop-click.directive';
|
||||
import { StopPropDirective } from 'jslib/angular/directives/stop-prop.directive';
|
||||
import { TrueFalseValueDirective } from 'jslib/angular/directives/true-false-value.directive';
|
||||
|
||||
import { ColorPasswordPipe } from 'jslib/angular/pipes/color-password.pipe';
|
||||
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 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 localeEnGb from '@angular/common/locales/en-GB';
|
||||
import localeEs from '@angular/common/locales/es';
|
||||
import localeEt from '@angular/common/locales/et';
|
||||
import localeFr from '@angular/common/locales/fr';
|
||||
import localeHe from '@angular/common/locales/he';
|
||||
import localeIt from '@angular/common/locales/it';
|
||||
import localeJa from '@angular/common/locales/ja';
|
||||
import localeKo from '@angular/common/locales/ko';
|
||||
import localeNb from '@angular/common/locales/nb';
|
||||
import localeNl from '@angular/common/locales/nl';
|
||||
import localePl from '@angular/common/locales/pl';
|
||||
@@ -152,15 +194,22 @@ import localePtPt from '@angular/common/locales/pt-PT';
|
||||
import localeRu from '@angular/common/locales/ru';
|
||||
import localeSk from '@angular/common/locales/sk';
|
||||
import localeSv from '@angular/common/locales/sv';
|
||||
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(localeDa, 'da');
|
||||
registerLocaleData(localeDe, 'de');
|
||||
registerLocaleData(localeEnGb, 'en-GB');
|
||||
registerLocaleData(localeEs, 'es');
|
||||
registerLocaleData(localeEt, 'et');
|
||||
registerLocaleData(localeFr, 'fr');
|
||||
registerLocaleData(localeHe, 'he');
|
||||
registerLocaleData(localeIt, 'it');
|
||||
registerLocaleData(localeJa, 'ja');
|
||||
registerLocaleData(localeKo, 'ko');
|
||||
registerLocaleData(localeNb, 'nb');
|
||||
registerLocaleData(localeNl, 'nl');
|
||||
registerLocaleData(localePl, 'pl');
|
||||
@@ -169,7 +218,9 @@ registerLocaleData(localePtPt, 'pt-PT');
|
||||
registerLocaleData(localeRu, 'ru');
|
||||
registerLocaleData(localeSk, 'sk');
|
||||
registerLocaleData(localeSv, 'sv');
|
||||
registerLocaleData(localeUk, 'uk');
|
||||
registerLocaleData(localeZhCn, 'zh-CN');
|
||||
registerLocaleData(localeZhTw, 'zh-TW');
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -184,10 +235,14 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
},
|
||||
}),
|
||||
ToasterModule.forRoot(),
|
||||
InfiniteScrollModule,
|
||||
DragDropModule,
|
||||
],
|
||||
declarations: [
|
||||
A11yTitleDirective,
|
||||
AcceptOrganizationComponent,
|
||||
AccountComponent,
|
||||
AddCreditComponent,
|
||||
AddEditComponent,
|
||||
AdjustPaymentComponent,
|
||||
AdjustSeatsComponent,
|
||||
@@ -207,23 +262,28 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
ChangeEmailComponent,
|
||||
ChangeKdfComponent,
|
||||
ChangePasswordComponent,
|
||||
ChangePlanComponent,
|
||||
CiphersComponent,
|
||||
CollectionsComponent,
|
||||
ColorPasswordPipe,
|
||||
CreateOrganizationComponent,
|
||||
DeauthorizeSessionsComponent,
|
||||
DeleteAccountComponent,
|
||||
DeleteOrganizationComponent,
|
||||
DomainRulesComponent,
|
||||
DownloadLicenseComponent,
|
||||
ExportComponent,
|
||||
ExposedPasswordsReportComponent,
|
||||
FallbackSrcDirective,
|
||||
FolderAddEditComponent,
|
||||
FooterComponent,
|
||||
FrontendLayoutComponent,
|
||||
GroupingsComponent,
|
||||
HintComponent,
|
||||
IconComponent,
|
||||
I18nPipe,
|
||||
IconComponent,
|
||||
ImportComponent,
|
||||
InactiveTwoFactorReportComponent,
|
||||
InputVerbatimDirective,
|
||||
LockComponent,
|
||||
LoginComponent,
|
||||
@@ -232,7 +292,10 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
OptionsComponent,
|
||||
OrgAccountComponent,
|
||||
OrgAddEditComponent,
|
||||
OrgApiKeyComponent,
|
||||
OrganizationBillingComponent,
|
||||
OrganizationPlansComponent,
|
||||
OrganizationSubscriptionComponent,
|
||||
OrgAttachmentsComponent,
|
||||
OrgCiphersComponent,
|
||||
OrgCollectionAddEditComponent,
|
||||
@@ -241,23 +304,33 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
OrgEntityUsersComponent,
|
||||
OrgEventsComponent,
|
||||
OrgExportComponent,
|
||||
OrgExposedPasswordsReportComponent,
|
||||
OrgImportComponent,
|
||||
OrgInactiveTwoFactorReportComponent,
|
||||
OrgGroupAddEditComponent,
|
||||
OrgGroupingsComponent,
|
||||
OrgGroupsComponent,
|
||||
OrgManageCollectionsComponent,
|
||||
OrgManageComponent,
|
||||
OrgPeopleComponent,
|
||||
OrgPolicyEditComponent,
|
||||
OrgPoliciesComponent,
|
||||
OrgReusedPasswordsReportComponent,
|
||||
OrgRotateApiKeyComponent,
|
||||
OrgSettingComponent,
|
||||
OrgToolsComponent,
|
||||
OrgTwoFactorSetupComponent,
|
||||
OrgUserAddEditComponent,
|
||||
OrgUserConfirmComponent,
|
||||
OrgUserGroupsComponent,
|
||||
OrganizationsComponent,
|
||||
OrganizationLayoutComponent,
|
||||
OrgUnsecuredWebsitesReportComponent,
|
||||
OrgVaultComponent,
|
||||
OrgWeakPasswordsReportComponent,
|
||||
PasswordGeneratorComponent,
|
||||
PasswordGeneratorHistoryComponent,
|
||||
PasswordStrengthComponent,
|
||||
PaymentComponent,
|
||||
PremiumComponent,
|
||||
ProfileComponent,
|
||||
@@ -265,8 +338,10 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
RecoverDeleteComponent,
|
||||
RecoverTwoFactorComponent,
|
||||
RegisterComponent,
|
||||
ReusedPasswordsReportComponent,
|
||||
SearchCiphersPipe,
|
||||
SearchPipe,
|
||||
SelectCopyDirective,
|
||||
SettingsComponent,
|
||||
ShareComponent,
|
||||
StopClickDirective,
|
||||
@@ -283,14 +358,17 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
TwoFactorU2fComponent,
|
||||
TwoFactorVerifyComponent,
|
||||
TwoFactorYubiKeyComponent,
|
||||
UnsecuredWebsitesReportComponent,
|
||||
UpdateKeyComponent,
|
||||
UpdateLicenseComponent,
|
||||
UserBillingComponent,
|
||||
UserLayoutComponent,
|
||||
UserSubscriptionComponent,
|
||||
VaultComponent,
|
||||
VerifyEmailComponent,
|
||||
VerifyEmailTokenComponent,
|
||||
VerifyRecoverDeleteComponent,
|
||||
WeakPasswordsReportComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddEditComponent,
|
||||
@@ -305,13 +383,17 @@ registerLocaleData(localeZhCn, 'zh-CN');
|
||||
FolderAddEditComponent,
|
||||
ModalComponent,
|
||||
OrgAddEditComponent,
|
||||
OrgApiKeyComponent,
|
||||
OrgAttachmentsComponent,
|
||||
OrgCollectionAddEditComponent,
|
||||
OrgCollectionsComponent,
|
||||
OrgEntityEventsComponent,
|
||||
OrgEntityUsersComponent,
|
||||
OrgGroupAddEditComponent,
|
||||
OrgPolicyEditComponent,
|
||||
OrgRotateApiKeyComponent,
|
||||
OrgUserAddEditComponent,
|
||||
OrgUserConfirmComponent,
|
||||
OrgUserGroupsComponent,
|
||||
PasswordGeneratorHistoryComponent,
|
||||
PurgeVaultComponent,
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<div class="callout callout-{{calloutStyle}}" role="alert">
|
||||
<h3 class="callout-heading" *ngIf="title">
|
||||
<i class="fa {{icon}}" *ngIf="icon"></i>
|
||||
{{title}}
|
||||
</h3>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
@@ -1,53 +0,0 @@
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-callout',
|
||||
templateUrl: 'callout.component.html',
|
||||
})
|
||||
export class CalloutComponent implements OnInit {
|
||||
@Input() type = 'info';
|
||||
@Input() icon: string;
|
||||
@Input() title: string;
|
||||
|
||||
calloutStyle: string;
|
||||
|
||||
constructor(private i18nService: I18nService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.calloutStyle = this.type;
|
||||
|
||||
if (this.type === 'warning' || this.type === 'danger') {
|
||||
if (this.type === 'danger') {
|
||||
this.calloutStyle = 'danger';
|
||||
}
|
||||
if (this.title === undefined) {
|
||||
this.title = this.i18nService.t('warning');
|
||||
}
|
||||
if (this.icon === undefined) {
|
||||
this.icon = 'fa-warning';
|
||||
}
|
||||
} else if (this.type === 'error') {
|
||||
this.calloutStyle = 'danger';
|
||||
if (this.title === undefined) {
|
||||
this.title = this.i18nService.t('error');
|
||||
}
|
||||
if (this.icon === undefined) {
|
||||
this.icon = 'fa-bolt';
|
||||
}
|
||||
} else if (this.type === 'tip') {
|
||||
this.calloutStyle = 'success';
|
||||
if (this.title === undefined) {
|
||||
this.title = this.i18nService.t('tip');
|
||||
}
|
||||
if (this.icon === undefined) {
|
||||
this.icon = 'fa-lightbulb-o';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/app/components/password-strength.component.html
Normal file
8
src/app/components/password-strength.component.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<div class="progress">
|
||||
<div class="progress-bar {{color}}" role="progressbar" [ngStyle]="{width: (scoreWidth + '%')}"
|
||||
attr.aria-valuenow="{{scoreWidth}}" aria-valuemin="0" aria-valuemax="100">
|
||||
<ng-container *ngIf="showText && text">
|
||||
{{text}}
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
44
src/app/components/password-strength.component.ts
Normal file
44
src/app/components/password-strength.component.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
OnChanges,
|
||||
} from '@angular/core';
|
||||
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-password-strength',
|
||||
templateUrl: 'password-strength.component.html',
|
||||
})
|
||||
export class PasswordStrengthComponent implements OnChanges {
|
||||
@Input() score?: number;
|
||||
@Input() showText = false;
|
||||
|
||||
scoreWidth = 0;
|
||||
color = 'bg-danger';
|
||||
text: string;
|
||||
|
||||
constructor(private i18nService: I18nService) { }
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.scoreWidth = this.score == null ? 0 : (this.score + 1) * 20;
|
||||
switch (this.score) {
|
||||
case 4:
|
||||
this.color = 'bg-success';
|
||||
this.text = this.i18nService.t('strong');
|
||||
break;
|
||||
case 3:
|
||||
this.color = 'bg-primary';
|
||||
this.text = this.i18nService.t('good');
|
||||
break;
|
||||
case 2:
|
||||
this.color = 'bg-warning';
|
||||
this.text = this.i18nService.t('weak');
|
||||
break;
|
||||
default:
|
||||
this.color = 'bg-danger';
|
||||
this.text = this.score != null ? this.i18nService.t('weak') : null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="container footer text-muted">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
© {{year}}, 8bit Solutions LLC
|
||||
© {{year}}, Bitwarden Inc.
|
||||
</div>
|
||||
<div class="col text-center"></div>
|
||||
<div class="col text-right">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<router-outlet></router-outlet>
|
||||
<div class="container my-5 text-muted text-center">
|
||||
© 2018, 8bit Solutions LLC
|
||||
© {{year}}, Bitwarden Inc.
|
||||
<br> {{'versionNumber' | i18n : version}}
|
||||
</div>
|
||||
|
||||
@@ -12,10 +12,12 @@ import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
})
|
||||
export class FrontendLayoutComponent implements OnInit, OnDestroy {
|
||||
version: string;
|
||||
year: string = '2015';
|
||||
|
||||
constructor(private platformUtilsService: PlatformUtilsService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.year = new Date().getFullYear().toString();
|
||||
this.version = this.platformUtilsService.getApplicationVersion();
|
||||
document.body.classList.add('layout_frontend');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<nav class="navbar navbar-expand navbar-dark bg-primary" [ngClass]="{'bg-secondary-alt': selfHosted}">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" routerLink="/" title="{{'pageTitle' | i18n : 'Bitwarden'}}">
|
||||
<i class="fa fa-shield"></i>
|
||||
<a class="navbar-brand" routerLink="/" appA11yTitle="{{'pageTitle' | i18n : 'Bitwarden'}}">
|
||||
<i class="fa fa-shield" aria-hidden="true"></i>
|
||||
</a>
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="navbar-nav">
|
||||
@@ -18,8 +18,9 @@
|
||||
</div>
|
||||
<ul class="navbar-nav flex-row ml-md-auto d-none d-md-flex">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-item nav-link dropdown-toggle" href="#" id="nav-profile" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fa fa-user-circle fa-lg"></i>
|
||||
<a class="nav-item nav-link dropdown-toggle" href="#" id="nav-profile" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fa fa-user-circle fa-lg" aria-hidden="true"></i>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="nav-profile">
|
||||
<div class="dropdown-item-text d-flex align-items-center" *ngIf="name" appStopProp>
|
||||
@@ -31,24 +32,24 @@
|
||||
</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#" routerLink="/settings/account">
|
||||
<i class="fa fa-fw fa-user"></i>
|
||||
<i class="fa fa-fw fa-user" aria-hidden="true"></i>
|
||||
{{'myAccount' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="https://help.bitwarden.com" target="_blank" rel="noopener">
|
||||
<i class="fa fa-fw fa-question-circle"></i>
|
||||
<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">
|
||||
<i class="fa fa-fw fa-download"></i>
|
||||
<i class="fa fa-fw fa-download" aria-hidden="true"></i>
|
||||
{{'getApps' | i18n}}
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button type="button" class="dropdown-item" (click)="lock()">
|
||||
<i class="fa fa-fw fa-lock"></i>
|
||||
<i class="fa fa-fw fa-lock" aria-hidden="true"></i>
|
||||
{{'lockNow' | i18n}}
|
||||
</button>
|
||||
<button type="button" class="dropdown-item" (click)="logOut()">
|
||||
<i class="fa fa-fw fa-sign-out"></i>
|
||||
<i class="fa fa-fw fa-sign-out" aria-hidden="true"></i>
|
||||
{{'logOut' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -9,33 +9,33 @@
|
||||
</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"></i>
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
{{'organizationIsDisabled' | i18n}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="nav nav-tabs" *ngIf="organization.isAdmin">
|
||||
<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"></i>
|
||||
<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"></i>
|
||||
<i class="fa fa-sliders" aria-hidden="true"></i>
|
||||
{{'manage' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" *ngIf="organization.isAdmin">
|
||||
<a class="nav-link" routerLink="tools" routerLinkActive="active">
|
||||
<i class="fa fa-wrench"></i>
|
||||
<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"></i>
|
||||
<i class="fa fa-cogs" aria-hidden="true"></i>
|
||||
{{'settings' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
import {
|
||||
Component,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
|
||||
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { Organization } from 'jslib/models/domain/organization';
|
||||
|
||||
const BroadcasterSubscriptionId = 'OrganizationLayoutComponent';
|
||||
|
||||
@Component({
|
||||
selector: 'app-organization-layout',
|
||||
templateUrl: 'organization-layout.component.html',
|
||||
})
|
||||
export class OrganizationLayoutComponent implements OnInit {
|
||||
export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
||||
organization: Organization;
|
||||
|
||||
private organizationId: string;
|
||||
|
||||
constructor(private route: ActivatedRoute, private userService: UserService) { }
|
||||
constructor(private route: ActivatedRoute, private userService: UserService,
|
||||
private broadcasterService: BroadcasterService, private ngZone: NgZone) { }
|
||||
|
||||
ngOnInit() {
|
||||
document.body.classList.remove('layout_frontend');
|
||||
@@ -25,6 +32,20 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
this.organizationId = params.organizationId;
|
||||
await this.load();
|
||||
});
|
||||
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case 'updatedOrgLicense':
|
||||
await this.load();
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
|
||||
async load() {
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
import { ModalComponent as BaseModalComponent } from 'jslib/angular/components/modal.component';
|
||||
import { Utils } from 'jslib/misc/utils';
|
||||
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-modal',
|
||||
template: `<ng-template #container></ng-template>`,
|
||||
@@ -17,18 +19,22 @@ import { Utils } from 'jslib/misc/utils';
|
||||
export class ModalComponent extends BaseModalComponent {
|
||||
el: any = null;
|
||||
|
||||
constructor(componentFactoryResolver: ComponentFactoryResolver) {
|
||||
super(componentFactoryResolver);
|
||||
constructor(componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService) {
|
||||
super(componentFactoryResolver, messagingService);
|
||||
}
|
||||
|
||||
ngOnDestroy() { /* Nothing */ }
|
||||
|
||||
show<T>(type: Type<T>, parentContainer: ViewContainerRef, fade: boolean = true): T {
|
||||
show<T>(type: Type<T>, parentContainer: ViewContainerRef, fade: boolean = true,
|
||||
setComponentParameters: (component: T) => void = null): T {
|
||||
this.parentContainer = parentContainer;
|
||||
this.fade = fade;
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory<T>(type);
|
||||
const componentRef = this.container.createComponent<T>(factory);
|
||||
if (setComponentParameters != null) {
|
||||
setComponentParameters(componentRef.instance);
|
||||
}
|
||||
|
||||
const modals = Array.from(document.querySelectorAll('.modal'));
|
||||
if (modals.length > 0) {
|
||||
@@ -37,18 +43,22 @@ export class ModalComponent extends BaseModalComponent {
|
||||
|
||||
this.el.on('show.bs.modal', () => {
|
||||
this.onShow.emit();
|
||||
this.messagingService.send('modalShow');
|
||||
});
|
||||
this.el.on('shown.bs.modal', () => {
|
||||
this.onShown.emit();
|
||||
this.messagingService.send('modalShown');
|
||||
if (!Utils.isMobileBrowser) {
|
||||
this.el.find('*[appAutoFocus]').focus();
|
||||
}
|
||||
});
|
||||
this.el.on('hide.bs.modal', () => {
|
||||
this.onClose.emit();
|
||||
this.messagingService.send('modalClose');
|
||||
});
|
||||
this.el.on('hidden.bs.modal', () => {
|
||||
this.onClosed.emit();
|
||||
this.messagingService.send('modalClosed');
|
||||
if (this.parentContainer != null) {
|
||||
this.parentContainer.clear();
|
||||
}
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
<div class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="collectionAddEditTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">{{title}}</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
|
||||
<h2 class="modal-title" id="collectionAddEditTitle">{{title}}</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="!loading">
|
||||
<div class="form-group">
|
||||
<label for="name">{{'name' | i18n}}</label>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="externalId">{{'externalId' | i18n}}</label>
|
||||
<input id="externalId" class="form-control" type="text" name="ExternalId" [(ngModel)]="externalId">
|
||||
<small class="form-text text-muted">{{'externalIdDesc' | i18n}}</small>
|
||||
</div>
|
||||
<ng-container *ngIf="accessGroups">
|
||||
<h3 class="mt-4 d-flex mb-0">
|
||||
{{'groupAccess' | i18n}}
|
||||
@@ -41,16 +47,20 @@
|
||||
<tbody>
|
||||
<tr *ngFor="let g of groups; let i = index">
|
||||
<td class="table-list-checkbox" (click)="check(g)">
|
||||
<input type="checkbox" [(ngModel)]="g.checked" name="Groups[{{i}}].Checked" [disabled]="g.accessAll">
|
||||
<input type="checkbox" [(ngModel)]="g.checked" name="Groups[{{i}}].Checked"
|
||||
[disabled]="g.accessAll" appStopProp>
|
||||
</td>
|
||||
<td (click)="check(g)">
|
||||
<span appStopProp>
|
||||
{{g.name}}
|
||||
<i class="fa fa-th text-muted fa-fw" *ngIf="g.accessAll" title="This group can access all items"></i>
|
||||
</span>
|
||||
{{g.name}}
|
||||
<ng-container *ngIf="g.accessAll">
|
||||
<i class="fa fa-th text-muted fa-fw" title="{{'groupAccessAllItems' | i18n}}"
|
||||
aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'groupAccessAllItems' | i18n}}</span>
|
||||
</ng-container>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" [(ngModel)]="g.readOnly" name="Groups[{{i}}].ReadOnly" [disabled]="!g.checked || g.accessAll">
|
||||
<input type="checkbox" [(ngModel)]="g.readOnly" name="Groups[{{i}}].ReadOnly"
|
||||
[disabled]="!g.checked || g.accessAll">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -59,15 +69,18 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<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" title="{{'delete' | i18n}}" *ngIf="editMode"
|
||||
[disabled]="deleteBtn.loading" [appApiAction]="deletePromise">
|
||||
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading"></i>
|
||||
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading" title="{{'loading' | i18n}}"></i>
|
||||
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
|
||||
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"
|
||||
[appApiAction]="deletePromise">
|
||||
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading" aria-hidden="true"></i>
|
||||
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"
|
||||
title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,7 @@ export class CollectionAddEditComponent implements OnInit {
|
||||
accessGroups: boolean = false;
|
||||
title: string;
|
||||
name: string;
|
||||
externalId: string;
|
||||
groups: GroupResponse[] = [];
|
||||
formPromise: Promise<any>;
|
||||
deletePromise: Promise<any>;
|
||||
@@ -65,6 +66,7 @@ export class CollectionAddEditComponent implements OnInit {
|
||||
try {
|
||||
const collection = await this.apiService.getCollectionDetails(this.organizationId, this.collectionId);
|
||||
this.name = await this.cryptoService.decryptToUtf8(new CipherString(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);
|
||||
@@ -109,6 +111,7 @@ 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));
|
||||
|
||||
|
||||
@@ -3,15 +3,19 @@
|
||||
<div class="ml-auto d-flex">
|
||||
<div>
|
||||
<label class="sr-only" for="search">{{'search' | i18n}}</label>
|
||||
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}" [(ngModel)]="searchText">
|
||||
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
|
||||
[(ngModel)]="searchText">
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="add()">
|
||||
<i class="fa fa-plus fa-fw"></i>
|
||||
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
|
||||
{{'newCollection' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<i class="fa fa-spinner fa-spin text-muted" *ngIf="loading"></i>
|
||||
<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 && (collections | search:searchText:'name':'id') as searchedCollections">
|
||||
<p *ngIf="!searchedCollections.length">{{'noCollectionsInList' | i18n}}</p>
|
||||
<table class="table table-hover table-list" *ngIf="searchedCollections.length">
|
||||
@@ -22,16 +26,17 @@
|
||||
</td>
|
||||
<td class="table-list-options">
|
||||
<div class="dropdown" appListDropdown>
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fa fa-cog fa-lg"></i>
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false" appA11yTitle="{{'options' | i18n}}">
|
||||
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="users(c)">
|
||||
<i class="fa fa-fw fa-users"></i>
|
||||
<i class="fa fa-fw fa-users" aria-hidden="true"></i>
|
||||
{{'users' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item text-danger" href="#" appStopClick (click)="delete(c)">
|
||||
<i class="fa fa-fw fa-trash-o"></i>
|
||||
<i class="fa fa-fw fa-trash-o" aria-hidden="true"></i>
|
||||
{{'delete' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -14,10 +14,15 @@ 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 { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { CollectionData } from 'jslib/models/data/collectionData';
|
||||
import { Collection } from 'jslib/models/domain/collection';
|
||||
import { CollectionDetailsResponse } from 'jslib/models/response/collectionResponse';
|
||||
import {
|
||||
CollectionDetailsResponse,
|
||||
CollectionResponse,
|
||||
} from 'jslib/models/response/collectionResponse';
|
||||
import { ListResponse } from 'jslib/models/response/listResponse';
|
||||
import { CollectionView } from 'jslib/models/view/collectionView';
|
||||
|
||||
import { ModalComponent } from '../../modal.component';
|
||||
@@ -42,21 +47,31 @@ export class CollectionsComponent implements OnInit {
|
||||
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 i18nService: I18nService, private platformUtilsService: PlatformUtilsService,
|
||||
private userService: UserService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organizationId = params.organizationId;
|
||||
await this.load();
|
||||
this.route.queryParams.subscribe(async (qParams) => {
|
||||
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
|
||||
this.searchText = qParams.search;
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async load() {
|
||||
const response = await this.apiService.getCollections(this.organizationId);
|
||||
const collections = response.data.map((r) =>
|
||||
const organization = await this.userService.getOrganization(this.organizationId);
|
||||
let response: ListResponse<CollectionResponse>;
|
||||
if (organization.isAdmin) {
|
||||
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) =>
|
||||
new Collection(new CollectionData(r as CollectionDetailsResponse)));
|
||||
this.collections = await this.collectionService.decryptMany(collections);
|
||||
this.loading = false;
|
||||
@@ -123,6 +138,10 @@ export class CollectionsComponent implements OnInit {
|
||||
childComponent.entityId = collection.id;
|
||||
childComponent.entityName = collection.name;
|
||||
|
||||
childComponent.onEditedUsers.subscribe(() => {
|
||||
this.load();
|
||||
this.modal.close();
|
||||
});
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
<div class="modal fade">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<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-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">
|
||||
<h2 class="modal-title" id="eventLogsTitle">
|
||||
{{'eventLogs' | i18n}}
|
||||
<small class="text-muted" *ngIf="name">{{name}}</small>
|
||||
</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
|
||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="!loaded">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="loaded">
|
||||
<div class="d-flex">
|
||||
<div class="form-inline">
|
||||
<label class="sr-only" for="start">{{'startDate' | i18n}}</label>
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="start" placeholder="{{'startDate' | i18n}}" [(ngModel)]="start"
|
||||
placeholder="YYYY-MM-DDTHH:MM">
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="start"
|
||||
placeholder="{{'startDate' | i18n}}" [(ngModel)]="start" placeholder="YYYY-MM-DDTHH:MM">
|
||||
<span class="mx-2">-</span>
|
||||
<label class="sr-only" for="end">{{'endDate' | i18n}}</label>
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="end" placeholder="{{'endDate' | i18n}}" [(ngModel)]="end"
|
||||
placeholder="YYYY-MM-DDTHH:MM">
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="end"
|
||||
placeholder="{{'endDate' | i18n}}" [(ngModel)]="end" placeholder="YYYY-MM-DDTHH:MM">
|
||||
</div>
|
||||
<button #refreshBtn [appApiAction]="refreshPromise" type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="loadEvents(true)"
|
||||
<button #refreshBtn [appApiAction]="refreshPromise" type="button"
|
||||
class="btn btn-sm btn-outline-primary ml-3" (click)="loadEvents(true)"
|
||||
[disabled]="loaded && refreshBtn.loading">
|
||||
<i class="fa fa-refresh fa-fw" [ngClass]="{'fa-spin': loaded && refreshBtn.loading}"></i>
|
||||
<i class="fa fa-refresh fa-fw" [ngClass]="{'fa-spin': loaded && refreshBtn.loading}"
|
||||
aria-hidden="true"></i>
|
||||
{{'refresh' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
@@ -49,18 +52,20 @@
|
||||
<tr *ngFor="let e of events">
|
||||
<td>{{e.date | date:'medium'}}</td>
|
||||
<td>
|
||||
<i class="text-muted fa fa-lg {{e.appIcon}}" title="{{e.appName}}, {{e.ip}}"></i>
|
||||
<i class="text-muted fa fa-lg {{e.appIcon}}" title="{{e.appName}}, {{e.ip}}"
|
||||
aria-hidden="true"></i>
|
||||
<span class="sr-only">{{e.appName}}, {{e.ip}}</span>
|
||||
</td>
|
||||
<td *ngIf="showUser">
|
||||
<span title="{{e.userEmail}}">{{e.userName}}</span>
|
||||
<span appA11yTitle="{{e.userEmail}}">{{e.userName}}</span>
|
||||
</td>
|
||||
<td [innerHTML]="e.message"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button #moreBtn [appApiAction]="morePromise" type="button" class="btn btn-block btn-link btn-submit" (click)="loadEvents(false)"
|
||||
[disabled]="loaded && moreBtn.loading" *ngIf="continuationToken">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<button #moreBtn [appApiAction]="morePromise" type="button" class="btn btn-block btn-link btn-submit"
|
||||
(click)="loadEvents(false)" [disabled]="loaded && moreBtn.loading" *ngIf="continuationToken">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'loadMore' | i18n}}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,57 +1,106 @@
|
||||
<div class="modal fade">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="userAccessTitle">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">
|
||||
<h2 class="modal-title" id="userAccessTitle">
|
||||
{{'userAccess' | i18n}}
|
||||
<small>{{entityName}}</small>
|
||||
</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
|
||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
|
||||
<div class="modal-body" *ngIf="loading || !users">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="!loading">
|
||||
<ng-container *ngIf="!users || !users.length">
|
||||
<div class="modal-body"
|
||||
*ngIf="!loading && users && (users | search:searchText:'name':'email':'id') as searchedUsers">
|
||||
<div class="d-flex">
|
||||
<div class="mr-3">
|
||||
<label class="sr-only" for="search">{{'search' | i18n}}</label>
|
||||
<input type="search" class="form-control form-control-sm" id="search"
|
||||
placeholder="{{'search' | i18n}}" name="SearchText" [(ngModel)]="searchText">
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary" [ngClass]="{active: !showSelected}"
|
||||
(click)="filterSelected(false)">
|
||||
{{'all' | i18n}}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" [ngClass]="{active: showSelected}"
|
||||
(click)="filterSelected(true)">
|
||||
{{'selected' | i18n}}
|
||||
<span class="badge badge-pill badge-info" *ngIf="selectedCount">{{selectedCount}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="!searchedUsers.length">
|
||||
<hr>
|
||||
{{'noUsersInList' | i18n}}
|
||||
</ng-container>
|
||||
<table class="table table-hover table-list mb-0" *ngIf="users && users.length">
|
||||
<tbody>
|
||||
<tr *ngFor="let u of users">
|
||||
<td width="30">
|
||||
<app-avatar [data]="u.name || u.email" [email]="u.email" size="25" [circle]="true" [fontSize]="14"></app-avatar>
|
||||
</td>
|
||||
<td>
|
||||
{{u.email}}
|
||||
<span class="badge badge-secondary" *ngIf="u.status === organizationUserStatusType.Invited">{{'invited' | i18n}}</span>
|
||||
<span class="badge badge-warning" *ngIf="u.status === organizationUserStatusType.Accepted">{{'accepted' | i18n}}</span>
|
||||
<small class="text-muted d-block" *ngIf="u.name">{{u.name}}</small>
|
||||
</td>
|
||||
<td *ngIf="entity === 'collection'">
|
||||
<i class="fa fa-th" *ngIf="u.accessAll" title="{{'userAccessAllItems' | i18n}}"></i>
|
||||
<i class="fa fa-eye" *ngIf="u.readOnly" title="{{'readOnly' | i18n}}"></i>
|
||||
</td>
|
||||
<td>
|
||||
<span *ngIf="u.type === organizationUserType.Owner">{{'owner' | i18n}}</span>
|
||||
<span *ngIf="u.type === organizationUserType.Admin">{{'admin' | i18n}}</span>
|
||||
<span *ngIf="u.type === organizationUserType.User">{{'user' | i18n}}</span>
|
||||
</td>
|
||||
<td class="table-list-options wider">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger btn-submit" (click)="remove(u)" #removeBtn [disabled]="removeBtn.loading"
|
||||
[appApiAction]="actionPromise" *ngIf="entity !== 'collection' || !u.accessAll">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'remove' | i18n}}</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<ng-container *ngIf="searchedUsers.length">
|
||||
<table class="table table-hover table-list mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th> </th>
|
||||
<th>{{'name' | i18n}}</th>
|
||||
<th *ngIf="entity === 'collection'"> </th>
|
||||
<th>{{'userType' | i18n}}</th>
|
||||
<th width="100" class="text-center" *ngIf="entity === 'collection'">{{'readOnly' |
|
||||
i18n}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let u of searchedUsers">
|
||||
<td class="table-list-checkbox" (click)="check(u)">
|
||||
<input type="checkbox" [(ngModel)]="u.checked" name="{{u.id.substr(0,8)}}_Checked"
|
||||
[disabled]="entity === 'collection' && u.accessAll"
|
||||
(change)="selectedChanged(u)" appStopProp>
|
||||
</td>
|
||||
<td width="30" (click)="check(u)">
|
||||
<app-avatar [data]="u.name || u.email" [email]="u.email" size="25" [circle]="true"
|
||||
[fontSize]="14"></app-avatar>
|
||||
</td>
|
||||
<td>
|
||||
{{u.email}}
|
||||
<span class="badge badge-secondary"
|
||||
*ngIf="u.status === organizationUserStatusType.Invited">{{'invited'
|
||||
| i18n}}</span>
|
||||
<span class="badge badge-warning"
|
||||
*ngIf="u.status === organizationUserStatusType.Accepted">{{'accepted'
|
||||
| i18n}}</span>
|
||||
<small class="text-muted d-block" *ngIf="u.name">{{u.name}}</small>
|
||||
</td>
|
||||
<td *ngIf="entity === 'collection'">
|
||||
<ng-container *ngIf="u.accessAll">
|
||||
<i class="fa fa-th" title="{{'userAccessAllItems' | i18n}}"
|
||||
aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'userAccessAllItems' | i18n}}</span>
|
||||
</ng-container>
|
||||
</td>
|
||||
<td>
|
||||
<span *ngIf="u.type === organizationUserType.Owner">{{'owner' | i18n}}</span>
|
||||
<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>
|
||||
</td>
|
||||
<td class="text-center" *ngIf="entity === 'collection'">
|
||||
<input type="checkbox" [(ngModel)]="u.readOnly" name="{{u.id.substr(0,8)}}_ReadOnly"
|
||||
[disabled]="u.accessAll || !u.checked">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'save' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,10 +11,11 @@ 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 { OrganizationUserStatusType } from 'jslib/enums/organizationUserStatusType';
|
||||
import { OrganizationUserType } from 'jslib/enums/organizationUserType';
|
||||
import { SelectionReadOnlyRequest } from 'jslib/models/request/selectionReadOnlyRequest';
|
||||
import { OrganizationUserUserDetailsResponse } from 'jslib/models/response/organizationUserResponse';
|
||||
|
||||
import { Utils } from 'jslib/misc/utils';
|
||||
|
||||
@@ -27,68 +28,110 @@ export class EntityUsersComponent implements OnInit {
|
||||
@Input() entityId: string;
|
||||
@Input() entityName: string;
|
||||
@Input() organizationId: string;
|
||||
@Output() onRemovedUser = new EventEmitter();
|
||||
@Output() onEditedUsers = new EventEmitter();
|
||||
|
||||
organizationUserType = OrganizationUserType;
|
||||
organizationUserStatusType = OrganizationUserStatusType;
|
||||
|
||||
showSelected = false;
|
||||
loading = true;
|
||||
users: any[] = [];
|
||||
actionPromise: Promise<any>;
|
||||
formPromise: Promise<any>;
|
||||
selectedCount = 0;
|
||||
searchText: string;
|
||||
|
||||
private allUsers: OrganizationUserUserDetailsResponse[] = [];
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private platformUtilsService: PlatformUtilsService) { }
|
||||
private analytics: Angulartics2, private toasterService: ToasterService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
await this.loadUsers();
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async loadUsers() {
|
||||
let users: any[] = [];
|
||||
if (this.entity === 'group') {
|
||||
const response = await this.apiService.getGroupUsers(this.organizationId, this.entityId);
|
||||
users = response.data.map((r) => r);
|
||||
} else if (this.entity === 'collection') {
|
||||
const response = await this.apiService.getCollectionUsers(this.organizationId, this.entityId);
|
||||
users = response.data.map((r) => r);
|
||||
get users() {
|
||||
if (this.showSelected) {
|
||||
return this.allUsers.filter((u) => (u as any).checked);
|
||||
} else {
|
||||
return this.allUsers;
|
||||
}
|
||||
users.sort(Utils.getSortFunction(this.i18nService, 'email'));
|
||||
this.users = users;
|
||||
}
|
||||
|
||||
async remove(user: any) {
|
||||
if (this.actionPromise != null || (this.entity === 'collection' && user.accessAll)) {
|
||||
async loadUsers() {
|
||||
const users = await this.apiService.getOrganizationUsers(this.organizationId);
|
||||
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);
|
||||
if (user != null && user.length > 0) {
|
||||
(user[0] as any).checked = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
} 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);
|
||||
if (user != null && user.length > 0) {
|
||||
(user[0] as any).checked = true;
|
||||
(user[0] as any).readOnly = s.readOnly;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.allUsers.forEach((u) => {
|
||||
if (this.entity === 'collection' && u.accessAll) {
|
||||
(u as any).checked = true;
|
||||
}
|
||||
if ((u as any).checked) {
|
||||
this.selectedCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
check(u: OrganizationUserUserDetailsResponse) {
|
||||
if (this.entity === 'collection' && u.accessAll) {
|
||||
return;
|
||||
}
|
||||
(u as any).checked = !(u as any).checked;
|
||||
this.selectedChanged(u);
|
||||
}
|
||||
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('removeUserConfirmation'), user.email,
|
||||
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
selectedChanged(u: OrganizationUserUserDetailsResponse) {
|
||||
if ((u as any).checked) {
|
||||
this.selectedCount++;
|
||||
} else {
|
||||
if (this.entity === 'collection') {
|
||||
(u as any).readOnly = false;
|
||||
}
|
||||
this.selectedCount--;
|
||||
}
|
||||
}
|
||||
|
||||
filterSelected(showSelected: boolean) {
|
||||
this.showSelected = showSelected;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
if (this.entity === 'group') {
|
||||
this.actionPromise = this.apiService.deleteGroupUser(this.organizationId, this.entityId,
|
||||
user.organizationUserId);
|
||||
await this.actionPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Removed User From Group' });
|
||||
} else if (this.entity === 'collection') {
|
||||
this.actionPromise = this.apiService.deleteCollectionUser(this.organizationId, this.entityId,
|
||||
user.organizationUserId);
|
||||
await this.actionPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Removed User From Collection' });
|
||||
}
|
||||
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('removedUserId', user.email));
|
||||
this.onRemovedUser.emit();
|
||||
const index = this.users.indexOf(user);
|
||||
if (index > -1) {
|
||||
this.users.splice(index, 1);
|
||||
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));
|
||||
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 { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,21 +3,24 @@
|
||||
<div class="ml-auto d-flex">
|
||||
<div class="form-inline">
|
||||
<label class="sr-only" for="start">{{'startDate' | i18n}}</label>
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="start" placeholder="{{'startDate' | i18n}}" [(ngModel)]="start"
|
||||
placeholder="YYYY-MM-DDTHH:MM">
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="start"
|
||||
placeholder="{{'startDate' | i18n}}" [(ngModel)]="start" placeholder="YYYY-MM-DDTHH:MM">
|
||||
<span class="mx-2">-</span>
|
||||
<label class="sr-only" for="end">{{'endDate' | i18n}}</label>
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="end" placeholder="{{'endDate' | i18n}}" [(ngModel)]="end"
|
||||
placeholder="YYYY-MM-DDTHH:MM">
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="end"
|
||||
placeholder="{{'endDate' | i18n}}" [(ngModel)]="end" placeholder="YYYY-MM-DDTHH:MM">
|
||||
</div>
|
||||
<button #refreshBtn [appApiAction]="refreshPromise" type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="loadEvents(true)"
|
||||
[disabled]="loaded && refreshBtn.loading">
|
||||
<i class="fa fa-refresh fa-fw" [ngClass]="{'fa-spin': loaded && refreshBtn.loading}"></i>
|
||||
<button #refreshBtn [appApiAction]="refreshPromise" type="button" class="btn btn-sm btn-outline-primary ml-3"
|
||||
(click)="loadEvents(true)" [disabled]="loaded && refreshBtn.loading">
|
||||
<i class="fa fa-refresh fa-fw" aria-hidden="true" [ngClass]="{'fa-spin': loaded && refreshBtn.loading}"></i>
|
||||
{{'refresh' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<i class="fa fa-spinner fa-spin text-muted" *ngIf="!loaded" title="{{'loading' | i18n}}"></i>
|
||||
<ng-container *ngIf="!loaded">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="loaded">
|
||||
<p *ngIf="!events || !events.length">{{'noEventsInList' | i18n}}</p>
|
||||
<table class="table table-hover" *ngIf="events && events.length">
|
||||
@@ -35,7 +38,8 @@
|
||||
<tr *ngFor="let e of events">
|
||||
<td>{{e.date | date:'medium'}}</td>
|
||||
<td>
|
||||
<i class="text-muted fa fa-lg {{e.appIcon}}" title="{{e.appName}}, {{e.ip}}"></i>
|
||||
<i class="text-muted fa fa-lg {{e.appIcon}}" title="{{e.appName}}, {{e.ip}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{e.appName}}, {{e.ip}}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span title="{{e.userEmail}}">{{e.userName}}</span>
|
||||
@@ -44,9 +48,9 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button #moreBtn [appApiAction]="morePromise" type="button" class="btn btn-block btn-link btn-submit" (click)="loadEvents(false)"
|
||||
[disabled]="loaded && moreBtn.loading" *ngIf="continuationToken">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<button #moreBtn [appApiAction]="morePromise" type="button" class="btn btn-block btn-link btn-submit"
|
||||
(click)="loadEvents(false)" [disabled]="loaded && moreBtn.loading" *ngIf="continuationToken">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'loadMore' | i18n}}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<div class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="groupAddEditTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">{{title}}</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
|
||||
<h2 class="modal-title" id="groupAddEditTitle">{{title}}</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="!loading">
|
||||
<div class="form-group">
|
||||
@@ -18,7 +19,7 @@
|
||||
<div class="form-group">
|
||||
<label for="externalId">{{'externalId' | i18n}}</label>
|
||||
<input id="externalId" class="form-control" type="text" name="ExternalId" [(ngModel)]="externalId">
|
||||
<small class="form-text text-muted">{{'externalIdGroupDesc' | i18n}}</small>
|
||||
<small class="form-text text-muted">{{'externalIdDesc' | i18n}}</small>
|
||||
</div>
|
||||
<h3 class="mt-4 d-flex">
|
||||
<div class="mb-2">
|
||||
@@ -35,13 +36,15 @@
|
||||
</h3>
|
||||
<div class="form-group" [ngClass]="{'mb-0': access !== 'selected'}">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="access" id="accessAll" value="all" [(ngModel)]="access">
|
||||
<input class="form-check-input" type="radio" name="access" id="accessAll" value="all"
|
||||
[(ngModel)]="access">
|
||||
<label class="form-check-label" for="accessAll">
|
||||
{{'groupAccessAllItems' | i18n}}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="access" id="accessSelected" value="selected" [(ngModel)]="access">
|
||||
<input class="form-check-input" type="radio" name="access" id="accessSelected" value="selected"
|
||||
[(ngModel)]="access">
|
||||
<label class="form-check-label" for="accessSelected">
|
||||
{{'groupAccessSelectedCollections' | i18n}}
|
||||
</label>
|
||||
@@ -62,13 +65,15 @@
|
||||
<tbody>
|
||||
<tr *ngFor="let c of collections; let i = index">
|
||||
<td class="table-list-checkbox" (click)="check(c)">
|
||||
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked">
|
||||
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked"
|
||||
appStopProp>
|
||||
</td>
|
||||
<td (click)="check(c)">
|
||||
<span appStopProp>{{c.name}}</span>
|
||||
{{c.name}}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" [(ngModel)]="c.readOnly" name="Collection[{{i}}].ReadOnly" [disabled]="!c.checked">
|
||||
<input type="checkbox" [(ngModel)]="c.readOnly" name="Collection[{{i}}].ReadOnly"
|
||||
[disabled]="!c.checked">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -77,15 +82,18 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<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" title="{{'delete' | i18n}}" *ngIf="editMode"
|
||||
[disabled]="deleteBtn.loading" [appApiAction]="deletePromise">
|
||||
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading"></i>
|
||||
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading" title="{{'loading' | i18n}}"></i>
|
||||
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
|
||||
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"
|
||||
[appApiAction]="deletePromise">
|
||||
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading" aria-hidden="true"></i>
|
||||
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading" aria-hidden="true"
|
||||
title="{{'loading' | i18n}}"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,15 +3,19 @@
|
||||
<div class="ml-auto d-flex">
|
||||
<div>
|
||||
<label class="sr-only" for="search">{{'search' | i18n}}</label>
|
||||
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}" [(ngModel)]="searchText">
|
||||
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
|
||||
[(ngModel)]="searchText">
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="add()">
|
||||
<i class="fa fa-plus fa-fw"></i>
|
||||
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
|
||||
{{'newGroup' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<i class="fa fa-spinner fa-spin text-muted" *ngIf="loading" title="{{'loading' | i18n}}"></i>
|
||||
<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 && (groups | search:searchText:'name':'id') as searchedGroups">
|
||||
<p *ngIf="!searchedGroups.length">{{'noGroupsInList' | i18n}}</p>
|
||||
<table class="table table-hover table-list" *ngIf="searchedGroups.length">
|
||||
@@ -22,16 +26,17 @@
|
||||
</td>
|
||||
<td class="table-list-options">
|
||||
<div class="dropdown" appListDropdown>
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fa fa-cog fa-lg"></i>
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false" appA11yTitle="{{'options' | i18n}}">
|
||||
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="users(g)">
|
||||
<i class="fa fa-fw fa-users"></i>
|
||||
<i class="fa fa-fw fa-users" aria-hidden="true"></i>
|
||||
{{'users' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item text-danger" href="#" appStopClick (click)="delete(g)">
|
||||
<i class="fa fa-fw fa-trash-o"></i>
|
||||
<i class="fa fa-fw fa-trash-o" aria-hidden="true"></i>
|
||||
{{'delete' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -56,8 +56,11 @@ export class GroupsComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
await this.load();
|
||||
this.route.queryParams.subscribe(async (qParams) => {
|
||||
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
|
||||
this.searchText = qParams.search;
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -131,6 +134,9 @@ export class GroupsComponent implements OnInit {
|
||||
childComponent.entityId = group.id;
|
||||
childComponent.entityName = group.name;
|
||||
|
||||
childComponent.onEditedUsers.subscribe(() => {
|
||||
this.modal.close();
|
||||
});
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
<div class="container page-content">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<div class="card">
|
||||
<div class="card" *ngIf="organization">
|
||||
<div class="card-header">{{'manage' | i18n}}</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a routerLink="people" class="list-group-item" routerLinkActive="active">
|
||||
<a routerLink="people" class="list-group-item" routerLinkActive="active"
|
||||
*ngIf="organization.isAdmin">
|
||||
{{'people' | i18n}}
|
||||
</a>
|
||||
<a routerLink="collections" class="list-group-item" routerLinkActive="active">
|
||||
{{'collections' | i18n}}
|
||||
</a>
|
||||
<a routerLink="groups" class="list-group-item" routerLinkActive="active" *ngIf="accessGroups">
|
||||
<a routerLink="groups" class="list-group-item" routerLinkActive="active"
|
||||
*ngIf="organization.isAdmin && accessGroups">
|
||||
{{'groups' | i18n}}
|
||||
</a>
|
||||
<a routerLink="events" class="list-group-item" routerLinkActive="active" *ngIf="accessEvents">
|
||||
<a routerLink="policies" class="list-group-item" routerLinkActive="active"
|
||||
*ngIf="organization.isAdmin && accessPolicies">
|
||||
{{'policies' | i18n}}
|
||||
</a>
|
||||
<a routerLink="events" class="list-group-item" routerLinkActive="active"
|
||||
*ngIf="organization.isAdmin && accessEvents">
|
||||
{{'eventLogs' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -6,11 +6,15 @@ import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { Organization } from 'jslib/models/domain/organization';
|
||||
|
||||
@Component({
|
||||
selector: 'app-org-manage',
|
||||
templateUrl: 'manage.component.html',
|
||||
})
|
||||
export class ManageComponent implements OnInit {
|
||||
organization: Organization;
|
||||
accessPolicies = false;
|
||||
accessGroups = false;
|
||||
accessEvents = false;
|
||||
|
||||
@@ -18,9 +22,10 @@ export class ManageComponent implements OnInit {
|
||||
|
||||
ngOnInit() {
|
||||
this.route.parent.params.subscribe(async (params) => {
|
||||
const organization = await this.userService.getOrganization(params.organizationId);
|
||||
this.accessEvents = organization.useEvents;
|
||||
this.accessGroups = organization.useGroups;
|
||||
this.organization = await this.userService.getOrganization(params.organizationId);
|
||||
this.accessPolicies = this.organization.usePolicies;
|
||||
this.accessEvents = this.organization.useEvents;
|
||||
this.accessGroups = this.organization.useGroups;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,19 @@
|
||||
<h1>{{'people' | i18n}}</h1>
|
||||
<div class="ml-auto d-flex">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary" [ngClass]="{active: status == null}" (click)="filter(null)">
|
||||
<button type="button" class="btn btn-outline-secondary" [ngClass]="{active: status == null}"
|
||||
(click)="filter(null)">
|
||||
{{'all' | i18n}}
|
||||
<span class="badge badge-pill badge-info" *ngIf="allCount">{{allCount}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" [ngClass]="{active: status == organizationUserStatusType.Invited}"
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
[ngClass]="{active: status == organizationUserStatusType.Invited}"
|
||||
(click)="filter(organizationUserStatusType.Invited)">
|
||||
{{'invited' | i18n}}
|
||||
<span class="badge badge-pill badge-info" *ngIf="invitedCount">{{invitedCount}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" [ngClass]="{active: status == organizationUserStatusType.Accepted}"
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
[ngClass]="{active: status == organizationUserStatusType.Accepted}"
|
||||
(click)="filter(organizationUserStatusType.Accepted)">
|
||||
{{'accepted' | i18n}}
|
||||
<span class="badge badge-pill badge-warning" *ngIf="acceptedCount">{{acceptedCount}}</span>
|
||||
@@ -18,15 +22,19 @@
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<label class="sr-only" for="search">{{'search' | i18n}}</label>
|
||||
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}" [(ngModel)]="searchText">
|
||||
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
|
||||
[(ngModel)]="searchText">
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="invite()">
|
||||
<i class="fa fa-plus fa-fw"></i>
|
||||
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
|
||||
{{'inviteUser' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<i class="fa fa-spinner fa-spin text-muted" *ngIf="loading" title="{{'loading' | i18n}}"></i>
|
||||
<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 && (users | search:searchText:'name':'email':'id') as searchedUsers">
|
||||
<p *ngIf="!searchedUsers.length">{{'noUsersInList' | i18n}}</p>
|
||||
<ng-container *ngIf="searchedUsers.length">
|
||||
@@ -37,43 +45,58 @@
|
||||
<tbody>
|
||||
<tr *ngFor="let u of searchedUsers">
|
||||
<td width="30">
|
||||
<app-avatar [data]="u.name || u.email" [email]="u.email" size="25" [circle]="true" [fontSize]="14"></app-avatar>
|
||||
<app-avatar [data]="u.name || u.email" [email]="u.email" size="25" [circle]="true"
|
||||
[fontSize]="14"></app-avatar>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" appStopClick (click)="edit(u)">{{u.email}}</a>
|
||||
<span class="badge badge-secondary" *ngIf="u.status === organizationUserStatusType.Invited">{{'invited' | i18n}}</span>
|
||||
<span class="badge badge-warning" *ngIf="u.status === organizationUserStatusType.Accepted">{{'accepted' | i18n}}</span>
|
||||
<span class="badge badge-secondary"
|
||||
*ngIf="u.status === organizationUserStatusType.Invited">{{'invited' | i18n}}</span>
|
||||
<span class="badge badge-warning"
|
||||
*ngIf="u.status === organizationUserStatusType.Accepted">{{'accepted' | i18n}}</span>
|
||||
<small class="text-muted d-block" *ngIf="u.name">{{u.name}}</small>
|
||||
</td>
|
||||
<td>
|
||||
<ng-container *ngIf="u.twoFactorEnabled">
|
||||
<i class="fa fa-lock" title="{{'userUsingTwoStep' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'userUsingTwoStep' | i18n}}</span>
|
||||
</ng-container>
|
||||
</td>
|
||||
<td>
|
||||
<span *ngIf="u.type === organizationUserType.Owner">{{'owner' | i18n}}</span>
|
||||
<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>
|
||||
</td>
|
||||
<td class="table-list-options">
|
||||
<div class="dropdown" appListDropdown>
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fa fa-cog fa-lg"></i>
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
|
||||
appA11yTitle="{{'options' | i18n}}">
|
||||
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="reinvite(u)" *ngIf="u.status === organizationUserStatusType.Invited">
|
||||
<i class="fa fa-fw fa-envelope-o"></i>
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="reinvite(u)"
|
||||
*ngIf="u.status === organizationUserStatusType.Invited">
|
||||
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
|
||||
{{'resendInvitation' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item text-success" href="#" appStopClick (click)="confirm(u)" *ngIf="u.status === organizationUserStatusType.Accepted">
|
||||
<i class="fa fa-fw fa-check"></i>
|
||||
<a class="dropdown-item text-success" href="#" appStopClick (click)="confirm(u)"
|
||||
*ngIf="u.status === organizationUserStatusType.Accepted">
|
||||
<i class="fa fa-fw fa-check" aria-hidden="true"></i>
|
||||
{{'confirm' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="groups(u)" *ngIf="accessGroups">
|
||||
<i class="fa fa-fw fa-sitemap"></i>
|
||||
<i class="fa fa-fw fa-sitemap" aria-hidden="true"></i>
|
||||
{{'groups' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="events(u)" *ngIf="accessEvents && u.status === organizationUserStatusType.Confirmed">
|
||||
<i class="fa fa-fw fa-file-text-o"></i>
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="events(u)"
|
||||
*ngIf="accessEvents && u.status === organizationUserStatusType.Confirmed">
|
||||
<i class="fa fa-fw fa-file-text-o" aria-hidden="true"></i>
|
||||
{{'eventLogs' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(u)">
|
||||
<i class="fa fa-fw fa-remove"></i>
|
||||
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
|
||||
{{'remove' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
@@ -87,3 +110,4 @@
|
||||
<ng-template #addEdit></ng-template>
|
||||
<ng-template #groupsTemplate></ng-template>
|
||||
<ng-template #eventsTemplate></ng-template>
|
||||
<ng-template #confirmTemplate></ng-template>
|
||||
|
||||
@@ -5,15 +5,21 @@ import {
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
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 { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { OrganizationUserConfirmRequest } from 'jslib/models/request/organizationUserConfirmRequest';
|
||||
@@ -28,6 +34,7 @@ import { Utils } from 'jslib/misc/utils';
|
||||
import { ModalComponent } from '../../modal.component';
|
||||
import { EntityEventsComponent } from './entity-events.component';
|
||||
import { UserAddEditComponent } from './user-add-edit.component';
|
||||
import { UserConfirmComponent } from './user-confirm.component';
|
||||
import { UserGroupsComponent } from './user-groups.component';
|
||||
|
||||
@Component({
|
||||
@@ -38,6 +45,7 @@ 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;
|
||||
|
||||
loading = true;
|
||||
organizationId: string;
|
||||
@@ -58,17 +66,22 @@ export class PeopleComponent implements OnInit {
|
||||
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private platformUtilsService: PlatformUtilsService, private analytics: Angulartics2,
|
||||
private toasterService: ToasterService, private cryptoService: CryptoService,
|
||||
private userService: UserService) { }
|
||||
private userService: UserService, private router: Router,
|
||||
private storageService: StorageService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organizationId = params.organizationId;
|
||||
const organization = await this.userService.getOrganization(this.organizationId);
|
||||
if (!organization.isAdmin) {
|
||||
this.router.navigate(['../collections'], { relativeTo: this.route });
|
||||
return;
|
||||
}
|
||||
this.accessEvents = organization.useEvents;
|
||||
this.accessGroups = organization.useGroups;
|
||||
await this.load();
|
||||
|
||||
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);
|
||||
@@ -76,6 +89,9 @@ export class PeopleComponent implements OnInit {
|
||||
this.events(user[0]);
|
||||
}
|
||||
}
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -105,6 +121,10 @@ export class PeopleComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
get allCount() {
|
||||
return this.allUsers.length;
|
||||
}
|
||||
|
||||
get invitedCount() {
|
||||
return this.statusMap.has(OrganizationUserStatusType.Invited) ?
|
||||
this.statusMap.get(OrganizationUserStatusType.Invited).length : 0;
|
||||
@@ -206,17 +226,48 @@ export class PeopleComponent implements OnInit {
|
||||
}
|
||||
|
||||
async confirm(user: OrganizationUserUserDetailsResponse) {
|
||||
function updateUser(self: PeopleComponent) {
|
||||
user.status = OrganizationUserStatusType.Confirmed;
|
||||
const mapIndex = self.statusMap.get(OrganizationUserStatusType.Accepted).indexOf(user);
|
||||
if (mapIndex > -1) {
|
||||
self.statusMap.get(OrganizationUserStatusType.Accepted).splice(mapIndex, 1);
|
||||
self.statusMap.get(OrganizationUserStatusType.Confirmed).push(user);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const autoConfirm = await this.storageService.get<boolean>(ConstantsService.autoConfirmFingerprints);
|
||||
if (autoConfirm == null || !autoConfirm) {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.confirmModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<UserConfirmComponent>(
|
||||
UserConfirmComponent, this.confirmModalRef);
|
||||
|
||||
childComponent.name = user != null ? user.name || user.email : null;
|
||||
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);
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionPromise = this.doConfirmation(user);
|
||||
await this.actionPromise;
|
||||
user.status = OrganizationUserStatusType.Confirmed;
|
||||
const mapIndex = this.statusMap.get(OrganizationUserStatusType.Accepted).indexOf(user);
|
||||
if (mapIndex > -1) {
|
||||
this.statusMap.get(OrganizationUserStatusType.Accepted).splice(mapIndex, 1);
|
||||
this.statusMap.get(OrganizationUserStatusType.Confirmed).push(user);
|
||||
}
|
||||
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;
|
||||
@@ -247,6 +298,11 @@ export class PeopleComponent implements OnInit {
|
||||
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;
|
||||
|
||||
19
src/app/organizations/manage/policies.component.html
Normal file
19
src/app/organizations/manage/policies.component.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<div class="page-header d-flex">
|
||||
<h1>{{'policies' | i18n}}</h1>
|
||||
</div>
|
||||
<ng-container *ngIf="loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</ng-container>
|
||||
<table class="table table-hover table-list" *ngIf="!loading">
|
||||
<tbody>
|
||||
<tr *ngFor="let p of policies">
|
||||
<td>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<ng-template #editTemplate></ng-template>
|
||||
114
src/app/organizations/manage/policies.component.ts
Normal file
114
src/app/organizations/manage/policies.component.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { PolicyType } from 'jslib/enums/policyType';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { PolicyResponse } from 'jslib/models/response/policyResponse';
|
||||
|
||||
import { ModalComponent } from '../../modal.component';
|
||||
|
||||
import { PolicyEditComponent } from './policy-edit.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-org-policies',
|
||||
templateUrl: 'policies.component.html',
|
||||
})
|
||||
export class PoliciesComponent implements OnInit {
|
||||
@ViewChild('editTemplate', { read: ViewContainerRef }) editModalRef: ViewContainerRef;
|
||||
|
||||
loading = true;
|
||||
organizationId: string;
|
||||
policies: any[];
|
||||
|
||||
private modal: ModalComponent = null;
|
||||
private orgPolicies: PolicyResponse[];
|
||||
private policiesEnabledMap: Map<PolicyType, boolean> = new Map<PolicyType, boolean>();
|
||||
|
||||
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,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
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;
|
||||
}
|
||||
await this.load();
|
||||
});
|
||||
}
|
||||
|
||||
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.policiesEnabledMap.set(op.type, op.enabled);
|
||||
});
|
||||
this.policies.forEach((p) => {
|
||||
p.enabled = this.policiesEnabledMap.has(p.type) && this.policiesEnabledMap.get(p.type);
|
||||
});
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
edit(p: any) {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.editModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<PolicyEditComponent>(
|
||||
PolicyEditComponent, this.editModalRef);
|
||||
|
||||
childComponent.name = p.name;
|
||||
childComponent.description = p.description;
|
||||
childComponent.type = p.type;
|
||||
childComponent.organizationId = this.organizationId;
|
||||
childComponent.onSavedPolicy.subscribe(() => {
|
||||
this.modal.close();
|
||||
this.load();
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
143
src/app/organizations/manage/policy-edit.component.html
Normal file
143
src/app/organizations/manage/policy-edit.component.html
Normal file
@@ -0,0 +1,143 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="policiesEditTitle">
|
||||
<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="policiesEditTitle">{{'editPolicy' | i18n}} - {{name}}</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="!loading">
|
||||
<p>{{description}}</p>
|
||||
<app-callout type="warning" *ngIf="type === policyType.TwoFactorAuthentication"
|
||||
title="{{'warning' | i18n}}" icon="fa-warning">
|
||||
{{'twoStepLoginPolicyWarning' | 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>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="type === policyType.MasterPassword">
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="masterPassMinComplexity">{{'minComplexityScore' | i18n}}</label>
|
||||
<select id="masterPassMinComplexity" name="MasterPassMinComplexity"
|
||||
[(ngModel)]="masterPassMinComplexity" class="form-control">
|
||||
<option *ngFor="let o of passwordScores" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6 form-group">
|
||||
<label for="masterPassMinLength">{{'minLength' | i18n}}</label>
|
||||
<input id="masterPassMinLength" class="form-control" type="number" min="8"
|
||||
name="MasterPassMinLength" [(ngModel)]="masterPassMinLength">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="masterPassRequireUpper"
|
||||
[(ngModel)]="masterPassRequireUpper" name="MasterPassRequireUpper">
|
||||
<label class="form-check-label" for="masterPassRequireUpper">A-Z</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="masterPassRequireLower"
|
||||
[(ngModel)]="masterPassRequireLower" name="MasterPassRequireLower">
|
||||
<label class="form-check-label" for="masterPassRequireLower">a-z</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="masterPassRequireNumbers"
|
||||
[(ngModel)]="masterPassRequireNumbers" name="MasterPassRequireNumbers">
|
||||
<label class="form-check-label" for="masterPassRequireNumbers">0-9</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="masterPassRequireSpecial"
|
||||
[(ngModel)]="masterPassRequireSpecial" name="MasterPassRequireSpecial">
|
||||
<label class="form-check-label" for="masterPassRequireSpecial">!@#$%^&*</label>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="type === policyType.PasswordGenerator">
|
||||
<div class="row">
|
||||
<div class="col-6 form-group mb-0">
|
||||
<label for="passGenDefaultType">{{'defaultType' | i18n}}</label>
|
||||
<select id="passGenDefaultType" name="PassGenDefaultType" [(ngModel)]="passGenDefaultType"
|
||||
class="form-control">
|
||||
<option *ngFor="let o of defaultTypes" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="mt-4">{{'password' | i18n}}</h3>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="passGenMinLength">{{'minLength' | i18n}}</label>
|
||||
<input id="passGenMinLength" class="form-control" type="number" name="PassGenMinLength"
|
||||
min="5" max="128" [(ngModel)]="passGenMinLength">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="passGenMinNumbers">{{'minNumbers' | i18n}}</label>
|
||||
<input id="passGenMinNumbers" class="form-control" type="number" name="PassGenMinNumbers"
|
||||
min="0" max="9" [(ngModel)]="passGenMinNumbers">
|
||||
</div>
|
||||
<div class="col-6 form-group">
|
||||
<label for="passGenMinSpecial">{{'minSpecial' | i18n}}</label>
|
||||
<input id="passGenMinSpecial" class="form-control" type="number" name="PassGenMinSpecial"
|
||||
min="0" max="9" [(ngModel)]="passGenMinSpecial">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="passGenUseUpper"
|
||||
[(ngModel)]="passGenUseUpper" name="PassGenUseUpper">
|
||||
<label class="form-check-label" for="passGenUseUpper">A-Z</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="passGenUseLower"
|
||||
[(ngModel)]="passGenUseLower" name="PassGenUseLower">
|
||||
<label class="form-check-label" for="passGenUseLower">a-z</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="passGenUseNumbers"
|
||||
[(ngModel)]="passGenUseNumbers" name="PassGenUseNumbers">
|
||||
<label class="form-check-label" for="passGenUseNumbers">0-9</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="passGenUseSpecial"
|
||||
[(ngModel)]="passGenUseSpecial" name="PassGenUseSpecial">
|
||||
<label class="form-check-label" for="passGenUseSpecial">!@#$%^&*</label>
|
||||
</div>
|
||||
<h3 class="mt-4">{{'passphrase' | i18n}}</h3>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="passGenMinNumberWords">{{'minimumNumberOfWords' | i18n}}</label>
|
||||
<input id="passGenMinNumberWords" class="form-control" type="number"
|
||||
name="PassGenMinNumberWords" min="3" max="20" [(ngModel)]="passGenMinNumberWords">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="passGenCapitalize"
|
||||
[(ngModel)]="passGenCapitalize" name="PassGenCapitalize">
|
||||
<label class="form-check-label" for="passGenCapitalize">{{'capitalize' | i18n}}</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="passGenIncludeNumber"
|
||||
[(ngModel)]="passGenIncludeNumber" name="PassGenIncludeNumber">
|
||||
<label class="form-check-label" for="passGenIncludeNumber">{{'includeNumber' | i18n}}</label>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'save' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
data-dismiss="modal">{{'cancel' | i18n}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
171
src/app/organizations/manage/policy-edit.component.ts
Normal file
171
src/app/organizations/manage/policy-edit.component.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
} 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';
|
||||
|
||||
import { PolicyType } from 'jslib/enums/policyType';
|
||||
|
||||
import { PolicyRequest } from 'jslib/models/request/policyRequest';
|
||||
|
||||
import { PolicyResponse } from 'jslib/models/response/policyResponse';
|
||||
|
||||
@Component({
|
||||
selector: 'app-policy-edit',
|
||||
templateUrl: 'policy-edit.component.html',
|
||||
})
|
||||
export class PolicyEditComponent implements OnInit {
|
||||
@Input() name: string;
|
||||
@Input() description: string;
|
||||
@Input() type: PolicyType;
|
||||
@Input() organizationId: string;
|
||||
@Output() onSavedPolicy = new EventEmitter();
|
||||
|
||||
policyType = PolicyType;
|
||||
loading = true;
|
||||
enabled = false;
|
||||
formPromise: Promise<any>;
|
||||
passwordScores: any[];
|
||||
defaultTypes: any[];
|
||||
|
||||
// Master password
|
||||
|
||||
masterPassMinComplexity?: number = null;
|
||||
masterPassMinLength?: number;
|
||||
masterPassRequireUpper?: number;
|
||||
masterPassRequireLower?: number;
|
||||
masterPassRequireNumbers?: number;
|
||||
masterPassRequireSpecial?: number;
|
||||
|
||||
// Password generator
|
||||
|
||||
passGenDefaultType?: string;
|
||||
passGenMinLength?: number;
|
||||
passGenUseUpper?: boolean;
|
||||
passGenUseLower?: boolean;
|
||||
passGenUseNumbers?: boolean;
|
||||
passGenUseSpecial?: boolean;
|
||||
passGenMinNumbers?: number;
|
||||
passGenMinSpecial?: number;
|
||||
passGenMinNumberWords?: number;
|
||||
passGenCapitalize?: boolean;
|
||||
passGenIncludeNumber?: boolean;
|
||||
|
||||
private policy: PolicyResponse;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService) {
|
||||
this.passwordScores = [
|
||||
{ name: '-- ' + i18nService.t('select') + ' --', value: null },
|
||||
{ name: i18nService.t('weak') + ' (0)', value: 0 },
|
||||
{ name: i18nService.t('weak') + ' (1)', value: 1 },
|
||||
{ name: i18nService.t('weak') + ' (2)', value: 2 },
|
||||
{ name: i18nService.t('good') + ' (3)', value: 3 },
|
||||
{ name: i18nService.t('strong') + ' (4)', value: 4 },
|
||||
];
|
||||
this.defaultTypes = [
|
||||
{ name: i18nService.t('userPreference'), value: null },
|
||||
{ name: i18nService.t('password'), value: 'password' },
|
||||
{ name: i18nService.t('passphrase'), value: 'passphrase' },
|
||||
];
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.load();
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async load() {
|
||||
try {
|
||||
this.policy = await this.apiService.getPolicy(this.organizationId, this.type);
|
||||
|
||||
if (this.policy != null) {
|
||||
this.enabled = this.policy.enabled;
|
||||
if (this.policy.data != null) {
|
||||
switch (this.type) {
|
||||
case PolicyType.PasswordGenerator:
|
||||
this.passGenDefaultType = this.policy.data.defaultType;
|
||||
this.passGenMinLength = this.policy.data.minLength;
|
||||
this.passGenUseUpper = this.policy.data.useUpper;
|
||||
this.passGenUseLower = this.policy.data.useLower;
|
||||
this.passGenUseNumbers = this.policy.data.useNumbers;
|
||||
this.passGenUseSpecial = this.policy.data.useSpecial;
|
||||
this.passGenMinNumbers = this.policy.data.minNumbers;
|
||||
this.passGenMinSpecial = this.policy.data.minSpecial;
|
||||
this.passGenMinNumberWords = this.policy.data.minNumberWords;
|
||||
this.passGenCapitalize = this.policy.data.capitalize;
|
||||
this.passGenIncludeNumber = this.policy.data.includeNumber;
|
||||
break;
|
||||
case PolicyType.MasterPassword:
|
||||
this.masterPassMinComplexity = this.policy.data.minComplexity;
|
||||
this.masterPassMinLength = this.policy.data.minLength;
|
||||
this.masterPassRequireUpper = this.policy.data.requireUpper;
|
||||
this.masterPassRequireLower = this.policy.data.requireLower;
|
||||
this.masterPassRequireNumbers = this.policy.data.requireNumbers;
|
||||
this.masterPassRequireSpecial = this.policy.data.requireSpecial;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.statusCode === 404) {
|
||||
this.enabled = false;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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,17 +1,18 @@
|
||||
<div class="modal fade">
|
||||
<div class="modal-dialog" [ngClass]="{'modal-lg': !editMode}">
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
|
||||
<div class="modal-dialog" [ngClass]="{'modal-lg': !editMode}" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">
|
||||
<h2 class="modal-title" id="userAddEditTitle">
|
||||
{{title}}
|
||||
<small class="text-muted" *ngIf="name">{{name}}</small>
|
||||
</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
|
||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="!loading">
|
||||
<ng-container *ngIf="!editMode">
|
||||
@@ -24,21 +25,32 @@
|
||||
</ng-container>
|
||||
<h3>{{'userType' | i18n}}</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">
|
||||
<input class="form-check-input" type="radio" name="userType" id="userTypeUser"
|
||||
[value]="organizationUserType.User" [(ngModel)]="type">
|
||||
<label class="form-check-label" for="userTypeUser">
|
||||
{{'user' | i18n}}
|
||||
<small>{{'userDesc' | i18n}}</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mt-2 form-check-block">
|
||||
<input class="form-check-input" type="radio" name="userType" id="userTypeAdmin" [value]="organizationUserType.Admin" [(ngModel)]="type">
|
||||
<input class="form-check-input" type="radio" name="userType" id="userTypeManager"
|
||||
[value]="organizationUserType.Manager" [(ngModel)]="type">
|
||||
<label class="form-check-label" for="userTypeManager">
|
||||
{{'manager' | i18n}}
|
||||
<small>{{'managerDesc' | i18n}}</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mt-2 form-check-block">
|
||||
<input class="form-check-input" type="radio" name="userType" id="userTypeAdmin"
|
||||
[value]="organizationUserType.Admin" [(ngModel)]="type">
|
||||
<label class="form-check-label" for="userTypeAdmin">
|
||||
{{'admin' | i18n}}
|
||||
<small>{{'adminDesc' | i18n}}</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mt-2 form-check-block">
|
||||
<input class="form-check-input" type="radio" name="userType" id="userTypeOwner" [value]="organizationUserType.Owner" [(ngModel)]="type">
|
||||
<input class="form-check-input" type="radio" name="userType" id="userTypeOwner"
|
||||
[value]="organizationUserType.Owner" [(ngModel)]="type">
|
||||
<label class="form-check-label" for="userTypeOwner">
|
||||
{{'owner' | i18n}}
|
||||
<small>{{'ownerDesc' | i18n}}</small>
|
||||
@@ -59,13 +71,15 @@
|
||||
</h3>
|
||||
<div class="form-group" [ngClass]="{'mb-0': access !== 'selected'}">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="access" id="accessAll" value="all" [(ngModel)]="access">
|
||||
<input class="form-check-input" type="radio" name="access" id="accessAll" value="all"
|
||||
[(ngModel)]="access">
|
||||
<label class="form-check-label" for="accessAll">
|
||||
{{'userAccessAllItems' | i18n}}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="access" id="accessSelected" value="selected" [(ngModel)]="access">
|
||||
<input class="form-check-input" type="radio" name="access" id="accessSelected" value="selected"
|
||||
[(ngModel)]="access">
|
||||
<label class="form-check-label" for="accessSelected">
|
||||
{{'userAccessSelectedCollections' | i18n}}
|
||||
</label>
|
||||
@@ -86,13 +100,15 @@
|
||||
<tbody>
|
||||
<tr *ngFor="let c of collections; let i = index">
|
||||
<td class="table-list-checkbox" (click)="check(c)">
|
||||
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked">
|
||||
<input type="checkbox" [(ngModel)]="c.checked" name="Collection[{{i}}].Checked"
|
||||
appStopProp>
|
||||
</td>
|
||||
<td (click)="check(c)">
|
||||
<span appStopProp>{{c.name}}</span>
|
||||
{{c.name}}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" [(ngModel)]="c.readOnly" name="Collection[{{i}}].ReadOnly" [disabled]="!c.checked">
|
||||
<input type="checkbox" [(ngModel)]="c.readOnly" name="Collection[{{i}}].ReadOnly"
|
||||
[disabled]="!c.checked">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -101,15 +117,18 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<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" title="{{'delete' | i18n}}" *ngIf="editMode"
|
||||
[disabled]="deleteBtn.loading" [appApiAction]="deletePromise">
|
||||
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading"></i>
|
||||
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading" title="{{'loading' | i18n}}"></i>
|
||||
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
|
||||
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"
|
||||
[appApiAction]="deletePromise">
|
||||
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading" aria-hidden="true"></i>
|
||||
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"
|
||||
title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
38
src/app/organizations/manage/user-confirm.component.html
Normal file
38
src/app/organizations/manage/user-confirm.component.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<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-header">
|
||||
<h2 class="modal-title" id="confirmUserTitle">
|
||||
{{'confirmUser' | i18n}}
|
||||
<small class="text-muted" *ngIf="name">{{name}}</small>
|
||||
</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
{{'fingerprintEnsureIntegrityVerify' | i18n}}
|
||||
<a href="https://help.bitwarden.com/article/fingerprint-phrase/" target="_blank" rel="noopener">
|
||||
{{'learnMore' | i18n}}</a>
|
||||
</p>
|
||||
<p><code>{{fingerprint}}</code></p>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="dontAskAgain" name="DontAskAgain"
|
||||
[(ngModel)]="dontAskAgain">
|
||||
<label class="form-check-label" for="dontAskAgain">
|
||||
{{'dontAskFingerprintAgain' | i18n}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'confirm' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
data-dismiss="modal">{{'cancel' | i18n}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
84
src/app/organizations/manage/user-confirm.component.ts
Normal file
84
src/app/organizations/manage/user-confirm.component.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
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({
|
||||
selector: 'app-user-confirm',
|
||||
templateUrl: 'user-confirm.component.html',
|
||||
})
|
||||
export class UserConfirmComponent implements OnInit {
|
||||
@Input() name: string;
|
||||
@Input() userId: string;
|
||||
@Input() organizationUserId: string;
|
||||
@Input() organizationId: string;
|
||||
@Output() onConfirmedUser = new EventEmitter();
|
||||
|
||||
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) { }
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
const publicKeyResponse = await this.apiService.getUserPublicKey(this.userId);
|
||||
if (publicKeyResponse != null) {
|
||||
this.publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
|
||||
const fingerprint = await this.cryptoService.getFingerprint(this.userId, this.publicKey.buffer);
|
||||
if (fingerprint != null) {
|
||||
this.fingerprint = fingerprint.join('-');
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.dontAskAgain) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
<div class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="groupAccessTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">
|
||||
<h2 class="modal-title" id="groupAccessTitle">
|
||||
{{'groupAccess' | i18n}}
|
||||
<small class="text-muted" *ngIf="name">{{name}}</small>
|
||||
</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
|
||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="!loading">
|
||||
<p>{{'groupAccessUserDesc' | i18n}}</p>
|
||||
@@ -22,10 +23,10 @@
|
||||
<tbody>
|
||||
<tr *ngFor="let g of groups; let i = index">
|
||||
<td class="table-list-checkbox" (click)="check(g)">
|
||||
<input type="checkbox" [(ngModel)]="g.checked" name="Groups[{{i}}].Checked">
|
||||
<input type="checkbox" [(ngModel)]="g.checked" name="Groups[{{i}}].Checked" appStopProp>
|
||||
</td>
|
||||
<td (click)="check(g)">
|
||||
<span appStopProp>{{g.name}}</span>
|
||||
{{g.name}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -33,10 +34,11 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
<h1>{{'myOrganization' | i18n}}</h1>
|
||||
</div>
|
||||
<div *ngIf="loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
|
||||
<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" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="row">
|
||||
@@ -13,11 +14,13 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="billingEmail">{{'billingEmail' | i18n}}</label>
|
||||
<input id="billingEmail" class="form-control" type="text" name="BillingEmail" [(ngModel)]="org.billingEmail">
|
||||
<input id="billingEmail" class="form-control" type="text" name="BillingEmail"
|
||||
[(ngModel)]="org.billingEmail">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="businessName">{{'businessName' | i18n}}</label>
|
||||
<input id="businessName" class="form-control" type="text" name="BusinessName" [(ngModel)]="org.businessName">
|
||||
<input id="businessName" class="form-control" type="text" name="BusinessName"
|
||||
[(ngModel)]="org.businessName">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
@@ -25,10 +28,23 @@
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'save' | i18n}}</span>
|
||||
</button>
|
||||
</form>
|
||||
<ng-container *ngIf="canUseApi">
|
||||
<div class="secondary-header border-0 mb-0">
|
||||
<h1>{{'apiKey' | i18n}}</h1>
|
||||
</div>
|
||||
<p>
|
||||
{{'apiKeyDesc' | i18n}}
|
||||
<a href="https://docs.bitwarden.com" target="_blank" rel="noopener">
|
||||
{{'learnMore' | i18n}}
|
||||
</a>
|
||||
</p>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="viewApiKey()">{{'viewApiKey' | i18n}}</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="rotateApiKey()">{{'rotateApiKey' | i18n}}</button>
|
||||
</ng-container>
|
||||
<div class="secondary-header border-0 mb-0">
|
||||
<h1>{{'taxInformation' | i18n}}</h1>
|
||||
</div>
|
||||
@@ -42,7 +58,12 @@
|
||||
<div class="card border-danger">
|
||||
<div class="card-body">
|
||||
<p>{{'dangerZoneDesc' | i18n}}</p>
|
||||
<button type="button" class="btn btn-outline-danger" (click)="deleteOrganization()">{{'deleteOrganization' | i18n}}</button>
|
||||
<button type="button" class="btn btn-outline-danger"
|
||||
(click)="deleteOrganization()">{{'deleteOrganization' | i18n}}</button>
|
||||
<button type="button" class="btn btn-outline-danger" (click)="purgeVault()">{{'purgeVault' | i18n}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #deleteOrganizationTemplate></ng-template>
|
||||
<ng-template #purgeOrganizationTemplate></ng-template>
|
||||
<ng-template #apiKeyTemplate></ng-template>
|
||||
<ng-template #rotateApiKeyTemplate></ng-template>
|
||||
|
||||
@@ -17,7 +17,10 @@ import { OrganizationUpdateRequest } from 'jslib/models/request/organizationUpda
|
||||
import { OrganizationResponse } from 'jslib/models/response/organizationResponse';
|
||||
|
||||
import { ModalComponent } from '../../modal.component';
|
||||
import { PurgeVaultComponent } from '../../settings/purge-vault.component';
|
||||
import { ApiKeyComponent } from './api-key.component';
|
||||
import { DeleteOrganizationComponent } from './delete-organization.component';
|
||||
import { RotateApiKeyComponent } from './rotate-api-key.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-org-account',
|
||||
@@ -25,8 +28,12 @@ import { DeleteOrganizationComponent } from './delete-organization.component';
|
||||
})
|
||||
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;
|
||||
|
||||
loading = true;
|
||||
canUseApi = false;
|
||||
org: OrganizationResponse;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
@@ -43,6 +50,7 @@ export class AccountComponent {
|
||||
this.organizationId = params.organizationId;
|
||||
try {
|
||||
this.org = await this.apiService.getOrganization(this.organizationId);
|
||||
this.canUseApi = this.org.useApi;
|
||||
} catch { }
|
||||
});
|
||||
this.loading = false;
|
||||
@@ -78,4 +86,49 @@ export class AccountComponent {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
purgeVault() {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.purgeModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<PurgeVaultComponent>(PurgeVaultComponent, this.purgeModalRef);
|
||||
childComponent.organizationId = this.organizationId;
|
||||
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
viewApiKey() {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
rotateApiKey() {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="card-body">
|
||||
<button type="button" class="close" appA11yTitle="{{'cancel' | i18n}}" (click)="cancel()"><span
|
||||
aria-hidden="true">×</span></button>
|
||||
<h3 class="card-body-header">{{(add ? 'addSeats' : 'removeSeats') | i18n}}</h3>
|
||||
<div class="row">
|
||||
<div class="form-group col-6">
|
||||
<label for="seatAdjustment">{{(add ? 'seatsToAdd' : 'seatsToRemove') | i18n}}</label>
|
||||
<input id="seatAdjustment" class="form-control" type="number" name="SeatAdjustment" [(ngModel)]="seatAdjustment" min="0"
|
||||
step="1" required>
|
||||
<input id="seatAdjustment" class="form-control" type="number" name="SeatAdjustment"
|
||||
[(ngModel)]="seatAdjustment" min="0" step="1" required>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="add" class="mb-3">
|
||||
@@ -13,7 +15,7 @@
|
||||
| currency:'$'}} /{{interval | i18n}}
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'submit' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()">
|
||||
@@ -24,3 +26,4 @@
|
||||
</small>
|
||||
</div>
|
||||
</form>
|
||||
<app-payment [showMethods]="false"></app-payment>
|
||||
|
||||
@@ -3,8 +3,14 @@ import {
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
@@ -13,6 +19,8 @@ import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
|
||||
import { SeatRequest } from 'jslib/models/request/seatRequest';
|
||||
|
||||
import { PaymentComponent } from '../../settings/payment.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-adjust-seats',
|
||||
templateUrl: 'adjust-seats.component.html',
|
||||
@@ -25,11 +33,14 @@ export class AdjustSeatsComponent {
|
||||
@Output() onAdjusted = new EventEmitter<number>();
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
|
||||
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
||||
|
||||
seatAdjustment = 0;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService) { }
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private router: Router, private activatedRoute: ActivatedRoute) { }
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
@@ -39,12 +50,32 @@ export class AdjustSeatsComponent {
|
||||
request.seatAdjustment *= -1;
|
||||
}
|
||||
|
||||
this.formPromise = this.apiService.postOrganizationSeat(this.organizationId, request);
|
||||
let paymentFailed = false;
|
||||
const action = async () => {
|
||||
const result = await this.apiService.postOrganizationSeat(this.organizationId, request);
|
||||
if (result != null && result.paymentIntentClientSecret != null) {
|
||||
try {
|
||||
await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null);
|
||||
} catch {
|
||||
paymentFailed = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
this.formPromise = action();
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({ action: this.add ? 'Added Seats' : 'Removed Seats' });
|
||||
this.toasterService.popAsync('success', null,
|
||||
this.i18nService.t('adjustedSeats', request.seatAdjustment.toString()));
|
||||
this.onAdjusted.emit(this.seatAdjustment);
|
||||
if (paymentFailed) {
|
||||
this.toasterService.popAsync({
|
||||
body: this.i18nService.t('couldNotChargeCardPayInvoice'),
|
||||
type: 'warning',
|
||||
timeout: 10000,
|
||||
});
|
||||
this.router.navigate(['../billing'], { relativeTo: this.activatedRoute });
|
||||
} else {
|
||||
this.toasterService.popAsync('success', null,
|
||||
this.i18nService.t('adjustedSeats', request.seatAdjustment.toString()));
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
|
||||
48
src/app/organizations/settings/api-key.component.html
Normal file
48
src/app/organizations/settings/api-key.component.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="apiKeyTitle">
|
||||
<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="apiKeyTitle">{{'apiKey' | 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>{{'apiKeyDesc' | 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>{{'viewApiKey' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
50
src/app/organizations/settings/api-key.component.ts
Normal file
50
src/app/organizations/settings/api-key.component.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
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-api-key',
|
||||
templateUrl: 'api-key.component.html',
|
||||
})
|
||||
export class ApiKeyComponent {
|
||||
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.postOrganizationApiKey(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: 'Viewed Organization API Key' });
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
11
src/app/organizations/settings/change-plan.component.html
Normal file
11
src/app/organizations/settings/change-plan.component.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<div class="card card-org-plans">
|
||||
<div class="card-body">
|
||||
<button type="button" class="close" appA11yTitle="{{'cancel' | i18n}}" (click)="cancel()"><span
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
34
src/app/organizations/settings/change-plan.component.ts
Normal file
34
src/app/organizations/settings/change-plan.component.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-change-plan',
|
||||
templateUrl: 'change-plan.component.html',
|
||||
})
|
||||
export class ChangePlanComponent {
|
||||
@Input() organizationId: string;
|
||||
@Output() onChanged = new EventEmitter();
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
|
||||
formPromise: Promise<any>;
|
||||
|
||||
constructor(private apiService: ApiService, private platformUtilsService: PlatformUtilsService) { }
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
this.platformUtilsService.eventTrack('Changed Plan');
|
||||
this.onChanged.emit();
|
||||
} catch { }
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.onCanceled.emit();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
<div class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="deleteOrganizationTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">{{'deleteOrganization' | i18n}}</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
|
||||
<h2 class="modal-title" id="deleteOrganizationTitle">{{'deleteOrganization' | i18n}}</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -11,12 +11,12 @@
|
||||
<p>{{'deleteOrganizationDesc' | i18n}}</p>
|
||||
<app-callout type="warning">{{'deleteOrganizationWarning' | i18n}}</app-callout>
|
||||
<label for="masterPassword">{{'masterPass' | i18n}}</label>
|
||||
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control" [(ngModel)]="masterPassword" required
|
||||
appAutofocus appInputVerbatim>
|
||||
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control"
|
||||
[(ngModel)]="masterPassword" required appAutofocus appInputVerbatim>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'deleteOrganization' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="card-body">
|
||||
<button type="button" class="close" appA11yTitle="{{'cancel' | i18n}}" (click)="cancel()"><span
|
||||
aria-hidden="true">×</span></button>
|
||||
<h3 class="card-body-header">{{'downloadLicense' | i18n}}</h3>
|
||||
<div class="row">
|
||||
<div class="form-group col-6">
|
||||
<div class="d-flex">
|
||||
<label for="installationId">{{'enterInstallationId' | i18n}}</label>
|
||||
<a class="ml-auto" target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}"
|
||||
href="https://help.bitwarden.com/article/licensing-on-premise/#organization-account-sharing">
|
||||
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
<input id="installationId" class="form-control" type="text" name="InstallationId"
|
||||
[(ngModel)]="installationId" required>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'submit' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()">
|
||||
{{'cancel' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
43
src/app/organizations/settings/download-license.component.ts
Normal file
43
src/app/organizations/settings/download-license.component.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-download-license',
|
||||
templateUrl: 'download-license.component.html',
|
||||
})
|
||||
export class DownloadLicenseComponent {
|
||||
@Input() organizationId: string;
|
||||
@Output() onDownloaded = new EventEmitter();
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
|
||||
installationId: string;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
constructor(private apiService: ApiService, private platformUtilsService: PlatformUtilsService) { }
|
||||
|
||||
async submit() {
|
||||
if (this.installationId == null || this.installationId === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.formPromise = this.apiService.getOrganizationLicense(this.organizationId, this.installationId);
|
||||
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 { }
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.onCanceled.emit();
|
||||
}
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
{{'billingAndLicensing' | i18n}}
|
||||
<small>
|
||||
<i class="fa fa-spinner fa-spin text-muted" *ngIf="firstLoaded && loading" title="{{'loading' | i18n}}"></i>
|
||||
</small>
|
||||
</h1>
|
||||
</div>
|
||||
<i class="fa fa-spinner fa-spin text-muted" *ngIf="!firstLoaded && loading" title="{{'loading' | i18n}}"></i>
|
||||
<ng-container *ngIf="billing">
|
||||
<app-callout type="warning" title="{{'canceled' | i18n}}" *ngIf="subscription && subscription.cancelled">{{'subscriptionCanceled' | i18n}}</app-callout>
|
||||
<app-callout type="warning" title="{{'pendingCancellation' | i18n}}" *ngIf="subscriptionMarkedForCancel">
|
||||
<p>{{'subscriptionPendingCanceled' | i18n}}</p>
|
||||
<button #reinstateBtn type="button" class="btn btn-outline-secondary btn-submit" (click)="reinstate()" [appApiAction]="reinstatePromise"
|
||||
[disabled]="reinstateBtn.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'reinstateSubscription' | i18n}}</span>
|
||||
</button>
|
||||
</app-callout>
|
||||
<dl *ngIf="selfHosted">
|
||||
<dt>{{'billingPlan' | i18n}}</dt>
|
||||
<dd>{{billing.plan}}</dd>
|
||||
<dt>{{'expiration' | i18n}}</dt>
|
||||
<dd *ngIf="billing.expiration">
|
||||
{{billing.expiration | date:'mediumDate'}}
|
||||
<span *ngIf="isExpired" class="text-danger ml-2">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
{{'licenseIsExpired' | i18n}}
|
||||
</span>
|
||||
</dd>
|
||||
<dd *ngIf="!billing.expiration">{{'neverExpires' | i18n}}</dd>
|
||||
</dl>
|
||||
<div class="row" *ngIf="!selfHosted">
|
||||
<div class="col-4">
|
||||
<dl>
|
||||
<dt>{{'billingPlan' | i18n}}</dt>
|
||||
<dd>{{billing.plan}}</dd>
|
||||
<ng-container *ngIf="subscription">
|
||||
<dt>{{'status' | i18n}}</dt>
|
||||
<dd>
|
||||
<span class="text-capitalize">{{subscription.status || '-'}}</span>
|
||||
<span class="badge badge-warning" *ngIf="subscriptionMarkedForCancel">{{'pendingCancellation' | i18n}}</span>
|
||||
</dd>
|
||||
<dt>{{'nextCharge' | i18n}}</dt>
|
||||
<dd>{{nextInvoice ? ((nextInvoice.date | date: 'mediumDate') + ', ' + (nextInvoice.amount | currency:'$'))
|
||||
: '-'}}
|
||||
</dd>
|
||||
</ng-container>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-8" *ngIf="subscription">
|
||||
<strong class="d-block mb-1">{{'details' | i18n}}</strong>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr *ngFor="let i of subscription.items">
|
||||
<td>
|
||||
{{i.name}} {{i.quantity > 1 ? '×' + i.quantity : ''}} @ {{i.amount | currency:'$'}}
|
||||
</td>
|
||||
<td>
|
||||
{{(i.quantity * i.amount) | currency:'$'}} /{{i.interval | i18n}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="selfHosted">
|
||||
<div>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="updateLicense()">
|
||||
{{'updateLicense' | i18n}}
|
||||
</button>
|
||||
<a href="https://vault.bitwarden.com" target="_blank" rel="noopener" class="btn btn-outline-secondary">
|
||||
{{'manageSubscription' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
<div class="card mt-3" *ngIf="showUpdateLicense">
|
||||
<div class="card-body">
|
||||
<h3 class="card-body-header">{{'updateLicense' | i18n}}</h3>
|
||||
<app-update-license [organizationId]="organizationId" (onUpdated)="closeUpdateLicense(true)" (onCanceled)="closeUpdateLicense(false)"></app-update-license>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!selfHosted">
|
||||
<div class="d-flex">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="changePlan()">
|
||||
{{'changeBillingPlan' | i18n}}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-submit ml-1" #licenseBtn [appApiAction]="licensePromise" [disabled]="licenseBtn.loading"
|
||||
(click)="downloadLicense()" *ngIf="canDownloadLicense">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'downloadLicense' | i18n}}</span>
|
||||
</button>
|
||||
<button #cancelBtn type="button" class="btn btn-outline-danger btn-submit ml-auto" (click)="cancel()" [appApiAction]="cancelPromise"
|
||||
[disabled]="cancelBtn.loading" *ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'cancelSubscription' | i18n}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<h2 class="spaced-header">{{'userSeats' | i18n}}</h2>
|
||||
<p>{{'subscriptionUserSeats' | i18n : billing.seats}}</p>
|
||||
<ng-container *ngIf="subscription && canAdjustSeats">
|
||||
<div class="mt-3">
|
||||
<div class="d-flex" *ngIf="!showAdjustSeats">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="adjustSeats(true)">
|
||||
{{'addSeats' | i18n}}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary ml-1" (click)="adjustSeats(false)">
|
||||
{{'removeSeats' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
<app-adjust-seats [seatPrice]="seatPrice" [add]="adjustSeatsAdd" [organizationId]="organizationId" [interval]="billingInterval"
|
||||
(onAdjusted)="closeSeats(true)" (onCanceled)="closeSeats(false)" *ngIf="showAdjustSeats"></app-adjust-seats>
|
||||
</div>
|
||||
</ng-container>
|
||||
<h2 class="spaced-header">{{'storage' | i18n}}</h2>
|
||||
<p>{{'subscriptionStorage' | i18n : billing.maxStorageGb || 0 : billing.storageName || '0 MB'}}</p>
|
||||
<div class="progress">
|
||||
<div class="progress-bar bg-success" role="progressbar" [ngStyle]="{width: storageProgressWidth + '%' }" [attr.aria-valuenow]="storagePercentage"
|
||||
aria-valuemin="0" aria-valuemax="100">{{(storagePercentage / 100) | percent}}</div>
|
||||
</div>
|
||||
<ng-container *ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel && paymentSource">
|
||||
<div class="mt-3">
|
||||
<div class="d-flex" *ngIf="!showAdjustStorage">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="adjustStorage(true)">
|
||||
{{'addStorage' | i18n}}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary ml-1" (click)="adjustStorage(false)">
|
||||
{{'removeStorage' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
<app-adjust-storage [storageGbPrice]="storageGbPrice" [add]="adjustStorageAdd" [organizationId]="organizationId" [interval]="billingInterval"
|
||||
(onAdjusted)="closeStorage(true)" (onCanceled)="closeStorage(false)" *ngIf="showAdjustStorage"></app-adjust-storage>
|
||||
</div>
|
||||
</ng-container>
|
||||
<h2 class="spaced-header">{{'paymentMethod' | i18n}}</h2>
|
||||
<p *ngIf="!paymentSource">{{'noPaymentMethod' | i18n}}</p>
|
||||
<ng-container *ngIf="paymentSource">
|
||||
<app-callout type="warning" title="{{'verifyBankAccount' | i18n}}" *ngIf="paymentSource.type === paymentMethodType.BankAccount && paymentSource.needsVerification">
|
||||
<p>{{'verifyBankAccountDesc' | i18n}} {{'verifyBankAccountFailureWarning' | i18n}}</p>
|
||||
<form #verifyForm class="form-inline" (ngSubmit)="verifyBank()" [appApiAction]="verifyBankPromise" ngNativeValidate>
|
||||
<label class="sr-only" for="verifyAmount1">{{'amount' | i18n : '1'}}</label>
|
||||
<div class="input-group mr-2">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">$0.</div>
|
||||
</div>
|
||||
<input type="number" class="form-control" id="verifyAmount1" placeholder="xx" name="Amount1" [(ngModel)]="verifyAmount1"
|
||||
min="1" max="99" step="1" required>
|
||||
</div>
|
||||
<label class="sr-only" for="verifyAmount2">{{'amount' | i18n : '2'}}</label>
|
||||
<div class="input-group mr-2">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">$0.</div>
|
||||
</div>
|
||||
<input type="number" class="form-control" id="verifyAmount2" placeholder="xx" name="Amount2" [(ngModel)]="verifyAmount2"
|
||||
min="1" max="99" step="1" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-outline-primary btn-submit" [disabled]="verifyForm.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"></i>
|
||||
<span>{{'verifyBankAccount' | i18n}}</span>
|
||||
</button>
|
||||
</form>
|
||||
</app-callout>
|
||||
<p>
|
||||
<i class="fa fa-fw" [ngClass]="{'fa-credit-card': paymentSource.type === paymentMethodType.Card,
|
||||
'fa-university': paymentSource.type === paymentMethodType.BankAccount,
|
||||
'fa-paypal text-primary': paymentSource.type === paymentMethodType.PayPal}"></i>
|
||||
{{paymentSource.description}}
|
||||
</p>
|
||||
</ng-container>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="changePayment()" *ngIf="!showAdjustPayment">
|
||||
{{(paymentSource ? 'changePaymentMethod' : 'addPaymentMethod') | i18n}}
|
||||
</button>
|
||||
<app-adjust-payment [currentType]="paymentSource != null ? paymentSource.type : null" [organizationId]="organizationId" (onAdjusted)="closePayment(true)"
|
||||
(onCanceled)="closePayment(false)" *ngIf="showAdjustPayment">
|
||||
</app-adjust-payment>
|
||||
<h2 class="spaced-header">{{'charges' | i18n}}</h2>
|
||||
<p *ngIf="!charges || !charges.length">{{'noCharges' | i18n}}</p>
|
||||
<table class="table mb-2" *ngIf="charges && charges.length">
|
||||
<tbody>
|
||||
<tr *ngFor="let c of charges">
|
||||
<td>
|
||||
<a href="#" appStopClick (click)="viewInvoice(c)" title="{{'invoice' | i18n}}">
|
||||
<i class="fa fa-file-pdf-o"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td>{{c.createdDate | date:'mediumDate'}}</td>
|
||||
<td>{{c.paymentSource ? c.paymentSource.description : '-'}}</td>
|
||||
<td class="text-capitalize">{{c.status}}</td>
|
||||
<td [ngClass]="{'text-strike':c.refunded}" title="{{(c.refunded ? 'refunded' : '') | i18n}}">{{c.amount | currency:'$'}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<small class="text-muted">* {{'chargesStatement' | i18n : 'BITWARDEN'}}</small>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
@@ -7,49 +7,21 @@ import { ActivatedRoute } from '@angular/router';
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { VerifyBankRequest } from 'jslib/models/request/verifyBankRequest';
|
||||
|
||||
import { BillingChargeResponse } from 'jslib/models/response/billingResponse';
|
||||
import { OrganizationBillingResponse } from 'jslib/models/response/organizationBillingResponse';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { TokenService } from 'jslib/abstractions/token.service';
|
||||
|
||||
import { PaymentMethodType } from 'jslib/enums/paymentMethodType';
|
||||
import { PlanType } from 'jslib/enums/planType';
|
||||
import { UserBillingComponent } from '../../settings/user-billing.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-org-billing',
|
||||
templateUrl: 'organization-billing.component.html',
|
||||
templateUrl: '../../settings/user-billing.component.html',
|
||||
})
|
||||
export class OrganizationBillingComponent implements OnInit {
|
||||
loading = false;
|
||||
firstLoaded = false;
|
||||
organizationId: string;
|
||||
adjustSeatsAdd = true;
|
||||
showAdjustSeats = false;
|
||||
adjustStorageAdd = true;
|
||||
showAdjustStorage = false;
|
||||
showAdjustPayment = false;
|
||||
showUpdateLicense = false;
|
||||
billing: OrganizationBillingResponse;
|
||||
paymentMethodType = PaymentMethodType;
|
||||
selfHosted = false;
|
||||
verifyAmount1: number;
|
||||
verifyAmount2: number;
|
||||
|
||||
cancelPromise: Promise<any>;
|
||||
reinstatePromise: Promise<any>;
|
||||
licensePromise: Promise<any>;
|
||||
verifyBankPromise: Promise<any>;
|
||||
|
||||
constructor(private tokenService: TokenService, private apiService: ApiService,
|
||||
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private route: ActivatedRoute) {
|
||||
this.selfHosted = platformUtilsService.isSelfHost();
|
||||
export class OrganizationBillingComponent extends UserBillingComponent implements OnInit {
|
||||
constructor(apiService: ApiService, i18nService: I18nService,
|
||||
analytics: Angulartics2, toasterService: ToasterService,
|
||||
private route: ActivatedRoute, platformUtilsService: PlatformUtilsService) {
|
||||
super(apiService, i18nService, analytics, toasterService, platformUtilsService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -59,222 +31,4 @@ export class OrganizationBillingComponent implements OnInit {
|
||||
this.firstLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
this.billing = await this.apiService.getOrganizationBilling(this.organizationId);
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async reinstate() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('reinstateConfirmation'),
|
||||
this.i18nService.t('reinstateSubscription'), this.i18nService.t('yes'), this.i18nService.t('cancel'));
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 { }
|
||||
}
|
||||
|
||||
async cancel() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('cancelConfirmation'),
|
||||
this.i18nService.t('cancelSubscription'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 { }
|
||||
}
|
||||
|
||||
async changePlan() {
|
||||
const contactSupport = await this.platformUtilsService.showDialog(this.i18nService.t('changeBillingPlanDesc'),
|
||||
this.i18nService.t('changeBillingPlan'), this.i18nService.t('contactSupport'), this.i18nService.t('close'));
|
||||
if (contactSupport) {
|
||||
this.platformUtilsService.launchUri('https://bitwarden.com/contact');
|
||||
}
|
||||
}
|
||||
|
||||
async downloadLicense() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const installationId = window.prompt(this.i18nService.t('enterInstallationId'));
|
||||
if (installationId == null || installationId === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.licensePromise = this.apiService.getOrganizationLicense(this.organizationId, installationId);
|
||||
const license = await this.licensePromise;
|
||||
const licenseString = JSON.stringify(license, null, 2);
|
||||
this.platformUtilsService.saveFile(window, licenseString, null, 'bitwarden_organization_license.json');
|
||||
} catch { }
|
||||
}
|
||||
|
||||
updateLicense() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
this.showUpdateLicense = true;
|
||||
}
|
||||
|
||||
async verifyBank() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const request = new VerifyBankRequest();
|
||||
request.amount1 = this.verifyAmount1;
|
||||
request.amount2 = this.verifyAmount2;
|
||||
this.verifyBankPromise = this.apiService.postOrganizationVerifyBank(this.organizationId, request);
|
||||
await this.verifyBankPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Verified Bank Account' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('verifiedBankAccount'));
|
||||
this.load();
|
||||
} catch { }
|
||||
}
|
||||
|
||||
closeUpdateLicense(load: boolean) {
|
||||
this.showUpdateLicense = false;
|
||||
if (load) {
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
adjustSeats(add: boolean) {
|
||||
this.adjustSeatsAdd = add;
|
||||
this.showAdjustSeats = true;
|
||||
}
|
||||
|
||||
closeSeats(load: boolean) {
|
||||
this.showAdjustSeats = false;
|
||||
if (load) {
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
adjustStorage(add: boolean) {
|
||||
this.adjustStorageAdd = add;
|
||||
this.showAdjustStorage = true;
|
||||
}
|
||||
|
||||
closeStorage(load: boolean) {
|
||||
this.showAdjustStorage = false;
|
||||
if (load) {
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
changePayment() {
|
||||
this.showAdjustPayment = true;
|
||||
}
|
||||
|
||||
closePayment(load: boolean) {
|
||||
this.showAdjustPayment = false;
|
||||
if (load) {
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
async viewInvoice(charge: BillingChargeResponse) {
|
||||
const token = await this.tokenService.getToken();
|
||||
const url = this.apiService.apiBaseUrl + '/organizations/' + this.organizationId +
|
||||
'/billing-invoice/' + charge.invoiceId + '?access_token=' + token;
|
||||
this.platformUtilsService.launchUri(url);
|
||||
}
|
||||
|
||||
get isExpired() {
|
||||
return this.billing != null && this.billing.expiration != null &&
|
||||
new Date(this.billing.expiration) < new Date();
|
||||
}
|
||||
|
||||
get subscriptionMarkedForCancel() {
|
||||
return this.subscription != null && !this.subscription.cancelled && this.subscription.cancelAtEndDate;
|
||||
}
|
||||
|
||||
get subscription() {
|
||||
return this.billing != null ? this.billing.subscription : null;
|
||||
}
|
||||
|
||||
get nextInvoice() {
|
||||
return this.billing != null ? this.billing.upcomingInvoice : null;
|
||||
}
|
||||
|
||||
get paymentSource() {
|
||||
return this.billing != null ? this.billing.paymentSource : null;
|
||||
}
|
||||
|
||||
get charges() {
|
||||
return this.billing != null ? this.billing.charges : null;
|
||||
}
|
||||
|
||||
get storagePercentage() {
|
||||
return this.billing != null && this.billing.maxStorageGb ?
|
||||
+(100 * (this.billing.storageGb / this.billing.maxStorageGb)).toFixed(2) : 0;
|
||||
}
|
||||
|
||||
get storageProgressWidth() {
|
||||
return this.storagePercentage < 5 ? 5 : 0;
|
||||
}
|
||||
|
||||
get billingInterval() {
|
||||
const monthly = this.billing.planType === PlanType.EnterpriseMonthly ||
|
||||
this.billing.planType === PlanType.TeamsMonthly;
|
||||
return monthly ? 'month' : 'year';
|
||||
}
|
||||
|
||||
get storageGbPrice() {
|
||||
return this.billingInterval === 'month' ? 0.5 : 4;
|
||||
}
|
||||
|
||||
get seatPrice() {
|
||||
switch (this.billing.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;
|
||||
}
|
||||
}
|
||||
|
||||
get canAdjustSeats() {
|
||||
return this.billing.planType === PlanType.EnterpriseMonthly ||
|
||||
this.billing.planType === PlanType.EnterpriseAnnually ||
|
||||
this.billing.planType === PlanType.TeamsMonthly || this.billing.planType === PlanType.TeamsAnnually;
|
||||
}
|
||||
|
||||
get canDownloadLicense() {
|
||||
return (this.billing.planType !== PlanType.Free && this.subscription == null) ||
|
||||
(this.subscription != null && !this.subscription.cancelled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
{{'subscription' | i18n}}
|
||||
<small *ngIf="firstLoaded && loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</small>
|
||||
</h1>
|
||||
</div>
|
||||
<ng-container *ngIf="!firstLoaded && loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="sub">
|
||||
<app-callout type="warning" title="{{'canceled' | i18n}}" *ngIf="subscription && subscription.cancelled">
|
||||
{{'subscriptionCanceled' | i18n}}</app-callout>
|
||||
<app-callout type="warning" title="{{'pendingCancellation' | i18n}}" *ngIf="subscriptionMarkedForCancel">
|
||||
<p>{{'subscriptionPendingCanceled' | i18n}}</p>
|
||||
<button #reinstateBtn type="button" class="btn btn-outline-secondary btn-submit" (click)="reinstate()"
|
||||
[appApiAction]="reinstatePromise" [disabled]="reinstateBtn.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'reinstateSubscription' | i18n}}</span>
|
||||
</button>
|
||||
</app-callout>
|
||||
<dl *ngIf="selfHosted">
|
||||
<dt>{{'billingPlan' | i18n}}</dt>
|
||||
<dd>{{sub.plan}}</dd>
|
||||
<dt>{{'expiration' | i18n}}</dt>
|
||||
<dd *ngIf="sub.expiration">
|
||||
{{sub.expiration | date:'mediumDate'}}
|
||||
<span *ngIf="isExpired" class="text-danger ml-2">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
{{'licenseIsExpired' | i18n}}
|
||||
</span>
|
||||
</dd>
|
||||
<dd *ngIf="!sub.expiration">{{'neverExpires' | i18n}}</dd>
|
||||
</dl>
|
||||
<div class="row" *ngIf="!selfHosted">
|
||||
<div class="col-4">
|
||||
<dl>
|
||||
<dt>{{'billingPlan' | i18n}}</dt>
|
||||
<dd>{{sub.plan}}</dd>
|
||||
<ng-container *ngIf="subscription">
|
||||
<dt>{{'status' | i18n}}</dt>
|
||||
<dd>
|
||||
<span class="text-capitalize">{{subscription.status || '-'}}</span>
|
||||
<span class="badge badge-warning"
|
||||
*ngIf="subscriptionMarkedForCancel">{{'pendingCancellation' | i18n}}</span>
|
||||
</dd>
|
||||
<dt>{{'nextCharge' | i18n}}</dt>
|
||||
<dd>{{nextInvoice ? ((nextInvoice.date | date: 'mediumDate') + ', ' + (nextInvoice.amount | currency:'$'))
|
||||
: '-'}}
|
||||
</dd>
|
||||
</ng-container>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-8" *ngIf="subscription">
|
||||
<strong class="d-block mb-1">{{'details' | i18n}}</strong>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr *ngFor="let i of subscription.items">
|
||||
<td>
|
||||
{{i.name}} {{i.quantity > 1 ? '×' + i.quantity : ''}} @ {{i.amount | currency:'$'}}
|
||||
</td>
|
||||
<td>
|
||||
{{(i.quantity * i.amount) | currency:'$'}} /{{i.interval | i18n}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="selfHosted">
|
||||
<div>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="updateLicense()">
|
||||
{{'updateLicense' | i18n}}
|
||||
</button>
|
||||
<a href="https://vault.bitwarden.com" target="_blank" rel="noopener" class="btn btn-outline-secondary">
|
||||
{{'manageSubscription' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
<div class="card mt-3" *ngIf="showUpdateLicense">
|
||||
<div class="card-body">
|
||||
<button type="button" class="close" appA11yTitle="{{'cancel' | i18n}}"
|
||||
(click)="closeUpdateLicense(false)"><span aria-hidden="true">×</span></button>
|
||||
<h3 class="card-body-header">{{'updateLicense' | i18n}}</h3>
|
||||
<app-update-license [organizationId]="organizationId" (onUpdated)="closeUpdateLicense(true)"
|
||||
(onCanceled)="closeUpdateLicense(false)"></app-update-license>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!selfHosted">
|
||||
<div class="d-flex">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="changePlan()" *ngIf="!showChangePlan">
|
||||
{{'changeBillingPlan' | i18n}}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary ml-1" (click)="downloadLicense()"
|
||||
*ngIf="canDownloadLicense" [disabled]="showDownloadLicense">
|
||||
{{'downloadLicense' | i18n}}
|
||||
</button>
|
||||
<button #cancelBtn type="button" class="btn btn-outline-danger btn-submit ml-auto" (click)="cancel()"
|
||||
[appApiAction]="cancelPromise" [disabled]="cancelBtn.loading"
|
||||
*ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'cancelSubscription' | i18n}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<app-change-plan [organizationId]="organizationId" (onChanged)="closeChangePlan(true)"
|
||||
(onCanceled)="closeChangePlan(false)" *ngIf="showChangePlan"></app-change-plan>
|
||||
<div class="mt-3" *ngIf="showDownloadLicense">
|
||||
<app-download-license [organizationId]="organizationId" (onDownloaded)="closeDownloadLicense()"
|
||||
(onCanceled)="closeDownloadLicense()"></app-download-license>
|
||||
</div>
|
||||
<h2 class="spaced-header">{{'userSeats' | i18n}}</h2>
|
||||
<p>{{'subscriptionUserSeats' | i18n : sub.seats}}</p>
|
||||
<ng-container *ngIf="subscription && canAdjustSeats && !subscription.cancelled && !subscriptionMarkedForCancel">
|
||||
<div class="mt-3">
|
||||
<div class="d-flex" *ngIf="!showAdjustSeats">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="adjustSeats(true)">
|
||||
{{'addSeats' | i18n}}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary ml-1" (click)="adjustSeats(false)">
|
||||
{{'removeSeats' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
<app-adjust-seats [seatPrice]="seatPrice" [add]="adjustSeatsAdd" [organizationId]="organizationId"
|
||||
[interval]="billingInterval" (onAdjusted)="closeSeats(true)" (onCanceled)="closeSeats(false)"
|
||||
*ngIf="showAdjustSeats"></app-adjust-seats>
|
||||
</div>
|
||||
</ng-container>
|
||||
<h2 class="spaced-header">{{'storage' | i18n}}</h2>
|
||||
<p>{{'subscriptionStorage' | i18n : sub.maxStorageGb || 0 : sub.storageName || '0 MB'}}</p>
|
||||
<div class="progress">
|
||||
<div class="progress-bar bg-success" role="progressbar" [ngStyle]="{width: storageProgressWidth + '%' }"
|
||||
[attr.aria-valuenow]="storagePercentage" aria-valuemin="0" aria-valuemax="100">
|
||||
{{(storagePercentage / 100) | percent}}</div>
|
||||
</div>
|
||||
<ng-container *ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel">
|
||||
<div class="mt-3">
|
||||
<div class="d-flex" *ngIf="!showAdjustStorage">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="adjustStorage(true)">
|
||||
{{'addStorage' | i18n}}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary ml-1" (click)="adjustStorage(false)">
|
||||
{{'removeStorage' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
<app-adjust-storage [storageGbPrice]="storageGbPrice" [add]="adjustStorageAdd"
|
||||
[organizationId]="organizationId" [interval]="billingInterval" (onAdjusted)="closeStorage(true)"
|
||||
(onCanceled)="closeStorage(false)" *ngIf="showAdjustStorage"></app-adjust-storage>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,229 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { OrganizationSubscriptionResponse } from 'jslib/models/response/organizationSubscriptionResponse';
|
||||
|
||||
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';
|
||||
|
||||
@Component({
|
||||
selector: 'app-org-subscription',
|
||||
templateUrl: 'organization-subscription.component.html',
|
||||
})
|
||||
export class OrganizationSubscriptionComponent implements OnInit {
|
||||
loading = false;
|
||||
firstLoaded = false;
|
||||
organizationId: string;
|
||||
adjustSeatsAdd = true;
|
||||
showAdjustSeats = false;
|
||||
adjustStorageAdd = true;
|
||||
showAdjustStorage = false;
|
||||
showUpdateLicense = false;
|
||||
showDownloadLicense = false;
|
||||
showChangePlan = false;
|
||||
sub: OrganizationSubscriptionResponse;
|
||||
selfHosted = false;
|
||||
|
||||
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,
|
||||
private messagingService: MessagingService, private route: ActivatedRoute) {
|
||||
this.selfHosted = platformUtilsService.isSelfHost();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organizationId = params.organizationId;
|
||||
await this.load();
|
||||
this.firstLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
this.sub = await this.apiService.getOrganizationSubscription(this.organizationId);
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async reinstate() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('reinstateConfirmation'),
|
||||
this.i18nService.t('reinstateSubscription'), this.i18nService.t('yes'), this.i18nService.t('cancel'));
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 { }
|
||||
}
|
||||
|
||||
async cancel() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('cancelConfirmation'),
|
||||
this.i18nService.t('cancelSubscription'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 { }
|
||||
}
|
||||
|
||||
async changePlan() {
|
||||
if (this.subscription == null && this.sub.planType === PlanType.Free) {
|
||||
this.showChangePlan = !this.showChangePlan;
|
||||
return;
|
||||
}
|
||||
const contactSupport = await this.platformUtilsService.showDialog(this.i18nService.t('changeBillingPlanDesc'),
|
||||
this.i18nService.t('changeBillingPlan'), this.i18nService.t('contactSupport'), this.i18nService.t('close'));
|
||||
if (contactSupport) {
|
||||
this.platformUtilsService.launchUri('https://bitwarden.com/contact');
|
||||
}
|
||||
}
|
||||
|
||||
closeChangePlan(changed: boolean) {
|
||||
this.showChangePlan = false;
|
||||
}
|
||||
|
||||
downloadLicense() {
|
||||
this.showDownloadLicense = !this.showDownloadLicense;
|
||||
}
|
||||
|
||||
closeDownloadLicense() {
|
||||
this.showDownloadLicense = false;
|
||||
}
|
||||
|
||||
updateLicense() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
this.showUpdateLicense = true;
|
||||
}
|
||||
|
||||
closeUpdateLicense(updated: boolean) {
|
||||
this.showUpdateLicense = false;
|
||||
if (updated) {
|
||||
this.load();
|
||||
this.messagingService.send('updatedOrgLicense');
|
||||
}
|
||||
}
|
||||
|
||||
adjustSeats(add: boolean) {
|
||||
this.adjustSeatsAdd = add;
|
||||
this.showAdjustSeats = true;
|
||||
}
|
||||
|
||||
closeSeats(load: boolean) {
|
||||
this.showAdjustSeats = false;
|
||||
if (load) {
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
adjustStorage(add: boolean) {
|
||||
this.adjustStorageAdd = add;
|
||||
this.showAdjustStorage = true;
|
||||
}
|
||||
|
||||
closeStorage(load: boolean) {
|
||||
this.showAdjustStorage = false;
|
||||
if (load) {
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
get isExpired() {
|
||||
return this.sub != null && this.sub.expiration != null &&
|
||||
new Date(this.sub.expiration) < new Date();
|
||||
}
|
||||
|
||||
get subscriptionMarkedForCancel() {
|
||||
return this.subscription != null && !this.subscription.cancelled && this.subscription.cancelAtEndDate;
|
||||
}
|
||||
|
||||
get subscription() {
|
||||
return this.sub != null ? this.sub.subscription : null;
|
||||
}
|
||||
|
||||
get nextInvoice() {
|
||||
return this.sub != null ? this.sub.upcomingInvoice : null;
|
||||
}
|
||||
|
||||
get storagePercentage() {
|
||||
return this.sub != null && this.sub.maxStorageGb ?
|
||||
+(100 * (this.sub.storageGb / this.sub.maxStorageGb)).toFixed(2) : 0;
|
||||
}
|
||||
|
||||
get storageProgressWidth() {
|
||||
return this.storagePercentage < 5 ? 5 : 0;
|
||||
}
|
||||
|
||||
get billingInterval() {
|
||||
const monthly = this.sub.planType === PlanType.EnterpriseMonthly ||
|
||||
this.sub.planType === PlanType.TeamsMonthly;
|
||||
return monthly ? 'month' : 'year';
|
||||
}
|
||||
|
||||
get storageGbPrice() {
|
||||
return this.billingInterval === 'month' ? 0.5 : 4;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
get canAdjustSeats() {
|
||||
return this.sub.planType === PlanType.EnterpriseMonthly ||
|
||||
this.sub.planType === PlanType.EnterpriseAnnually ||
|
||||
this.sub.planType === PlanType.TeamsMonthly || this.sub.planType === PlanType.TeamsAnnually;
|
||||
}
|
||||
|
||||
get canDownloadLicense() {
|
||||
return (this.sub.planType !== PlanType.Free && this.subscription == null) ||
|
||||
(this.subscription != null && !this.subscription.cancelled);
|
||||
}
|
||||
}
|
||||
48
src/app/organizations/settings/rotate-api-key.component.html
Normal file
48
src/app/organizations/settings/rotate-api-key.component.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<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>
|
||||
50
src/app/organizations/settings/rotate-api-key.component.ts
Normal file
50
src/app/organizations/settings/rotate-api-key.component.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
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 { }
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,11 @@
|
||||
<a routerLink="account" class="list-group-item" routerLinkActive="active">
|
||||
{{'myOrganization' | i18n}}
|
||||
</a>
|
||||
<a routerLink="billing" class="list-group-item" routerLinkActive="active">
|
||||
{{'billingAndLicensing' | i18n}}
|
||||
<a routerLink="subscription" class="list-group-item" routerLinkActive="active">
|
||||
{{'subscription' | i18n}}
|
||||
</a>
|
||||
<a routerLink="billing" class="list-group-item" routerLinkActive="active" *ngIf="!selfHosted">
|
||||
{{'billing' | i18n}}
|
||||
</a>
|
||||
<a routerLink="two-factor" class="list-group-item" routerLinkActive="active" *ngIf="access2fa">
|
||||
{{'twoStepLogin' | i18n}}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
@Component({
|
||||
@@ -9,11 +10,14 @@ import { UserService } from 'jslib/abstractions/user.service';
|
||||
})
|
||||
export class SettingsComponent {
|
||||
access2fa = false;
|
||||
selfHosted: boolean;
|
||||
|
||||
constructor(private route: ActivatedRoute, private userService: UserService) { }
|
||||
constructor(private route: ActivatedRoute, private userService: UserService,
|
||||
private platformUtilsService: PlatformUtilsService) { }
|
||||
|
||||
ngOnInit() {
|
||||
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;
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { PolicyService } from 'jslib/abstractions/policy.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType';
|
||||
@@ -20,8 +21,8 @@ import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from '../../se
|
||||
export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent {
|
||||
constructor(apiService: ApiService, userService: UserService,
|
||||
componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService,
|
||||
private route: ActivatedRoute) {
|
||||
super(apiService, userService, componentFactoryResolver, messagingService);
|
||||
policyService: PolicyService, private route: ActivatedRoute) {
|
||||
super(apiService, userService, componentFactoryResolver, messagingService, policyService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { EventService } from 'jslib/abstractions/event.service';
|
||||
import { ExportService } from 'jslib/abstractions/export.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
|
||||
import { ExportComponent as BaseExportComponent } from '../../tools/export.component';
|
||||
|
||||
import { EventType } from 'jslib/enums/eventType';
|
||||
|
||||
@Component({
|
||||
selector: 'app-org-export',
|
||||
templateUrl: '../../tools/export.component.html',
|
||||
@@ -18,11 +18,10 @@ import { ExportComponent as BaseExportComponent } from '../../tools/export.compo
|
||||
export class ExportComponent extends BaseExportComponent {
|
||||
organizationId: string;
|
||||
|
||||
constructor(analytics: Angulartics2, toasterService: ToasterService,
|
||||
cryptoService: CryptoService, i18nService: I18nService,
|
||||
constructor(cryptoService: CryptoService, i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService, exportService: ExportService,
|
||||
private route: ActivatedRoute) {
|
||||
super(analytics, toasterService, cryptoService, i18nService, platformUtilsService, exportService);
|
||||
eventService: EventService, private route: ActivatedRoute) {
|
||||
super(cryptoService, i18nService, platformUtilsService, exportService, eventService);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -32,10 +31,15 @@ export class ExportComponent extends BaseExportComponent {
|
||||
}
|
||||
|
||||
getExportData() {
|
||||
return this.exportService.getOrganizationExport(this.organizationId, 'csv');
|
||||
return this.exportService.getOrganizationExport(this.organizationId, this.format);
|
||||
}
|
||||
|
||||
getFileName() {
|
||||
return super.getFileName('org');
|
||||
}
|
||||
|
||||
async collectEvent(): Promise<any> {
|
||||
// TODO
|
||||
// await this.eventService.collect(EventType.Organization_ClientExportedVault);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { AuditService } from 'jslib/abstractions/audit.service';
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import {
|
||||
ExposedPasswordsReportComponent as BaseExposedPasswordsReportComponent,
|
||||
} from '../../tools/exposed-passwords-report.component';
|
||||
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
|
||||
@Component({
|
||||
selector: 'app-exposed-passwords-report',
|
||||
templateUrl: '../../tools/exposed-passwords-report.component.html',
|
||||
})
|
||||
export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportComponent {
|
||||
constructor(cipherService: CipherService, auditService: AuditService,
|
||||
componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService,
|
||||
userService: UserService, private route: ActivatedRoute) {
|
||||
super(cipherService, auditService, componentFactoryResolver, messagingService, userService);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organization = await this.userService.getOrganization(params.organizationId);
|
||||
super.ngOnInit();
|
||||
});
|
||||
}
|
||||
|
||||
getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import {
|
||||
InactiveTwoFactorReportComponent as BaseInactiveTwoFactorReportComponent,
|
||||
} from '../../tools/inactive-two-factor-report.component';
|
||||
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
|
||||
@Component({
|
||||
selector: 'app-inactive-two-factor-report',
|
||||
templateUrl: '../../tools/inactive-two-factor-report.component.html',
|
||||
})
|
||||
export class InactiveTwoFactorReportComponent extends BaseInactiveTwoFactorReportComponent {
|
||||
constructor(cipherService: CipherService, componentFactoryResolver: ComponentFactoryResolver,
|
||||
messagingService: MessagingService, userService: UserService,
|
||||
private route: ActivatedRoute) {
|
||||
super(cipherService, componentFactoryResolver, messagingService, userService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organization = await this.userService.getOrganization(params.organizationId);
|
||||
await super.ngOnInit();
|
||||
});
|
||||
}
|
||||
|
||||
getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
|
||||
import {
|
||||
ReusedPasswordsReportComponent as BaseReusedPasswordsReportComponent,
|
||||
} from '../../tools/reused-passwords-report.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-reused-passwords-report',
|
||||
templateUrl: '../../tools/reused-passwords-report.component.html',
|
||||
})
|
||||
export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportComponent {
|
||||
constructor(cipherService: CipherService, componentFactoryResolver: ComponentFactoryResolver,
|
||||
messagingService: MessagingService, userService: UserService,
|
||||
private route: ActivatedRoute) {
|
||||
super(cipherService, componentFactoryResolver, messagingService, userService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organization = await this.userService.getOrganization(params.organizationId);
|
||||
await super.ngOnInit();
|
||||
});
|
||||
}
|
||||
|
||||
getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="container page-content">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<div class="card">
|
||||
<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">
|
||||
@@ -12,6 +12,34 @@
|
||||
</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}}
|
||||
</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>
|
||||
|
||||
@@ -1,7 +1,32 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { Organization } from 'jslib/models/domain/organization';
|
||||
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-org-tools',
|
||||
templateUrl: 'tools.component.html',
|
||||
})
|
||||
export class ToolsComponent { }
|
||||
export class ToolsComponent {
|
||||
organization: Organization;
|
||||
accessReports = false;
|
||||
|
||||
constructor(private route: ActivatedRoute, private userService: UserService,
|
||||
private messagingService: MessagingService) { }
|
||||
|
||||
ngOnInit() {
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
upgradeOrganization() {
|
||||
this.messagingService.send('upgradeOrganization', { organizationId: this.organization.id });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import {
|
||||
UnsecuredWebsitesReportComponent as BaseUnsecuredWebsitesReportComponent,
|
||||
} from '../../tools/unsecured-websites-report.component';
|
||||
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
|
||||
@Component({
|
||||
selector: 'app-unsecured-websites-report',
|
||||
templateUrl: '../../tools/unsecured-websites-report.component.html',
|
||||
})
|
||||
export class UnsecuredWebsitesReportComponent extends BaseUnsecuredWebsitesReportComponent {
|
||||
constructor(cipherService: CipherService, componentFactoryResolver: ComponentFactoryResolver,
|
||||
messagingService: MessagingService, userService: UserService,
|
||||
private route: ActivatedRoute) {
|
||||
super(cipherService, componentFactoryResolver, messagingService, userService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organization = await this.userService.getOrganization(params.organizationId);
|
||||
await super.ngOnInit();
|
||||
});
|
||||
}
|
||||
|
||||
getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
|
||||
import {
|
||||
WeakPasswordsReportComponent as BaseWeakPasswordsReportComponent,
|
||||
} from '../../tools/weak-passwords-report.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-weak-passwords-report',
|
||||
templateUrl: '../../tools/weak-passwords-report.component.html',
|
||||
})
|
||||
export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportComponent {
|
||||
constructor(cipherService: CipherService, passwordGenerationService: PasswordGenerationService,
|
||||
componentFactoryResolver: ComponentFactoryResolver, messagingService: MessagingService,
|
||||
userService: UserService, private route: ActivatedRoute) {
|
||||
super(cipherService, passwordGenerationService, componentFactoryResolver, messagingService, userService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organization = await this.userService.getOrganization(params.organizationId);
|
||||
await super.ngOnInit();
|
||||
});
|
||||
}
|
||||
|
||||
getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,10 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { AuditService } from 'jslib/abstractions/audit.service';
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { CollectionService } from 'jslib/abstractions/collection.service';
|
||||
import { EventService } from 'jslib/abstractions/event.service';
|
||||
import { FolderService } from 'jslib/abstractions/folder.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
@@ -21,6 +17,7 @@ import { UserService } from 'jslib/abstractions/user.service';
|
||||
import { CipherData } from 'jslib/models/data/cipherData';
|
||||
import { Cipher } from 'jslib/models/domain/cipher';
|
||||
import { Organization } from 'jslib/models/domain/organization';
|
||||
import { CipherCreateRequest } from 'jslib/models/request/cipherCreateRequest';
|
||||
import { CipherRequest } from 'jslib/models/request/cipherRequest';
|
||||
|
||||
import { AddEditComponent as BaseAddEditComponent } from '../../vault/add-edit.component';
|
||||
@@ -29,20 +26,38 @@ import { AddEditComponent as BaseAddEditComponent } from '../../vault/add-edit.c
|
||||
selector: 'app-org-vault-add-edit',
|
||||
templateUrl: '../../vault/add-edit.component.html',
|
||||
})
|
||||
export class AddEditComponent extends BaseAddEditComponent implements OnInit {
|
||||
export class AddEditComponent extends BaseAddEditComponent {
|
||||
organization: Organization;
|
||||
originalCipher: Cipher = null;
|
||||
|
||||
constructor(cipherService: CipherService, folderService: FolderService,
|
||||
i18nService: I18nService, platformUtilsService: PlatformUtilsService,
|
||||
analytics: Angulartics2, toasterService: ToasterService,
|
||||
auditService: AuditService, stateService: StateService,
|
||||
userService: UserService, totpService: TotpService,
|
||||
passwordGenerationService: PasswordGenerationService, private apiService: ApiService,
|
||||
messagingService: MessagingService) {
|
||||
super(cipherService, folderService, i18nService, platformUtilsService, analytics,
|
||||
toasterService, auditService, stateService, userService, totpService, passwordGenerationService,
|
||||
messagingService);
|
||||
userService: UserService, collectionService: CollectionService,
|
||||
totpService: TotpService, passwordGenerationService: PasswordGenerationService,
|
||||
private apiService: ApiService, messagingService: MessagingService,
|
||||
eventService: EventService) {
|
||||
super(cipherService, folderService, i18nService, platformUtilsService, auditService, stateService,
|
||||
userService, collectionService, totpService, passwordGenerationService, messagingService,
|
||||
eventService);
|
||||
}
|
||||
|
||||
protected allowOwnershipAssignment() {
|
||||
if (this.ownershipOptions != null && this.ownershipOptions.length > 1) {
|
||||
if (this.organization != null) {
|
||||
return this.cloneMode && this.organization.isAdmin;
|
||||
} else {
|
||||
return !this.editMode || this.cloneMode;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected loadCollections() {
|
||||
if (!this.organization.isAdmin) {
|
||||
return super.loadCollections();
|
||||
}
|
||||
return Promise.resolve(this.collections);
|
||||
}
|
||||
|
||||
protected async loadCipher() {
|
||||
@@ -56,9 +71,6 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit {
|
||||
}
|
||||
|
||||
protected encryptCipher() {
|
||||
if (!this.editMode) {
|
||||
this.cipher.organizationId = this.organization.id;
|
||||
}
|
||||
if (!this.organization.isAdmin) {
|
||||
return super.encryptCipher();
|
||||
}
|
||||
@@ -66,13 +78,14 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit {
|
||||
}
|
||||
|
||||
protected async saveCipher(cipher: Cipher) {
|
||||
if (!this.organization.isAdmin) {
|
||||
if (!this.organization.isAdmin || cipher.organizationId == null) {
|
||||
return super.saveCipher(cipher);
|
||||
}
|
||||
const request = new CipherRequest(cipher);
|
||||
if (this.editMode) {
|
||||
if (this.editMode && !this.cloneMode) {
|
||||
const request = new CipherRequest(cipher);
|
||||
return this.apiService.putCipherAdmin(this.cipherId, request);
|
||||
} else {
|
||||
const request = new CipherCreateRequest(cipher);
|
||||
return this.apiService.postCipherAdmin(request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
@@ -14,6 +11,8 @@ import { CipherData } from 'jslib/models/data/cipherData';
|
||||
import { Cipher } from 'jslib/models/domain/cipher';
|
||||
import { Organization } from 'jslib/models/domain/organization';
|
||||
|
||||
import { AttachmentView } from 'jslib/models/view/attachmentView';
|
||||
|
||||
import { AttachmentsComponent as BaseAttachmentsComponent } from '../../vault/attachments.component';
|
||||
|
||||
@Component({
|
||||
@@ -23,12 +22,16 @@ import { AttachmentsComponent as BaseAttachmentsComponent } from '../../vault/at
|
||||
export class AttachmentsComponent extends BaseAttachmentsComponent {
|
||||
organization: Organization;
|
||||
|
||||
constructor(cipherService: CipherService, analytics: Angulartics2,
|
||||
toasterService: ToasterService, i18nService: I18nService,
|
||||
constructor(cipherService: CipherService, i18nService: I18nService,
|
||||
cryptoService: CryptoService, userService: UserService,
|
||||
platformUtilsService: PlatformUtilsService, private apiService: ApiService) {
|
||||
super(cipherService, analytics, toasterService, i18nService, cryptoService, userService,
|
||||
platformUtilsService);
|
||||
super(cipherService, i18nService, cryptoService, userService, platformUtilsService);
|
||||
}
|
||||
|
||||
protected async reupload(attachment: AttachmentView) {
|
||||
if (this.organization.isAdmin && this.showFixOldAttachments(attachment)) {
|
||||
await super.reuploadCipherAttachment(attachment, true);
|
||||
}
|
||||
}
|
||||
|
||||
protected async loadCipher() {
|
||||
@@ -49,4 +52,8 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
|
||||
}
|
||||
return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId);
|
||||
}
|
||||
|
||||
protected showFixOldAttachments(attachment: AttachmentView) {
|
||||
return attachment.key == null && this.organization.isAdmin;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,11 @@ import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { EventService } from 'jslib/abstractions/event.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { SearchService } from 'jslib/abstractions/search.service';
|
||||
|
||||
import { CipherData } from 'jslib/models/data/cipherData';
|
||||
import { Cipher } from 'jslib/models/domain/cipher';
|
||||
import { Organization } from 'jslib/models/domain/organization';
|
||||
import { CipherView } from 'jslib/models/view/cipherView';
|
||||
|
||||
@@ -35,31 +34,18 @@ export class CiphersComponent extends BaseCiphersComponent {
|
||||
constructor(searchService: SearchService, analytics: Angulartics2,
|
||||
toasterService: ToasterService, i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService, cipherService: CipherService,
|
||||
private apiService: ApiService) {
|
||||
super(searchService, analytics, toasterService, i18nService, platformUtilsService, cipherService);
|
||||
private apiService: ApiService, eventService: EventService) {
|
||||
super(searchService, analytics, toasterService, i18nService, platformUtilsService,
|
||||
cipherService, eventService);
|
||||
}
|
||||
|
||||
async load(filter: (cipher: CipherView) => boolean = null) {
|
||||
if (!this.organization.isAdmin) {
|
||||
await super.load();
|
||||
await super.load(filter);
|
||||
return;
|
||||
}
|
||||
this.accessEvents = this.organization.useEvents;
|
||||
const ciphers = await this.apiService.getCiphersOrganization(this.organization.id);
|
||||
if (ciphers != null && ciphers.data != null && ciphers.data.length) {
|
||||
const decCiphers: CipherView[] = [];
|
||||
const promises: any[] = [];
|
||||
ciphers.data.forEach((r) => {
|
||||
const data = new CipherData(r);
|
||||
const cipher = new Cipher(data);
|
||||
promises.push(cipher.decrypt().then((c) => decCiphers.push(c)));
|
||||
});
|
||||
await Promise.all(promises);
|
||||
decCiphers.sort(this.cipherService.getLocaleSortingFunction());
|
||||
this.allCiphers = decCiphers;
|
||||
} else {
|
||||
this.allCiphers = [];
|
||||
}
|
||||
this.allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organization.id);
|
||||
this.applyFilter(filter);
|
||||
this.loaded = true;
|
||||
}
|
||||
@@ -73,7 +59,7 @@ export class CiphersComponent extends BaseCiphersComponent {
|
||||
}
|
||||
}
|
||||
|
||||
search(timeout: number = null) {
|
||||
async search(timeout: number = null) {
|
||||
if (!this.organization.isAdmin) {
|
||||
return super.search(timeout);
|
||||
}
|
||||
@@ -87,6 +73,7 @@ export class CiphersComponent extends BaseCiphersComponent {
|
||||
} else {
|
||||
this.ciphers = this.searchService.searchCiphersBasic(filteredCiphers, this.searchText);
|
||||
}
|
||||
await this.resetPaging();
|
||||
}
|
||||
|
||||
checkCipher(c: CipherView) {
|
||||
@@ -103,4 +90,8 @@ export class CiphersComponent extends BaseCiphersComponent {
|
||||
}
|
||||
return this.apiService.deleteCipherAdmin(id);
|
||||
}
|
||||
|
||||
protected showFixOldAttachments(c: CipherView) {
|
||||
return this.organization.isAdmin && c.hasOldAttachments;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
import { CollectionService } from 'jslib/abstractions/collection.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
|
||||
import { CipherData } from 'jslib/models/data/cipherData';
|
||||
import { Cipher } from 'jslib/models/domain/cipher';
|
||||
@@ -22,10 +20,11 @@ import { CollectionsComponent as BaseCollectionsComponent } from '../../vault/co
|
||||
export class CollectionsComponent extends BaseCollectionsComponent {
|
||||
organization: Organization;
|
||||
|
||||
constructor(collectionService: CollectionService, analytics: Angulartics2,
|
||||
toasterService: ToasterService, i18nService: I18nService,
|
||||
cipherService: CipherService, private apiService: ApiService) {
|
||||
super(collectionService, analytics, toasterService, i18nService, cipherService);
|
||||
constructor(collectionService: CollectionService, platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService, cipherService: CipherService,
|
||||
private apiService: ApiService) {
|
||||
super(collectionService, platformUtilsService, i18nService, cipherService);
|
||||
this.allowSelectNone = true;
|
||||
}
|
||||
|
||||
protected async loadCipher() {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CollectionService } from 'jslib/abstractions/collection.service';
|
||||
import { FolderService } from 'jslib/abstractions/folder.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { CollectionData } from 'jslib/models/data/collectionData';
|
||||
import { Collection } from 'jslib/models/domain/collection';
|
||||
@@ -21,8 +23,9 @@ export class GroupingsComponent extends BaseGroupingsComponent {
|
||||
organization: Organization;
|
||||
|
||||
constructor(collectionService: CollectionService, folderService: FolderService,
|
||||
storageService: StorageService, userService: UserService,
|
||||
private apiService: ApiService, private i18nService: I18nService) {
|
||||
super(collectionService, folderService);
|
||||
super(collectionService, folderService, storageService, userService);
|
||||
}
|
||||
|
||||
async loadCollections() {
|
||||
@@ -30,6 +33,7 @@ export class GroupingsComponent extends BaseGroupingsComponent {
|
||||
await super.loadCollections(this.organization.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const collections = await this.apiService.getCollections(this.organization.id);
|
||||
if (collections != null && collections.data != null && collections.data.length) {
|
||||
const collectionDomains = collections.data.map((r) =>
|
||||
@@ -45,5 +49,14 @@ export class GroupingsComponent extends BaseGroupingsComponent {
|
||||
unassignedCollection.organizationId = this.organization.id;
|
||||
unassignedCollection.readOnly = true;
|
||||
this.collections.push(unassignedCollection);
|
||||
this.nestedCollections = await this.collectionService.getAllNested(this.collections);
|
||||
}
|
||||
|
||||
collapse(grouping: CollectionView) {
|
||||
super.collapse(grouping, 'org_');
|
||||
}
|
||||
|
||||
isCollapsed(grouping: CollectionView) {
|
||||
return super.isCollapsed(grouping, 'org_');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<div class="container page-content">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<app-org-vault-groupings [showFolders]="false" [showFavorites]="false" (onAllClicked)="clearGroupingFilters()" (onCipherTypeClicked)="filterCipherType($event)"
|
||||
<app-org-vault-groupings [showFolders]="false" [showFavorites]="false"
|
||||
(onAllClicked)="clearGroupingFilters()" (onCipherTypeClicked)="filterCipherType($event)"
|
||||
(onCollectionClicked)="filterCollection($event.id)" (onSearchTextChanged)="filterSearchText($event)">
|
||||
</app-org-vault-groupings>
|
||||
</div>
|
||||
@@ -10,15 +11,21 @@
|
||||
<h1>
|
||||
{{'vault' | i18n}}
|
||||
<small #actionSpinner [appApiAction]="ciphersComponent.actionPromise">
|
||||
<i *ngIf="actionSpinner.loading" class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
|
||||
<ng-container *ngIf="actionSpinner.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>
|
||||
</small>
|
||||
</h1>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm ml-auto" (click)="addCipher()" *ngIf="showAdd">
|
||||
<i class="fa fa-plus fa-fw"></i>{{'addItem' | i18n}}
|
||||
<button type="button" class="btn btn-outline-primary btn-sm ml-auto" (click)="addCipher()">
|
||||
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>{{'addItem' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
<app-org-vault-ciphers (onCipherClicked)="editCipher($event)" (onAttachmentsClicked)="editCipherAttachments($event)" (onAddCipher)="addCipher()"
|
||||
(onCollectionsClicked)="editCipherCollections($event)" (onEventsClicked)="viewEvents($event)">
|
||||
<app-org-vault-ciphers (onCipherClicked)="editCipher($event)"
|
||||
(onAttachmentsClicked)="editCipherAttachments($event)" (onAddCipher)="addCipher()"
|
||||
(onCollectionsClicked)="editCipherCollections($event)" (onEventsClicked)="viewEvents($event)"
|
||||
(onCloneClicked)="cloneCipher($event)">
|
||||
</app-org-vault-ciphers>
|
||||
</div>
|
||||
</div>
|
||||
@@ -26,4 +33,4 @@
|
||||
<ng-template #attachments></ng-template>
|
||||
<ng-template #cipherAddEdit></ng-template>
|
||||
<ng-template #collections></ng-template>
|
||||
<ng-template #eventsTemplate></ng-template>
|
||||
<ng-template #eventsTemplate></ng-template>
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Location } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
@@ -52,25 +51,22 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
organization: Organization;
|
||||
collectionId: string;
|
||||
type: CipherType;
|
||||
showAdd = true;
|
||||
|
||||
private modal: ModalComponent = null;
|
||||
|
||||
constructor(private route: ActivatedRoute, private userService: UserService,
|
||||
private location: Location, private router: Router,
|
||||
private router: Router, private changeDetectorRef: ChangeDetectorRef,
|
||||
private syncService: SyncService, private i18nService: I18nService,
|
||||
private componentFactoryResolver: ComponentFactoryResolver, private messagingService: MessagingService,
|
||||
private broadcasterService: BroadcasterService, private ngZone: NgZone,
|
||||
private changeDetectorRef: ChangeDetectorRef) { }
|
||||
private broadcasterService: BroadcasterService, private ngZone: NgZone) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.route.parent.params.subscribe(async (params) => {
|
||||
this.organization = await this.userService.getOrganization(params.organizationId);
|
||||
this.showAdd = this.organization.isAdmin;
|
||||
this.groupingsComponent.organization = this.organization;
|
||||
this.ciphersComponent.organization = this.organization;
|
||||
|
||||
this.route.queryParams.subscribe(async (qParams) => {
|
||||
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
|
||||
this.ciphersComponent.searchText = this.groupingsComponent.searchText = qParams.search;
|
||||
if (!this.organization.isAdmin) {
|
||||
await this.syncService.fullSync(false);
|
||||
@@ -94,7 +90,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (qParams == null) {
|
||||
this.groupingsComponent.selectedAll = true;
|
||||
await this.ciphersComponent.load();
|
||||
await this.ciphersComponent.reload();
|
||||
} else {
|
||||
if (qParams.type) {
|
||||
const t = parseInt(qParams.type, null);
|
||||
@@ -105,7 +101,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
await this.filterCollection(qParams.collectionId, true);
|
||||
} else {
|
||||
this.groupingsComponent.selectedAll = true;
|
||||
await this.ciphersComponent.load();
|
||||
await this.ciphersComponent.reload();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +111,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.viewEvents(cipher[0]);
|
||||
}
|
||||
}
|
||||
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -136,7 +136,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchType');
|
||||
const filter = (c: CipherView) => c.type === type;
|
||||
if (load) {
|
||||
await this.ciphersComponent.load(filter);
|
||||
await this.ciphersComponent.reload(filter);
|
||||
} else {
|
||||
await this.ciphersComponent.applyFilter(filter);
|
||||
}
|
||||
@@ -146,7 +146,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async filterCollection(collectionId: string, load = false) {
|
||||
this.ciphersComponent.showAddNew = false;
|
||||
this.ciphersComponent.showAddNew = true;
|
||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchCollection');
|
||||
const filter = (c: CipherView) => {
|
||||
if (collectionId === 'unassigned') {
|
||||
@@ -156,7 +156,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
};
|
||||
if (load) {
|
||||
await this.ciphersComponent.load(filter);
|
||||
await this.ciphersComponent.reload(filter);
|
||||
} else {
|
||||
await this.ciphersComponent.applyFilter(filter);
|
||||
}
|
||||
@@ -226,7 +226,14 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
addCipher() {
|
||||
const component = this.editCipher(null);
|
||||
component.organizationId = this.organization.id;
|
||||
component.type = this.type;
|
||||
if (this.organization.isAdmin) {
|
||||
component.collections = this.groupingsComponent.collections.filter((c) => !c.readOnly);
|
||||
}
|
||||
if (this.collectionId != null) {
|
||||
component.collectionIds = [this.collectionId];
|
||||
}
|
||||
}
|
||||
|
||||
editCipher(cipher: CipherView) {
|
||||
@@ -256,6 +263,18 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
return childComponent;
|
||||
}
|
||||
|
||||
cloneCipher(cipher: CipherView) {
|
||||
const component = this.editCipher(cipher);
|
||||
component.cloneMode = true;
|
||||
component.organizationId = this.organization.id;
|
||||
if (this.organization.isAdmin) {
|
||||
component.collections = this.groupingsComponent.collections.filter((c) => !c.readOnly);
|
||||
}
|
||||
// Regardless of Admin state, the collection Ids need to passed manually as they are not assigned value
|
||||
// in the add-edit componenet
|
||||
component.collectionIds = cipher.collectionIds;
|
||||
}
|
||||
|
||||
async viewEvents(cipher: CipherView) {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
@@ -290,8 +309,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
}
|
||||
|
||||
const url = this.router.createUrlTree(['organizations', this.organization.id, 'vault'],
|
||||
{ queryParams: queryParams }).toString();
|
||||
this.location.go(url);
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: queryParams,
|
||||
replaceUrl: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user