mirror of
https://github.com/bitwarden/web
synced 2025-12-06 00:03:28 +00:00
Compare commits
172 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08b2184e12 | ||
|
|
b73161882c | ||
|
|
e2186ecd62 | ||
|
|
b407402f3f | ||
|
|
8bb4132458 | ||
|
|
443822fd52 | ||
|
|
68427fd2de | ||
|
|
c3d3369601 | ||
|
|
3c5022d628 | ||
|
|
832ddddc58 | ||
|
|
0fc1415a06 | ||
|
|
1ab408c591 | ||
|
|
3160d3f275 | ||
|
|
d083f1ddc3 | ||
|
|
5fbc09b135 | ||
|
|
6282fabf98 | ||
|
|
2b528bad97 | ||
|
|
c3be8195fd | ||
|
|
39471d0421 | ||
|
|
7a50c0536c | ||
|
|
4ccd9501a8 | ||
|
|
75c05a4a85 | ||
|
|
ca7e12370f | ||
|
|
8bc9dafff2 | ||
|
|
dcb0416fd6 | ||
|
|
bbb69bba26 | ||
|
|
c1838b48ff | ||
|
|
d53f40002c | ||
|
|
866954b180 | ||
|
|
befa9cbf08 | ||
|
|
859f44db43 | ||
|
|
cca9c3c561 | ||
|
|
27e68e4c75 | ||
|
|
5c92350ed2 | ||
|
|
b94c62d1e5 | ||
|
|
de888d8a37 | ||
|
|
f8d6816101 | ||
|
|
119c6d5817 | ||
|
|
aaa21daa29 | ||
|
|
10f41bf288 | ||
|
|
91582691d8 | ||
|
|
463efc2254 | ||
|
|
0333354271 | ||
|
|
b85f56c681 | ||
|
|
be491be2cd | ||
|
|
4be4a8115d | ||
|
|
c0eb499f4d | ||
|
|
1b43f3facd | ||
|
|
26d41d3cb9 | ||
|
|
179765f6e4 | ||
|
|
df2e332134 | ||
|
|
2952f9d158 | ||
|
|
3c9face597 | ||
|
|
25f2e9c1b7 | ||
|
|
a6f8e1b9a3 | ||
|
|
d832031cec | ||
|
|
7a1a3ab64d | ||
|
|
19491a684e | ||
|
|
757224287e | ||
|
|
c9b5426f6f | ||
|
|
bf885c184f | ||
|
|
0d2bf4f7a1 | ||
|
|
01ffc68fc2 | ||
|
|
16892239fb | ||
|
|
d5765d8814 | ||
|
|
8d6a96074d | ||
|
|
f54884eb79 | ||
|
|
828149b2d6 | ||
|
|
501c4fc263 | ||
|
|
1d0b45e17d | ||
|
|
a0f7ed68fb | ||
|
|
7bd0c17188 | ||
|
|
1ea9d28523 | ||
|
|
8a3fb92bbe | ||
|
|
de3a9b9903 | ||
|
|
9834f3d2aa | ||
|
|
ac079b9d88 | ||
|
|
9e96906f32 | ||
|
|
90c079e743 | ||
|
|
4ecf307285 | ||
|
|
6cf4c453d9 | ||
|
|
d2899d14c7 | ||
|
|
f3b438d514 | ||
|
|
2997f694f8 | ||
|
|
b78ab4db27 | ||
|
|
37dddea515 | ||
|
|
e307d1e87d | ||
|
|
62e1dbb642 | ||
|
|
b8a425f530 | ||
|
|
cafb6fa694 | ||
|
|
0482ddea2c | ||
|
|
b411176c8d | ||
|
|
2f13449cb6 | ||
|
|
b0c1b7b683 | ||
|
|
7e8978c7fc | ||
|
|
d58b422bd0 | ||
|
|
3563601382 | ||
|
|
d42e6ca3fd | ||
|
|
7f0d8c99e3 | ||
|
|
48a67dc2b3 | ||
|
|
8d0b42492d | ||
|
|
e4076e95dd | ||
|
|
30a2b878f6 | ||
|
|
e17f94a67d | ||
|
|
4dd60c3844 | ||
|
|
9d76990f24 | ||
|
|
ed3d15f075 | ||
|
|
2c36a2aa96 | ||
|
|
16930aa422 | ||
|
|
263f5ba147 | ||
|
|
6a60c00e22 | ||
|
|
f3eaf644b0 | ||
|
|
a57110b935 | ||
|
|
cae8beaa8f | ||
|
|
df94d81d07 | ||
|
|
f03c22cc07 | ||
|
|
5b31fe37f2 | ||
|
|
c60a596995 | ||
|
|
b52ecd8085 | ||
|
|
4323341d19 | ||
|
|
e13992ba27 | ||
|
|
52a4317d09 | ||
|
|
d53187935b | ||
|
|
0d6c96e38b | ||
|
|
b0832578a4 | ||
|
|
805393b4db | ||
|
|
c3653577c6 | ||
|
|
1eb5a99ba3 | ||
|
|
a035d73545 | ||
|
|
79fc3056a6 | ||
|
|
e44cf6e7ee | ||
|
|
641c76ae62 | ||
|
|
1efcd69148 | ||
|
|
49ee41f7d3 | ||
|
|
598c7ea068 | ||
|
|
001a116c8b | ||
|
|
106e71fe54 | ||
|
|
cd93d6cc32 | ||
|
|
d63c89bae7 | ||
|
|
fb3a7733a3 | ||
|
|
852363cb77 | ||
|
|
7f6ee21a8e | ||
|
|
2963516d5c | ||
|
|
1f26ff5c80 | ||
|
|
de3f310082 | ||
|
|
4af2edafd3 | ||
|
|
4de08f2e71 | ||
|
|
d978e1dfa3 | ||
|
|
f828288b84 | ||
|
|
7a36f13034 | ||
|
|
422b48fa36 | ||
|
|
fe9e29a057 | ||
|
|
88c302ca2e | ||
|
|
52f3032483 | ||
|
|
b13edfeeae | ||
|
|
4046339569 | ||
|
|
52f4a9d961 | ||
|
|
ca0fb6d66a | ||
|
|
7c93c82d24 | ||
|
|
3b71760f9e | ||
|
|
c4d2045884 | ||
|
|
d28c59544f | ||
|
|
acff0b19d6 | ||
|
|
94bfcb2865 | ||
|
|
1bb6244337 | ||
|
|
a132ec4fd7 | ||
|
|
8291fa0ce1 | ||
|
|
37364ecd7e | ||
|
|
48d9e626f5 | ||
|
|
f0fbf664d4 | ||
|
|
7b8b4dc164 | ||
|
|
21635dd728 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -199,5 +199,4 @@ FakesAssemblies/
|
||||
*.opt
|
||||
|
||||
# Other
|
||||
package-lock.json
|
||||
src/js/*.min.js
|
||||
15
Dockerfile
15
Dockerfile
@@ -1,10 +1,17 @@
|
||||
FROM bitwarden/server
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
gosu \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV ASPNETCORE_URLS http://+:5000
|
||||
WORKDIR /app
|
||||
EXPOSE 5000
|
||||
COPY ./dist .
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
COPY entrypoint.sh /
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
RUN groupadd -g 999 bitwarden \
|
||||
&& chmod +x /entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
||||
5
ISSUE_TEMPLATE.md
Normal file
5
ISSUE_TEMPLATE.md
Normal file
@@ -0,0 +1,5 @@
|
||||
<!--
|
||||
Please do not submit feature requests. The [Community Forums][1] has a
|
||||
section for submitting, voting for, and discussing product feature requests.
|
||||
[1]: https://community.bitwarden.com
|
||||
-->
|
||||
@@ -1,8 +1,8 @@
|
||||
[](https://ci.appveyor.com/project/bitwarden/web) [](https://gitter.im/bitwarden/Lobby)
|
||||
[](https://ci.appveyor.com/project/bitwarden/web) [](https://hub.docker.com/u/bitwarden/) [](https://gitter.im/bitwarden/Lobby)
|
||||
|
||||
# bitwarden Web
|
||||
# Bitwarden Web Vault
|
||||
|
||||
The bitwarden Web project is an AngularJS application that powers the web vault (https://vault.bitwarden.com/).
|
||||
The Bitwarden web project is an AngularJS application that powers the web vault (https://vault.bitwarden.com/).
|
||||
|
||||
<img src="https://i.imgur.com/rxrykeX.png" alt="" width="791" height="739" />
|
||||
|
||||
|
||||
16
SECURITY.md
16
SECURITY.md
@@ -1,4 +1,4 @@
|
||||
bitwarden believes that working with security researchers across the globe is crucial to keeping our
|
||||
Bitwarden believes that working with security researchers across the globe is crucial to keeping our
|
||||
users safe. If you believe you've found a security issue in our product or service, we encourage you to
|
||||
notify us. We welcome working with you to resolve the issue promptly. Thanks in advance!
|
||||
|
||||
@@ -16,7 +16,7 @@ notify us. We welcome working with you to resolve the issue promptly. Thanks in
|
||||
|
||||
# In-scope
|
||||
|
||||
- Security issues in any current release of bitwarden. This includes the web vault, browser extension,
|
||||
- Security issues in any current release of Bitwarden. This includes the web vault, browser extension,
|
||||
and mobile apps (iOS and Android). Product downloads are available at https://bitwarden.com. Source
|
||||
code is available at https://github.com/bitwarden.
|
||||
|
||||
@@ -24,14 +24,14 @@ notify us. We welcome working with you to resolve the issue promptly. Thanks in
|
||||
|
||||
The following bug classes are out-of scope:
|
||||
|
||||
- Bugs that are already reported on any of bitwarden's issue trackers (https://github.com/bitwarden),
|
||||
- Bugs that are already reported on any of Bitwarden's issue trackers (https://github.com/bitwarden),
|
||||
or that we already know of. Note that some of our issue tracking is private.
|
||||
- Issues in an upstream software dependency (ex: Xamarin, ASP.NET) which are already reported to the
|
||||
upstream maintainer.
|
||||
- Attacks requiring physical access to a user's device.
|
||||
- Self-XSS
|
||||
- Issues related to software or protocols not under bitwarden's control
|
||||
- Vulnerabilities in outdated versions of bitwarden
|
||||
- Issues related to software or protocols not under Bitwarden's control
|
||||
- Vulnerabilities in outdated versions of Bitwarden
|
||||
- Missing security best practices that do not directly lead to a vulnerability
|
||||
- Issues that do not have any impact on the general public
|
||||
|
||||
@@ -39,7 +39,7 @@ While researching, we'd like to ask you to refrain from:
|
||||
|
||||
- Denial of service
|
||||
- Spamming
|
||||
- Social engineering (including phishing) of bitwarden staff or contractors
|
||||
- Any physical attempts against bitwarden property or data centers
|
||||
- Social engineering (including phishing) of Bitwarden staff or contractors
|
||||
- Any physical attempts against Bitwarden property or data centers
|
||||
|
||||
Thank you for helping keep bitwarden and our users safe!
|
||||
Thank you for helping keep Bitwarden and our users safe!
|
||||
|
||||
36
build.sh
36
build.sh
@@ -4,16 +4,30 @@ set -e
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
echo ""
|
||||
echo "# Building Web"
|
||||
|
||||
echo ""
|
||||
echo "Building app"
|
||||
echo "npm version $(npm --version)"
|
||||
echo "gulp version $(gulp --version)"
|
||||
npm install
|
||||
gulp dist:selfHosted
|
||||
if [ $# -gt 1 -a "$1" == "push" ]
|
||||
then
|
||||
TAG=$2
|
||||
echo "# Pushing Web ($TAG)"
|
||||
echo ""
|
||||
docker push bitwarden/web:$TAG
|
||||
elif [ $# -gt 1 -a "$1" == "tag" ]
|
||||
then
|
||||
TAG=$2
|
||||
echo "Tagging Web as '$TAG'"
|
||||
docker tag bitwarden/web bitwarden/web:$TAG
|
||||
else
|
||||
echo "# Building Web"
|
||||
|
||||
echo ""
|
||||
echo "Building docker image"
|
||||
docker --version
|
||||
docker build -t bitwarden/web $DIR/.
|
||||
echo ""
|
||||
echo "Building app"
|
||||
echo "npm version $(npm --version)"
|
||||
echo "gulp version $(gulp --version)"
|
||||
npm install
|
||||
gulp dist:selfHosted
|
||||
|
||||
echo ""
|
||||
echo "Building docker image"
|
||||
docker --version
|
||||
docker build -t bitwarden/web $DIR/.
|
||||
fi
|
||||
|
||||
1
dist/.publish
vendored
Submodule
1
dist/.publish
vendored
Submodule
Submodule dist/.publish added at 66eacea870
@@ -1,5 +1,33 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
USERNAME="bitwarden"
|
||||
NOUSER=`id -u $USERNAME > /dev/null 2>&1; echo $?`
|
||||
LUID=${LOCAL_UID:-999}
|
||||
|
||||
# Step down from host root
|
||||
if [ $LUID == 0 ]
|
||||
then
|
||||
LUID=999
|
||||
fi
|
||||
|
||||
if [ $NOUSER == 0 ] && [ `id -u $USERNAME` != $LUID ]
|
||||
then
|
||||
usermod -u $LUID $USERNAME
|
||||
elif [ $NOUSER == 1 ]
|
||||
then
|
||||
useradd -r -u $LUID -g $USERNAME $USERNAME
|
||||
fi
|
||||
|
||||
if [ ! -d "/home/$USERNAME" ]
|
||||
then
|
||||
mkhomedir_helper $USERNAME
|
||||
fi
|
||||
|
||||
chown -R $USERNAME:$USERNAME /etc/bitwarden
|
||||
cp /etc/bitwarden/web/settings.js /app/js/settings.js
|
||||
cp /etc/bitwarden/web/app-id.json /app/app-id.json
|
||||
dotnet /bitwarden_server/Server.dll /contentRoot=/app /webRoot=. /serveUnknown=false
|
||||
chown -R $USERNAME:$USERNAME /app
|
||||
chown -R $USERNAME:$USERNAME /bitwarden_server
|
||||
|
||||
gosu $USERNAME:$USERNAME dotnet /bitwarden_server/Server.dll \
|
||||
/contentRoot=/app /webRoot=. /serveUnknown=false
|
||||
|
||||
31
gulpfile.js
31
gulpfile.js
@@ -12,6 +12,7 @@ var gulp = require('gulp'),
|
||||
ngAnnotate = require('gulp-ng-annotate'),
|
||||
preprocess = require('gulp-preprocess'),
|
||||
runSequence = require('run-sequence'),
|
||||
jeditor = require("gulp-json-editor"),
|
||||
merge = require('merge-stream'),
|
||||
ngConfig = require('gulp-ng-config'),
|
||||
settings = require('./settings.json'),
|
||||
@@ -25,7 +26,7 @@ var gulp = require('gulp'),
|
||||
|
||||
var paths = {};
|
||||
paths.dist = './dist/';
|
||||
paths.webroot = './src/'
|
||||
paths.webroot = './src/';
|
||||
paths.js = paths.webroot + 'js/**/*.js';
|
||||
paths.minJs = paths.webroot + 'js/**/*.min.js';
|
||||
paths.concatJsDest = paths.webroot + 'js/bw.min.js';
|
||||
@@ -71,12 +72,13 @@ gulp.task('min:js', ['clean:js'], function () {
|
||||
'!' + paths.minJs,
|
||||
'!' + paths.jsDir + 'fallback*.js',
|
||||
'!' + paths.jsDir + 'u2f-connector.js',
|
||||
'!' + paths.jsDir + 'duo-connector.js',
|
||||
'!' + paths.jsDir + 'duo.js',
|
||||
'!' + paths.jsDir + 'settings.js'
|
||||
], { base: '.' })
|
||||
.pipe(preprocess({ context: { cacheTag: randomString, selfHosted: selfHosted } }))
|
||||
.pipe(concat(paths.concatJsDest))
|
||||
.pipe(uglify())
|
||||
//.pipe(uglify())
|
||||
.pipe(gulp.dest('.'));
|
||||
});
|
||||
|
||||
@@ -304,7 +306,7 @@ gulp.task('dist:move', function () {
|
||||
src: [
|
||||
paths.npmDir + 'bootstrap/dist/**/bootstrap.min.js',
|
||||
paths.npmDir + 'bootstrap/dist/**/bootstrap.min.css',
|
||||
paths.npmDir + 'bootstrap/dist/**/fonts/**/*',
|
||||
paths.npmDir + 'bootstrap/dist/**/fonts/**/*'
|
||||
],
|
||||
dest: paths.dist + 'lib/bootstrap'
|
||||
},
|
||||
@@ -335,6 +337,10 @@ gulp.task('dist:move', function () {
|
||||
src: paths.jsDir + 'duo.js',
|
||||
dest: paths.dist + 'js'
|
||||
},
|
||||
{
|
||||
src: paths.jsDir + 'duo-connector.js',
|
||||
dest: paths.dist + 'js'
|
||||
},
|
||||
{
|
||||
src: paths.jsDir + 'settings.js',
|
||||
dest: paths.dist + 'js'
|
||||
@@ -351,6 +357,7 @@ gulp.task('dist:move', function () {
|
||||
paths.webroot + 'u2f-connector.html',
|
||||
paths.webroot + 'duo-connector.html',
|
||||
paths.webroot + 'favicon.ico',
|
||||
paths.webroot + 'manifest.json',
|
||||
paths.webroot + 'app-id.json'
|
||||
],
|
||||
dest: paths.dist
|
||||
@@ -389,7 +396,7 @@ gulp.task('dist:js:app', function () {
|
||||
.pipe(preprocess({ context: { cacheTag: randomString, selfHosted: selfHosted } }))
|
||||
.pipe(concat(paths.dist + '/js/app.min.js'))
|
||||
.pipe(ngAnnotate())
|
||||
.pipe(uglify())
|
||||
//.pipe(uglify())
|
||||
.pipe(gulp.dest('.'));
|
||||
});
|
||||
|
||||
@@ -401,7 +408,7 @@ gulp.task('dist:js:fallback', function () {
|
||||
|
||||
merge(mainStream)
|
||||
.pipe(preprocess({ context: { cacheTag: randomString, selfHosted: selfHosted } }))
|
||||
.pipe(uglify())
|
||||
//.pipe(uglify())
|
||||
.pipe(rename({ suffix: '.min' }))
|
||||
.pipe(gulp.dest(paths.dist + 'js'));
|
||||
});
|
||||
@@ -414,7 +421,7 @@ gulp.task('dist:js:u2f', function () {
|
||||
|
||||
merge(mainStream)
|
||||
.pipe(concat(paths.dist + '/js/u2f.min.js'))
|
||||
.pipe(uglify())
|
||||
//.pipe(uglify())
|
||||
.pipe(gulp.dest('.'));
|
||||
});
|
||||
|
||||
@@ -429,7 +436,7 @@ gulp.task('dist:js:lib', function () {
|
||||
'!' + paths.libDir + 'jquery/**/*'
|
||||
])
|
||||
.pipe(concat(paths.dist + '/js/lib.min.js'))
|
||||
.pipe(uglify())
|
||||
//.pipe(uglify())
|
||||
.pipe(gulp.dest('.'));
|
||||
});
|
||||
|
||||
@@ -442,10 +449,16 @@ gulp.task('dist:preprocess', function () {
|
||||
.pipe(gulp.dest('.'));
|
||||
});
|
||||
|
||||
gulp.task('dist:version', function () {
|
||||
gulp.src(paths.webroot + 'version.json').pipe(jeditor({
|
||||
'version': project.version
|
||||
})).pipe(gulp.dest(paths.dist));
|
||||
});
|
||||
|
||||
gulp.task('dist', ['build'], function (cb) {
|
||||
return runSequence(
|
||||
'dist:clean',
|
||||
['dist:move', 'dist:css', 'dist:js:app', 'dist:js:lib', 'dist:js:fallback', 'dist:js:u2f'],
|
||||
['dist:move', 'dist:css', 'dist:js:app', 'dist:js:lib', 'dist:js:fallback', 'dist:js:u2f', 'dist:version'],
|
||||
'dist:preprocess',
|
||||
cb);
|
||||
});
|
||||
@@ -465,7 +478,7 @@ gulp.task('deploy-preview', ['dist'], function () {
|
||||
return gulp.src(paths.dist + '**/*')
|
||||
.pipe(ghPages({
|
||||
cacheDir: paths.dist + '.publish',
|
||||
remoteUrl: 'git@github.com:kspearrin/bitwarden-web-preview.git'
|
||||
remoteUrl: 'git@github.com:bitwarden/web-preview.git'
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
9469
package-lock.json
generated
Normal file
9469
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
package.json
35
package.json
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "bitwarden",
|
||||
"version": "1.17.1",
|
||||
"version": "1.25.1",
|
||||
"env": "Production",
|
||||
"devDependencies": {
|
||||
"connect": "3.6.3",
|
||||
"connect": "3.6.5",
|
||||
"lodash": "4.17.4",
|
||||
"gulp": "3.9.1",
|
||||
"gulp-concat": "2.6.1",
|
||||
@@ -11,41 +11,42 @@
|
||||
"gulp-less": "3.3.2",
|
||||
"gulp-rename": "1.2.2",
|
||||
"gulp-uglify": "3.0.0",
|
||||
"gulp-gh-pages": "0.5.4",
|
||||
"gulp-gh-pages": "git+https://github.com/tekd/gulp-gh-pages.git#update-dependency",
|
||||
"gulp-preprocess": "2.0.0",
|
||||
"gulp-ng-annotate": "2.0.0",
|
||||
"gulp-ng-config": "1.4.0",
|
||||
"gulp-ng-config": "1.5.0",
|
||||
"gulp-connect": "5.0.0",
|
||||
"gulp-json-editor": "2.2.2",
|
||||
"jshint": "2.9.5",
|
||||
"gulp-jshint": "2.0.4",
|
||||
"rimraf": "2.6.1",
|
||||
"run-sequence": "2.1.0",
|
||||
"rimraf": "2.6.2",
|
||||
"run-sequence": "2.2.0",
|
||||
"merge-stream": "1.0.1",
|
||||
"jquery": "2.2.4",
|
||||
"jquery": "3.2.1",
|
||||
"font-awesome": "4.7.0",
|
||||
"bootstrap": "3.3.7",
|
||||
"angular": "1.6.6",
|
||||
"angular-resource": "1.6.6",
|
||||
"angular-sanitize": "1.6.6",
|
||||
"angular-ui-bootstrap": "2.5.0",
|
||||
"angular": "1.6.7",
|
||||
"angular-resource": "1.6.7",
|
||||
"angular-sanitize": "1.6.7",
|
||||
"angular-ui-bootstrap": "2.5.6",
|
||||
"angular-ui-router": "0.4.2",
|
||||
"angular-jwt": "0.1.9",
|
||||
"angular-cookies": "1.6.6",
|
||||
"angular-cookies": "1.6.7",
|
||||
"admin-lte": "2.3.11",
|
||||
"angular-toastr": "2.1.1",
|
||||
"angular-bootstrap-show-errors": "2.3.0",
|
||||
"angular-messages": "1.6.6",
|
||||
"angular-messages": "1.6.7",
|
||||
"ngstorage": "0.3.11",
|
||||
"papaparse": "4.3.5",
|
||||
"papaparse": "4.3.6",
|
||||
"clipboard": "1.7.1",
|
||||
"ngclipboard": "1.1.1",
|
||||
"angulartics": "1.4.0",
|
||||
"ngclipboard": "1.1.2",
|
||||
"angulartics": "1.5.0",
|
||||
"angulartics-google-analytics": "0.4.0",
|
||||
"node-forge": "0.7.1",
|
||||
"webpack-stream": "4.0.0",
|
||||
"angular-stripe": "5.0.0",
|
||||
"angular-credit-cards": "3.1.6",
|
||||
"browserify": "14.4.0",
|
||||
"browserify": "14.5.0",
|
||||
"vinyl-source-stream": "1.1.0",
|
||||
"gulp-derequire": "2.1.0",
|
||||
"exposify": "0.5.0",
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
{
|
||||
"appSettings": {
|
||||
"apiUri": "https://preview-api.bitwarden.com",
|
||||
"identityUri": "https://preview-identity.bitwarden.com",
|
||||
"apiUri": "/api",
|
||||
"identityUri": "/identity",
|
||||
"iconsUri": "https://icons.bitwarden.com",
|
||||
"stripeKey": "pk_test_KPoCfZXu7mznb9uSCPZ2JpTD",
|
||||
"braintreeKey": "sandbox_r72q8jq6_9pnxkwm75f87sdc2",
|
||||
"whitelistDomains": [
|
||||
"preview-api.bitwarden.com"
|
||||
]
|
||||
"braintreeKey": "sandbox_r72q8jq6_9pnxkwm75f87sdc2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
{
|
||||
"appSettings": {
|
||||
"apiUri": "https://api.bitwarden.com",
|
||||
"identityUri": "https://identity.bitwarden.com",
|
||||
"apiUri": "/api",
|
||||
"identityUri": "/identity",
|
||||
"iconsUri": "https://icons.bitwarden.com",
|
||||
"stripeKey": "pk_live_bpN0P37nMxrMQkcaHXtAybJk",
|
||||
"braintreeKey": "production_qfbsv8kc_njj2zjtyngtjmbjd",
|
||||
"whitelistDomains": [
|
||||
"api.bitwarden.com"
|
||||
]
|
||||
"braintreeKey": "production_qfbsv8kc_njj2zjtyngtjmbjd"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
"appSettings": {
|
||||
"apiUri": "http://localhost:4000",
|
||||
"identityUri": "http://localhost:33656",
|
||||
"iconsUri": "https://icons.bitwarden.com",
|
||||
"stripeKey": "pk_test_KPoCfZXu7mznb9uSCPZ2JpTD",
|
||||
"braintreeKey": "sandbox_r72q8jq6_9pnxkwm75f87sdc2",
|
||||
"whitelistDomains": [
|
||||
"localhost"
|
||||
]
|
||||
"braintreeKey": "sandbox_r72q8jq6_9pnxkwm75f87sdc2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,8 @@
|
||||
</p>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="code" class="sr-only">Token</label>
|
||||
<input type="password" id="code" name="Token" class="form-control" ng-model="token" required api-field />
|
||||
<input type="password" id="code" name="Token" class="form-control" ng-model="token"
|
||||
autocomplete="new-password" required api-field />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-7">
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<p class="text-center"><strong>{{state.params.email}}</strong></p>
|
||||
<p>
|
||||
You've been invited to join the organization listed above.
|
||||
To accept the invitation, you need to log in or create a new bitwarden account.
|
||||
To accept the invitation, you need to log in or create a new Bitwarden account.
|
||||
</p>
|
||||
<hr />
|
||||
<div class="row">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<i class="fa fa-shield"></i> <b>bit</b>warden
|
||||
</div>
|
||||
<div class="login-box-body">
|
||||
<p class="login-box-msg">Enter your email address below to recover & delete your bitwarden account.</p>
|
||||
<p class="login-box-msg">Enter your email address below to recover & delete your Bitwarden account.</p>
|
||||
<div ng-show="success" class="text-center">
|
||||
<div class="callout callout-success">
|
||||
If your account exists ({{model.email}}) we've sent you an email with further instructions.
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
This will permanently delete your account. This cannot be undone.
|
||||
</div>
|
||||
<p>
|
||||
You have requested to delete your bitwarden account (<b>{{email}}</b>).
|
||||
You have requested to delete your Bitwarden account (<b>{{email}}</b>).
|
||||
Click the button below to confirm and proceed.
|
||||
</p>
|
||||
<button ng-click="delete()" class="btn btn-danger btn-block btn-flat">Delete Account</button>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
angular
|
||||
.module('bit')
|
||||
|
||||
.factory('apiInterceptor', function ($injector, $q, toastr) {
|
||||
.factory('apiInterceptor', function ($injector, $q, toastr, appSettings, utilsService) {
|
||||
return {
|
||||
request: function (config) {
|
||||
if (config.url.indexOf(appSettings.apiUri + '/') === 0) {
|
||||
config.headers['Device-Type'] = utilsService.getDeviceType();
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
response: function (response) {
|
||||
|
||||
@@ -4,21 +4,23 @@ angular
|
||||
.config(function ($stateProvider, $urlRouterProvider, $httpProvider, jwtInterceptorProvider, jwtOptionsProvider,
|
||||
$uibTooltipProvider, toastrConfig, $locationProvider, $qProvider, appSettings
|
||||
// @if !selfHosted
|
||||
/* jshint ignore:start */
|
||||
, stripeProvider
|
||||
/* jshint ignore:end */
|
||||
// @endif
|
||||
) {
|
||||
angular.extend(appSettings, window.bitwardenAppSettings);
|
||||
|
||||
$qProvider.errorOnUnhandledRejections(false);
|
||||
$locationProvider.hashPrefix('');
|
||||
|
||||
jwtOptionsProvider.config({
|
||||
// Using Content-Language header since it is unused and is a CORS-safelisted header. This avoids pre-flights.
|
||||
authHeader: 'Content-Language',
|
||||
whiteListedDomains: appSettings.whitelistDomains
|
||||
whiteListedDomains: ['localhost', 'api.bitwarden.com', 'vault.bitwarden.com', 'haveibeenpwned.com']
|
||||
});
|
||||
|
||||
var refreshPromise;
|
||||
jwtInterceptorProvider.tokenGetter = /*@ngInject*/ function (options, tokenService, authService) {
|
||||
if (options.url.indexOf(appSettings.apiUri) !== 0) {
|
||||
if (options.url.indexOf(appSettings.apiUri + '/') !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,7 +37,12 @@ angular
|
||||
return token;
|
||||
}
|
||||
|
||||
refreshPromise = authService.refreshAccessToken().then(function (newToken) {
|
||||
var p = authService.refreshAccessToken();
|
||||
if (!p) {
|
||||
return;
|
||||
}
|
||||
|
||||
refreshPromise = p.then(function (newToken) {
|
||||
refreshPromise = null;
|
||||
return newToken || token;
|
||||
});
|
||||
@@ -58,12 +65,6 @@ angular
|
||||
appendToBody: true
|
||||
});
|
||||
|
||||
if ($httpProvider.defaults.headers.post) {
|
||||
$httpProvider.defaults.headers.post = {};
|
||||
}
|
||||
|
||||
$httpProvider.defaults.headers.post['Content-Type'] = 'text/plain; charset=utf-8';
|
||||
|
||||
// stop IE from caching get requests
|
||||
if (navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0) {
|
||||
if (!$httpProvider.defaults.headers.get) {
|
||||
@@ -103,12 +104,6 @@ angular
|
||||
refreshFromServer: false
|
||||
}
|
||||
})
|
||||
.state('backend.user.shared', {
|
||||
url: '^/shared',
|
||||
templateUrl: 'app/vault/views/vaultShared.html',
|
||||
controller: 'vaultSharedController',
|
||||
data: { pageTitle: 'Shared' }
|
||||
})
|
||||
.state('backend.user.settings', {
|
||||
url: '^/settings',
|
||||
templateUrl: 'app/settings/views/settings.html',
|
||||
@@ -157,12 +152,6 @@ angular
|
||||
controller: 'reportsBreachController',
|
||||
data: { pageTitle: 'Data Breach Report' }
|
||||
})
|
||||
.state('backend.user.apps', {
|
||||
url: '^/apps',
|
||||
templateUrl: 'app/views/apps.html',
|
||||
controller: 'appsController',
|
||||
data: { pageTitle: 'Get the Apps' }
|
||||
})
|
||||
.state('backend.org', {
|
||||
templateUrl: 'app/views/organizationLayout.html',
|
||||
abstract: true
|
||||
@@ -174,13 +163,13 @@ angular
|
||||
data: { pageTitle: 'Organization Dashboard' }
|
||||
})
|
||||
.state('backend.org.people', {
|
||||
url: '/organization/:orgId/people',
|
||||
url: '/organization/:orgId/people?viewEvents&search',
|
||||
templateUrl: 'app/organization/views/organizationPeople.html',
|
||||
controller: 'organizationPeopleController',
|
||||
data: { pageTitle: 'Organization People' }
|
||||
})
|
||||
.state('backend.org.collections', {
|
||||
url: '/organization/:orgId/collections',
|
||||
url: '/organization/:orgId/collections?search',
|
||||
templateUrl: 'app/organization/views/organizationCollections.html',
|
||||
controller: 'organizationCollectionsController',
|
||||
data: { pageTitle: 'Organization Collections' }
|
||||
@@ -198,17 +187,26 @@ angular
|
||||
data: { pageTitle: 'Organization Billing' }
|
||||
})
|
||||
.state('backend.org.vault', {
|
||||
url: '/organization/:orgId/vault',
|
||||
url: '/organization/:orgId/vault?viewEvents&search',
|
||||
templateUrl: 'app/organization/views/organizationVault.html',
|
||||
controller: 'organizationVaultController',
|
||||
data: { pageTitle: 'Organization Vault' }
|
||||
data: {
|
||||
pageTitle: 'Organization Vault',
|
||||
controlSidebar: true
|
||||
}
|
||||
})
|
||||
.state('backend.org.groups', {
|
||||
url: '/organization/:orgId/groups',
|
||||
url: '/organization/:orgId/groups?search',
|
||||
templateUrl: 'app/organization/views/organizationGroups.html',
|
||||
controller: 'organizationGroupsController',
|
||||
data: { pageTitle: 'Organization Groups' }
|
||||
})
|
||||
.state('backend.org.events', {
|
||||
url: '/organization/:orgId/events',
|
||||
templateUrl: 'app/organization/views/organizationEvents.html',
|
||||
controller: 'organizationEventsController',
|
||||
data: { pageTitle: 'Organization Events' }
|
||||
})
|
||||
|
||||
// Frontend
|
||||
.state('frontend', {
|
||||
@@ -354,7 +352,7 @@ angular
|
||||
// user is guaranteed to be authenticated becuase of previous check
|
||||
if (toState.name.indexOf('backend.org.') > -1 && toParams.orgId) {
|
||||
// clear vault rootScope when visiting org admin section
|
||||
$rootScope.vaultLogins = $rootScope.vaultFolders = null;
|
||||
$rootScope.vaultCiphers = $rootScope.vaultFolders = $rootScope.vaultCollections = null;
|
||||
|
||||
authService.getUserProfile().then(function (profile) {
|
||||
var orgs = profile.organizations;
|
||||
|
||||
@@ -28,6 +28,71 @@ angular.module('bit')
|
||||
email: 1,
|
||||
remember: 5
|
||||
},
|
||||
cipherType: {
|
||||
login: 1,
|
||||
secureNote: 2,
|
||||
card: 3,
|
||||
identity: 4
|
||||
},
|
||||
fieldType: {
|
||||
text: 0,
|
||||
hidden: 1,
|
||||
boolean: 2
|
||||
},
|
||||
deviceType: {
|
||||
android: 0,
|
||||
ios: 1,
|
||||
chromeExt: 2,
|
||||
firefoxExt: 3,
|
||||
operaExt: 4,
|
||||
edgeExt: 5,
|
||||
windowsDesktop: 6,
|
||||
macOsDesktop: 7,
|
||||
linuxDesktop: 8,
|
||||
chrome: 9,
|
||||
firefox: 10,
|
||||
opera: 11,
|
||||
edge: 12,
|
||||
ie: 13,
|
||||
unknown: 14,
|
||||
uwp: 16,
|
||||
safari: 17,
|
||||
vivaldi: 18,
|
||||
vivaldiExt: 19
|
||||
},
|
||||
eventType: {
|
||||
User_LoggedIn: 1000,
|
||||
User_ChangedPassword: 1001,
|
||||
User_Enabled2fa: 1002,
|
||||
User_Disabled2fa: 1003,
|
||||
User_Recovered2fa: 1004,
|
||||
User_FailedLogIn: 1005,
|
||||
User_FailedLogIn2fa: 1006,
|
||||
|
||||
Cipher_Created: 1100,
|
||||
Cipher_Updated: 1101,
|
||||
Cipher_Deleted: 1102,
|
||||
Cipher_AttachmentCreated: 1103,
|
||||
Cipher_AttachmentDeleted: 1104,
|
||||
Cipher_Shared: 1105,
|
||||
Cipher_UpdatedCollections: 1106,
|
||||
|
||||
Collection_Created: 1300,
|
||||
Collection_Updated: 1301,
|
||||
Collection_Deleted: 1302,
|
||||
|
||||
Group_Created: 1400,
|
||||
Group_Updated: 1401,
|
||||
Group_Deleted: 1402,
|
||||
|
||||
OrganizationUser_Invited: 1500,
|
||||
OrganizationUser_Confirmed: 1501,
|
||||
OrganizationUser_Updated: 1502,
|
||||
OrganizationUser_Removed: 1503,
|
||||
OrganizationUser_UpdatedGroups: 1504,
|
||||
|
||||
Organization_Updated: 1600
|
||||
},
|
||||
twoFactorProviderInfo: [
|
||||
{
|
||||
type: 0,
|
||||
@@ -95,14 +160,12 @@ angular.module('bit')
|
||||
noPayment: true,
|
||||
upgradeSortOrder: -1
|
||||
},
|
||||
personal: {
|
||||
families: {
|
||||
basePrice: 1,
|
||||
annualBasePrice: 12,
|
||||
baseSeats: 5,
|
||||
seatPrice: 1,
|
||||
annualSeatPrice: 12,
|
||||
maxAdditionalSeats: 5,
|
||||
annualPlanType: 'personalAnnually',
|
||||
noAdditionalSeats: true,
|
||||
annualPlanType: 'familiesAnnually',
|
||||
upgradeSortOrder: 1
|
||||
},
|
||||
teams: {
|
||||
|
||||
11
src/app/directives/fallbackSrcDirective.js
Normal file
11
src/app/directives/fallbackSrcDirective.js
Normal file
@@ -0,0 +1,11 @@
|
||||
angular
|
||||
.module('bit.directives')
|
||||
|
||||
.directive('fallbackSrc', function () {
|
||||
return function (scope, element, attrs) {
|
||||
var el = $(element);
|
||||
el.bind('error', function (event) {
|
||||
el.attr('src', attrs.fallbackSrc);
|
||||
});
|
||||
};
|
||||
});
|
||||
@@ -6,7 +6,7 @@ angular
|
||||
link: function (scope, element) {
|
||||
var listener = function (event, toState, toParams, fromState, fromParams) {
|
||||
// Default title
|
||||
var title = 'bitwarden Web Vault';
|
||||
var title = 'Bitwarden Web Vault';
|
||||
if (toState.data && toState.data.pageTitle) {
|
||||
title = toState.data.pageTitle + ' - ' + title;
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
angular
|
||||
.module('bit.global')
|
||||
|
||||
.controller('appsController', function ($scope, $state) {
|
||||
|
||||
});
|
||||
@@ -49,16 +49,16 @@ angular
|
||||
vm.openControlSidebar = vm.usingControlSidebar && $document.width() > 768;
|
||||
});
|
||||
|
||||
$scope.addLogin = function () {
|
||||
$scope.$broadcast('vaultAddLogin');
|
||||
$scope.addCipher = function () {
|
||||
$scope.$broadcast('vaultAddCipher');
|
||||
};
|
||||
|
||||
$scope.addFolder = function () {
|
||||
$scope.$broadcast('vaultAddFolder');
|
||||
};
|
||||
|
||||
$scope.addOrganizationLogin = function () {
|
||||
$scope.$broadcast('organizationVaultAddLogin');
|
||||
$scope.addOrganizationCipher = function () {
|
||||
$scope.$broadcast('organizationVaultAddCipher');
|
||||
};
|
||||
|
||||
$scope.addOrganizationCollection = function () {
|
||||
|
||||
@@ -51,14 +51,6 @@ angular
|
||||
$state.go('backend.org.dashboard', { orgId: org.id });
|
||||
};
|
||||
|
||||
$scope.searchVault = function () {
|
||||
$state.go('backend.user.vault');
|
||||
};
|
||||
|
||||
$scope.searchOrganizationVault = function () {
|
||||
$state.go('backend.org.vault', { orgId: $state.params.orgId });
|
||||
};
|
||||
|
||||
$scope.isOrgOwner = function (org) {
|
||||
return org && org.type === constants.orgUserType.owner;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
.controller('organizationBillingChangePaymentController', function ($scope, $state, $uibModalInstance, apiService,
|
||||
$analytics, toastr, existingPaymentMethod
|
||||
// @if !selfHosted
|
||||
/* jshint ignore:start */
|
||||
, stripe
|
||||
/* jshint ignore:end */
|
||||
// @endif
|
||||
) {
|
||||
$analytics.eventTrack('organizationBillingChangePaymentController', { category: 'Modal' });
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
.module('bit.organization')
|
||||
|
||||
.controller('organizationBillingController', function ($scope, apiService, $state, $uibModal, toastr, $analytics,
|
||||
appSettings) {
|
||||
appSettings, tokenService, $window) {
|
||||
$scope.selfHosted = appSettings.selfHosted;
|
||||
$scope.charges = [];
|
||||
$scope.paymentSource = null;
|
||||
@@ -59,7 +59,7 @@
|
||||
};
|
||||
|
||||
$scope.adjustSeats = function (add) {
|
||||
if ($scope.selfHosted) {
|
||||
if ($scope.selfHosted || !$scope.canAdjustSeats) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -208,10 +208,20 @@
|
||||
});
|
||||
};
|
||||
|
||||
$scope.viewInvoice = function (charge) {
|
||||
if ($scope.selfHosted) {
|
||||
return;
|
||||
}
|
||||
var url = appSettings.apiUri + '/organizations/' + $state.params.orgId +
|
||||
'/billing-invoice/' + charge.invoiceId + '?access_token=' + tokenService.getToken();
|
||||
$window.open(url);
|
||||
};
|
||||
|
||||
function load() {
|
||||
apiService.organizations.getBilling({ id: $state.params.orgId }, function (org) {
|
||||
$scope.loading = false;
|
||||
$scope.noSubscription = org.PlanType === 0;
|
||||
$scope.canAdjustSeats = org.PlanType > 1;
|
||||
|
||||
var i = 0;
|
||||
$scope.expiration = org.Expiration;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
.module('bit.organization')
|
||||
|
||||
.controller('organizationCollectionsController', function ($scope, $state, apiService, $uibModal, cipherService, $filter,
|
||||
toastr, $analytics) {
|
||||
toastr, $analytics, $uibModalStack) {
|
||||
$scope.collections = [];
|
||||
$scope.loading = true;
|
||||
$scope.$on('$viewContentLoaded', function () {
|
||||
@@ -96,6 +96,12 @@
|
||||
apiService.collections.listOrganization({ orgId: $state.params.orgId }, function (list) {
|
||||
$scope.collections = cipherService.decryptCollections(list.Data, $state.params.orgId, true);
|
||||
$scope.loading = false;
|
||||
|
||||
if ($state.params.search) {
|
||||
$uibModalStack.dismissAll();
|
||||
$scope.filterSearch = $state.params.search;
|
||||
$('#filterSearch').focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
angular
|
||||
.module('bit.organization')
|
||||
|
||||
.controller('organizationDashboardController', function ($scope, authService, $state) {
|
||||
.controller('organizationDashboardController', function ($scope, authService, $state, appSettings) {
|
||||
$scope.selfHosted = appSettings.selfHosted;
|
||||
|
||||
$scope.$on('$viewContentLoaded', function () {
|
||||
authService.getUserProfile().then(function (userProfile) {
|
||||
if (!userProfile.organizations) {
|
||||
@@ -10,4 +12,8 @@
|
||||
$scope.orgProfile = userProfile.organizations[$state.params.orgId];
|
||||
});
|
||||
});
|
||||
|
||||
$scope.goBilling = function () {
|
||||
$state.go('backend.org.billing', { orgId: $state.params.orgId });
|
||||
};
|
||||
});
|
||||
|
||||
100
src/app/organization/organizationEventsController.js
Normal file
100
src/app/organization/organizationEventsController.js
Normal file
@@ -0,0 +1,100 @@
|
||||
angular
|
||||
.module('bit.organization')
|
||||
|
||||
.controller('organizationEventsController', function ($scope, $state, apiService, $uibModal, $filter,
|
||||
toastr, $analytics, constants, eventService, $compile, $sce) {
|
||||
$scope.events = [];
|
||||
$scope.orgUsers = [];
|
||||
$scope.loading = true;
|
||||
$scope.continuationToken = null;
|
||||
|
||||
var defaultFilters = eventService.getDefaultDateFilters();
|
||||
$scope.filterStart = defaultFilters.start;
|
||||
$scope.filterEnd = defaultFilters.end;
|
||||
|
||||
$scope.$on('$viewContentLoaded', function () {
|
||||
load();
|
||||
});
|
||||
|
||||
$scope.refresh = function () {
|
||||
loadEvents(true);
|
||||
};
|
||||
|
||||
$scope.next = function () {
|
||||
loadEvents(false);
|
||||
};
|
||||
|
||||
var i = 0,
|
||||
orgUsersUserIdDict = {},
|
||||
orgUsersIdDict = {};
|
||||
|
||||
function load() {
|
||||
apiService.organizationUsers.list({ orgId: $state.params.orgId }).$promise.then(function (list) {
|
||||
var users = [];
|
||||
for (i = 0; i < list.Data.length; i++) {
|
||||
var user = {
|
||||
id: list.Data[i].Id,
|
||||
userId: list.Data[i].UserId,
|
||||
name: list.Data[i].Name,
|
||||
email: list.Data[i].Email
|
||||
};
|
||||
|
||||
users.push(user);
|
||||
|
||||
var displayName = user.name || user.email;
|
||||
orgUsersUserIdDict[user.userId] = displayName;
|
||||
orgUsersIdDict[user.id] = displayName;
|
||||
}
|
||||
|
||||
$scope.orgUsers = users;
|
||||
|
||||
return loadEvents(true);
|
||||
});
|
||||
}
|
||||
|
||||
function loadEvents(clearExisting) {
|
||||
var filterResult = eventService.formatDateFilters($scope.filterStart, $scope.filterEnd);
|
||||
if (filterResult.error) {
|
||||
alert(filterResult.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (clearExisting) {
|
||||
$scope.continuationToken = null;
|
||||
$scope.events = [];
|
||||
}
|
||||
|
||||
$scope.loading = true;
|
||||
return apiService.events.listOrganization({
|
||||
orgId: $state.params.orgId,
|
||||
start: filterResult.start,
|
||||
end: filterResult.end,
|
||||
continuationToken: $scope.continuationToken
|
||||
}).$promise.then(function (list) {
|
||||
$scope.continuationToken = list.ContinuationToken;
|
||||
|
||||
var events = [];
|
||||
for (i = 0; i < list.Data.length; i++) {
|
||||
var userId = list.Data[i].ActingUserId || list.Data[i].UserId;
|
||||
var eventInfo = eventService.getEventInfo(list.Data[i]);
|
||||
var htmlMessage = $compile('<span>' + eventInfo.message + '</span>')($scope);
|
||||
events.push({
|
||||
message: $sce.trustAsHtml(htmlMessage[0].outerHTML),
|
||||
appIcon: eventInfo.appIcon,
|
||||
appName: eventInfo.appName,
|
||||
userId: userId,
|
||||
userName: userId ? (orgUsersUserIdDict[userId] || '-') : '-',
|
||||
date: list.Data[i].Date,
|
||||
ip: list.Data[i].IpAddress
|
||||
});
|
||||
}
|
||||
if ($scope.events && $scope.events.length > 0) {
|
||||
$scope.events = $scope.events.concat(events);
|
||||
}
|
||||
else {
|
||||
$scope.events = events;
|
||||
}
|
||||
$scope.loading = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -2,7 +2,7 @@
|
||||
.module('bit.organization')
|
||||
|
||||
.controller('organizationGroupsController', function ($scope, $state, apiService, $uibModal, $filter,
|
||||
toastr, $analytics) {
|
||||
toastr, $analytics, $uibModalStack) {
|
||||
$scope.groups = [];
|
||||
$scope.loading = true;
|
||||
$scope.$on('$viewContentLoaded', function () {
|
||||
@@ -88,6 +88,12 @@
|
||||
}
|
||||
$scope.groups = groups;
|
||||
$scope.loading = false;
|
||||
|
||||
if ($state.params.search) {
|
||||
$uibModalStack.dismissAll();
|
||||
$scope.filterSearch = $state.params.search;
|
||||
$('#filterSearch').focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
.module('bit.organization')
|
||||
|
||||
.controller('organizationPeopleController', function ($scope, $state, $uibModal, cryptoService, apiService, authService,
|
||||
toastr, $analytics) {
|
||||
toastr, $analytics, $filter, $uibModalStack) {
|
||||
$scope.users = [];
|
||||
$scope.useGroups = false;
|
||||
$scope.useEvents = false;
|
||||
|
||||
$scope.$on('$viewContentLoaded', function () {
|
||||
loadList();
|
||||
@@ -13,6 +14,7 @@
|
||||
if (profile.organizations) {
|
||||
var org = profile.organizations[$state.params.orgId];
|
||||
$scope.useGroups = !!org.useGroups;
|
||||
$scope.useEvents = !!org.useEvents;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -110,6 +112,18 @@
|
||||
});
|
||||
};
|
||||
|
||||
$scope.events = function (user) {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/organization/views/organizationPeopleEvents.html',
|
||||
controller: 'organizationPeopleEventsController',
|
||||
resolve: {
|
||||
orgUser: function () { return user; },
|
||||
orgId: function () { return $state.params.orgId; }
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function loadList() {
|
||||
apiService.organizationUsers.list({ orgId: $state.params.orgId }, function (list) {
|
||||
var users = [];
|
||||
@@ -129,6 +143,20 @@
|
||||
}
|
||||
|
||||
$scope.users = users;
|
||||
|
||||
if ($state.params.search) {
|
||||
$uibModalStack.dismissAll();
|
||||
$scope.filterSearch = $state.params.search;
|
||||
$('#filterSearch').focus();
|
||||
}
|
||||
|
||||
if ($state.params.viewEvents) {
|
||||
$uibModalStack.dismissAll();
|
||||
var eventUser = $filter('filter')($scope.users, { id: $state.params.viewEvents });
|
||||
if (eventUser && eventUser.length) {
|
||||
$scope.events(eventUser[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
75
src/app/organization/organizationPeopleEventsController.js
Normal file
75
src/app/organization/organizationPeopleEventsController.js
Normal file
@@ -0,0 +1,75 @@
|
||||
angular
|
||||
.module('bit.organization')
|
||||
|
||||
.controller('organizationPeopleEventsController', function ($scope, apiService, $uibModalInstance,
|
||||
orgUser, $analytics, eventService, orgId, $compile, $sce) {
|
||||
$analytics.eventTrack('organizationPeopleEventsController', { category: 'Modal' });
|
||||
$scope.email = orgUser.email;
|
||||
$scope.events = [];
|
||||
$scope.loading = true;
|
||||
$scope.continuationToken = null;
|
||||
|
||||
var defaultFilters = eventService.getDefaultDateFilters();
|
||||
$scope.filterStart = defaultFilters.start;
|
||||
$scope.filterEnd = defaultFilters.end;
|
||||
|
||||
$uibModalInstance.opened.then(function () {
|
||||
loadEvents(true);
|
||||
});
|
||||
|
||||
$scope.refresh = function () {
|
||||
loadEvents(true);
|
||||
};
|
||||
|
||||
$scope.next = function () {
|
||||
loadEvents(false);
|
||||
};
|
||||
|
||||
function loadEvents(clearExisting) {
|
||||
var filterResult = eventService.formatDateFilters($scope.filterStart, $scope.filterEnd);
|
||||
if (filterResult.error) {
|
||||
alert(filterResult.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (clearExisting) {
|
||||
$scope.continuationToken = null;
|
||||
$scope.events = [];
|
||||
}
|
||||
|
||||
$scope.loading = true;
|
||||
return apiService.events.listOrganizationUser({
|
||||
orgId: orgId,
|
||||
id: orgUser.id,
|
||||
start: filterResult.start,
|
||||
end: filterResult.end,
|
||||
continuationToken: $scope.continuationToken
|
||||
}).$promise.then(function (list) {
|
||||
$scope.continuationToken = list.ContinuationToken;
|
||||
|
||||
var events = [];
|
||||
for (var i = 0; i < list.Data.length; i++) {
|
||||
var eventInfo = eventService.getEventInfo(list.Data[i]);
|
||||
var htmlMessage = $compile('<span>' + eventInfo.message + '</span>')($scope);
|
||||
events.push({
|
||||
message: $sce.trustAsHtml(htmlMessage[0].outerHTML),
|
||||
appIcon: eventInfo.appIcon,
|
||||
appName: eventInfo.appName,
|
||||
date: list.Data[i].Date,
|
||||
ip: list.Data[i].IpAddress
|
||||
});
|
||||
}
|
||||
if ($scope.events && $scope.events.length > 0) {
|
||||
$scope.events = $scope.events.concat(events);
|
||||
}
|
||||
else {
|
||||
$scope.events = events;
|
||||
}
|
||||
$scope.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
});
|
||||
@@ -10,7 +10,12 @@
|
||||
$scope.model = {
|
||||
name: org.Name,
|
||||
billingEmail: org.BillingEmail,
|
||||
businessName: org.BusinessName
|
||||
businessName: org.BusinessName,
|
||||
businessAddress1: org.BusinessAddress1,
|
||||
businessAddress2: org.BusinessAddress2,
|
||||
businessAddress3: org.BusinessAddress3,
|
||||
businessCountry: org.BusinessCountry,
|
||||
businessTaxNumber: org.BusinessTaxNumber
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
.module('bit.organization')
|
||||
|
||||
.controller('organizationSettingsExportController', function ($scope, apiService, $uibModalInstance, cipherService,
|
||||
$q, toastr, $analytics, $state) {
|
||||
$q, toastr, $analytics, $state, constants) {
|
||||
$analytics.eventTrack('organizationSettingsExportController', { category: 'Modal' });
|
||||
$scope.export = function (model) {
|
||||
$scope.startedExport = true;
|
||||
var decLogins = [],
|
||||
var decCiphers = [],
|
||||
decCollections = [];
|
||||
|
||||
var collectionsPromise = apiService.collections.listOrganization({ orgId: $state.params.orgId },
|
||||
@@ -14,53 +14,94 @@
|
||||
decCollections = cipherService.decryptCollections(collections.Data, $state.params.orgId, true);
|
||||
}).$promise;
|
||||
|
||||
var loginsPromise = apiService.ciphers.listOrganizationDetails({ organizationId: $state.params.orgId },
|
||||
var ciphersPromise = apiService.ciphers.listOrganizationDetails({ organizationId: $state.params.orgId },
|
||||
function (ciphers) {
|
||||
for (var i = 0; i < ciphers.Data.length; i++) {
|
||||
if (ciphers.Data[i].Type === 1) {
|
||||
var decLogin = cipherService.decryptLogin(ciphers.Data[i]);
|
||||
decLogins.push(decLogin);
|
||||
}
|
||||
}
|
||||
decCiphers = cipherService.decryptCiphers(ciphers.Data);
|
||||
}).$promise;
|
||||
|
||||
$q.all([collectionsPromise, loginsPromise]).then(function () {
|
||||
if (!decLogins.length) {
|
||||
$q.all([collectionsPromise, ciphersPromise]).then(function () {
|
||||
if (!decCiphers.length) {
|
||||
toastr.error('Nothing to export.', 'Error!');
|
||||
$scope.close();
|
||||
return;
|
||||
}
|
||||
|
||||
var i;
|
||||
var collectionsDict = {};
|
||||
for (var i = 0; i < decCollections.length; i++) {
|
||||
for (i = 0; i < decCollections.length; i++) {
|
||||
collectionsDict[decCollections[i].id] = decCollections[i];
|
||||
}
|
||||
|
||||
try {
|
||||
var exportLogins = [];
|
||||
for (i = 0; i < decLogins.length; i++) {
|
||||
var login = {
|
||||
name: decLogins[i].name,
|
||||
uri: decLogins[i].uri,
|
||||
username: decLogins[i].username,
|
||||
password: decLogins[i].password,
|
||||
notes: decLogins[i].notes,
|
||||
totp: decLogins[i].totp,
|
||||
collections: []
|
||||
var exportCiphers = [];
|
||||
for (i = 0; i < decCiphers.length; i++) {
|
||||
// only export logins and secure notes
|
||||
if (decCiphers[i].type !== constants.cipherType.login &&
|
||||
decCiphers[i].type !== constants.cipherType.secureNote) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var cipher = {
|
||||
collections: [],
|
||||
type: null,
|
||||
name: decCiphers[i].name,
|
||||
notes: decCiphers[i].notes,
|
||||
fields: null,
|
||||
// Login props
|
||||
login_uri: null,
|
||||
login_username: null,
|
||||
login_password: null,
|
||||
login_totp: null
|
||||
};
|
||||
|
||||
if (decLogins[i].collectionIds) {
|
||||
for (var j = 0; j < decLogins[i].collectionIds.length; j++) {
|
||||
if (collectionsDict.hasOwnProperty(decLogins[i].collectionIds[j])) {
|
||||
login.collections.push(collectionsDict[decLogins[i].collectionIds[j]].name);
|
||||
var j;
|
||||
if (decCiphers[i].collectionIds) {
|
||||
for (j = 0; j < decCiphers[i].collectionIds.length; j++) {
|
||||
if (collectionsDict.hasOwnProperty(decCiphers[i].collectionIds[j])) {
|
||||
cipher.collections.push(collectionsDict[decCiphers[i].collectionIds[j]].name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exportLogins.push(login);
|
||||
if (decCiphers[i].fields) {
|
||||
for (j = 0; j < decCiphers[i].fields.length; j++) {
|
||||
if (!cipher.fields) {
|
||||
cipher.fields = '';
|
||||
}
|
||||
else {
|
||||
cipher.fields += '\n';
|
||||
}
|
||||
|
||||
cipher.fields += ((decCiphers[i].fields[j].name || '') + ': ' + decCiphers[i].fields[j].value);
|
||||
}
|
||||
}
|
||||
|
||||
switch (decCiphers[i].type) {
|
||||
case constants.cipherType.login:
|
||||
cipher.type = 'login';
|
||||
cipher.login_uri = null;
|
||||
cipher.login_username = decCiphers[i].login.username;
|
||||
cipher.login_password = decCiphers[i].login.password;
|
||||
cipher.login_totp = decCiphers[i].login.totp;
|
||||
|
||||
if (decCiphers[i].login.uris && decCiphers[i].login.uris.length) {
|
||||
cipher.login_uri = [];
|
||||
for (j = 0; j < decCiphers[i].login.uris.length; j++) {
|
||||
cipher.login_uri.push(decCiphers[i].login.uris[j].uri);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case constants.cipherType.secureNote:
|
||||
cipher.type = 'note';
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
exportCiphers.push(cipher);
|
||||
}
|
||||
|
||||
var csvString = Papa.unparse(exportLogins);
|
||||
var csvString = Papa.unparse(exportCiphers);
|
||||
var csvBlob = new Blob([csvString]);
|
||||
|
||||
// IE hack. ref http://msdn.microsoft.com/en-us/library/ie/hh779016.aspx
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
$scope.options = [
|
||||
{
|
||||
id: 'bitwardencsv',
|
||||
name: 'bitwarden (csv)',
|
||||
name: 'Bitwarden (csv)',
|
||||
featured: true,
|
||||
sort: 1,
|
||||
instructions: $sce.trustAsHtml('Export using the web vault (vault.bitwarden.com). ' +
|
||||
@@ -55,23 +55,23 @@
|
||||
importService.importOrg(model.source, file || model.fileContents, importSuccess, importError);
|
||||
};
|
||||
|
||||
function importSuccess(collections, logins, collectionRelationships) {
|
||||
if (!collections.length && !logins.length) {
|
||||
function importSuccess(collections, ciphers, collectionRelationships) {
|
||||
if (!collections.length && !ciphers.length) {
|
||||
importError('Nothing was imported.');
|
||||
return;
|
||||
}
|
||||
else if (logins.length) {
|
||||
var halfway = Math.floor(logins.length / 2);
|
||||
var last = logins.length - 1;
|
||||
if (loginIsBadData(logins[0]) && loginIsBadData(logins[halfway]) && loginIsBadData(logins[last])) {
|
||||
importError('CSV data is not formatted correctly. Please check your import file and try again.');
|
||||
else if (ciphers.length) {
|
||||
var halfway = Math.floor(ciphers.length / 2);
|
||||
var last = ciphers.length - 1;
|
||||
if (cipherIsBadData(ciphers[0]) && cipherIsBadData(ciphers[halfway]) && cipherIsBadData(ciphers[last])) {
|
||||
importError('Data is not formatted correctly. Please check your import file and try again.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
apiService.ciphers.importOrg({ orgId: $state.params.orgId }, {
|
||||
collections: cipherService.encryptCollections(collections, $state.params.orgId),
|
||||
ciphers: cipherService.encryptLogins(logins, cryptoService.getOrgKey($state.params.orgId)),
|
||||
ciphers: cipherService.encryptCiphers(ciphers, cryptoService.getOrgKey($state.params.orgId)),
|
||||
collectionRelationships: collectionRelationships
|
||||
}, function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
@@ -82,8 +82,9 @@
|
||||
}, importError);
|
||||
}
|
||||
|
||||
function loginIsBadData(login) {
|
||||
return (login.name === null || login.name === '--') && (login.password === null || login.password === '');
|
||||
function cipherIsBadData(cipher) {
|
||||
return (cipher.name === null || cipher.name === '--') &&
|
||||
(cipher.login && (cipher.login.password === null || cipher.login.password === ''));
|
||||
}
|
||||
|
||||
function importError(error) {
|
||||
|
||||
141
src/app/organization/organizationVaultAddCipherController.js
Normal file
141
src/app/organization/organizationVaultAddCipherController.js
Normal file
@@ -0,0 +1,141 @@
|
||||
angular
|
||||
.module('bit.organization')
|
||||
|
||||
.controller('organizationVaultAddCipherController', function ($scope, apiService, $uibModalInstance, cryptoService,
|
||||
cipherService, passwordService, $analytics, authService, orgId, $uibModal, constants, selectedType) {
|
||||
$analytics.eventTrack('organizationVaultAddCipherController', { category: 'Modal' });
|
||||
$scope.constants = constants;
|
||||
$scope.selectedType = selectedType ? selectedType.toString() : constants.cipherType.login.toString();
|
||||
$scope.cipher = {
|
||||
type: selectedType || constants.cipherType.login,
|
||||
login: {
|
||||
uris: [{
|
||||
uri: null,
|
||||
match: null,
|
||||
matchValue: null
|
||||
}]
|
||||
},
|
||||
identity: {},
|
||||
card: {},
|
||||
secureNote: {
|
||||
type: '0'
|
||||
}
|
||||
};
|
||||
$scope.hideFolders = $scope.hideFavorite = $scope.fromOrg = true;
|
||||
|
||||
authService.getUserProfile().then(function (userProfile) {
|
||||
var orgProfile = userProfile.organizations[orgId];
|
||||
$scope.useTotp = orgProfile.useTotp;
|
||||
});
|
||||
|
||||
$scope.typeChanged = function () {
|
||||
$scope.cipher.type = parseInt($scope.selectedType);
|
||||
};
|
||||
|
||||
$scope.savePromise = null;
|
||||
$scope.save = function () {
|
||||
$scope.cipher.organizationId = orgId;
|
||||
var cipher = cipherService.encryptCipher($scope.cipher);
|
||||
$scope.savePromise = apiService.ciphers.postAdmin(cipher, function (cipherResponse) {
|
||||
$analytics.eventTrack('Created Organization Cipher');
|
||||
var decCipher = cipherService.decryptCipherPreview(cipherResponse);
|
||||
$uibModalInstance.close(decCipher);
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
$scope.generatePassword = function () {
|
||||
if (!$scope.cipher.login.password || confirm('Are you sure you want to overwrite the current password?')) {
|
||||
$analytics.eventTrack('Generated Password From Add');
|
||||
$scope.cipher.login.password = passwordService.generatePassword({ length: 14, special: true });
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addUri = function () {
|
||||
if (!$scope.cipher.login) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$scope.cipher.login.uris) {
|
||||
$scope.cipher.login.uris = [];
|
||||
}
|
||||
|
||||
$scope.cipher.login.uris.push({
|
||||
uri: null,
|
||||
match: null,
|
||||
matchValue: null
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeUri = function (uri) {
|
||||
if (!$scope.cipher.login || !$scope.cipher.login.uris) {
|
||||
return;
|
||||
}
|
||||
|
||||
var index = $scope.cipher.login.uris.indexOf(uri);
|
||||
if (index > -1) {
|
||||
$scope.cipher.login.uris.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.uriMatchChanged = function (uri) {
|
||||
if ((!uri.matchValue && uri.matchValue !== 0) || uri.matchValue === '') {
|
||||
uri.match = null;
|
||||
}
|
||||
else {
|
||||
uri.match = parseInt(uri.matchValue);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addField = function () {
|
||||
if (!$scope.cipher.fields) {
|
||||
$scope.cipher.fields = [];
|
||||
}
|
||||
|
||||
$scope.cipher.fields.push({
|
||||
type: constants.fieldType.text.toString(),
|
||||
name: null,
|
||||
value: null
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeField = function (field) {
|
||||
var index = $scope.cipher.fields.indexOf(field);
|
||||
if (index > -1) {
|
||||
$scope.cipher.fields.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.clipboardSuccess = function (e) {
|
||||
e.clearSelection();
|
||||
selectPassword(e);
|
||||
};
|
||||
|
||||
$scope.clipboardError = function (e, password) {
|
||||
if (password) {
|
||||
selectPassword(e);
|
||||
}
|
||||
alert('Your web browser does not support easy clipboard copying. Copy it manually instead.');
|
||||
};
|
||||
|
||||
function selectPassword(e) {
|
||||
var target = $(e.trigger).parent().prev();
|
||||
if (target.attr('type') === 'text') {
|
||||
target.select();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('close');
|
||||
};
|
||||
|
||||
$scope.showUpgrade = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/views/paidOrgRequired.html',
|
||||
controller: 'paidOrgRequiredController',
|
||||
resolve: {
|
||||
orgId: function () { return orgId; }
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
@@ -1,85 +0,0 @@
|
||||
angular
|
||||
.module('bit.organization')
|
||||
|
||||
.controller('organizationVaultAddLoginController', function ($scope, apiService, $uibModalInstance, cryptoService,
|
||||
cipherService, passwordService, $analytics, authService, orgId, $uibModal) {
|
||||
$analytics.eventTrack('organizationVaultAddLoginController', { category: 'Modal' });
|
||||
$scope.login = {};
|
||||
$scope.hideFolders = $scope.hideFavorite = $scope.fromOrg = true;
|
||||
|
||||
authService.getUserProfile().then(function (userProfile) {
|
||||
var orgProfile = userProfile.organizations[orgId];
|
||||
$scope.useTotp = orgProfile.useTotp;
|
||||
});
|
||||
|
||||
$scope.savePromise = null;
|
||||
$scope.save = function (model) {
|
||||
model.organizationId = orgId;
|
||||
var login = cipherService.encryptLogin(model);
|
||||
$scope.savePromise = apiService.ciphers.postAdmin(login, function (loginResponse) {
|
||||
$analytics.eventTrack('Created Organization Login');
|
||||
var decLogin = cipherService.decryptLogin(loginResponse);
|
||||
$uibModalInstance.close(decLogin);
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
$scope.generatePassword = function () {
|
||||
if (!$scope.login.password || confirm('Are you sure you want to overwrite the current password?')) {
|
||||
$analytics.eventTrack('Generated Password From Add');
|
||||
$scope.login.password = passwordService.generatePassword({ length: 12, special: true });
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addField = function () {
|
||||
if (!$scope.login.fields) {
|
||||
$scope.login.fields = [];
|
||||
}
|
||||
|
||||
$scope.login.fields.push({
|
||||
type: '0',
|
||||
name: null,
|
||||
value: null
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeField = function (field) {
|
||||
var index = $scope.login.fields.indexOf(field);
|
||||
if (index > -1) {
|
||||
$scope.login.fields.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.clipboardSuccess = function (e) {
|
||||
e.clearSelection();
|
||||
selectPassword(e);
|
||||
};
|
||||
|
||||
$scope.clipboardError = function (e, password) {
|
||||
if (password) {
|
||||
selectPassword(e);
|
||||
}
|
||||
alert('Your web browser does not support easy clipboard copying. Copy it manually instead.');
|
||||
};
|
||||
|
||||
function selectPassword(e) {
|
||||
var target = $(e.trigger).parent().prev();
|
||||
if (target.attr('type') === 'text') {
|
||||
target.select();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('close');
|
||||
};
|
||||
|
||||
$scope.showUpgrade = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/views/paidOrgRequired.html',
|
||||
controller: 'paidOrgRequiredController',
|
||||
resolve: {
|
||||
orgId: function () { return orgId; }
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
@@ -2,16 +2,16 @@
|
||||
.module('bit.organization')
|
||||
|
||||
.controller('organizationVaultAttachmentsController', function ($scope, apiService, $uibModalInstance, cryptoService,
|
||||
cipherService, loginId, $analytics, validationService, toastr, $timeout) {
|
||||
cipherService, cipherId, $analytics, validationService, toastr, $timeout) {
|
||||
$analytics.eventTrack('organizationVaultAttachmentsController', { category: 'Modal' });
|
||||
$scope.login = {};
|
||||
$scope.cipher = {};
|
||||
$scope.loading = true;
|
||||
$scope.isPremium = true;
|
||||
$scope.canUseAttachments = true;
|
||||
var closing = false;
|
||||
|
||||
apiService.ciphers.getAdmin({ id: loginId }, function (login) {
|
||||
$scope.login = cipherService.decryptLogin(login);
|
||||
apiService.ciphers.getAdmin({ id: cipherId }, function (cipher) {
|
||||
$scope.cipher = cipherService.decryptCipher(cipher);
|
||||
$scope.loading = false;
|
||||
}, function () {
|
||||
$scope.loading = false;
|
||||
@@ -24,30 +24,26 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var key = cryptoService.getOrgKey($scope.login.organizationId);
|
||||
var key = cryptoService.getOrgKey($scope.cipher.organizationId);
|
||||
$scope.savePromise = cipherService.encryptAttachmentFile(key, files[0]).then(function (encValue) {
|
||||
var fd = new FormData();
|
||||
var blob = new Blob([encValue.data], { type: 'application/octet-stream' });
|
||||
fd.append('data', blob, encValue.fileName);
|
||||
return apiService.ciphers.postAttachment({ id: loginId }, fd).$promise;
|
||||
return apiService.ciphers.postAttachmentAdmin({ id: cipherId }, fd).$promise;
|
||||
}).then(function (response) {
|
||||
$analytics.eventTrack('Added Attachment');
|
||||
toastr.success('The attachment has been added.');
|
||||
closing = true;
|
||||
$uibModalInstance.close(true);
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
validationService.addError(form, 'file', err, true);
|
||||
}
|
||||
else {
|
||||
validationService.addError(form, 'file', 'Something went wrong.', true);
|
||||
}
|
||||
}, function (e) {
|
||||
var errors = validationService.parseErrors(e);
|
||||
toastr.error(errors.length ? errors[0] : 'An error occurred.');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.download = function (attachment) {
|
||||
attachment.loading = true;
|
||||
var key = cryptoService.getOrgKey($scope.login.organizationId);
|
||||
var key = cryptoService.getOrgKey($scope.cipher.organizationId);
|
||||
cipherService.downloadAndDecryptAttachment(key, attachment, true).then(function (res) {
|
||||
$timeout(function () {
|
||||
attachment.loading = false;
|
||||
@@ -65,12 +61,12 @@
|
||||
}
|
||||
|
||||
attachment.loading = true;
|
||||
apiService.ciphers.delAttachment({ id: loginId, attachmentId: attachment.id }).$promise.then(function () {
|
||||
apiService.ciphers.delAttachmentAdmin({ id: cipherId, attachmentId: attachment.id }).$promise.then(function () {
|
||||
attachment.loading = false;
|
||||
$analytics.eventTrack('Deleted Organization Attachment');
|
||||
var index = $scope.login.attachments.indexOf(attachment);
|
||||
var index = $scope.cipher.attachments.indexOf(attachment);
|
||||
if (index > -1) {
|
||||
$scope.login.attachments.splice(index, 1);
|
||||
$scope.cipher.attachments.splice(index, 1);
|
||||
}
|
||||
}, function () {
|
||||
toastr.error('Cannot delete attachment.');
|
||||
@@ -89,6 +85,6 @@
|
||||
|
||||
e.preventDefault();
|
||||
closing = true;
|
||||
$uibModalInstance.close(!!$scope.login.attachments && $scope.login.attachments.length > 0);
|
||||
$uibModalInstance.close(!!$scope.cipher.attachments && $scope.cipher.attachments.length > 0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
angular
|
||||
.module('bit.organization')
|
||||
|
||||
.controller('organizationVaultLoginCollectionsController', function ($scope, apiService, $uibModalInstance, cipherService,
|
||||
.controller('organizationVaultCipherCollectionsController', function ($scope, apiService, $uibModalInstance, cipherService,
|
||||
cipher, $analytics, collections) {
|
||||
$analytics.eventTrack('organizationVaultLoginCollectionsController', { category: 'Modal' });
|
||||
$analytics.eventTrack('organizationVaultCipherCollectionsController', { category: 'Modal' });
|
||||
$scope.cipher = {};
|
||||
$scope.collections = [];
|
||||
$scope.selectedCollections = {};
|
||||
@@ -69,7 +69,7 @@
|
||||
|
||||
$scope.submitPromise = apiService.ciphers.putCollectionsAdmin({ id: cipher.id }, request)
|
||||
.$promise.then(function (response) {
|
||||
$analytics.eventTrack('Edited Login Collections');
|
||||
$analytics.eventTrack('Edited Cipher Collections');
|
||||
$uibModalInstance.close({
|
||||
action: 'collectionsEdit',
|
||||
collectionIds: request.collectionIds
|
||||
104
src/app/organization/organizationVaultCipherEventsController.js
Normal file
104
src/app/organization/organizationVaultCipherEventsController.js
Normal file
@@ -0,0 +1,104 @@
|
||||
angular
|
||||
.module('bit.organization')
|
||||
|
||||
.controller('organizationVaultCipherEventsController', function ($scope, apiService, $uibModalInstance,
|
||||
cipher, $analytics, eventService) {
|
||||
$analytics.eventTrack('organizationVaultCipherEventsController', { category: 'Modal' });
|
||||
$scope.cipher = cipher;
|
||||
$scope.events = [];
|
||||
$scope.loading = true;
|
||||
$scope.continuationToken = null;
|
||||
|
||||
var defaultFilters = eventService.getDefaultDateFilters();
|
||||
$scope.filterStart = defaultFilters.start;
|
||||
$scope.filterEnd = defaultFilters.end;
|
||||
|
||||
$uibModalInstance.opened.then(function () {
|
||||
load();
|
||||
});
|
||||
|
||||
$scope.refresh = function () {
|
||||
loadEvents(true);
|
||||
};
|
||||
|
||||
$scope.next = function () {
|
||||
loadEvents(false);
|
||||
};
|
||||
|
||||
var i = 0,
|
||||
orgUsersUserIdDict = {},
|
||||
orgUsersIdDict = {};
|
||||
|
||||
function load() {
|
||||
apiService.organizationUsers.list({ orgId: cipher.organizationId }).$promise.then(function (list) {
|
||||
var users = [];
|
||||
for (i = 0; i < list.Data.length; i++) {
|
||||
var user = {
|
||||
id: list.Data[i].Id,
|
||||
userId: list.Data[i].UserId,
|
||||
name: list.Data[i].Name,
|
||||
email: list.Data[i].Email
|
||||
};
|
||||
|
||||
users.push(user);
|
||||
|
||||
var displayName = user.name || user.email;
|
||||
orgUsersUserIdDict[user.userId] = displayName;
|
||||
orgUsersIdDict[user.id] = displayName;
|
||||
}
|
||||
|
||||
$scope.orgUsers = users;
|
||||
|
||||
return loadEvents(true);
|
||||
});
|
||||
}
|
||||
|
||||
function loadEvents(clearExisting) {
|
||||
var filterResult = eventService.formatDateFilters($scope.filterStart, $scope.filterEnd);
|
||||
if (filterResult.error) {
|
||||
alert(filterResult.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (clearExisting) {
|
||||
$scope.continuationToken = null;
|
||||
$scope.events = [];
|
||||
}
|
||||
|
||||
$scope.loading = true;
|
||||
return apiService.events.listCipher({
|
||||
id: cipher.id,
|
||||
start: filterResult.start,
|
||||
end: filterResult.end,
|
||||
continuationToken: $scope.continuationToken
|
||||
}).$promise.then(function (list) {
|
||||
$scope.continuationToken = list.ContinuationToken;
|
||||
|
||||
var events = [];
|
||||
for (i = 0; i < list.Data.length; i++) {
|
||||
var userId = list.Data[i].ActingUserId || list.Data[i].UserId;
|
||||
var eventInfo = eventService.getEventInfo(list.Data[i], { cipherInfo: false });
|
||||
events.push({
|
||||
message: eventInfo.message,
|
||||
appIcon: eventInfo.appIcon,
|
||||
appName: eventInfo.appName,
|
||||
userId: userId,
|
||||
userName: userId ? (orgUsersUserIdDict[userId] || '-') : '-',
|
||||
date: list.Data[i].Date,
|
||||
ip: list.Data[i].IpAddress
|
||||
});
|
||||
}
|
||||
if ($scope.events && $scope.events.length > 0) {
|
||||
$scope.events = $scope.events.concat(events);
|
||||
}
|
||||
else {
|
||||
$scope.events = events;
|
||||
}
|
||||
$scope.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
});
|
||||
@@ -2,23 +2,35 @@
|
||||
.module('bit.organization')
|
||||
|
||||
.controller('organizationVaultController', function ($scope, apiService, cipherService, $analytics, $q, $state,
|
||||
$localStorage, $uibModal, $filter, authService) {
|
||||
$scope.logins = [];
|
||||
$localStorage, $uibModal, $filter, authService, $uibModalStack, constants, $timeout) {
|
||||
$scope.ciphers = [];
|
||||
$scope.collections = [];
|
||||
$scope.loading = true;
|
||||
$scope.useEvents = false;
|
||||
$scope.constants = constants;
|
||||
$scope.filter = undefined;
|
||||
$scope.selectedType = undefined;
|
||||
$scope.selectedCollection = undefined;
|
||||
$scope.selectedAll = true;
|
||||
$scope.selectedTitle = 'All';
|
||||
$scope.selectedIcon = 'fa-th';
|
||||
|
||||
$scope.$on('$viewContentLoaded', function () {
|
||||
authService.getUserProfile().then(function (profile) {
|
||||
if (profile.organizations) {
|
||||
var org = profile.organizations[$state.params.orgId];
|
||||
$scope.useEvents = !!org.useEvents;
|
||||
}
|
||||
});
|
||||
|
||||
var collectionPromise = apiService.collections.listOrganization({ orgId: $state.params.orgId }, function (collections) {
|
||||
var decCollections = [{
|
||||
id: null,
|
||||
name: 'Unassigned',
|
||||
collapsed: $localStorage.collapsedOrgCollections && 'unassigned' in $localStorage.collapsedOrgCollections
|
||||
name: 'Unassigned'
|
||||
}];
|
||||
|
||||
for (var i = 0; i < collections.Data.length; i++) {
|
||||
var decCollection = cipherService.decryptCollection(collections.Data[i], null, true);
|
||||
decCollection.collapsed = $localStorage.collapsedOrgCollections &&
|
||||
decCollection.id in $localStorage.collapsedOrgCollections;
|
||||
decCollections.push(decCollection);
|
||||
}
|
||||
|
||||
@@ -27,32 +39,38 @@
|
||||
|
||||
var cipherPromise = apiService.ciphers.listOrganizationDetails({ organizationId: $state.params.orgId },
|
||||
function (ciphers) {
|
||||
var decLogins = [];
|
||||
var decCiphers = [];
|
||||
|
||||
for (var i = 0; i < ciphers.Data.length; i++) {
|
||||
if (ciphers.Data[i].Type === 1) {
|
||||
var decLogin = cipherService.decryptLoginPreview(ciphers.Data[i]);
|
||||
decLogins.push(decLogin);
|
||||
}
|
||||
var decCipher = cipherService.decryptCipherPreview(ciphers.Data[i]);
|
||||
decCiphers.push(decCipher);
|
||||
}
|
||||
|
||||
$scope.logins = decLogins;
|
||||
$scope.ciphers = decCiphers;
|
||||
}).$promise;
|
||||
|
||||
$q.all([collectionPromise, cipherPromise]).then(function () {
|
||||
$scope.loading = false;
|
||||
});
|
||||
});
|
||||
$timeout(function () {
|
||||
if ($('body').hasClass('control-sidebar-open')) {
|
||||
$("#search").focus();
|
||||
}
|
||||
}, 500);
|
||||
|
||||
$scope.filterByCollection = function (collection) {
|
||||
return function (cipher) {
|
||||
if (!cipher.collectionIds || !cipher.collectionIds.length) {
|
||||
return collection.id === null;
|
||||
if ($state.params.search) {
|
||||
$uibModalStack.dismissAll();
|
||||
$scope.searchVaultText = $state.params.search;
|
||||
}
|
||||
|
||||
return cipher.collectionIds.indexOf(collection.id) > -1;
|
||||
};
|
||||
};
|
||||
if ($state.params.viewEvents) {
|
||||
$uibModalStack.dismissAll();
|
||||
var cipher = $filter('filter')($scope.ciphers, { id: $state.params.viewEvents });
|
||||
if (cipher && cipher.length) {
|
||||
$scope.viewEvents(cipher[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$scope.collectionSort = function (item) {
|
||||
if (!item.id) {
|
||||
@@ -62,70 +80,60 @@
|
||||
return item.name.toLowerCase();
|
||||
};
|
||||
|
||||
$scope.collapseExpand = function (collection) {
|
||||
if (!$localStorage.collapsedOrgCollections) {
|
||||
$localStorage.collapsedOrgCollections = {};
|
||||
}
|
||||
|
||||
var id = collection.id || 'unassigned';
|
||||
|
||||
if (id in $localStorage.collapsedOrgCollections) {
|
||||
delete $localStorage.collapsedOrgCollections[id];
|
||||
}
|
||||
else {
|
||||
$localStorage.collapsedOrgCollections[id] = true;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.editLogin = function (login) {
|
||||
$scope.editCipher = function (cipher) {
|
||||
var editModel = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/vault/views/vaultEditLogin.html',
|
||||
controller: 'organizationVaultEditLoginController',
|
||||
templateUrl: 'app/vault/views/vaultEditCipher.html',
|
||||
controller: 'organizationVaultEditCipherController',
|
||||
resolve: {
|
||||
loginId: function () { return login.id; },
|
||||
cipherId: function () { return cipher.id; },
|
||||
orgId: function () { return $state.params.orgId; }
|
||||
}
|
||||
});
|
||||
|
||||
editModel.result.then(function (returnVal) {
|
||||
var index;
|
||||
if (returnVal.action === 'edit') {
|
||||
login.name = returnVal.data.name;
|
||||
login.username = returnVal.data.username;
|
||||
index = $scope.ciphers.indexOf(cipher);
|
||||
if (index > -1) {
|
||||
returnVal.data.collectionIds = $scope.ciphers[index].collectionIds;
|
||||
$scope.ciphers[index] = returnVal.data;
|
||||
}
|
||||
}
|
||||
else if (returnVal.action === 'delete') {
|
||||
var index = $scope.logins.indexOf(login);
|
||||
index = $scope.ciphers.indexOf(cipher);
|
||||
if (index > -1) {
|
||||
$scope.logins.splice(index, 1);
|
||||
$scope.ciphers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('organizationVaultAddLogin', function (event, args) {
|
||||
$scope.addLogin();
|
||||
$scope.$on('organizationVaultAddCipher', function (event, args) {
|
||||
$scope.addCipher();
|
||||
});
|
||||
|
||||
$scope.addLogin = function () {
|
||||
$scope.addCipher = function () {
|
||||
var addModel = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/vault/views/vaultAddLogin.html',
|
||||
controller: 'organizationVaultAddLoginController',
|
||||
templateUrl: 'app/vault/views/vaultAddCipher.html',
|
||||
controller: 'organizationVaultAddCipherController',
|
||||
resolve: {
|
||||
orgId: function () { return $state.params.orgId; }
|
||||
orgId: function () { return $state.params.orgId; },
|
||||
selectedType: function () { return $scope.selectedType; }
|
||||
}
|
||||
});
|
||||
|
||||
addModel.result.then(function (addedLogin) {
|
||||
$scope.logins.push(addedLogin);
|
||||
addModel.result.then(function (addedCipher) {
|
||||
$scope.ciphers.push(addedCipher);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.editCollections = function (cipher) {
|
||||
var modal = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/organization/views/organizationVaultLoginCollections.html',
|
||||
controller: 'organizationVaultLoginCollectionsController',
|
||||
templateUrl: 'app/organization/views/organizationVaultCipherCollections.html',
|
||||
controller: 'organizationVaultCipherCollectionsController',
|
||||
resolve: {
|
||||
cipher: function () { return cipher; },
|
||||
collections: function () { return $scope.collections; }
|
||||
@@ -139,9 +147,20 @@
|
||||
});
|
||||
};
|
||||
|
||||
$scope.attachments = function (login) {
|
||||
$scope.viewEvents = function (cipher) {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/organization/views/organizationVaultCipherEvents.html',
|
||||
controller: 'organizationVaultCipherEventsController',
|
||||
resolve: {
|
||||
cipher: function () { return cipher; }
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.attachments = function (cipher) {
|
||||
authService.getUserProfile().then(function (profile) {
|
||||
return !!profile.organizations[login.organizationId].maxStorageGb;
|
||||
return !!profile.organizations[cipher.organizationId].maxStorageGb;
|
||||
}).then(function (useStorage) {
|
||||
if (!useStorage) {
|
||||
$uibModal.open({
|
||||
@@ -149,7 +168,7 @@
|
||||
templateUrl: 'app/views/paidOrgRequired.html',
|
||||
controller: 'paidOrgRequiredController',
|
||||
resolve: {
|
||||
orgId: function () { return login.organizationId; }
|
||||
orgId: function () { return cipher.organizationId; }
|
||||
}
|
||||
});
|
||||
return;
|
||||
@@ -160,49 +179,95 @@
|
||||
templateUrl: 'app/vault/views/vaultAttachments.html',
|
||||
controller: 'organizationVaultAttachmentsController',
|
||||
resolve: {
|
||||
loginId: function () { return login.id; }
|
||||
cipherId: function () { return cipher.id; }
|
||||
}
|
||||
});
|
||||
|
||||
attachmentModel.result.then(function (hasAttachments) {
|
||||
login.hasAttachments = hasAttachments;
|
||||
cipher.hasAttachments = hasAttachments;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeLogin = function (login, collection) {
|
||||
if (!confirm('Are you sure you want to remove this login (' + login.name + ') from the ' +
|
||||
'collection (' + collection.name + ') ?')) {
|
||||
$scope.deleteCipher = function (cipher) {
|
||||
if (!confirm('Are you sure you want to delete this item (' + cipher.name + ')?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var request = {
|
||||
collectionIds: []
|
||||
};
|
||||
|
||||
for (var i = 0; i < login.collectionIds.length; i++) {
|
||||
if (login.collectionIds[i] !== collection.id) {
|
||||
request.collectionIds.push(login.collectionIds[i]);
|
||||
apiService.ciphers.delAdmin({ id: cipher.id }, function () {
|
||||
$analytics.eventTrack('Deleted Cipher');
|
||||
var index = $scope.ciphers.indexOf(cipher);
|
||||
if (index > -1) {
|
||||
$scope.ciphers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
apiService.ciphers.putCollections({ id: login.id }, request).$promise.then(function (response) {
|
||||
$analytics.eventTrack('Removed Login From Collection');
|
||||
login.collectionIds = request.collectionIds;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteLogin = function (login) {
|
||||
if (!confirm('Are you sure you want to delete this login (' + login.name + ')?')) {
|
||||
return;
|
||||
}
|
||||
$scope.filterCollection = function (col) {
|
||||
resetSelected();
|
||||
$scope.selectedCollection = col;
|
||||
$scope.selectedIcon = 'fa-cube';
|
||||
$scope.filter = function (c) {
|
||||
return c.collectionIds && c.collectionIds.indexOf(col.id) > -1;
|
||||
};
|
||||
fixLayout();
|
||||
};
|
||||
|
||||
apiService.ciphers.delAdmin({ id: login.id }, function () {
|
||||
$analytics.eventTrack('Deleted Login');
|
||||
var index = $scope.logins.indexOf(login);
|
||||
if (index > -1) {
|
||||
$scope.logins.splice(index, 1);
|
||||
}
|
||||
});
|
||||
$scope.filterType = function (t) {
|
||||
resetSelected();
|
||||
$scope.selectedType = t;
|
||||
switch (t) {
|
||||
case constants.cipherType.login:
|
||||
$scope.selectedTitle = 'Login';
|
||||
$scope.selectedIcon = 'fa-globe';
|
||||
break;
|
||||
case constants.cipherType.card:
|
||||
$scope.selectedTitle = 'Card';
|
||||
$scope.selectedIcon = 'fa-credit-card';
|
||||
break;
|
||||
case constants.cipherType.identity:
|
||||
$scope.selectedTitle = 'Identity';
|
||||
$scope.selectedIcon = 'fa-id-card-o';
|
||||
break;
|
||||
case constants.cipherType.secureNote:
|
||||
$scope.selectedTitle = 'Secure Note';
|
||||
$scope.selectedIcon = 'fa-sticky-note-o';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
$scope.filter = function (c) {
|
||||
return c.type === t;
|
||||
};
|
||||
fixLayout();
|
||||
};
|
||||
|
||||
$scope.filterAll = function () {
|
||||
resetSelected();
|
||||
$scope.selectedAll = true;
|
||||
$scope.selectedTitle = 'All';
|
||||
$scope.selectedIcon = 'fa-th';
|
||||
$scope.filter = null;
|
||||
fixLayout();
|
||||
};
|
||||
|
||||
function resetSelected() {
|
||||
$scope.selectedCollection = undefined;
|
||||
$scope.selectedType = undefined;
|
||||
$scope.selectedAll = false;
|
||||
}
|
||||
|
||||
function fixLayout() {
|
||||
if ($.AdminLTE && $.AdminLTE.layout) {
|
||||
$timeout(function () {
|
||||
$.AdminLTE.layout.fix();
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.cipherFilter = function () {
|
||||
return function (cipher) {
|
||||
return !$scope.filter || $scope.filter(cipher);
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
148
src/app/organization/organizationVaultEditCipherController.js
Normal file
148
src/app/organization/organizationVaultEditCipherController.js
Normal file
@@ -0,0 +1,148 @@
|
||||
angular
|
||||
.module('bit.organization')
|
||||
|
||||
.controller('organizationVaultEditCipherController', function ($scope, apiService, $uibModalInstance, cryptoService,
|
||||
cipherService, passwordService, cipherId, $analytics, orgId, $uibModal, constants) {
|
||||
$analytics.eventTrack('organizationVaultEditCipherController', { category: 'Modal' });
|
||||
$scope.cipher = {};
|
||||
$scope.hideFolders = $scope.hideFavorite = $scope.fromOrg = true;
|
||||
$scope.constants = constants;
|
||||
|
||||
apiService.ciphers.getAdmin({ id: cipherId }, function (cipher) {
|
||||
$scope.cipher = cipherService.decryptCipher(cipher);
|
||||
$scope.useTotp = $scope.cipher.organizationUseTotp;
|
||||
setUriMatchValues();
|
||||
});
|
||||
|
||||
$scope.save = function (model) {
|
||||
var cipher = cipherService.encryptCipher(model, $scope.cipher.type);
|
||||
$scope.savePromise = apiService.ciphers.putAdmin({ id: cipherId }, cipher, function (cipherResponse) {
|
||||
$analytics.eventTrack('Edited Organization Cipher');
|
||||
var decCipher = cipherService.decryptCipherPreview(cipherResponse);
|
||||
$uibModalInstance.close({
|
||||
action: 'edit',
|
||||
data: decCipher
|
||||
});
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
$scope.generatePassword = function () {
|
||||
if (!$scope.cipher.login.password || confirm('Are you sure you want to overwrite the current password?')) {
|
||||
$analytics.eventTrack('Generated Password From Edit');
|
||||
$scope.cipher.login.password = passwordService.generatePassword({ length: 14, special: true });
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addUri = function () {
|
||||
if (!$scope.cipher.login) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$scope.cipher.login.uris) {
|
||||
$scope.cipher.login.uris = [];
|
||||
}
|
||||
|
||||
$scope.cipher.login.uris.push({
|
||||
uri: null,
|
||||
match: null,
|
||||
matchValue: null
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeUri = function (uri) {
|
||||
if (!$scope.cipher.login || !$scope.cipher.login.uris) {
|
||||
return;
|
||||
}
|
||||
|
||||
var index = $scope.cipher.login.uris.indexOf(uri);
|
||||
if (index > -1) {
|
||||
$scope.cipher.login.uris.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.uriMatchChanged = function (uri) {
|
||||
if ((!uri.matchValue && uri.matchValue !== 0) || uri.matchValue === '') {
|
||||
uri.match = null;
|
||||
}
|
||||
else {
|
||||
uri.match = parseInt(uri.matchValue);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addField = function () {
|
||||
if (!$scope.cipher.login.fields) {
|
||||
$scope.cipher.login.fields = [];
|
||||
}
|
||||
|
||||
$scope.cipher.fields.push({
|
||||
type: constants.fieldType.text.toString(),
|
||||
name: null,
|
||||
value: null
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeField = function (field) {
|
||||
var index = $scope.cipher.fields.indexOf(field);
|
||||
if (index > -1) {
|
||||
$scope.cipher.fields.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.clipboardSuccess = function (e) {
|
||||
e.clearSelection();
|
||||
selectPassword(e);
|
||||
};
|
||||
|
||||
$scope.clipboardError = function (e, password) {
|
||||
if (password) {
|
||||
selectPassword(e);
|
||||
}
|
||||
alert('Your web browser does not support easy clipboard copying. Copy it manually instead.');
|
||||
};
|
||||
|
||||
function selectPassword(e) {
|
||||
var target = $(e.trigger).parent().prev();
|
||||
if (target.attr('type') === 'text') {
|
||||
target.select();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.delete = function () {
|
||||
if (!confirm('Are you sure you want to delete this item (' + $scope.cipher.name + ')?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
apiService.ciphers.delAdmin({ id: $scope.cipher.id }, function () {
|
||||
$analytics.eventTrack('Deleted Organization Cipher From Edit');
|
||||
$uibModalInstance.close({
|
||||
action: 'delete',
|
||||
data: $scope.cipher.id
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
|
||||
$scope.showUpgrade = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/views/paidOrgRequired.html',
|
||||
controller: 'paidOrgRequiredController',
|
||||
resolve: {
|
||||
orgId: function () { return orgId; }
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function setUriMatchValues() {
|
||||
if ($scope.cipher.login && $scope.cipher.login.uris) {
|
||||
for (var i = 0; i < $scope.cipher.login.uris.length; i++) {
|
||||
$scope.cipher.login.uris[i].matchValue =
|
||||
$scope.cipher.login.uris[i].match || $scope.cipher.login.uris[i].match === 0 ?
|
||||
$scope.cipher.login.uris[i].match.toString() : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,100 +0,0 @@
|
||||
angular
|
||||
.module('bit.organization')
|
||||
|
||||
.controller('organizationVaultEditLoginController', function ($scope, apiService, $uibModalInstance, cryptoService,
|
||||
cipherService, passwordService, loginId, $analytics, orgId, $uibModal) {
|
||||
$analytics.eventTrack('organizationVaultEditLoginController', { category: 'Modal' });
|
||||
$scope.login = {};
|
||||
$scope.hideFolders = $scope.hideFavorite = $scope.fromOrg = true;
|
||||
|
||||
apiService.ciphers.getAdmin({ id: loginId }, function (login) {
|
||||
$scope.login = cipherService.decryptLogin(login);
|
||||
$scope.useTotp = $scope.login.organizationUseTotp;
|
||||
});
|
||||
|
||||
$scope.save = function (model) {
|
||||
var login = cipherService.encryptLogin(model);
|
||||
$scope.savePromise = apiService.ciphers.putAdmin({ id: loginId }, login, function (loginResponse) {
|
||||
$analytics.eventTrack('Edited Organization Login');
|
||||
var decLogin = cipherService.decryptLogin(loginResponse);
|
||||
$uibModalInstance.close({
|
||||
action: 'edit',
|
||||
data: decLogin
|
||||
});
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
$scope.generatePassword = function () {
|
||||
if (!$scope.login.password || confirm('Are you sure you want to overwrite the current password?')) {
|
||||
$analytics.eventTrack('Generated Password From Edit');
|
||||
$scope.login.password = passwordService.generatePassword({ length: 12, special: true });
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addField = function () {
|
||||
if (!$scope.login.fields) {
|
||||
$scope.login.fields = [];
|
||||
}
|
||||
|
||||
$scope.login.fields.push({
|
||||
type: '0',
|
||||
name: null,
|
||||
value: null
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeField = function (field) {
|
||||
var index = $scope.login.fields.indexOf(field);
|
||||
if (index > -1) {
|
||||
$scope.login.fields.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.clipboardSuccess = function (e) {
|
||||
e.clearSelection();
|
||||
selectPassword(e);
|
||||
};
|
||||
|
||||
$scope.clipboardError = function (e, password) {
|
||||
if (password) {
|
||||
selectPassword(e);
|
||||
}
|
||||
alert('Your web browser does not support easy clipboard copying. Copy it manually instead.');
|
||||
};
|
||||
|
||||
function selectPassword(e) {
|
||||
var target = $(e.trigger).parent().prev();
|
||||
if (target.attr('type') === 'text') {
|
||||
target.select();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.delete = function () {
|
||||
if (!confirm('Are you sure you want to delete this login (' + $scope.login.name + ')?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
apiService.ciphers.delAdmin({ id: $scope.login.id }, function () {
|
||||
$analytics.eventTrack('Deleted Organization Login From Edit');
|
||||
$uibModalInstance.close({
|
||||
action: 'delete',
|
||||
data: $scope.login.id
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
|
||||
$scope.showUpgrade = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/views/paidOrgRequired.html',
|
||||
controller: 'paidOrgRequiredController',
|
||||
resolve: {
|
||||
orgId: function () { return orgId; }
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
@@ -116,10 +116,10 @@
|
||||
Loading...
|
||||
</div>
|
||||
<div ng-show="!loading">
|
||||
You plan currently has a total of <b>{{plan.seats}}</b> seats.
|
||||
Your plan currently has a total of <b>{{plan.seats}}</b> seats.
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer" ng-if="!selfHosted && !noSubscription">
|
||||
<div class="box-footer" ng-if="!selfHosted && !noSubscription && canAdjustSeats">
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="adjustSeats(true)">
|
||||
Add Seats
|
||||
</button>
|
||||
@@ -134,7 +134,7 @@
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<p>
|
||||
You plan has a total of {{storage.maxGb}} GB of encrypted file storage.
|
||||
Your plan has a total of {{storage.maxGb}} GB of encrypted file storage.
|
||||
You are currently using {{storage.currentName}}.
|
||||
</p>
|
||||
<div class="progress" style="margin: 0;">
|
||||
@@ -201,6 +201,11 @@
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr ng-repeat="charge in charges">
|
||||
<td style="width: 30px">
|
||||
<a href="#" stop-click ng-click="viewInvoice(charge)" title="Invoice">
|
||||
<i class="fa fa-file-pdf-o"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 200px">
|
||||
{{charge.date | date: 'mediumDate'}}
|
||||
</td>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<div class="box-filters hidden-xs">
|
||||
<div class="form-group form-group-sm has-feedback has-feedback-left">
|
||||
<input type="text" id="search" class="form-control" placeholder="Search collections..."
|
||||
<input type="text" id="filterSearch" class="form-control" placeholder="Search collections..."
|
||||
style="width: 200px;" ng-model="filterSearch">
|
||||
<span class="fa fa-search form-control-feedback text-muted" aria-hidden="true"></span>
|
||||
</div>
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
<section class="content">
|
||||
<div class="callout callout-warning" ng-if="!orgProfile.enabled">
|
||||
<h4><i class="fa fa-warning"></i> Organization Disabled</h4>
|
||||
<p>
|
||||
This organization is currently disabled. Users will not see your shared logins or collections.
|
||||
Contact us if you would like to reinstate this organization.
|
||||
</p>
|
||||
<p>This organization is currently disabled. Users will not see your shared logins or collections.</p>
|
||||
<p ng-if="!selfHosted">Contact us if you would like to reinstate this organization.</p>
|
||||
<p ng-if="selfHosted">Update your license to reinstate this organization.</p>
|
||||
<a ng-if="selfHosted" class="btn btn-default btn-flat" href="#" stop-click ng-click="goBilling()">
|
||||
Billing & Licensing
|
||||
</a>
|
||||
<a class="btn btn-default btn-flat" href="https://bitwarden.com/contact/" target="_blank">
|
||||
Contact Us
|
||||
</a>
|
||||
@@ -20,7 +22,7 @@
|
||||
<h3 class="box-title">Let's Get Started!</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<p>Dashboard features are coming soon. Get started by inviting users and creating your collections.</p>
|
||||
<p>Get started by inviting users and creating your collections.</p>
|
||||
<a class="btn btn-default btn-flat" ui-sref="backend.org.people({orgId: orgProfile.id})">
|
||||
Invite Users
|
||||
</a>
|
||||
|
||||
67
src/app/organization/views/organizationEvents.html
Normal file
67
src/app/organization/views/organizationEvents.html
Normal file
@@ -0,0 +1,67 @@
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
Events
|
||||
<small>audit your organization</small>
|
||||
</h1>
|
||||
</section>
|
||||
<section class="content">
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
|
||||
<div class="box-filters hidden-xs hidden-sm">
|
||||
<input type="datetime-local" ng-model="filterStart" required
|
||||
class="form-control input-sm" style="width:initial;" />
|
||||
-
|
||||
<input type="datetime-local" ng-model="filterEnd" required
|
||||
class="form-control input-sm" style="width:initial;" />
|
||||
</div>
|
||||
<div class="box-tools">
|
||||
<button type="button" class="btn btn-primary btn-sm btn-flat" ng-click="refresh()">
|
||||
<i class="fa fa-fw fa-refresh" ng-class="{'fa-spin': loading}"></i> Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-body" ng-class="{'no-padding': filteredEvents.length}">
|
||||
<div ng-show="loading && !events.length">
|
||||
Loading...
|
||||
</div>
|
||||
<div ng-show="!loading && !events.length">
|
||||
<p>There are no events to list.</p>
|
||||
</div>
|
||||
<div class="table-responsive" ng-show="events.length">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th><span class="sr-only">App</span></th>
|
||||
<th>User</th>
|
||||
<th>Event</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="event in filteredEvents = (events)">
|
||||
<td style="width: 210px; min-width: 100px;">
|
||||
{{event.date | date:'medium'}}
|
||||
</td>
|
||||
<td style="width: 20px;" class="text-center">
|
||||
<i class="text-muted fa fa-lg {{event.appIcon}}" title="{{event.appName}}, {{event.ip}}"></i>
|
||||
</td>
|
||||
<td style="width: 150px; min-width: 100px;">
|
||||
{{event.userName}}
|
||||
</td>
|
||||
<td>
|
||||
<div ng-bind-html="event.message"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer text-center" ng-show="continuationToken">
|
||||
<button class="btn btn-link btn-block" ng-click="next()" ng-if="!loading">
|
||||
Load more...
|
||||
</button>
|
||||
<i class="fa fa-fw fa-refresh fa-spin text-muted" ng-if="loading"></i>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<div class="box-filters hidden-xs">
|
||||
<div class="form-group form-group-sm has-feedback has-feedback-left">
|
||||
<input type="text" id="search" class="form-control" placeholder="Search groups..."
|
||||
<input type="text" id="filterSearch" class="form-control" placeholder="Search groups..."
|
||||
style="width: 200px;" ng-model="filterSearch">
|
||||
<span class="fa fa-search form-control-feedback text-muted" aria-hidden="true"></span>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<div class="box-filters hidden-xs">
|
||||
<div class="form-group form-group-sm has-feedback has-feedback-left">
|
||||
<input type="text" id="search" class="form-control" placeholder="Search people..."
|
||||
<input type="text" id="filterSearch" class="form-control" placeholder="Search people..."
|
||||
style="width: 200px;" ng-model="filterSearch">
|
||||
<span class="fa fa-search form-control-feedback text-muted" aria-hidden="true"></span>
|
||||
</div>
|
||||
@@ -46,6 +46,12 @@
|
||||
<i class="fa fa-fw fa-sitemap"></i> Groups
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" stop-click ng-click="events(user)"
|
||||
ng-if="useEvents && user.status === 2">
|
||||
<i class="fa fa-fw fa-file-text-o"></i> Event Logs
|
||||
</a>
|
||||
</li>
|
||||
<li ng-show="user.status === 1">
|
||||
<a href="#" stop-click ng-click="confirm(user)">
|
||||
<i class="fa fa-fw fa-check"></i> Confirm
|
||||
|
||||
56
src/app/organization/views/organizationPeopleEvents.html
Normal file
56
src/app/organization/views/organizationPeopleEvents.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title"><i class="fa fa-file-text-o"></i> User Event Logs <small>{{email}}</small></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="hidden-xs">
|
||||
<input type="datetime-local" ng-model="filterStart" required
|
||||
class="form-control input-sm" style="width:initial; display: inline;" />
|
||||
-
|
||||
<input type="datetime-local" ng-model="filterEnd" required
|
||||
class="form-control input-sm" style="width:initial; display: inline;" />
|
||||
<button type="button" class="btn btn-primary btn-sm btn-flat" ng-click="refresh()">
|
||||
<i class="fa fa-fw fa-refresh" ng-class="{'fa-spin': loading}"></i> Refresh
|
||||
</button>
|
||||
<hr />
|
||||
</div>
|
||||
<div ng-show="loading && !events.length">
|
||||
Loading...
|
||||
</div>
|
||||
<div ng-show="!loading && !events.length">
|
||||
<p>There are no events to list.</p>
|
||||
</div>
|
||||
<div class="table-responsive" ng-show="events.length" style="margin: 0;">
|
||||
<table class="table table-striped table-hover" style="{{ !continuationToken ? 'margin: 0;' : '' }}">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th><span class="sr-only">App</span></th>
|
||||
<th>Event</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="event in filteredEvents = (events)">
|
||||
<td style="width: 210px; min-width: 100px;">
|
||||
{{event.date | date:'medium'}}
|
||||
</td>
|
||||
<td style="width: 20px;" class="text-center">
|
||||
<i class="text-muted fa fa-lg {{event.appIcon}}" title="{{event.appName}}, {{event.ip}}"></i>
|
||||
</td>
|
||||
<td>
|
||||
<div ng-bind-html="event.message"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="text-center" ng-show="continuationToken">
|
||||
<button class="btn btn-link btn-block" ng-click="next()" ng-if="!loading">
|
||||
Load more...
|
||||
</button>
|
||||
<i class="fa fa-fw fa-refresh fa-spin text-muted" ng-if="loading"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
|
||||
</div>
|
||||
@@ -5,8 +5,8 @@
|
||||
<form name="inviteForm" ng-submit="inviteForm.$valid && submit(model)" api-form="submitPromise" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Invite a new user to your organization by entering their bitwarden account email address below. If they do not have
|
||||
a bitwarden account already, they will be prompted to create a new account.
|
||||
Invite a new user to your organization by entering their Bitwarden account email address below. If they do not have
|
||||
a Bitwarden account already, they will be prompted to create a new account.
|
||||
</p>
|
||||
<div class="callout callout-danger validation-errors" ng-show="inviteForm.$errors">
|
||||
<h4>Errors have occurred</h4>
|
||||
|
||||
@@ -25,15 +25,28 @@
|
||||
<input type="text" id="name" name="Name" ng-model="model.name" class="form-control"
|
||||
required api-field ng-readonly="selfHosted" />
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="name">Billing Email</label>
|
||||
<input type="email" id="billingEmail" name="BillingEmail" ng-model="model.billingEmail"
|
||||
class="form-control" required api-field ng-readonly="selfHosted" />
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="name">Business Name</label>
|
||||
<input type="text" id="businessName" name="BusinessName" ng-model="model.businessName"
|
||||
class="form-control" api-field ng-readonly="selfHosted" />
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="name">Billing Email</label>
|
||||
<input type="email" id="billingEmail" name="BillingEmail" ng-model="model.billingEmail"
|
||||
class="form-control" required api-field ng-readonly="selfHosted" />
|
||||
<div ng-if="!selfHosted">
|
||||
<hr />
|
||||
<strong>Tax Information</strong>
|
||||
<div>{{model.businessAddress1}}</div>
|
||||
<div>{{model.businessAddress2}}</div>
|
||||
<div>{{model.businessAddress3}}</div>
|
||||
<div>{{model.businessCountry}}</div>
|
||||
<div>{{model.businessTaxNumber}}</div>
|
||||
<p class="help-block">
|
||||
Please <a href="https://bitwarden.com/contact/" target="_blank">contact support</a>
|
||||
to provide (or update) tax information for your invoices.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3 settings-photo">
|
||||
|
||||
@@ -5,36 +5,27 @@
|
||||
<span ng-pluralize
|
||||
count="collections.length > 0 ? collections.length - 1 : 0"
|
||||
when="{'1': '{} collection', 'other': '{} collections'}"></span>,
|
||||
<span ng-pluralize count="logins.length" when="{'1': '{} login', 'other': '{} logins'}"></span>
|
||||
<span ng-pluralize count="ciphers.length" when="{'1': '{} item', 'other': '{} items'}"></span>
|
||||
</small>
|
||||
</h1>
|
||||
</section>
|
||||
<section class="content">
|
||||
<p ng-show="loading && !collections.length">Loading...</p>
|
||||
<div class="box" ng-class="{'collapsed-box': collection.collapsed}" ng-repeat="collection in collections |
|
||||
orderBy: collectionSort track by collection.id"
|
||||
ng-show="collections.length && (!main.searchVaultText || collectionLogins.length)">
|
||||
<p ng-show="loading">Loading...</p>
|
||||
<div class="box" ng-show="!loading">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">
|
||||
<i class="fa" ng-class="{'fa-cubes': collection.id, 'fa-sitemap': !collection.id}"></i>
|
||||
{{collection.name}}
|
||||
<small ng-pluralize count="collectionLogins.length" when="{'1': '{} login', 'other': '{} logins'}"></small>
|
||||
<i class="fa {{selectedIcon}}"></i>
|
||||
{{selectedCollection ? selectedCollection.name : selectedTitle}}
|
||||
<small ng-pluralize count="filteredCiphers.length" when="{'1': '{} item', 'other': '{} items'}"></small>
|
||||
</h3>
|
||||
<div class="box-tools">
|
||||
<button type="button" class="btn btn-box-tool" data-widget="collapse" title="Collapse/Expand"
|
||||
ng-click="collapseExpand(collection)">
|
||||
<i class="fa" ng-class="{'fa-minus': !collection.collapsed, 'fa-plus': collection.collapsed}"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-body" ng-class="{'no-padding': collectionLogins.length}">
|
||||
<div ng-show="!collectionLogins.length && collection.id">No logins in this collection.</div>
|
||||
<div ng-show="!collectionLogins.length && !collection.id">No unassigned logins.</div>
|
||||
<div class="table-responsive" ng-show="collectionLogins.length">
|
||||
<div class="box-body" ng-class="{'no-padding': filteredCiphers.length}">
|
||||
<div ng-show="!filteredCiphers.length">No items to list.</div>
|
||||
<div class="table-responsive" ng-show="filteredCiphers.length">
|
||||
<table class="table table-striped table-hover table-vmiddle">
|
||||
<tbody>
|
||||
<tr ng-repeat="login in collectionLogins = (logins | filter: filterByCollection(collection) |
|
||||
filter: (main.searchVaultText || '') | orderBy: ['name', 'username']) track by login.id">
|
||||
<tr ng-repeat="cipher in filteredCiphers = (ciphers | filter: cipherFilter() |
|
||||
filter: (searchVaultText || '') | orderBy: ['name', 'subTitle']) track by cipher.id">
|
||||
<td style="width: 70px;">
|
||||
<div class="btn-group" data-append-to="body">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
@@ -42,37 +33,43 @@
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="#" stop-click ng-click="editLogin(login)">
|
||||
<a href="#" stop-click ng-click="editCipher(cipher)">
|
||||
<i class="fa fa-fw fa-pencil"></i> Edit
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" stop-click ng-click="attachments(login)">
|
||||
<a href="#" stop-click ng-click="attachments(cipher)">
|
||||
<i class="fa fa-fw fa-paperclip"></i> Attachments
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" stop-click ng-click="editCollections(login)">
|
||||
<a href="#" stop-click ng-click="editCollections(cipher)">
|
||||
<i class="fa fa-fw fa-cubes"></i> Collections
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" stop-click ng-click="removeLogin(login, collection)" class="text-red"
|
||||
ng-if="collection.id">
|
||||
<i class="fa fa-fw fa-remove"></i> Remove
|
||||
<a href="#" stop-click ng-click="viewEvents(cipher)" ng-if="useEvents">
|
||||
<i class="fa fa-fw fa-file-text-o"></i> Event Logs
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" stop-click ng-click="deleteLogin(login)" class="text-red">
|
||||
<a href="#" stop-click ng-click="deleteCipher(cipher)" class="text-red">
|
||||
<i class="fa fa-fw fa-trash"></i> Delete
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
<td class="vault-icon">
|
||||
<i class="fa fa-fw fa-lg {{::cipher.icon}}" ng-if="!cipher.meta.image"></i>
|
||||
<img alt="" ng-if="cipher.meta.image" ng-src="{{cipher.meta.image}}"
|
||||
fallback-src="images/fa-globe.png" />
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" stop-click ng-click="editLogin(login)">{{login.name}}</a>
|
||||
<div class="text-sm text-muted">{{login.username}}</div>
|
||||
<a href="#" stop-click ng-click="editCipher(cipher)">{{cipher.name}}</a>
|
||||
<i class="fa fa-paperclip text-muted" title="Attachments" ng-if="cipher.hasAttachments"
|
||||
stop-prop></i>
|
||||
<div class="text-sm text-muted">{{cipher.subTitle}}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -81,3 +78,65 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<aside class="control-sidebar control-sidebar-light">
|
||||
<div class="tab-content">
|
||||
<form class="search-form">
|
||||
<label for="search" class="sr-only">Search</label>
|
||||
<div class="form-group has-feedback">
|
||||
<input type="search" id="search" class="form-control" placeholder="Search org vault..."
|
||||
ng-model="searchVaultText" />
|
||||
<span class="fa fa-search form-control-feedback" aria-hidden="true"></span>
|
||||
</div>
|
||||
</form>
|
||||
<ul class="control-sidebar-menu">
|
||||
<li ng-class="{active: selectedAll}">
|
||||
<a href="#" stop-click ng-click="filterAll()">
|
||||
<i class="fa fa-th fa-fw"></i> All Items
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h3 class="control-sidebar-heading">Types</h3>
|
||||
<div class="control-sidebar-section">
|
||||
<ul class="control-sidebar-menu">
|
||||
<li ng-class="{active: constants.cipherType.login === selectedType}">
|
||||
<a href="#" stop-click ng-click="filterType(constants.cipherType.login)">
|
||||
<i class="fa fa-globe fa-fw"></i> Login
|
||||
</a>
|
||||
</li>
|
||||
<li ng-class="{active: constants.cipherType.card === selectedType}">
|
||||
<a href="#" stop-click ng-click="filterType(constants.cipherType.card)">
|
||||
<i class="fa fa-credit-card fa-fw"></i> Card
|
||||
</a>
|
||||
</li>
|
||||
<li ng-class="{active: constants.cipherType.identity === selectedType}">
|
||||
<a href="#" stop-click ng-click="filterType(constants.cipherType.identity)">
|
||||
<i class="fa fa-id-card-o fa-fw"></i> Identity
|
||||
</a>
|
||||
</li>
|
||||
<li ng-class="{active: constants.cipherType.secureNote === selectedType}">
|
||||
<a href="#" stop-click ng-click="filterType(constants.cipherType.secureNote)">
|
||||
<i class="fa fa-sticky-note-o fa-fw"></i> Secure Note
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h3 class="control-sidebar-heading">Collections</h3>
|
||||
<div ng-show="loading && !collections.length">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
<div ng-show="!loading && !collections.length">
|
||||
<p>No collections.</p>
|
||||
</div>
|
||||
<div class="control-sidebar-section" ng-show="!loading && collections.length">
|
||||
<ul class="control-sidebar-menu">
|
||||
<li ng-repeat="collection in collections | orderBy: [collectionSort] track by collection.id"
|
||||
ng-class="{active: selectedCollection && collection.id === selectedCollection.id}">
|
||||
<a href="#" stop-click ng-click="filterCollection(collection)">
|
||||
<i class="fa fa-caret-right fa-fw"></i>
|
||||
{{collection.name}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</div>
|
||||
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<p>Edit the collections that this login is being shared with.</p>
|
||||
<p>Edit the collections that this item is being shared with.</p>
|
||||
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
|
||||
<h4>Errors have occurred</h4>
|
||||
<ul>
|
||||
@@ -0,0 +1,60 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title"><i class="fa fa-file-text-o"></i> Event Logs <small>{{cipher.name}}</small></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="hidden-xs">
|
||||
<input type="datetime-local" ng-model="filterStart" required
|
||||
class="form-control input-sm" style="width:initial; display: inline;" />
|
||||
-
|
||||
<input type="datetime-local" ng-model="filterEnd" required
|
||||
class="form-control input-sm" style="width:initial; display: inline;" />
|
||||
<button type="button" class="btn btn-primary btn-sm btn-flat" ng-click="refresh()">
|
||||
<i class="fa fa-fw fa-refresh" ng-class="{'fa-spin': loading}"></i> Refresh
|
||||
</button>
|
||||
<hr />
|
||||
</div>
|
||||
<div ng-show="loading && !events.length">
|
||||
Loading...
|
||||
</div>
|
||||
<div ng-show="!loading && !events.length">
|
||||
<p>There are no events to list.</p>
|
||||
</div>
|
||||
<div class="table-responsive" ng-show="events.length" style="margin: 0;">
|
||||
<table class="table table-striped table-hover" style="{{ !continuationToken ? 'margin: 0;' : '' }}">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th><span class="sr-only">App</span></th>
|
||||
<th>User</th>
|
||||
<th>Event</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="event in filteredEvents = (events)">
|
||||
<td style="width: 210px; min-width: 100px;">
|
||||
{{event.date | date:'medium'}}
|
||||
</td>
|
||||
<td style="width: 20px;" class="text-center">
|
||||
<i class="text-muted fa fa-lg {{event.appIcon}}" title="{{event.appName}}, {{event.ip}}"></i>
|
||||
</td>
|
||||
<td style="width: 150px; min-width: 100px;">
|
||||
{{event.userName}}
|
||||
</td>
|
||||
<td>
|
||||
{{event.message}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="text-center" ng-show="continuationToken">
|
||||
<button class="btn btn-link btn-block" ng-click="next()" ng-if="!loading">
|
||||
Load more...
|
||||
</button>
|
||||
<i class="fa fa-fw fa-refresh fa-spin text-muted" ng-if="loading"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
|
||||
</div>
|
||||
@@ -1,7 +1,7 @@
|
||||
angular
|
||||
.module('bit.services')
|
||||
|
||||
.factory('apiService', function ($resource, tokenService, appSettings, $httpParamSerializer) {
|
||||
.factory('apiService', function ($resource, tokenService, appSettings, $httpParamSerializer, utilsService) {
|
||||
var _service = {},
|
||||
_apiUri = appSettings.apiUri,
|
||||
_identityUri = appSettings.identityUri;
|
||||
@@ -35,19 +35,27 @@
|
||||
delAdmin: { url: _apiUri + '/ciphers/:id/delete-admin', method: 'POST', params: { id: '@id' } },
|
||||
delMany: { url: _apiUri + '/ciphers/delete', method: 'POST' },
|
||||
moveMany: { url: _apiUri + '/ciphers/move', method: 'POST' },
|
||||
purge: { url: _apiUri + '/ciphers/purge', method: 'POST' },
|
||||
postAttachment: {
|
||||
url: _apiUri + '/ciphers/:id/attachment',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': undefined },
|
||||
params: { id: '@id' }
|
||||
},
|
||||
postAttachmentAdmin: {
|
||||
url: _apiUri + '/ciphers/:id/attachment-admin',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': undefined },
|
||||
params: { id: '@id' }
|
||||
},
|
||||
postShareAttachment: {
|
||||
url: _apiUri + '/ciphers/:id/attachment/:attachmentId/share?organizationId=:orgId',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': undefined },
|
||||
params: { id: '@id', attachmentId: '@attachmentId', orgId: '@orgId' }
|
||||
},
|
||||
delAttachment: { url: _apiUri + '/ciphers/:id/attachment/:attachmentId/delete', method: 'POST', params: { id: '@id', attachmentId: '@attachmentId' } }
|
||||
delAttachment: { url: _apiUri + '/ciphers/:id/attachment/:attachmentId/delete', method: 'POST', params: { id: '@id', attachmentId: '@attachmentId' } },
|
||||
delAttachmentAdmin: { url: _apiUri + '/ciphers/:id/attachment/:attachmentId/delete-admin', method: 'POST', params: { id: '@id', attachmentId: '@attachmentId' } }
|
||||
});
|
||||
|
||||
_service.organizations = $resource(_apiUri + '/organizations/:id', {}, {
|
||||
@@ -178,11 +186,21 @@
|
||||
getPublicKey: { url: _apiUri + '/users/:id/public-key', method: 'GET', params: { id: '@id' } }
|
||||
});
|
||||
|
||||
_service.events = $resource(_apiUri + '/events', {}, {
|
||||
list: { method: 'GET', params: {} },
|
||||
listOrganization: { url: _apiUri + '/organizations/:orgId/events', method: 'GET', params: { id: '@orgId' } },
|
||||
listCipher: { url: _apiUri + '/ciphers/:id/events', method: 'GET', params: { id: '@id' } },
|
||||
listOrganizationUser: { url: _apiUri + '/organizations/:orgId/users/:id/events', method: 'GET', params: { orgId: '@orgId', id: '@id' } }
|
||||
});
|
||||
|
||||
_service.identity = $resource(_identityUri + '/connect', {}, {
|
||||
token: {
|
||||
url: _identityUri + '/connect/token',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' },
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
|
||||
'Device-Type': utilsService.getDeviceType()
|
||||
},
|
||||
transformRequest: transformUrlEncoded,
|
||||
skipAuthorization: true,
|
||||
params: {}
|
||||
|
||||
@@ -95,7 +95,7 @@ angular
|
||||
_service.logOut = function () {
|
||||
tokenService.clearTokens();
|
||||
cryptoService.clearKeys();
|
||||
$rootScope.vaultFolders = $rootScope.vaultLogins = null;
|
||||
$rootScope.vaultCiphers = $rootScope.vaultFolders = $rootScope.vaultCollections = null;
|
||||
_userProfile = null;
|
||||
};
|
||||
|
||||
@@ -150,6 +150,8 @@ angular
|
||||
maxStorageGb: profile.Organizations[i].MaxStorageGb,
|
||||
seats: profile.Organizations[i].Seats,
|
||||
useGroups: profile.Organizations[i].UseGroups,
|
||||
useDirectory: profile.Organizations[i].UseDirectory,
|
||||
useEvents: profile.Organizations[i].UseEvents,
|
||||
useTotp: profile.Organizations[i].UseTotp
|
||||
};
|
||||
}
|
||||
@@ -158,8 +160,8 @@ angular
|
||||
cryptoService.setOrgKeys(orgs);
|
||||
_setDeferred.resolve(_userProfile);
|
||||
}
|
||||
}, function () {
|
||||
_setDeferred.reject();
|
||||
}, function (error) {
|
||||
_setDeferred.reject(error);
|
||||
});
|
||||
|
||||
return _setDeferred.promise;
|
||||
@@ -183,6 +185,8 @@ angular
|
||||
maxStorageGb: org.MaxStorageGb,
|
||||
seats: org.Seats,
|
||||
useGroups: org.UseGroups,
|
||||
useDirectory: org.UseDirectory,
|
||||
useEvents: org.UseEvents,
|
||||
useTotp: org.UseTotp
|
||||
};
|
||||
profile.organizations[o.id] = o;
|
||||
@@ -233,7 +237,9 @@ angular
|
||||
_service.refreshAccessToken = function () {
|
||||
var refreshToken = tokenService.getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
return null;
|
||||
return $q(function (resolve, reject) {
|
||||
resolve(null);
|
||||
});
|
||||
}
|
||||
|
||||
return apiService.identity.token({
|
||||
|
||||
@@ -1,64 +1,23 @@
|
||||
angular
|
||||
.module('bit.services')
|
||||
|
||||
.factory('cipherService', function (cryptoService, apiService, $q, $window) {
|
||||
var _service = {};
|
||||
|
||||
_service.decryptLogins = function (encryptedLogins) {
|
||||
if (!encryptedLogins) throw "encryptedLogins is undefined or null";
|
||||
|
||||
var unencryptedLogins = [];
|
||||
for (var i = 0; i < encryptedLogins.length; i++) {
|
||||
unencryptedLogins.push(_service.decryptLogin(encryptedLogins[i]));
|
||||
}
|
||||
|
||||
return unencryptedLogins;
|
||||
.factory('cipherService', function (cryptoService, apiService, $q, $window, constants, appSettings, $localStorage) {
|
||||
var _service = {
|
||||
disableWebsiteIcons: $localStorage.disableWebsiteIcons
|
||||
};
|
||||
|
||||
_service.decryptLogin = function (encryptedLogin, isCipher) {
|
||||
if (!encryptedLogin) throw "encryptedLogin is undefined or null";
|
||||
_service.decryptCiphers = function (encryptedCiphers) {
|
||||
if (!encryptedCiphers) throw "encryptedCiphers is undefined or null";
|
||||
|
||||
var key = null;
|
||||
if (encryptedLogin.OrganizationId) {
|
||||
key = cryptoService.getOrgKey(encryptedLogin.OrganizationId);
|
||||
var unencryptedCiphers = [];
|
||||
for (var i = 0; i < encryptedCiphers.length; i++) {
|
||||
unencryptedCiphers.push(_service.decryptCipher(encryptedCiphers[i]));
|
||||
}
|
||||
|
||||
var login = {
|
||||
id: encryptedLogin.Id,
|
||||
organizationId: encryptedLogin.OrganizationId,
|
||||
collectionIds: encryptedLogin.CollectionIds || [],
|
||||
'type': 1,
|
||||
folderId: encryptedLogin.FolderId,
|
||||
favorite: encryptedLogin.Favorite,
|
||||
edit: encryptedLogin.Edit,
|
||||
organizationUseTotp: encryptedLogin.OrganizationUseTotp,
|
||||
attachments: null
|
||||
};
|
||||
|
||||
var loginData = encryptedLogin.Data;
|
||||
if (loginData) {
|
||||
login.name = cryptoService.decrypt(loginData.Name, key);
|
||||
login.uri = loginData.Uri && loginData.Uri !== '' ? cryptoService.decrypt(loginData.Uri, key) : null;
|
||||
login.username = loginData.Username && loginData.Username !== '' ? cryptoService.decrypt(loginData.Username, key) : null;
|
||||
login.password = loginData.Password && loginData.Password !== '' ? cryptoService.decrypt(loginData.Password, key) : null;
|
||||
login.notes = loginData.Notes && loginData.Notes !== '' ? cryptoService.decrypt(loginData.Notes, key) : null;
|
||||
login.totp = loginData.Totp && loginData.Totp !== '' ? cryptoService.decrypt(loginData.Totp, key) : null;
|
||||
login.fields = _service.decryptFields(key, loginData.Fields);
|
||||
}
|
||||
|
||||
if (!encryptedLogin.Attachments) {
|
||||
return login;
|
||||
}
|
||||
|
||||
login.attachments = [];
|
||||
for (var i = 0; i < encryptedLogin.Attachments.length; i++) {
|
||||
login.attachments.push(_service.decryptAttachment(key, encryptedLogin.Attachments[i]));
|
||||
}
|
||||
|
||||
return login;
|
||||
return unencryptedCiphers;
|
||||
};
|
||||
|
||||
_service.decryptLoginPreview = function (encryptedCipher) {
|
||||
_service.decryptCipher = function (encryptedCipher) {
|
||||
if (!encryptedCipher) throw "encryptedCipher is undefined or null";
|
||||
|
||||
var key = null;
|
||||
@@ -66,27 +25,209 @@ angular
|
||||
key = cryptoService.getOrgKey(encryptedCipher.OrganizationId);
|
||||
}
|
||||
|
||||
var login = {
|
||||
var cipher = {
|
||||
id: encryptedCipher.Id,
|
||||
organizationId: encryptedCipher.OrganizationId,
|
||||
collectionIds: encryptedCipher.CollectionIds || [],
|
||||
'type': encryptedCipher.Type,
|
||||
name: cryptoService.decrypt(encryptedCipher.Name, key),
|
||||
notes: _service.decryptProperty(encryptedCipher.Notes, key, true, false),
|
||||
fields: _service.decryptFields(key, encryptedCipher.Fields),
|
||||
folderId: encryptedCipher.FolderId,
|
||||
favorite: encryptedCipher.Favorite,
|
||||
edit: encryptedCipher.Edit,
|
||||
organizationUseTotp: encryptedCipher.OrganizationUseTotp,
|
||||
hasAttachments: !!encryptedCipher.Attachments && encryptedCipher.Attachments.length > 0
|
||||
attachments: null,
|
||||
icon: null
|
||||
};
|
||||
|
||||
var loginData = encryptedCipher.Data;
|
||||
if (loginData) {
|
||||
login.name = _service.decryptProperty(loginData.Name, key, false);
|
||||
login.username = _service.decryptProperty(loginData.Username, key, true);
|
||||
login.password = _service.decryptProperty(loginData.Password, key, true);
|
||||
var i;
|
||||
switch (cipher.type) {
|
||||
case constants.cipherType.login:
|
||||
cipher.login = {
|
||||
username: _service.decryptProperty(encryptedCipher.Login.Username, key, true, false),
|
||||
password: _service.decryptProperty(encryptedCipher.Login.Password, key, true, false),
|
||||
totp: _service.decryptProperty(encryptedCipher.Login.Totp, key, true, false),
|
||||
uris: null
|
||||
};
|
||||
if (encryptedCipher.Login.Uris) {
|
||||
cipher.login.uris = [];
|
||||
for (i = 0; i < encryptedCipher.Login.Uris.length; i++) {
|
||||
cipher.login.uris.push({
|
||||
uri: _service.decryptProperty(encryptedCipher.Login.Uris[i].Uri, key, true, false),
|
||||
match: encryptedCipher.Login.Uris[i].Match
|
||||
});
|
||||
}
|
||||
}
|
||||
cipher.icon = 'fa-globe';
|
||||
break;
|
||||
case constants.cipherType.secureNote:
|
||||
cipher.secureNote = {
|
||||
type: encryptedCipher.SecureNote.Type
|
||||
};
|
||||
cipher.icon = 'fa-sticky-note-o';
|
||||
break;
|
||||
case constants.cipherType.card:
|
||||
cipher.card = {
|
||||
cardholderName: _service.decryptProperty(encryptedCipher.Card.CardholderName, key, true, false),
|
||||
number: _service.decryptProperty(encryptedCipher.Card.Number, key, true, false),
|
||||
brand: _service.decryptProperty(encryptedCipher.Card.Brand, key, true, false),
|
||||
expMonth: _service.decryptProperty(encryptedCipher.Card.ExpMonth, key, true, false),
|
||||
expYear: _service.decryptProperty(encryptedCipher.Card.ExpYear, key, true, false),
|
||||
code: _service.decryptProperty(encryptedCipher.Card.Code, key, true, false)
|
||||
};
|
||||
cipher.icon = 'fa-credit-card';
|
||||
break;
|
||||
case constants.cipherType.identity:
|
||||
cipher.identity = {
|
||||
title: _service.decryptProperty(encryptedCipher.Identity.Title, key, true, false),
|
||||
firstName: _service.decryptProperty(encryptedCipher.Identity.FirstName, key, true, false),
|
||||
middleName: _service.decryptProperty(encryptedCipher.Identity.MiddleName, key, true, false),
|
||||
lastName: _service.decryptProperty(encryptedCipher.Identity.LastName, key, true, false),
|
||||
address1: _service.decryptProperty(encryptedCipher.Identity.Address1, key, true, false),
|
||||
address2: _service.decryptProperty(encryptedCipher.Identity.Address2, key, true, false),
|
||||
address3: _service.decryptProperty(encryptedCipher.Identity.Address3, key, true, false),
|
||||
city: _service.decryptProperty(encryptedCipher.Identity.City, key, true, false),
|
||||
state: _service.decryptProperty(encryptedCipher.Identity.State, key, true, false),
|
||||
postalCode: _service.decryptProperty(encryptedCipher.Identity.PostalCode, key, true, false),
|
||||
country: _service.decryptProperty(encryptedCipher.Identity.Country, key, true, false),
|
||||
company: _service.decryptProperty(encryptedCipher.Identity.Company, key, true, false),
|
||||
email: _service.decryptProperty(encryptedCipher.Identity.Email, key, true, false),
|
||||
phone: _service.decryptProperty(encryptedCipher.Identity.Phone, key, true, false),
|
||||
ssn: _service.decryptProperty(encryptedCipher.Identity.SSN, key, true, false),
|
||||
username: _service.decryptProperty(encryptedCipher.Identity.Username, key, true, false),
|
||||
passportNumber: _service.decryptProperty(encryptedCipher.Identity.PassportNumber, key, true, false),
|
||||
licenseNumber: _service.decryptProperty(encryptedCipher.Identity.LicenseNumber, key, true, false)
|
||||
};
|
||||
cipher.icon = 'fa-id-card-o';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return login;
|
||||
if (!encryptedCipher.Attachments) {
|
||||
return cipher;
|
||||
}
|
||||
|
||||
cipher.attachments = [];
|
||||
for (i = 0; i < encryptedCipher.Attachments.length; i++) {
|
||||
cipher.attachments.push(_service.decryptAttachment(key, encryptedCipher.Attachments[i]));
|
||||
}
|
||||
|
||||
return cipher;
|
||||
};
|
||||
|
||||
_service.decryptCipherPreview = function (encryptedCipher) {
|
||||
if (!encryptedCipher) throw "encryptedCipher is undefined or null";
|
||||
|
||||
var key = null;
|
||||
if (encryptedCipher.OrganizationId) {
|
||||
key = cryptoService.getOrgKey(encryptedCipher.OrganizationId);
|
||||
}
|
||||
|
||||
var cipher = {
|
||||
id: encryptedCipher.Id,
|
||||
organizationId: encryptedCipher.OrganizationId,
|
||||
collectionIds: encryptedCipher.CollectionIds || [],
|
||||
'type': encryptedCipher.Type,
|
||||
name: _service.decryptProperty(encryptedCipher.Name, key, false, true),
|
||||
folderId: encryptedCipher.FolderId,
|
||||
favorite: encryptedCipher.Favorite,
|
||||
edit: encryptedCipher.Edit,
|
||||
organizationUseTotp: encryptedCipher.OrganizationUseTotp,
|
||||
hasAttachments: !!encryptedCipher.Attachments && encryptedCipher.Attachments.length > 0,
|
||||
meta: {},
|
||||
icon: null
|
||||
};
|
||||
|
||||
switch (cipher.type) {
|
||||
case constants.cipherType.login:
|
||||
cipher.subTitle = _service.decryptProperty(encryptedCipher.Login.Username, key, true, true);
|
||||
cipher.meta.password = _service.decryptProperty(encryptedCipher.Login.Password, key, true, true);
|
||||
cipher.meta.uri = null;
|
||||
if (encryptedCipher.Login.Uris && encryptedCipher.Login.Uris.length) {
|
||||
cipher.meta.uri = _service.decryptProperty(encryptedCipher.Login.Uris[0].Uri, key, true, true);
|
||||
}
|
||||
setLoginIcon(cipher, cipher.meta.uri, true);
|
||||
break;
|
||||
case constants.cipherType.secureNote:
|
||||
cipher.subTitle = null;
|
||||
cipher.icon = 'fa-sticky-note-o';
|
||||
break;
|
||||
case constants.cipherType.card:
|
||||
cipher.subTitle = '';
|
||||
cipher.meta.number = _service.decryptProperty(encryptedCipher.Card.Number, key, true, true);
|
||||
var brand = _service.decryptProperty(encryptedCipher.Card.Brand, key, true, true);
|
||||
if (brand) {
|
||||
cipher.subTitle = brand;
|
||||
}
|
||||
if (cipher.meta.number && cipher.meta.number.length >= 4) {
|
||||
if (cipher.subTitle !== '') {
|
||||
cipher.subTitle += ', ';
|
||||
}
|
||||
cipher.subTitle += ('*' + cipher.meta.number.substr(cipher.meta.number.length - 4));
|
||||
}
|
||||
cipher.icon = 'fa-credit-card';
|
||||
break;
|
||||
case constants.cipherType.identity:
|
||||
var firstName = _service.decryptProperty(encryptedCipher.Identity.FirstName, key, true, true);
|
||||
var lastName = _service.decryptProperty(encryptedCipher.Identity.LastName, key, true, true);
|
||||
cipher.subTitle = '';
|
||||
if (firstName) {
|
||||
cipher.subTitle = firstName;
|
||||
}
|
||||
if (lastName) {
|
||||
if (cipher.subTitle !== '') {
|
||||
cipher.subTitle += ' ';
|
||||
}
|
||||
cipher.subTitle += lastName;
|
||||
}
|
||||
cipher.icon = 'fa-id-card-o';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (cipher.subTitle === '') {
|
||||
cipher.subTitle = null;
|
||||
}
|
||||
|
||||
return cipher;
|
||||
};
|
||||
|
||||
function setLoginIcon(cipher, uri, setImage) {
|
||||
if (!_service.disableWebsiteIcons && uri) {
|
||||
var hostnameUri = uri,
|
||||
isWebsite = false;
|
||||
|
||||
if (hostnameUri.indexOf('androidapp://') === 0) {
|
||||
cipher.icon = 'fa-android';
|
||||
}
|
||||
else if (hostnameUri.indexOf('iosapp://') === 0) {
|
||||
cipher.icon = 'fa-apple';
|
||||
}
|
||||
else if (hostnameUri.indexOf('://') === -1 && hostnameUri.indexOf('.') > -1) {
|
||||
hostnameUri = "http://" + hostnameUri;
|
||||
isWebsite = true;
|
||||
}
|
||||
else {
|
||||
isWebsite = hostnameUri.indexOf('http') === 0 && hostnameUri.indexOf('.') > -1;
|
||||
}
|
||||
|
||||
if (setImage && isWebsite) {
|
||||
try {
|
||||
var url = new URL(hostnameUri);
|
||||
cipher.meta.image = appSettings.iconsUri + '/' + url.hostname + '/icon.png';
|
||||
}
|
||||
catch (e) { }
|
||||
}
|
||||
}
|
||||
|
||||
if (!cipher.icon) {
|
||||
cipher.icon = 'fa-globe';
|
||||
}
|
||||
}
|
||||
|
||||
_service.decryptAttachment = function (key, encryptedAttachment) {
|
||||
if (!encryptedAttachment) throw "encryptedAttachment is undefined or null";
|
||||
|
||||
@@ -182,7 +323,7 @@ angular
|
||||
|
||||
return {
|
||||
id: encryptedFolder.Id,
|
||||
name: _service.decryptProperty(encryptedFolder.Name, null, false)
|
||||
name: _service.decryptProperty(encryptedFolder.Name, null, false, true)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -206,12 +347,12 @@ angular
|
||||
|
||||
return {
|
||||
id: encryptedCollection.Id,
|
||||
name: catchError ? _service.decryptProperty(encryptedCollection.Name, key, false) :
|
||||
name: catchError ? _service.decryptProperty(encryptedCollection.Name, key, false, true) :
|
||||
cryptoService.decrypt(encryptedCollection.Name, key)
|
||||
};
|
||||
};
|
||||
|
||||
_service.decryptProperty = function (property, key, checkEmpty) {
|
||||
_service.decryptProperty = function (property, key, checkEmpty, showError) {
|
||||
if (checkEmpty && (!property || property === '')) {
|
||||
return null;
|
||||
}
|
||||
@@ -223,53 +364,109 @@ angular
|
||||
property = null;
|
||||
}
|
||||
|
||||
return property || '[error: cannot decrypt]';
|
||||
return property || (showError ? '[error: cannot decrypt]' : null);
|
||||
};
|
||||
|
||||
_service.encryptLogins = function (unencryptedLogins, key) {
|
||||
if (!unencryptedLogins) throw "unencryptedLogins is undefined or null";
|
||||
_service.encryptCiphers = function (unencryptedCiphers, key) {
|
||||
if (!unencryptedCiphers) throw "unencryptedCiphers is undefined or null";
|
||||
|
||||
var encryptedLogins = [];
|
||||
for (var i = 0; i < unencryptedLogins.length; i++) {
|
||||
encryptedLogins.push(_service.encryptLogin(unencryptedLogins[i], key));
|
||||
var encryptedCiphers = [];
|
||||
for (var i = 0; i < unencryptedCiphers.length; i++) {
|
||||
encryptedCiphers.push(_service.encryptCipher(unencryptedCiphers[i], null, key));
|
||||
}
|
||||
|
||||
return encryptedLogins;
|
||||
return encryptedCiphers;
|
||||
};
|
||||
|
||||
_service.encryptLogin = function (unencryptedLogin, key, attachments) {
|
||||
if (!unencryptedLogin) throw "unencryptedLogin is undefined or null";
|
||||
_service.encryptCipher = function (unencryptedCipher, type, key, attachments) {
|
||||
if (!unencryptedCipher) throw "unencryptedCipher is undefined or null";
|
||||
|
||||
if (unencryptedLogin.organizationId) {
|
||||
key = key || cryptoService.getOrgKey(unencryptedLogin.organizationId);
|
||||
if (unencryptedCipher.organizationId) {
|
||||
key = key || cryptoService.getOrgKey(unencryptedCipher.organizationId);
|
||||
}
|
||||
|
||||
var login = {
|
||||
id: unencryptedLogin.id,
|
||||
'type': 1,
|
||||
organizationId: unencryptedLogin.organizationId || null,
|
||||
folderId: unencryptedLogin.folderId === '' ? null : unencryptedLogin.folderId,
|
||||
favorite: unencryptedLogin.favorite !== null ? unencryptedLogin.favorite : false,
|
||||
name: cryptoService.encrypt(unencryptedLogin.name, key),
|
||||
notes: !unencryptedLogin.notes || unencryptedLogin.notes === '' ? null : cryptoService.encrypt(unencryptedLogin.notes, key),
|
||||
login: {
|
||||
uri: !unencryptedLogin.uri || unencryptedLogin.uri === '' ? null : cryptoService.encrypt(unencryptedLogin.uri, key),
|
||||
username: !unencryptedLogin.username || unencryptedLogin.username === '' ? null : cryptoService.encrypt(unencryptedLogin.username, key),
|
||||
password: !unencryptedLogin.password || unencryptedLogin.password === '' ? null : cryptoService.encrypt(unencryptedLogin.password, key),
|
||||
totp: !unencryptedLogin.totp || unencryptedLogin.totp === '' ? null : cryptoService.encrypt(unencryptedLogin.totp, key)
|
||||
},
|
||||
fields: _service.encryptFields(unencryptedLogin.fields, key)
|
||||
var cipher = {
|
||||
id: unencryptedCipher.id,
|
||||
'type': type || unencryptedCipher.type,
|
||||
organizationId: unencryptedCipher.organizationId || null,
|
||||
folderId: unencryptedCipher.folderId === '' ? null : unencryptedCipher.folderId,
|
||||
favorite: unencryptedCipher.favorite !== null ? unencryptedCipher.favorite : false,
|
||||
name: cryptoService.encrypt(unencryptedCipher.name, key),
|
||||
notes: encryptProperty(unencryptedCipher.notes, key),
|
||||
fields: _service.encryptFields(unencryptedCipher.fields, key)
|
||||
};
|
||||
|
||||
if (unencryptedLogin.attachments && attachments) {
|
||||
login.attachments = {};
|
||||
for (var i = 0; i < unencryptedLogin.attachments.length; i++) {
|
||||
login.attachments[unencryptedLogin.attachments[i].id] =
|
||||
cryptoService.encrypt(unencryptedLogin.attachments[i].fileName, key);
|
||||
var i;
|
||||
switch (cipher.type) {
|
||||
case constants.cipherType.login:
|
||||
var loginData = unencryptedCipher.login;
|
||||
cipher.login = {
|
||||
username: encryptProperty(loginData.username, key),
|
||||
password: encryptProperty(loginData.password, key),
|
||||
totp: encryptProperty(loginData.totp, key)
|
||||
};
|
||||
if (loginData.uris && loginData.uris.length) {
|
||||
cipher.login.uris = [];
|
||||
for (i = 0; i < loginData.uris.length; i++) {
|
||||
cipher.login.uris.push({
|
||||
uri: encryptProperty(loginData.uris[i].uri, key),
|
||||
match: loginData.uris[i].match
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
case constants.cipherType.secureNote:
|
||||
cipher.secureNote = {
|
||||
type: unencryptedCipher.secureNote.type
|
||||
};
|
||||
break;
|
||||
case constants.cipherType.card:
|
||||
var cardData = unencryptedCipher.card;
|
||||
cipher.card = {
|
||||
cardholderName: encryptProperty(cardData.cardholderName, key),
|
||||
brand: encryptProperty(cardData.brand, key),
|
||||
number: encryptProperty(cardData.number, key),
|
||||
expMonth: encryptProperty(cardData.expMonth, key),
|
||||
expYear: encryptProperty(cardData.expYear, key),
|
||||
code: encryptProperty(cardData.code, key)
|
||||
};
|
||||
break;
|
||||
case constants.cipherType.identity:
|
||||
var identityData = unencryptedCipher.identity;
|
||||
cipher.identity = {
|
||||
title: encryptProperty(identityData.title, key),
|
||||
firstName: encryptProperty(identityData.firstName, key),
|
||||
middleName: encryptProperty(identityData.middleName, key),
|
||||
lastName: encryptProperty(identityData.lastName, key),
|
||||
address1: encryptProperty(identityData.address1, key),
|
||||
address2: encryptProperty(identityData.address2, key),
|
||||
address3: encryptProperty(identityData.address3, key),
|
||||
city: encryptProperty(identityData.city, key),
|
||||
state: encryptProperty(identityData.state, key),
|
||||
postalCode: encryptProperty(identityData.postalCode, key),
|
||||
country: encryptProperty(identityData.country, key),
|
||||
company: encryptProperty(identityData.company, key),
|
||||
email: encryptProperty(identityData.email, key),
|
||||
phone: encryptProperty(identityData.phone, key),
|
||||
ssn: encryptProperty(identityData.ssn, key),
|
||||
username: encryptProperty(identityData.username, key),
|
||||
passportNumber: encryptProperty(identityData.passportNumber, key),
|
||||
licenseNumber: encryptProperty(identityData.licenseNumber, key)
|
||||
};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (unencryptedCipher.attachments && attachments) {
|
||||
cipher.attachments = {};
|
||||
for (i = 0; i < unencryptedCipher.attachments.length; i++) {
|
||||
cipher.attachments[unencryptedCipher.attachments[i].id] =
|
||||
cryptoService.encrypt(unencryptedCipher.attachments[i].fileName, key);
|
||||
}
|
||||
}
|
||||
|
||||
return login;
|
||||
return cipher;
|
||||
};
|
||||
|
||||
_service.encryptAttachmentFile = function (key, unencryptedFile) {
|
||||
@@ -365,5 +562,9 @@ angular
|
||||
};
|
||||
};
|
||||
|
||||
function encryptProperty(property, key) {
|
||||
return !property || property === '' ? null : cryptoService.encrypt(property, key);
|
||||
}
|
||||
|
||||
return _service;
|
||||
});
|
||||
|
||||
@@ -540,7 +540,7 @@ angular
|
||||
if (key.macKey && encPieces.length > 2) {
|
||||
var macBytes = forge.util.decode64(encPieces[2]);
|
||||
var computedMacBytes = computeMac(ivBytes + ctBytes, key.macKey, false);
|
||||
if (!macsEqual(key.macKey, macBytes, computedMacBytes)) {
|
||||
if (!macsEqual(macBytes, computedMacBytes)) {
|
||||
console.error('MAC failed.');
|
||||
return null;
|
||||
}
|
||||
@@ -623,6 +623,10 @@ angular
|
||||
throw 'Encryption key unavailable.';
|
||||
}
|
||||
|
||||
if (key.macKey && !macBuf) {
|
||||
throw 'macBuf required for this type of key.';
|
||||
}
|
||||
|
||||
if (encType !== key.encType) {
|
||||
throw 'encType unavailable.';
|
||||
}
|
||||
@@ -646,7 +650,7 @@ angular
|
||||
if (computedMacBuf === null) {
|
||||
return null;
|
||||
}
|
||||
return macsEqualWC(keyBuf.macKey, macBuf, computedMacBuf);
|
||||
return macsEqualWC(macBuf, computedMacBuf);
|
||||
}).then(function (macsMatch) {
|
||||
if (macsMatch === false) {
|
||||
console.error('MAC failed.');
|
||||
@@ -704,7 +708,7 @@ angular
|
||||
if (key && key.macKey && encPieces.length > 1) {
|
||||
var macBytes = forge.util.decode64(encPieces[1]);
|
||||
var computedMacBytes = computeMac(ctBytes, key.macKey, false);
|
||||
if (!macsEqual(key.macKey, macBytes, computedMacBytes)) {
|
||||
if (!macsEqual(macBytes, computedMacBytes)) {
|
||||
console.error('MAC failed.');
|
||||
return null;
|
||||
}
|
||||
@@ -747,10 +751,11 @@ angular
|
||||
|
||||
// Safely compare two MACs in a way that protects against timing attacks (Double HMAC Verification).
|
||||
// ref: https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/
|
||||
function macsEqual(macKey, mac1, mac2) {
|
||||
// ref: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy
|
||||
function macsEqual(mac1, mac2) {
|
||||
var hmac = forge.hmac.create();
|
||||
|
||||
hmac.start('sha256', macKey);
|
||||
hmac.start('sha256', getRandomBytes(32));
|
||||
hmac.update(mac1);
|
||||
mac1 = hmac.digest().getBytes();
|
||||
|
||||
@@ -761,11 +766,14 @@ angular
|
||||
return mac1 === mac2;
|
||||
}
|
||||
|
||||
function macsEqualWC(macKeyBuf, mac1Buf, mac2Buf) {
|
||||
function macsEqualWC(mac1Buf, mac2Buf) {
|
||||
var mac1,
|
||||
macKey;
|
||||
|
||||
return window.crypto.subtle.importKey('raw', macKeyBuf, { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign'])
|
||||
var compareKey = new Uint8Array(32);
|
||||
_crypto.getRandomValues(compareKey);
|
||||
|
||||
return window.crypto.subtle.importKey('raw', compareKey.buffer, { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign'])
|
||||
.then(function (key) {
|
||||
macKey = key;
|
||||
return window.crypto.subtle.sign({ name: 'HMAC', hash: { name: 'SHA-256' } }, macKey, mac1Buf);
|
||||
@@ -932,5 +940,15 @@ angular
|
||||
return new Uint8Array(result);
|
||||
}
|
||||
|
||||
function getRandomBytes(byteLength) {
|
||||
var bytes = new Uint32Array(byteLength / 4);
|
||||
_crypto.getRandomValues(bytes);
|
||||
var buffer = forge.util.createBuffer();
|
||||
for (var i = 0; i < bytes.length; i++) {
|
||||
buffer.putInt32(bytes[i]);
|
||||
}
|
||||
return buffer.getBytes();
|
||||
}
|
||||
|
||||
return _service;
|
||||
});
|
||||
269
src/app/services/eventService.js
Normal file
269
src/app/services/eventService.js
Normal file
@@ -0,0 +1,269 @@
|
||||
angular
|
||||
.module('bit.services')
|
||||
|
||||
.factory('eventService', function (constants, $filter) {
|
||||
var _service = {};
|
||||
|
||||
_service.getDefaultDateFilters = function () {
|
||||
var d = new Date();
|
||||
var filterEnd = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59);
|
||||
d.setDate(d.getDate() - 30);
|
||||
var filterStart = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0);
|
||||
|
||||
return {
|
||||
start: filterStart,
|
||||
end: filterEnd
|
||||
};
|
||||
};
|
||||
|
||||
_service.formatDateFilters = function (filterStart, filterEnd) {
|
||||
var result = {
|
||||
start: null,
|
||||
end: null,
|
||||
error: null
|
||||
};
|
||||
|
||||
try {
|
||||
var format = 'yyyy-MM-ddTHH:mm';
|
||||
result.start = $filter('date')(filterStart, format + 'Z', 'UTC');
|
||||
result.end = $filter('date')(filterEnd, format + ':59.999Z', 'UTC');
|
||||
} catch (e) { }
|
||||
|
||||
if (!result.start || !result.end || result.end < result.start) {
|
||||
result.error = 'Invalid date range.';
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
_service.getEventInfo = function (ev, options) {
|
||||
options = options || {
|
||||
cipherInfo: true
|
||||
};
|
||||
|
||||
var appInfo = getAppInfo(ev);
|
||||
|
||||
return {
|
||||
message: getEventMessage(ev, options),
|
||||
appIcon: appInfo.icon,
|
||||
appName: appInfo.name
|
||||
};
|
||||
};
|
||||
|
||||
function getEventMessage(ev, options) {
|
||||
var msg = '';
|
||||
switch (ev.Type) {
|
||||
// User
|
||||
case constants.eventType.User_LoggedIn:
|
||||
msg = 'Logged in.';
|
||||
break;
|
||||
case constants.eventType.User_ChangedPassword:
|
||||
msg = 'Changed account password.';
|
||||
break;
|
||||
case constants.eventType.User_Enabled2fa:
|
||||
msg = 'Enabled two-step login.';
|
||||
break;
|
||||
case constants.eventType.User_Disabled2fa:
|
||||
msg = 'Disabled two-step login.';
|
||||
break;
|
||||
case constants.eventType.User_Recovered2fa:
|
||||
msg = 'Recovered account from two-step login.';
|
||||
break;
|
||||
case constants.eventType.User_FailedLogIn:
|
||||
msg = 'Login attempt failed with incorrect password.';
|
||||
break;
|
||||
case constants.eventType.User_FailedLogIn2fa:
|
||||
msg = 'Login attempt failed with incorrect two-step login.';
|
||||
break;
|
||||
// Cipher
|
||||
case constants.eventType.Cipher_Created:
|
||||
msg = options.cipherInfo ? 'Created item ' + formatCipherId(ev) + '.' : 'Created.';
|
||||
break;
|
||||
case constants.eventType.Cipher_Updated:
|
||||
msg = options.cipherInfo ? 'Edited item ' + formatCipherId(ev) + '.' : 'Edited.';
|
||||
break;
|
||||
case constants.eventType.Cipher_Deleted:
|
||||
msg = options.cipherInfo ? 'Deleted item ' + formatCipherId(ev) + '.' : 'Deleted';
|
||||
break;
|
||||
case constants.eventType.Cipher_AttachmentCreated:
|
||||
msg = options.cipherInfo ? 'Created attachment for item ' + formatCipherId(ev) + '.' :
|
||||
'Created attachment.';
|
||||
break;
|
||||
case constants.eventType.Cipher_AttachmentDeleted:
|
||||
msg = options.cipherInfo ? 'Deleted attachment for item ' + formatCipherId(ev) + '.' :
|
||||
'Deleted attachment.';
|
||||
break;
|
||||
case constants.eventType.Cipher_Shared:
|
||||
msg = options.cipherInfo ? 'Shared item ' + formatCipherId(ev) + '.' : 'Shared.';
|
||||
break;
|
||||
case constants.eventType.Cipher_UpdatedCollections:
|
||||
msg = options.cipherInfo ? 'Update collections for item ' + formatCipherId(ev) + '.' :
|
||||
'Updated collections.';
|
||||
break;
|
||||
// Collection
|
||||
case constants.eventType.Collection_Created:
|
||||
msg = 'Created collection ' + formatCollectionId(ev) + '.';
|
||||
break;
|
||||
case constants.eventType.Collection_Updated:
|
||||
msg = 'Edited collection ' + formatCollectionId(ev) + '.';
|
||||
break;
|
||||
case constants.eventType.Collection_Deleted:
|
||||
msg = 'Deleted collection ' + formatCollectionId(ev) + '.';
|
||||
break;
|
||||
// Group
|
||||
case constants.eventType.Group_Created:
|
||||
msg = 'Created group ' + formatGroupId(ev) + '.';
|
||||
break;
|
||||
case constants.eventType.Group_Updated:
|
||||
msg = 'Edited group ' + formatGroupId(ev) + '.';
|
||||
break;
|
||||
case constants.eventType.Group_Deleted:
|
||||
msg = 'Deleted group ' + formatGroupId(ev) + '.';
|
||||
break;
|
||||
// Org user
|
||||
case constants.eventType.OrganizationUser_Invited:
|
||||
msg = 'Invited user ' + formatOrgUserId(ev) + '.';
|
||||
break;
|
||||
case constants.eventType.OrganizationUser_Confirmed:
|
||||
msg = 'Confirmed user ' + formatOrgUserId(ev) + '.';
|
||||
break;
|
||||
case constants.eventType.OrganizationUser_Updated:
|
||||
msg = 'Edited user ' + formatOrgUserId(ev) + '.';
|
||||
break;
|
||||
case constants.eventType.OrganizationUser_Removed:
|
||||
msg = 'Removed user ' + formatOrgUserId(ev) + '.';
|
||||
break;
|
||||
case constants.eventType.OrganizationUser_UpdatedGroups:
|
||||
msg = 'Edited groups for user ' + formatOrgUserId(ev) + '.';
|
||||
break;
|
||||
// Org
|
||||
case constants.eventType.Organization_Updated:
|
||||
msg = 'Edited organization settings.';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return msg === '' ? null : msg;
|
||||
}
|
||||
|
||||
function getAppInfo(ev) {
|
||||
var appInfo = {
|
||||
icon: 'fa-globe',
|
||||
name: 'Unknown'
|
||||
};
|
||||
|
||||
switch (ev.DeviceType) {
|
||||
case constants.deviceType.android:
|
||||
appInfo.icon = 'fa-android';
|
||||
appInfo.name = 'Mobile App - Android';
|
||||
break;
|
||||
case constants.deviceType.ios:
|
||||
appInfo.icon = 'fa-apple';
|
||||
appInfo.name = 'Mobile App - iOS';
|
||||
break;
|
||||
case constants.deviceType.uwp:
|
||||
appInfo.icon = 'fa-windows';
|
||||
appInfo.name = 'Mobile App - Windows';
|
||||
break;
|
||||
case constants.deviceType.chromeExt:
|
||||
appInfo.icon = 'fa-chrome';
|
||||
appInfo.name = 'Extension - Chrome';
|
||||
break;
|
||||
case constants.deviceType.firefoxExt:
|
||||
appInfo.icon = 'fa-firefox';
|
||||
appInfo.name = 'Extension - Firefox';
|
||||
break;
|
||||
case constants.deviceType.operaExt:
|
||||
appInfo.icon = 'fa-opera';
|
||||
appInfo.name = 'Extension - Opera';
|
||||
break;
|
||||
case constants.deviceType.edgeExt:
|
||||
appInfo.icon = 'fa-edge';
|
||||
appInfo.name = 'Extension - Edge';
|
||||
break;
|
||||
case constants.deviceType.vivaldiExt:
|
||||
appInfo.icon = 'fa-puzzle-piece';
|
||||
appInfo.name = 'Extension - Vivaldi';
|
||||
break;
|
||||
case constants.deviceType.windowsDesktop:
|
||||
appInfo.icon = 'fa-windows';
|
||||
appInfo.name = 'Desktop - Windows';
|
||||
break;
|
||||
case constants.deviceType.macOsDesktop:
|
||||
appInfo.icon = 'fa-apple';
|
||||
appInfo.name = 'Desktop - macOS';
|
||||
break;
|
||||
case constants.deviceType.linuxDesktop:
|
||||
appInfo.icon = 'fa-linux';
|
||||
appInfo.name = 'Desktop - Linux';
|
||||
break;
|
||||
case constants.deviceType.chrome:
|
||||
appInfo.icon = 'fa-globe';
|
||||
appInfo.name = 'Web Vault - Chrome';
|
||||
break;
|
||||
case constants.deviceType.firefox:
|
||||
appInfo.icon = 'fa-globe';
|
||||
appInfo.name = 'Web Vault - Firefox';
|
||||
break;
|
||||
case constants.deviceType.opera:
|
||||
appInfo.icon = 'fa-globe';
|
||||
appInfo.name = 'Web Vault - Opera';
|
||||
break;
|
||||
case constants.deviceType.safari:
|
||||
appInfo.icon = 'fa-globe';
|
||||
appInfo.name = 'Web Vault - Safari';
|
||||
break;
|
||||
case constants.deviceType.vivaldi:
|
||||
appInfo.icon = 'fa-globe';
|
||||
appInfo.name = 'Web Vault - Vivaldi';
|
||||
break;
|
||||
case constants.deviceType.edge:
|
||||
appInfo.icon = 'fa-globe';
|
||||
appInfo.name = 'Web Vault - Edge';
|
||||
break;
|
||||
case constants.deviceType.ie:
|
||||
appInfo.icon = 'fa-globe';
|
||||
appInfo.name = 'Web Vault - IE';
|
||||
break;
|
||||
case constants.deviceType.unknown:
|
||||
appInfo.icon = 'fa-globe';
|
||||
appInfo.name = 'Web Vault - Unknown';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return appInfo;
|
||||
}
|
||||
|
||||
function formatCipherId(ev) {
|
||||
var shortId = ev.CipherId.substring(0, 8);
|
||||
if (!ev.OrganizationId) {
|
||||
return '<code>' + shortId + '</code>';
|
||||
}
|
||||
|
||||
return '<a title="View item ' + ev.CipherId + '" ui-sref="backend.org.vault({orgId:\'' + ev.OrganizationId + '\',search:\'' + shortId + '\',viewEvents:\'' + ev.CipherId + '\'})">' +
|
||||
'<code>' + shortId + '</code></a>';
|
||||
}
|
||||
|
||||
function formatGroupId(ev) {
|
||||
var shortId = ev.GroupId.substring(0, 8);
|
||||
return '<a title="View group ' + ev.GroupId + '" ui-sref="backend.org.groups({orgId:\'' + ev.OrganizationId + '\',search:\'' + shortId + '\'})">' +
|
||||
'<code>' + shortId + '</code></a>';
|
||||
}
|
||||
|
||||
function formatCollectionId(ev) {
|
||||
var shortId = ev.CollectionId.substring(0, 8);
|
||||
return '<a title="View collection ' + ev.CollectionId + '" ui-sref="backend.org.collections({orgId:\'' + ev.OrganizationId + '\',search:\'' + shortId + '\'})">' +
|
||||
'<code>' + shortId + '</code></a>';
|
||||
}
|
||||
|
||||
function formatOrgUserId(ev) {
|
||||
var shortId = ev.OrganizationUserId.substring(0, 8);
|
||||
return '<a title="View user ' + ev.OrganizationUserId + '" ui-sref="backend.org.people({orgId:\'' + ev.OrganizationId + '\',search:\'' + shortId + '\'})">' +
|
||||
'<code>' + shortId + '</code></a>';
|
||||
}
|
||||
|
||||
return _service;
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
47
src/app/services/utilsService.js
Normal file
47
src/app/services/utilsService.js
Normal file
@@ -0,0 +1,47 @@
|
||||
angular
|
||||
.module('bit.services')
|
||||
|
||||
.factory('utilsService', function (constants) {
|
||||
var _service = {};
|
||||
var _browserCache;
|
||||
|
||||
_service.getDeviceType = function (token) {
|
||||
if (_browserCache) {
|
||||
return _browserCache;
|
||||
}
|
||||
|
||||
if (navigator.userAgent.indexOf(' Vivaldi/') >= 0) {
|
||||
_browserCache = constants.deviceType.vivaldi;
|
||||
}
|
||||
else if (!!window.chrome && !!window.chrome.webstore) {
|
||||
_browserCache = constants.deviceType.chrome;
|
||||
}
|
||||
else if (typeof InstallTrigger !== 'undefined') {
|
||||
_browserCache = constants.deviceType.firefox;
|
||||
}
|
||||
else if ((!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0) {
|
||||
_browserCache = constants.deviceType.firefox;
|
||||
}
|
||||
else if (/constructor/i.test(window.HTMLElement) ||
|
||||
safariCheck(!window.safari || (typeof safari !== 'undefined' && safari.pushNotification))) {
|
||||
_browserCache = constants.deviceType.opera;
|
||||
}
|
||||
else if (!!document.documentMode) {
|
||||
_browserCache = constants.deviceType.ie;
|
||||
}
|
||||
else if (!!window.StyleMedia) {
|
||||
_browserCache = constants.deviceType.edge;
|
||||
}
|
||||
else {
|
||||
_browserCache = constants.deviceType.unknown;
|
||||
}
|
||||
|
||||
return _browserCache;
|
||||
};
|
||||
|
||||
function safariCheck(p) {
|
||||
return p.toString() === '[object SafariRemoteNotification]';
|
||||
}
|
||||
|
||||
return _service;
|
||||
});
|
||||
@@ -62,5 +62,41 @@
|
||||
}
|
||||
};
|
||||
|
||||
_service.parseErrors = function (reason) {
|
||||
var data = reason.data;
|
||||
var defaultErrorMessage = 'An unexpected error has occurred.';
|
||||
var errors = [];
|
||||
|
||||
if (!data || !angular.isObject(data)) {
|
||||
errors.push(defaultErrorMessage);
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (data && data.ErrorModel) {
|
||||
data = data.ErrorModel;
|
||||
}
|
||||
|
||||
if (!data.ValidationErrors) {
|
||||
if (data.Message) {
|
||||
errors.push(data.Message);
|
||||
}
|
||||
else {
|
||||
errors.push(defaultErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
for (var key in data.ValidationErrors) {
|
||||
if (!data.ValidationErrors.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var i = 0; i < data.ValidationErrors[key].length; i++) {
|
||||
errors.push(data.ValidationErrors[key][i]);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
return _service;
|
||||
});
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
angular.module("bit")
|
||||
.constant("appSettings", {"apiUri":"https://api.bitwarden.com","identityUri":"https://identity.bitwarden.com","stripeKey":"pk_live_bpN0P37nMxrMQkcaHXtAybJk","braintreeKey":"production_qfbsv8kc_njj2zjtyngtjmbjd","whitelistDomains":["api.bitwarden.com"],"selfHosted":false,"version":"1.17.1","environment":"Production"});
|
||||
.constant("appSettings", {"apiUri":"/api","identityUri":"/identity","iconsUri":"https://icons.bitwarden.com","stripeKey":"pk_live_bpN0P37nMxrMQkcaHXtAybJk","braintreeKey":"production_qfbsv8kc_njj2zjtyngtjmbjd","selfHosted":false,"version":"1.25.1","environment":"Production"});
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
.controller('settingsBillingChangePaymentController', function ($scope, $state, $uibModalInstance, apiService,
|
||||
$analytics, toastr, existingPaymentMethod, appSettings, $timeout
|
||||
// @if !selfHosted
|
||||
/* jshint ignore:start */
|
||||
, stripe
|
||||
/* jshint ignore:end */
|
||||
// @endif
|
||||
) {
|
||||
$analytics.eventTrack('settingsBillingChangePaymentController', { category: 'Modal' });
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
angular
|
||||
.module('bit.settings')
|
||||
|
||||
.controller('settingsController', function ($scope, $state, $uibModal, apiService, toastr, authService) {
|
||||
.controller('settingsController', function ($scope, $state, $uibModal, apiService, toastr, authService, $localStorage,
|
||||
$rootScope, cipherService) {
|
||||
$scope.model = {
|
||||
profile: {},
|
||||
twoFactorEnabled: false,
|
||||
email: null
|
||||
email: null,
|
||||
disableWebsiteIcons: false
|
||||
};
|
||||
|
||||
$scope.$on('$viewContentLoaded', function () {
|
||||
@@ -17,7 +18,7 @@
|
||||
culture: user.Culture
|
||||
},
|
||||
email: user.Email,
|
||||
twoFactorEnabled: user.TwoFactorEnabled
|
||||
disableWebsiteIcons: $localStorage.disableWebsiteIcons
|
||||
};
|
||||
|
||||
if (user.Organizations) {
|
||||
@@ -58,6 +59,13 @@
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
$scope.optionsSave = function () {
|
||||
$localStorage.disableWebsiteIcons = cipherService.disableWebsiteIcons = $scope.model.disableWebsiteIcons;
|
||||
$rootScope.vaultCiphers = null;
|
||||
|
||||
toastr.success('Options have been updated.', 'Success!');
|
||||
};
|
||||
|
||||
$scope.changePassword = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
@@ -121,6 +129,14 @@
|
||||
});
|
||||
};
|
||||
|
||||
$scope.purge = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/settings/views/settingsPurge.html',
|
||||
controller: 'settingsPurgeController'
|
||||
});
|
||||
};
|
||||
|
||||
function scrollToTop() {
|
||||
$('html, body').animate({ scrollTop: 0 }, 200);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
.controller('settingsCreateOrganizationController', function ($scope, $state, apiService, cryptoService,
|
||||
toastr, $analytics, authService, constants, appSettings, validationService
|
||||
// @if !selfHosted
|
||||
/* jshint ignore:start */
|
||||
, stripe
|
||||
/* jshint ignore:end */
|
||||
// @endif
|
||||
) {
|
||||
$scope.plans = constants.plans;
|
||||
@@ -113,6 +115,7 @@
|
||||
additionalStorageGb: model.additionalStorageGb,
|
||||
billingEmail: model.billingEmail,
|
||||
businessName: model.ownedBusiness ? model.businessName : null,
|
||||
country: $scope.paymentMethod === 'card' ? model.card.address_country : null,
|
||||
collectionName: defaultCollectionCt
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
.controller('settingsPremiumController', function ($scope, $state, apiService, toastr, $analytics, authService,
|
||||
constants, $timeout, appSettings, validationService
|
||||
// @if !selfHosted
|
||||
/* jshint ignore:start */
|
||||
, stripe
|
||||
/* jshint ignore:end */
|
||||
// @endif
|
||||
) {
|
||||
var profile = null;
|
||||
|
||||
24
src/app/settings/settingsPurgeController.js
Normal file
24
src/app/settings/settingsPurgeController.js
Normal file
@@ -0,0 +1,24 @@
|
||||
angular
|
||||
.module('bit.settings')
|
||||
|
||||
.controller('settingsPurgeController', function ($scope, $state, apiService, $uibModalInstance, cryptoService,
|
||||
authService, toastr, $analytics, tokenService) {
|
||||
$analytics.eventTrack('settingsPurgeController', { category: 'Modal' });
|
||||
$scope.submit = function (model) {
|
||||
$scope.submitPromise = cryptoService.hashPassword(model.masterPassword).then(function (hash) {
|
||||
return apiService.ciphers.purge({
|
||||
masterPasswordHash: hash
|
||||
}).$promise;
|
||||
}).then(function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
$analytics.eventTrack('Purged Vault');
|
||||
return $state.go('backend.user.vault', { refreshFromServer: true });
|
||||
}).then(function () {
|
||||
toastr.success('All items in your vault have been deleted.', 'Vault Purged');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
});
|
||||
@@ -4,7 +4,7 @@
|
||||
.controller('settingsTwoStepAuthenticatorController', function ($scope, apiService, $uibModalInstance, cryptoService,
|
||||
authService, $q, toastr, $analytics, constants, $timeout) {
|
||||
$analytics.eventTrack('settingsTwoStepAuthenticatorController', { category: 'Modal' });
|
||||
var _issuer = 'bitwarden',
|
||||
var _issuer = 'Bitwarden',
|
||||
_profile = null,
|
||||
_masterPasswordHash,
|
||||
_key = null;
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
$analytics.eventTrack('Print Recovery Code');
|
||||
var w = window.open();
|
||||
w.document.write('<div style="font-size: 18px; text-align: center;"><p>bitwarden two-step login recovery code:</p>' +
|
||||
w.document.write('<div style="font-size: 18px; text-align: center;"><p>Bitwarden two-step login recovery code:</p>' +
|
||||
'<code style="font-family: Menlo, Monaco, Consolas, \'Courier New\', monospace;">' + $scope.code + '</code>' +
|
||||
'</div><p style="text-align: center;">' + new Date() + '</p>');
|
||||
w.print();
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
}, function (e) {
|
||||
throw e ? e : 'Error occurred.';
|
||||
}).then(function () {
|
||||
toastr.success('Please log back in. If you are using other bitwarden applications, ' +
|
||||
toastr.success('Please log back in. If you are using other Bitwarden applications, ' +
|
||||
'log out and back in to those as well.', 'Key Updated', { timeOut: 10000 });
|
||||
});
|
||||
};
|
||||
@@ -31,19 +31,19 @@
|
||||
function updateKey(masterPasswordHash) {
|
||||
var madeEncKey = cryptoService.makeEncKey(null);
|
||||
|
||||
var reencryptedLogins = [];
|
||||
var loginsPromise = apiService.ciphers.list({}, function (encryptedLogins) {
|
||||
var filteredEncryptedLogins = [];
|
||||
for (var i = 0; i < encryptedLogins.Data.length; i++) {
|
||||
if (encryptedLogins.Data[i].OrganizationId) {
|
||||
var reencryptedCiphers = [];
|
||||
var ciphersPromise = apiService.ciphers.list({}, function (encryptedCiphers) {
|
||||
var filteredEncryptedCiphers = [];
|
||||
for (var i = 0; i < encryptedCiphers.Data.length; i++) {
|
||||
if (encryptedCiphers.Data[i].OrganizationId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
filteredEncryptedLogins.push(encryptedLogins.Data[i]);
|
||||
filteredEncryptedCiphers.push(encryptedCiphers.Data[i]);
|
||||
}
|
||||
|
||||
var unencryptedLogins = cipherService.decryptLogins(filteredEncryptedLogins);
|
||||
reencryptedLogins = cipherService.encryptLogins(unencryptedLogins, madeEncKey.encKey);
|
||||
var unencryptedCiphers = cipherService.decryptCiphers(filteredEncryptedCiphers);
|
||||
reencryptedCiphers = cipherService.encryptCiphers(unencryptedCiphers, madeEncKey.encKey);
|
||||
}).$promise;
|
||||
|
||||
var reencryptedFolders = [];
|
||||
@@ -58,10 +58,10 @@
|
||||
reencryptedPrivateKey = cryptoService.encrypt(privateKey, madeEncKey.encKey, 'raw');
|
||||
}
|
||||
|
||||
return $q.all([loginsPromise, foldersPromise]).then(function () {
|
||||
return $q.all([ciphersPromise, foldersPromise]).then(function () {
|
||||
var request = {
|
||||
masterPasswordHash: masterPasswordHash,
|
||||
ciphers: reencryptedLogins,
|
||||
ciphers: reencryptedCiphers,
|
||||
folders: reencryptedFolders,
|
||||
privateKey: reencryptedPrivateKey,
|
||||
key: madeEncKey.encKeyEnc
|
||||
|
||||
@@ -86,6 +86,27 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="box box-default">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Web Vault Options</h3>
|
||||
</div>
|
||||
<form role="form" name="optionsForm" ng-submit="optionsForm.$valid && optionsSave()" autocomplete="off">
|
||||
<div class="box-body">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="model.disableWebsiteIcons">
|
||||
Disable Website Icons
|
||||
</label>
|
||||
<p class="help-block">Website Icons provide a recognizable image next to each login item in your vault.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="optionsForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="optionsForm.$loading"></i>Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="box box-default">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Organizations</h3>
|
||||
@@ -129,10 +150,13 @@
|
||||
Careful, these actions are not reversible!
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button type="submit" class="btn btn-default btn-flat" ng-click="sessions()">
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="sessions()">
|
||||
Deauthorize Sessions
|
||||
</button>
|
||||
<button type="submit" class="btn btn-default btn-flat" ng-click="delete()">
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="purge()">
|
||||
Purge Vault
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="delete()">
|
||||
Delete Account
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<p>
|
||||
You membership has a total of {{storage.maxGb}} GB of encrypted file storage.
|
||||
Your membership has a total of {{storage.maxGb}} GB of encrypted file storage.
|
||||
You are currently using {{storage.currentName}}.
|
||||
</p>
|
||||
<div class="progress" style="margin: 0;">
|
||||
|
||||
@@ -93,17 +93,17 @@
|
||||
</div>
|
||||
<div class="radio radio-block" ng-if="!model.ownedBusiness" ng-click="changedPlan()">
|
||||
<label>
|
||||
<input type="radio" ng-model="model.plan" name="PlanType" value="personal">
|
||||
Personal
|
||||
<span>For personal users such as families & friends.</span>
|
||||
<span>- Add and share with up to 10 users (5 included with base price)</span>
|
||||
<input type="radio" ng-model="model.plan" name="PlanType" value="families">
|
||||
Families
|
||||
<span>For personal use, to share with family & friends.</span>
|
||||
<span>- Add and share with up to 5 users</span>
|
||||
<span>- Create unlimited collections</span>
|
||||
<span>- 1 GB encrypted file storage</span>
|
||||
<span>- Self-hosting (optional)</span>
|
||||
<span>- Priority customer support</span>
|
||||
<span>- 7 day free trial, cancel anytime</span>
|
||||
<span class="bottom-line">
|
||||
{{plans.personal.basePrice | currency:'$'}} /month includes {{plans.personal.baseSeats}} users,
|
||||
additional users {{plans.personal.seatPrice | currency:'$'}} /month
|
||||
{{plans.families.basePrice | currency:'$'}} /month includes {{plans.families.baseSeats}} users
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -243,7 +243,7 @@
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio radio-block" ng-if="model.plan !== 'personal'">
|
||||
<div class="radio radio-block" ng-if="model.plan !== 'families'">
|
||||
<label>
|
||||
<input type="radio" ng-model="model.interval" name="BillingInterval" value="month">
|
||||
Monthly
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<section class="content">
|
||||
<p>
|
||||
If you have the same login across multiple different website domains, you can mark the website as "equivalent".
|
||||
"Global" domains are ones already created for you by bitwarden.
|
||||
"Global" domains are ones already created for you by Bitwarden.
|
||||
</p>
|
||||
<form name="customForm" ng-submit="customForm.$valid && saveCustom()" api-form="customPromise" autocomplete="off">
|
||||
<div class="box box-default">
|
||||
|
||||
33
src/app/settings/views/settingsPurge.html
Normal file
33
src/app/settings/views/settingsPurge.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title"><i class="fa fa-trash"></i> Purge Vault</h4>
|
||||
</div>
|
||||
<form name="form" ng-submit="form.$valid && submit(model)" api-form="submitPromise">
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Continue below to delete all items in your vault. Items that belong to an organization that you share
|
||||
with will not be deleted.
|
||||
</p>
|
||||
<div class="callout callout-warning">
|
||||
<h4><i class="fa fa-warning"></i> Warning</h4>
|
||||
Purging your vault is permanent. It cannot be undone.
|
||||
</div>
|
||||
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
|
||||
<h4>Errors have occurred</h4>
|
||||
<ul>
|
||||
<li ng-repeat="e in form.$errors">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="masterPassword">Master Password</label>
|
||||
<input type="password" id="masterPassword" name="MasterPasswordHash" ng-model="model.masterPassword"
|
||||
class="form-control" required api-field />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="form.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="form.$loading"></i>Purge
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -8,7 +8,7 @@
|
||||
</div>
|
||||
<div class="box-body">
|
||||
The recovery code allows you to access your account in the event that you can no longer use your normal
|
||||
two-step login provider (ex. you lose your device). bitwarden support will not be able to assist you if you lose
|
||||
two-step login provider (ex. you lose your device). Bitwarden support will not be able to assist you if you lose
|
||||
access to your account. We recommend you write down or print the recovery code and keep it in a safe place.
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<li ng-repeat="e in submitTwoStepForm.$errors">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>Enter the bitwarden application information from your Duo Admin panel:</p>
|
||||
<p>Enter the Bitwarden application information from your Duo Admin panel:</p>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="ikey">Integration Key</label>
|
||||
<input type="text" id="ikey" name="IntegrationKey" ng-model="updateModel.ikey" class="form-control"
|
||||
@@ -57,7 +57,7 @@
|
||||
<div class="form-group" show-errors>
|
||||
<label for="skey">Secret Key</label>
|
||||
<input type="password" id="skey" name="SecretKey" ng-model="updateModel.skey" class="form-control"
|
||||
required api-field />
|
||||
required api-field autocomplete="new-password" />
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="host">API Hostname</label>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<div class="callout callout-warning">
|
||||
<h4><i class="fa fa-warning"></i> Warning <i class="fa fa-warning"></i></h4>
|
||||
<p>
|
||||
Due to platform limitations, FIDO U2F cannot be used on all bitwarden applications. You should enable
|
||||
Due to platform limitations, FIDO U2F cannot be used on all Bitwarden applications. You should enable
|
||||
another two-step login provider so that you can access your account when FIDO U2F cannot be used.
|
||||
</p>
|
||||
<p>Supported platforms:</p>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<div class="callout callout-warning">
|
||||
<h4><i class="fa fa-warning"></i> Warning <i class="fa fa-warning"></i></h4>
|
||||
<p>
|
||||
Due to platform limitations, YubiKeys cannot be used on all bitwarden applications. You should enable
|
||||
Due to platform limitations, YubiKeys cannot be used on all Bitwarden applications. You should enable
|
||||
another two-step login provider so that you can access your account when YubiKeys cannot be used.
|
||||
</p>
|
||||
<p>Supported platforms:</p>
|
||||
@@ -77,7 +77,7 @@
|
||||
{{updateModel.key1.existingKey}}
|
||||
</div>
|
||||
<input type="password" id="key1" name="Key1" ng-model="updateModel.key1.key" class="form-control" api-field
|
||||
ng-show="!updateModel.key1.existingKey" />
|
||||
ng-show="!updateModel.key1.existingKey" autocomplete="new-password" />
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="key2">YubiKey #2</label>
|
||||
@@ -88,7 +88,7 @@
|
||||
{{updateModel.key2.existingKey}}
|
||||
</div>
|
||||
<input type="password" id="key2" name="Key2" ng-model="updateModel.key2.key" class="form-control" api-field
|
||||
ng-show="!updateModel.key2.existingKey" />
|
||||
ng-show="!updateModel.key2.existingKey" autocomplete="new-password" />
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="key3">YubiKey #3</label>
|
||||
@@ -99,7 +99,7 @@
|
||||
{{updateModel.key3.existingKey}}
|
||||
</div>
|
||||
<input type="password" id="key3" name="Key3" ng-model="updateModel.key3.key" class="form-control" api-field
|
||||
ng-show="!updateModel.key3.existingKey" />
|
||||
ng-show="!updateModel.key3.existingKey" autocomplete="new-password" />
|
||||
</div>
|
||||
<strong>NFC Support</strong>
|
||||
<div class="checkbox">
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<hr />
|
||||
<div class="callout callout-warning">
|
||||
<h4><i class="fa fa-warning"></i> Warning</h4>
|
||||
After updating your encryption key, you are required to log out and back in to all bitwarden applications that you
|
||||
After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you
|
||||
are currently using (such as the mobile app or browser extensions). Failure to log out and back
|
||||
in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out
|
||||
automatically, however it may be delayed.
|
||||
|
||||
@@ -2,23 +2,23 @@
|
||||
.module('bit.tools')
|
||||
|
||||
.controller('toolsExportController', function ($scope, apiService, $uibModalInstance, cipherService, $q,
|
||||
toastr, $analytics) {
|
||||
toastr, $analytics, constants) {
|
||||
$analytics.eventTrack('toolsExportController', { category: 'Modal' });
|
||||
$scope.export = function (model) {
|
||||
$scope.startedExport = true;
|
||||
var decLogins = [],
|
||||
var decCiphers = [],
|
||||
decFolders = [];
|
||||
|
||||
var folderPromise = apiService.folders.list({}, function (folders) {
|
||||
decFolders = cipherService.decryptFolders(folders.Data);
|
||||
}).$promise;
|
||||
|
||||
var loginsPromise = apiService.ciphers.list({}, function (logins) {
|
||||
decLogins = cipherService.decryptLogins(logins.Data);
|
||||
var ciphersPromise = apiService.ciphers.list({}, function (ciphers) {
|
||||
decCiphers = cipherService.decryptCiphers(ciphers.Data);
|
||||
}).$promise;
|
||||
|
||||
$q.all([folderPromise, loginsPromise]).then(function () {
|
||||
if (!decLogins.length) {
|
||||
$q.all([folderPromise, ciphersPromise]).then(function () {
|
||||
if (!decCiphers.length) {
|
||||
toastr.error('Nothing to export.', 'Error!');
|
||||
$scope.close();
|
||||
return;
|
||||
@@ -30,24 +30,68 @@
|
||||
}
|
||||
|
||||
try {
|
||||
var exportLogins = [];
|
||||
for (i = 0; i < decLogins.length; i++) {
|
||||
var login = {
|
||||
name: decLogins[i].name,
|
||||
uri: decLogins[i].uri,
|
||||
username: decLogins[i].username,
|
||||
password: decLogins[i].password,
|
||||
notes: decLogins[i].notes,
|
||||
folder: decLogins[i].folderId && (decLogins[i].folderId in foldersDict) ?
|
||||
foldersDict[decLogins[i].folderId].name : null,
|
||||
favorite: decLogins[i].favorite ? 1 : null,
|
||||
totp: decLogins[i].totp
|
||||
var exportCiphers = [];
|
||||
for (i = 0; i < decCiphers.length; i++) {
|
||||
// only export logins and secure notes
|
||||
if (decCiphers[i].type !== constants.cipherType.login &&
|
||||
decCiphers[i].type !== constants.cipherType.secureNote) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var cipher = {
|
||||
folder: decCiphers[i].folderId && (decCiphers[i].folderId in foldersDict) ?
|
||||
foldersDict[decCiphers[i].folderId].name : null,
|
||||
favorite: decCiphers[i].favorite ? 1 : null,
|
||||
type: null,
|
||||
name: decCiphers[i].name,
|
||||
notes: decCiphers[i].notes,
|
||||
fields: null,
|
||||
// Login props
|
||||
login_uri: null,
|
||||
login_username: null,
|
||||
login_password: null,
|
||||
login_totp: null
|
||||
};
|
||||
|
||||
exportLogins.push(login);
|
||||
var j;
|
||||
if (decCiphers[i].fields) {
|
||||
for (j = 0; j < decCiphers[i].fields.length; j++) {
|
||||
if (!cipher.fields) {
|
||||
cipher.fields = '';
|
||||
}
|
||||
else {
|
||||
cipher.fields += '\n';
|
||||
}
|
||||
|
||||
cipher.fields += ((decCiphers[i].fields[j].name || '') + ': ' + decCiphers[i].fields[j].value);
|
||||
}
|
||||
}
|
||||
|
||||
switch (decCiphers[i].type) {
|
||||
case constants.cipherType.login:
|
||||
cipher.type = 'login';
|
||||
cipher.login_username = decCiphers[i].login.username;
|
||||
cipher.login_password = decCiphers[i].login.password;
|
||||
cipher.login_totp = decCiphers[i].login.totp;
|
||||
|
||||
if (decCiphers[i].login.uris && decCiphers[i].login.uris.length) {
|
||||
cipher.login_uri = [];
|
||||
for (j = 0; j < decCiphers[i].login.uris.length; j++) {
|
||||
cipher.login_uri.push(decCiphers[i].login.uris[j].uri);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case constants.cipherType.secureNote:
|
||||
cipher.type = 'note';
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
exportCiphers.push(cipher);
|
||||
}
|
||||
|
||||
var csvString = Papa.unparse(exportLogins);
|
||||
var csvString = Papa.unparse(exportCiphers);
|
||||
var csvBlob = new Blob([csvString]);
|
||||
|
||||
// IE hack. ref http://msdn.microsoft.com/en-us/library/ie/hh779016.aspx
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
$scope.options = [
|
||||
{
|
||||
id: 'bitwardencsv',
|
||||
name: 'bitwarden (csv)',
|
||||
name: 'Bitwarden (csv)',
|
||||
featured: true,
|
||||
sort: 1,
|
||||
instructions: $sce.trustAsHtml('Export using the web vault (vault.bitwarden.com). ' +
|
||||
@@ -102,7 +102,8 @@
|
||||
id: 'enpasscsv',
|
||||
name: 'Enpass (csv)',
|
||||
instructions: $sce.trustAsHtml('Using the Enpass desktop application, navigate to "File" > "Export" > ' +
|
||||
'"As CSV". Select "Yes" to the warning alert and save the CSV file.')
|
||||
'"As CSV". Select "Yes" to the warning alert and save the CSV file. Note that the importer only fully ' +
|
||||
'supports files exported while Enpass is set to the English language, so adjust your settings accordingly.')
|
||||
},
|
||||
{
|
||||
id: 'safeincloudxml',
|
||||
@@ -242,7 +243,7 @@
|
||||
'python script by Luke Plant to your desktop as <code>pw_helper.py</code>. Open terminal and run ' +
|
||||
'<code>chmod +rx Desktop/pw_helper.py</code> and then ' +
|
||||
'<code>python Desktop/pw_helper.py export Desktop/my_passwords.json</code>. Then upload ' +
|
||||
'the resulting <code>my_passwords.json</code> file here to bitwarden.')
|
||||
'the resulting <code>my_passwords.json</code> file here to Bitwarden.')
|
||||
}
|
||||
];
|
||||
|
||||
@@ -272,23 +273,23 @@
|
||||
importService.import(model.source, file || model.fileContents, importSuccess, importError);
|
||||
};
|
||||
|
||||
function importSuccess(folders, logins, folderRelationships) {
|
||||
if (!folders.length && !logins.length) {
|
||||
function importSuccess(folders, ciphers, folderRelationships) {
|
||||
if (!folders.length && !ciphers.length) {
|
||||
importError('Nothing was imported.');
|
||||
return;
|
||||
}
|
||||
else if (logins.length) {
|
||||
var halfway = Math.floor(logins.length / 2);
|
||||
var last = logins.length - 1;
|
||||
if (loginIsBadData(logins[0]) && loginIsBadData(logins[halfway]) && loginIsBadData(logins[last])) {
|
||||
importError('CSV data is not formatted correctly. Please check your import file and try again.');
|
||||
else if (ciphers.length) {
|
||||
var halfway = Math.floor(ciphers.length / 2);
|
||||
var last = ciphers.length - 1;
|
||||
if (cipherIsBadData(ciphers[0]) && cipherIsBadData(ciphers[halfway]) && cipherIsBadData(ciphers[last])) {
|
||||
importError('Data is not formatted correctly. Please check your import file and try again.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
apiService.ciphers.import({
|
||||
folders: cipherService.encryptFolders(folders),
|
||||
ciphers: cipherService.encryptLogins(logins),
|
||||
ciphers: cipherService.encryptCiphers(ciphers),
|
||||
folderRelationships: folderRelationships
|
||||
}, function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
@@ -299,8 +300,9 @@
|
||||
}, importError);
|
||||
}
|
||||
|
||||
function loginIsBadData(login) {
|
||||
return (login.name === null || login.name === '--') && (login.password === null || login.password === '');
|
||||
function cipherIsBadData(cipher) {
|
||||
return (cipher.name === null || cipher.name === '--') &&
|
||||
(cipher.login && (cipher.login.password === null || cipher.login.password === ''));
|
||||
}
|
||||
|
||||
function importError(error) {
|
||||
|
||||
151
src/app/vault/vaultAddCipherController.js
Normal file
151
src/app/vault/vaultAddCipherController.js
Normal file
@@ -0,0 +1,151 @@
|
||||
angular
|
||||
.module('bit.vault')
|
||||
|
||||
.controller('vaultAddCipherController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService,
|
||||
passwordService, selectedFolder, $analytics, checkedFavorite, $rootScope, authService, $uibModal, constants,
|
||||
$filter, selectedType) {
|
||||
$analytics.eventTrack('vaultAddCipherController', { category: 'Modal' });
|
||||
$scope.folders = $rootScope.vaultFolders;
|
||||
$scope.constants = constants;
|
||||
$scope.selectedType = selectedType ? selectedType.toString() : constants.cipherType.login.toString();
|
||||
$scope.cipher = {
|
||||
folderId: selectedFolder ? selectedFolder.id : null,
|
||||
favorite: checkedFavorite === true,
|
||||
type: selectedType || constants.cipherType.login,
|
||||
login: {
|
||||
uris: [{
|
||||
uri: null,
|
||||
match: null,
|
||||
matchValue: null
|
||||
}]
|
||||
},
|
||||
identity: {},
|
||||
card: {},
|
||||
secureNote: {
|
||||
type: 0
|
||||
}
|
||||
};
|
||||
|
||||
authService.getUserProfile().then(function (profile) {
|
||||
$scope.useTotp = profile.premium;
|
||||
});
|
||||
|
||||
$scope.typeChanged = function () {
|
||||
$scope.cipher.type = parseInt($scope.selectedType);
|
||||
};
|
||||
|
||||
$scope.savePromise = null;
|
||||
$scope.save = function () {
|
||||
var cipher = cipherService.encryptCipher($scope.cipher);
|
||||
$scope.savePromise = apiService.ciphers.post(cipher, function (cipherResponse) {
|
||||
$analytics.eventTrack('Created Cipher');
|
||||
var decCipher = cipherService.decryptCipherPreview(cipherResponse);
|
||||
$uibModalInstance.close(decCipher);
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
$scope.generatePassword = function () {
|
||||
if (!$scope.cipher.login.password || confirm('Are you sure you want to overwrite the current password?')) {
|
||||
$analytics.eventTrack('Generated Password From Add');
|
||||
$scope.cipher.login.password = passwordService.generatePassword({ length: 14, special: true });
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addUri = function () {
|
||||
if (!$scope.cipher.login) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$scope.cipher.login.uris) {
|
||||
$scope.cipher.login.uris = [];
|
||||
}
|
||||
|
||||
$scope.cipher.login.uris.push({
|
||||
uri: null,
|
||||
match: null,
|
||||
matchValue: null
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeUri = function (uri) {
|
||||
if (!$scope.cipher.login || !$scope.cipher.login.uris) {
|
||||
return;
|
||||
}
|
||||
|
||||
var index = $scope.cipher.login.uris.indexOf(uri);
|
||||
if (index > -1) {
|
||||
$scope.cipher.login.uris.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.uriMatchChanged = function (uri) {
|
||||
if ((!uri.matchValue && uri.matchValue !== 0) || uri.matchValue === '') {
|
||||
uri.match = null;
|
||||
}
|
||||
else {
|
||||
uri.match = parseInt(uri.matchValue);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addField = function () {
|
||||
if (!$scope.cipher.fields) {
|
||||
$scope.cipher.fields = [];
|
||||
}
|
||||
|
||||
$scope.cipher.fields.push({
|
||||
type: constants.fieldType.text.toString(),
|
||||
name: null,
|
||||
value: null
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeField = function (field) {
|
||||
var index = $scope.cipher.fields.indexOf(field);
|
||||
if (index > -1) {
|
||||
$scope.cipher.fields.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.toggleFavorite = function () {
|
||||
$scope.cipher.favorite = !$scope.cipher.favorite;
|
||||
};
|
||||
|
||||
$scope.clipboardSuccess = function (e) {
|
||||
e.clearSelection();
|
||||
selectPassword(e);
|
||||
};
|
||||
|
||||
$scope.clipboardError = function (e, password) {
|
||||
if (password) {
|
||||
selectPassword(e);
|
||||
}
|
||||
alert('Your web browser does not support easy clipboard copying. Copy it manually instead.');
|
||||
};
|
||||
|
||||
$scope.folderSort = function (item) {
|
||||
if (!item.id) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return item.name.toLowerCase();
|
||||
};
|
||||
|
||||
function selectPassword(e) {
|
||||
var target = $(e.trigger).parent().prev();
|
||||
if (target.attr('type') === 'text') {
|
||||
target.select();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('close');
|
||||
};
|
||||
|
||||
$scope.showUpgrade = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/views/premiumRequired.html',
|
||||
controller: 'premiumRequiredController'
|
||||
});
|
||||
};
|
||||
});
|
||||
@@ -1,95 +0,0 @@
|
||||
angular
|
||||
.module('bit.vault')
|
||||
|
||||
.controller('vaultAddLoginController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService,
|
||||
passwordService, selectedFolder, $analytics, checkedFavorite, $rootScope, authService, $uibModal) {
|
||||
$analytics.eventTrack('vaultAddLoginController', { category: 'Modal' });
|
||||
$scope.folders = $rootScope.vaultFolders;
|
||||
$scope.login = {
|
||||
folderId: selectedFolder ? selectedFolder.id : null,
|
||||
favorite: checkedFavorite === true
|
||||
};
|
||||
|
||||
authService.getUserProfile().then(function (profile) {
|
||||
$scope.useTotp = profile.premium;
|
||||
});
|
||||
|
||||
$scope.savePromise = null;
|
||||
$scope.save = function (model) {
|
||||
var login = cipherService.encryptLogin(model);
|
||||
$scope.savePromise = apiService.ciphers.post(login, function (loginResponse) {
|
||||
$analytics.eventTrack('Created Login');
|
||||
var decLogin = cipherService.decryptLogin(loginResponse);
|
||||
$uibModalInstance.close(decLogin);
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
$scope.generatePassword = function () {
|
||||
if (!$scope.login.password || confirm('Are you sure you want to overwrite the current password?')) {
|
||||
$analytics.eventTrack('Generated Password From Add');
|
||||
$scope.login.password = passwordService.generatePassword({ length: 12, special: true });
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addField = function () {
|
||||
if (!$scope.login.fields) {
|
||||
$scope.login.fields = [];
|
||||
}
|
||||
|
||||
$scope.login.fields.push({
|
||||
type: '0',
|
||||
name: null,
|
||||
value: null
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeField = function (field) {
|
||||
var index = $scope.login.fields.indexOf(field);
|
||||
if (index > -1) {
|
||||
$scope.login.fields.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.toggleFavorite = function () {
|
||||
$scope.login.favorite = !$scope.login.favorite;
|
||||
};
|
||||
|
||||
$scope.clipboardSuccess = function (e) {
|
||||
e.clearSelection();
|
||||
selectPassword(e);
|
||||
};
|
||||
|
||||
$scope.clipboardError = function (e, password) {
|
||||
if (password) {
|
||||
selectPassword(e);
|
||||
}
|
||||
alert('Your web browser does not support easy clipboard copying. Copy it manually instead.');
|
||||
};
|
||||
|
||||
$scope.folderSort = function (item) {
|
||||
if (!item.id) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return item.name.toLowerCase();
|
||||
};
|
||||
|
||||
function selectPassword(e) {
|
||||
var target = $(e.trigger).parent().prev();
|
||||
if (target.attr('type') === 'text') {
|
||||
target.select();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('close');
|
||||
};
|
||||
|
||||
$scope.showUpgrade = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/views/premiumRequired.html',
|
||||
controller: 'premiumRequiredController'
|
||||
});
|
||||
};
|
||||
});
|
||||
@@ -2,9 +2,9 @@
|
||||
.module('bit.vault')
|
||||
|
||||
.controller('vaultAttachmentsController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService,
|
||||
loginId, $analytics, validationService, toastr, $timeout, authService, $uibModal) {
|
||||
cipherId, $analytics, validationService, toastr, $timeout, authService, $uibModal) {
|
||||
$analytics.eventTrack('vaultAttachmentsController', { category: 'Modal' });
|
||||
$scope.login = {};
|
||||
$scope.cipher = {};
|
||||
$scope.readOnly = true;
|
||||
$scope.loading = true;
|
||||
$scope.isPremium = true;
|
||||
@@ -13,11 +13,11 @@
|
||||
|
||||
authService.getUserProfile().then(function (profile) {
|
||||
$scope.isPremium = profile.premium;
|
||||
return apiService.ciphers.get({ id: loginId }).$promise;
|
||||
return apiService.ciphers.get({ id: cipherId }).$promise;
|
||||
}).then(function (cipher) {
|
||||
$scope.login = cipherService.decryptLogin(cipher);
|
||||
$scope.readOnly = !$scope.login.edit;
|
||||
$scope.canUseAttachments = $scope.isPremium || $scope.login.organizationId;
|
||||
$scope.cipher = cipherService.decryptCipher(cipher);
|
||||
$scope.readOnly = !$scope.cipher.edit;
|
||||
$scope.canUseAttachments = $scope.isPremium || $scope.cipher.organizationId;
|
||||
$scope.loading = false;
|
||||
}, function () {
|
||||
$scope.loading = false;
|
||||
@@ -31,27 +31,23 @@
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.savePromise = cipherService.encryptAttachmentFile(getKeyForLogin(), files[0]).then(function (encValue) {
|
||||
$scope.savePromise = cipherService.encryptAttachmentFile(getKeyForCipher(), files[0]).then(function (encValue) {
|
||||
var fd = new FormData();
|
||||
var blob = new Blob([encValue.data], { type: 'application/octet-stream' });
|
||||
fd.append('data', blob, encValue.fileName);
|
||||
return apiService.ciphers.postAttachment({ id: loginId }, fd).$promise;
|
||||
return apiService.ciphers.postAttachment({ id: cipherId }, fd).$promise;
|
||||
}).then(function (response) {
|
||||
$analytics.eventTrack('Added Attachment');
|
||||
$scope.login = cipherService.decryptLogin(response);
|
||||
$scope.cipher = cipherService.decryptCipher(response);
|
||||
|
||||
// reset file input
|
||||
// ref: https://stackoverflow.com/a/20552042
|
||||
fileEl.type = '';
|
||||
fileEl.type = 'file';
|
||||
fileEl.value = '';
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
validationService.addError(form, 'file', err, true);
|
||||
}
|
||||
else {
|
||||
validationService.addError(form, 'file', 'Something went wrong.', true);
|
||||
}
|
||||
}, function (e) {
|
||||
var errors = validationService.parseErrors(e);
|
||||
toastr.error(errors.length ? errors[0] : 'An error occurred.');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -64,7 +60,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
cipherService.downloadAndDecryptAttachment(getKeyForLogin(), attachment, true).then(function (res) {
|
||||
cipherService.downloadAndDecryptAttachment(getKeyForCipher(), attachment, true).then(function (res) {
|
||||
$timeout(function () {
|
||||
attachment.loading = false;
|
||||
});
|
||||
@@ -75,9 +71,9 @@
|
||||
});
|
||||
};
|
||||
|
||||
function getKeyForLogin() {
|
||||
if ($scope.login.organizationId) {
|
||||
return cryptoService.getOrgKey($scope.login.organizationId);
|
||||
function getKeyForCipher() {
|
||||
if ($scope.cipher.organizationId) {
|
||||
return cryptoService.getOrgKey($scope.cipher.organizationId);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -89,12 +85,12 @@
|
||||
}
|
||||
|
||||
attachment.loading = true;
|
||||
apiService.ciphers.delAttachment({ id: loginId, attachmentId: attachment.id }).$promise.then(function () {
|
||||
apiService.ciphers.delAttachment({ id: cipherId, attachmentId: attachment.id }).$promise.then(function () {
|
||||
attachment.loading = false;
|
||||
$analytics.eventTrack('Deleted Attachment');
|
||||
var index = $scope.login.attachments.indexOf(attachment);
|
||||
var index = $scope.cipher.attachments.indexOf(attachment);
|
||||
if (index > -1) {
|
||||
$scope.login.attachments.splice(index, 1);
|
||||
$scope.cipher.attachments.splice(index, 1);
|
||||
}
|
||||
}, function () {
|
||||
toastr.error('Cannot delete attachment.');
|
||||
@@ -113,7 +109,7 @@
|
||||
|
||||
e.preventDefault();
|
||||
closing = true;
|
||||
$uibModalInstance.close(!!$scope.login.attachments && $scope.login.attachments.length > 0);
|
||||
$uibModalInstance.close(!!$scope.cipher.attachments && $scope.cipher.attachments.length > 0);
|
||||
});
|
||||
|
||||
$scope.showUpgrade = function () {
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
angular
|
||||
.module('bit.vault')
|
||||
|
||||
.controller('vaultLoginCollectionsController', function ($scope, apiService, $uibModalInstance, cipherService,
|
||||
loginId, $analytics) {
|
||||
$analytics.eventTrack('vaultLoginCollectionsController', { category: 'Modal' });
|
||||
$scope.login = {};
|
||||
.controller('vaultCipherCollectionsController', function ($scope, apiService, $uibModalInstance, cipherService,
|
||||
cipherId, $analytics) {
|
||||
$analytics.eventTrack('vaultCipherCollectionsController', { category: 'Modal' });
|
||||
$scope.cipher = {};
|
||||
$scope.readOnly = false;
|
||||
$scope.loadingLogin = true;
|
||||
$scope.loadingCipher = true;
|
||||
$scope.loadingCollections = true;
|
||||
$scope.selectedCollections = {};
|
||||
$scope.collections = [];
|
||||
|
||||
var cipherAndCols = null;
|
||||
$uibModalInstance.opened.then(function () {
|
||||
apiService.ciphers.getDetails({ id: loginId }).$promise.then(function (cipher) {
|
||||
$scope.loadingLogin = false;
|
||||
apiService.ciphers.getDetails({ id: cipherId }).$promise.then(function (cipher) {
|
||||
$scope.loadingCipher = false;
|
||||
|
||||
$scope.readOnly = !cipher.Edit;
|
||||
if (cipher.Edit && cipher.OrganizationId) {
|
||||
if (cipher.Type === 1) {
|
||||
$scope.login = cipherService.decryptLoginPreview(cipher);
|
||||
$scope.cipher = cipherService.decryptCipherPreview(cipher);
|
||||
}
|
||||
|
||||
var collections = {};
|
||||
@@ -35,34 +36,40 @@
|
||||
}
|
||||
|
||||
return null;
|
||||
}).then(function (cipherAndCols) {
|
||||
if (!cipherAndCols) {
|
||||
}).then(function (result) {
|
||||
if (!result) {
|
||||
$scope.loadingCollections = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
cipherAndCols = result;
|
||||
return apiService.collections.listMe({ writeOnly: true }).$promise;
|
||||
}).then(function (response) {
|
||||
if (response === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
apiService.collections.listMe({ writeOnly: true }, function (response) {
|
||||
var collections = [];
|
||||
var selectedCollections = {};
|
||||
var collections = [];
|
||||
var selectedCollections = {};
|
||||
var writeableCollections = response.Data;
|
||||
|
||||
for (var i = 0; i < response.Data.length; i++) {
|
||||
// clean out selectCollections that aren't from this organization or read only
|
||||
if (response.Data[i].Id in cipherAndCols.cipherCollections &&
|
||||
response.Data[i].OrganizationId === cipherAndCols.cipher.OrganizationId) {
|
||||
selectedCollections[response.Data[i].Id] = true;
|
||||
}
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
|
||||
var decCollection = cipherService.decryptCollection(response.Data[i]);
|
||||
collections.push(decCollection);
|
||||
for (var i = 0; i < writeableCollections.length; i++) {
|
||||
// clean out selectCollections that aren't from this organization
|
||||
if (writeableCollections[i].OrganizationId !== cipherAndCols.cipher.OrganizationId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$scope.loadingCollections = false;
|
||||
$scope.collections = collections;
|
||||
$scope.selectedCollections = selectedCollections;
|
||||
});
|
||||
if (writeableCollections[i].Id in cipherAndCols.cipherCollections) {
|
||||
selectedCollections[writeableCollections[i].Id] = true;
|
||||
}
|
||||
|
||||
var decCollection = cipherService.decryptCollection(writeableCollections[i]);
|
||||
collections.push(decCollection);
|
||||
}
|
||||
|
||||
$scope.loadingCollections = false;
|
||||
$scope.collections = collections;
|
||||
$scope.selectedCollections = selectedCollections;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -105,9 +112,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
$scope.submitPromise = apiService.ciphers.putCollections({ id: loginId }, request)
|
||||
$scope.submitPromise = apiService.ciphers.putCollections({ id: cipherId }, request)
|
||||
.$promise.then(function (response) {
|
||||
$analytics.eventTrack('Edited Login Collections');
|
||||
$analytics.eventTrack('Edited Cipher Collections');
|
||||
$uibModalInstance.close({
|
||||
action: 'collectionsEdit',
|
||||
collectionIds: request.collectionIds
|
||||
@@ -2,21 +2,36 @@
|
||||
.module('bit.vault')
|
||||
|
||||
.controller('vaultController', function ($scope, $uibModal, apiService, $filter, cryptoService, authService, toastr,
|
||||
cipherService, $q, $localStorage, $timeout, $rootScope, $state, $analytics) {
|
||||
$scope.loading = true;
|
||||
$scope.logins = [];
|
||||
$scope.favoriteCollapsed = $localStorage.collapsedFolders && 'favorite' in $localStorage.collapsedFolders;
|
||||
$scope.folderIdFilter = undefined;
|
||||
cipherService, $q, $localStorage, $timeout, $rootScope, $state, $analytics, constants, validationService) {
|
||||
$scope.loadingCiphers = true;
|
||||
$scope.loadingGroupings = true;
|
||||
$scope.ciphers = [];
|
||||
$scope.folders = [];
|
||||
$scope.collections = [];
|
||||
$scope.constants = constants;
|
||||
$scope.filter = undefined;
|
||||
$scope.selectedType = undefined;
|
||||
$scope.selectedFolder = undefined;
|
||||
$scope.selectedCollection = undefined;
|
||||
$scope.selectedFavorites = false;
|
||||
$scope.selectedAll = true;
|
||||
$scope.selectedTitle = 'All';
|
||||
$scope.selectedIcon = 'fa-th';
|
||||
|
||||
if ($state.params.refreshFromServer) {
|
||||
$rootScope.vaultFolders = $rootScope.vaultLogins = null;
|
||||
$rootScope.vaultFolders = $rootScope.vaultCollections = $rootScope.vaultCiphers = null;
|
||||
}
|
||||
|
||||
$scope.$on('$viewContentLoaded', function () {
|
||||
if ($rootScope.vaultFolders && $rootScope.vaultLogins) {
|
||||
$scope.loading = false;
|
||||
loadFolderData($rootScope.vaultFolders);
|
||||
loadLoginData($rootScope.vaultLogins);
|
||||
$timeout(function () {
|
||||
if ($('body').hasClass('control-sidebar-open')) {
|
||||
$("#search").focus();
|
||||
}
|
||||
}, 500);
|
||||
|
||||
if (($rootScope.vaultFolders || $rootScope.vaultCollections) && $rootScope.vaultCiphers) {
|
||||
$scope.loadingCiphers = $scope.loadingGroupings = false;
|
||||
loadCipherData($rootScope.vaultCiphers);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -24,64 +39,53 @@
|
||||
});
|
||||
|
||||
function loadDataFromServer() {
|
||||
var folderPromise = apiService.folders.list({}, function (folders) {
|
||||
var decFolders = [{
|
||||
id: null,
|
||||
name: 'No Folder'
|
||||
}];
|
||||
var decFolders = [{
|
||||
id: null,
|
||||
name: 'No Folder'
|
||||
}];
|
||||
|
||||
var decCollections = [];
|
||||
|
||||
var collectionPromise = apiService.collections.listMe({ writeOnly: false }, function (collections) {
|
||||
for (var i = 0; i < collections.Data.length; i++) {
|
||||
var decCollection = cipherService.decryptCollection(collections.Data[i], null, true);
|
||||
decCollections.push(decCollection);
|
||||
}
|
||||
}).$promise;
|
||||
|
||||
var folderPromise = apiService.folders.list({}, function (folders) {
|
||||
for (var i = 0; i < folders.Data.length; i++) {
|
||||
var decFolder = cipherService.decryptFolderPreview(folders.Data[i]);
|
||||
decFolders.push(decFolder);
|
||||
}
|
||||
|
||||
loadFolderData(decFolders);
|
||||
}).$promise;
|
||||
|
||||
var cipherPromise = apiService.ciphers.list({}, function (ciphers) {
|
||||
var decLogins = [];
|
||||
var groupingPromise = $q.all([collectionPromise, folderPromise]).then(function () {
|
||||
$rootScope.vaultCollections = decCollections;
|
||||
$rootScope.vaultFolders = decFolders;
|
||||
$scope.loadingGroupings = false;
|
||||
});
|
||||
|
||||
apiService.ciphers.list({}, function (ciphers) {
|
||||
var decCiphers = [];
|
||||
|
||||
for (var i = 0; i < ciphers.Data.length; i++) {
|
||||
if (ciphers.Data[i].Type === 1) {
|
||||
var decLogin = cipherService.decryptLoginPreview(ciphers.Data[i]);
|
||||
decLogins.push(decLogin);
|
||||
}
|
||||
var decCipher = cipherService.decryptCipherPreview(ciphers.Data[i]);
|
||||
decCiphers.push(decCipher);
|
||||
}
|
||||
|
||||
$q.when(folderPromise).then(function () {
|
||||
loadLoginData(decLogins);
|
||||
groupingPromise.then(function () {
|
||||
loadCipherData(decCiphers);
|
||||
});
|
||||
}).$promise;
|
||||
|
||||
$q.all([cipherPromise, folderPromise]).then(function () {
|
||||
$scope.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
function loadFolderData(decFolders) {
|
||||
$rootScope.vaultFolders = $filter('orderBy')(decFolders, folderSort);
|
||||
}
|
||||
function loadCipherData(decCiphers) {
|
||||
$rootScope.vaultCiphers = $scope.ciphers = $filter('orderBy')(decCiphers, ['sort', 'name', 'subTitle']);
|
||||
|
||||
function loadLoginData(decLogins) {
|
||||
angular.forEach($rootScope.vaultFolders, function (folderValue, folderIndex) {
|
||||
folderValue.collapsed = $localStorage.collapsedFolders &&
|
||||
(folderValue.id || 'none') in $localStorage.collapsedFolders;
|
||||
|
||||
angular.forEach(decLogins, function (loginValue) {
|
||||
if (loginValue.favorite) {
|
||||
loginValue.sort = -1;
|
||||
}
|
||||
else if (loginValue.folderId == folderValue.id) {
|
||||
loginValue.sort = folderIndex;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$rootScope.vaultLogins = $scope.logins = $filter('orderBy')(decLogins, ['sort', 'name', 'username']);
|
||||
|
||||
var chunks = chunk($rootScope.vaultLogins, 400);
|
||||
var chunks = chunk($rootScope.vaultCiphers, 200);
|
||||
if (chunks.length > 0) {
|
||||
$scope.logins = chunks[0];
|
||||
$scope.ciphers = chunks[0];
|
||||
var delay = 200;
|
||||
angular.forEach(chunks, function (value, index) {
|
||||
delay += 200;
|
||||
@@ -89,15 +93,17 @@
|
||||
// skip the first chuck
|
||||
if (index > 0) {
|
||||
$timeout(function () {
|
||||
Array.prototype.push.apply($scope.logins, value);
|
||||
Array.prototype.push.apply($scope.ciphers, value);
|
||||
}, delay);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$scope.loadingCiphers = false;
|
||||
}
|
||||
|
||||
function sortScopedLoginData() {
|
||||
$rootScope.vaultLogins = $scope.logins = $filter('orderBy')($rootScope.vaultLogins, ['name', 'username']);
|
||||
function sortScopedCipherData() {
|
||||
$rootScope.vaultCiphers = $scope.ciphers = $filter('orderBy')($rootScope.vaultCiphers, ['name', 'subTitle']);
|
||||
}
|
||||
|
||||
function chunk(arr, len) {
|
||||
@@ -110,116 +116,103 @@
|
||||
return chunks;
|
||||
}
|
||||
|
||||
function folderSort(item) {
|
||||
$scope.groupingSort = function (item) {
|
||||
if (!item.id) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return item.name.toLowerCase();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.clipboardError = function (e) {
|
||||
alert('Your web browser does not support easy clipboard copying. ' +
|
||||
'Edit the login and copy it manually instead.');
|
||||
'Edit the item and copy it manually instead.');
|
||||
};
|
||||
|
||||
$scope.collapseExpand = function (folder, favorite) {
|
||||
if (!$localStorage.collapsedFolders) {
|
||||
$localStorage.collapsedFolders = {};
|
||||
}
|
||||
|
||||
var id = favorite ? 'favorite' : (folder.id || 'none');
|
||||
if (id in $localStorage.collapsedFolders) {
|
||||
delete $localStorage.collapsedFolders[id];
|
||||
}
|
||||
else {
|
||||
$localStorage.collapsedFolders[id] = true;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.editLogin = function (login) {
|
||||
$scope.editCipher = function (cipher) {
|
||||
var editModel = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/vault/views/vaultEditLogin.html',
|
||||
controller: 'vaultEditLoginController',
|
||||
templateUrl: 'app/vault/views/vaultEditCipher.html',
|
||||
controller: 'vaultEditCipherController',
|
||||
resolve: {
|
||||
loginId: function () { return login.id; }
|
||||
cipherId: function () { return cipher.id; }
|
||||
}
|
||||
});
|
||||
|
||||
editModel.result.then(function (returnVal) {
|
||||
if (returnVal.action === 'edit') {
|
||||
login.folderId = returnVal.data.folderId;
|
||||
login.name = returnVal.data.name;
|
||||
login.username = returnVal.data.username;
|
||||
login.password = returnVal.data.password;
|
||||
login.favorite = returnVal.data.favorite;
|
||||
|
||||
sortScopedLoginData();
|
||||
var index = $scope.ciphers.indexOf(cipher);
|
||||
if (index > -1) {
|
||||
// restore collection ids since those cannot change on edit here.
|
||||
returnVal.data.collectionIds = $rootScope.vaultCiphers[index].collectionIds;
|
||||
$rootScope.vaultCiphers[index] = returnVal.data;
|
||||
}
|
||||
sortScopedCipherData();
|
||||
}
|
||||
else if (returnVal.action === 'partialEdit') {
|
||||
login.folderId = returnVal.data.folderId;
|
||||
login.favorite = returnVal.data.favorite;
|
||||
cipher.folderId = returnVal.data.folderId;
|
||||
cipher.favorite = returnVal.data.favorite;
|
||||
}
|
||||
else if (returnVal.action === 'delete') {
|
||||
removeLoginFromScopes(login);
|
||||
removeCipherFromScopes(cipher);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('vaultAddLogin', function (event, args) {
|
||||
$scope.addLogin();
|
||||
$scope.$on('vaultAddCipher', function (event, args) {
|
||||
$scope.addCipher();
|
||||
});
|
||||
|
||||
$scope.addLogin = function (folder, favorite) {
|
||||
$scope.addCipher = function () {
|
||||
var addModel = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/vault/views/vaultAddLogin.html',
|
||||
controller: 'vaultAddLoginController',
|
||||
templateUrl: 'app/vault/views/vaultAddCipher.html',
|
||||
controller: 'vaultAddCipherController',
|
||||
resolve: {
|
||||
selectedFolder: function () { return folder; },
|
||||
checkedFavorite: function () { return favorite; }
|
||||
selectedFolder: function () { return $scope.selectedFolder; },
|
||||
selectedType: function () { return $scope.selectedType; },
|
||||
checkedFavorite: function () { return $scope.selectedFavorites; }
|
||||
}
|
||||
});
|
||||
|
||||
addModel.result.then(function (addedLogin) {
|
||||
$rootScope.vaultLogins.push(addedLogin);
|
||||
sortScopedLoginData();
|
||||
addModel.result.then(function (addedCipher) {
|
||||
$rootScope.vaultCiphers.push(addedCipher);
|
||||
sortScopedCipherData();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteLogin = function (login) {
|
||||
if (!confirm('Are you sure you want to delete this login (' + login.name + ')?')) {
|
||||
$scope.deleteCipher = function (cipher) {
|
||||
if (!confirm('Are you sure you want to delete this item (' + cipher.name + ')?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
apiService.ciphers.del({ id: login.id }, function () {
|
||||
$analytics.eventTrack('Deleted Login');
|
||||
removeLoginFromScopes(login);
|
||||
apiService.ciphers.del({ id: cipher.id }, function () {
|
||||
$analytics.eventTrack('Deleted Item');
|
||||
removeCipherFromScopes(cipher);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.attachments = function (login) {
|
||||
$scope.attachments = function (cipher) {
|
||||
authService.getUserProfile().then(function (profile) {
|
||||
return {
|
||||
isPremium: profile.premium,
|
||||
orgUseStorage: login.organizationId && !!profile.organizations[login.organizationId].maxStorageGb
|
||||
orgUseStorage: cipher.organizationId && !!profile.organizations[cipher.organizationId].maxStorageGb
|
||||
};
|
||||
}).then(function (perms) {
|
||||
if (!login.hasAttachments) {
|
||||
if (login.organizationId && !perms.orgUseStorage) {
|
||||
if (!cipher.hasAttachments) {
|
||||
if (cipher.organizationId && !perms.orgUseStorage) {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/views/paidOrgRequired.html',
|
||||
controller: 'paidOrgRequiredController',
|
||||
resolve: {
|
||||
orgId: function () { return login.organizationId; }
|
||||
orgId: function () { return cipher.organizationId; }
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!login.organizationId && !perms.isPremium) {
|
||||
if (!cipher.organizationId && !perms.isPremium) {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/views/premiumRequired.html',
|
||||
@@ -229,7 +222,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (!login.organizationId && !cryptoService.getEncKey()) {
|
||||
if (!cipher.organizationId && !cryptoService.getEncKey()) {
|
||||
toastr.error('You cannot use this feature until you update your encryption key.', 'Feature Unavailable');
|
||||
return;
|
||||
}
|
||||
@@ -239,17 +232,21 @@
|
||||
templateUrl: 'app/vault/views/vaultAttachments.html',
|
||||
controller: 'vaultAttachmentsController',
|
||||
resolve: {
|
||||
loginId: function () { return login.id; }
|
||||
cipherId: function () { return cipher.id; }
|
||||
}
|
||||
});
|
||||
|
||||
attachmentModel.result.then(function (hasAttachments) {
|
||||
login.hasAttachments = hasAttachments;
|
||||
cipher.hasAttachments = hasAttachments;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.editFolder = function (folder) {
|
||||
if (!folder.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
var editModel = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/vault/views/vaultEditFolder.html',
|
||||
@@ -279,12 +276,16 @@
|
||||
|
||||
addModel.result.then(function (addedFolder) {
|
||||
$rootScope.vaultFolders.push(addedFolder);
|
||||
loadFolderData($rootScope.vaultFolders);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteFolder = function (folder) {
|
||||
if (!confirm('Are you sure you want to delete this folder (' + folder.name + ')?')) {
|
||||
if (!folder.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('Are you sure you want to delete this folder (' + folder.name + ')? ' +
|
||||
'Any items will be moved to "No Folder".')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -293,86 +294,148 @@
|
||||
var index = $rootScope.vaultFolders.indexOf(folder);
|
||||
if (index > -1) {
|
||||
$rootScope.vaultFolders.splice(index, 1);
|
||||
$scope.filterAll();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.canDeleteFolder = function (folder) {
|
||||
if (!folder || !folder.id || !$rootScope.vaultLogins) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var logins = $filter('filter')($rootScope.vaultLogins, { folderId: folder.id });
|
||||
return logins && logins.length === 0;
|
||||
};
|
||||
|
||||
$scope.share = function (login) {
|
||||
$scope.share = function (cipher) {
|
||||
var modal = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/vault/views/vaultShareLogin.html',
|
||||
controller: 'vaultShareLoginController',
|
||||
templateUrl: 'app/vault/views/vaultShareCipher.html',
|
||||
controller: 'vaultShareCipherController',
|
||||
resolve: {
|
||||
loginId: function () { return login.id; }
|
||||
cipherId: function () { return cipher.id; }
|
||||
}
|
||||
});
|
||||
|
||||
modal.result.then(function (orgId) {
|
||||
login.organizationId = orgId;
|
||||
modal.result.then(function (returned) {
|
||||
cipher.organizationId = returned.orgId;
|
||||
cipher.collectionIds = returned.collectionIds || [];
|
||||
});
|
||||
};
|
||||
|
||||
$scope.collections = function (login) {
|
||||
$scope.editCollections = function (cipher) {
|
||||
var modal = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/vault/views/vaultLoginCollections.html',
|
||||
controller: 'vaultLoginCollectionsController',
|
||||
templateUrl: 'app/vault/views/vaultCipherCollections.html',
|
||||
controller: 'vaultCipherCollectionsController',
|
||||
resolve: {
|
||||
loginId: function () { return login.id; }
|
||||
cipherId: function () { return cipher.id; }
|
||||
}
|
||||
});
|
||||
|
||||
modal.result.then(function (response) {
|
||||
if (response.collectionIds && !response.collectionIds.length) {
|
||||
removeLoginFromScopes(login);
|
||||
removeCipherFromScopes(cipher);
|
||||
}
|
||||
else if (response.collectionIds) {
|
||||
cipher.collectionIds = response.collectionIds;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.filterFolder = function (folder) {
|
||||
$scope.folderIdFilter = folder.id;
|
||||
$scope.filterCollection = function (col) {
|
||||
resetSelected();
|
||||
$scope.selectedCollection = col;
|
||||
$scope.selectedIcon = 'fa-cube';
|
||||
$scope.filter = function (c) {
|
||||
return c.collectionIds && c.collectionIds.indexOf(col.id) > -1;
|
||||
};
|
||||
fixLayout();
|
||||
};
|
||||
|
||||
$scope.filterFolder = function (f) {
|
||||
resetSelected();
|
||||
$scope.selectedFolder = f;
|
||||
$scope.selectedIcon = 'fa-folder-open' + (!f.id ? '-o' : '');
|
||||
$scope.filter = function (c) {
|
||||
return c.folderId === f.id;
|
||||
};
|
||||
fixLayout();
|
||||
};
|
||||
|
||||
$scope.filterType = function (t) {
|
||||
resetSelected();
|
||||
$scope.selectedType = t;
|
||||
switch (t) {
|
||||
case constants.cipherType.login:
|
||||
$scope.selectedTitle = 'Login';
|
||||
$scope.selectedIcon = 'fa-globe';
|
||||
break;
|
||||
case constants.cipherType.card:
|
||||
$scope.selectedTitle = 'Card';
|
||||
$scope.selectedIcon = 'fa-credit-card';
|
||||
break;
|
||||
case constants.cipherType.identity:
|
||||
$scope.selectedTitle = 'Identity';
|
||||
$scope.selectedIcon = 'fa-id-card-o';
|
||||
break;
|
||||
case constants.cipherType.secureNote:
|
||||
$scope.selectedTitle = 'Secure Note';
|
||||
$scope.selectedIcon = 'fa-sticky-note-o';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
$scope.filter = function (c) {
|
||||
return c.type === t;
|
||||
};
|
||||
fixLayout();
|
||||
};
|
||||
|
||||
$scope.filterFavorites = function () {
|
||||
resetSelected();
|
||||
$scope.selectedFavorites = true;
|
||||
$scope.selectedTitle = 'Favorites';
|
||||
$scope.selectedIcon = 'fa-star';
|
||||
$scope.filter = function (c) {
|
||||
return !!c.favorite;
|
||||
};
|
||||
fixLayout();
|
||||
};
|
||||
|
||||
$scope.filterAll = function () {
|
||||
resetSelected();
|
||||
$scope.selectedAll = true;
|
||||
$scope.selectedTitle = 'All';
|
||||
$scope.selectedIcon = 'fa-th';
|
||||
$scope.filter = null;
|
||||
fixLayout();
|
||||
};
|
||||
|
||||
function resetSelected() {
|
||||
$scope.selectedFolder = undefined;
|
||||
$scope.selectedCollection = undefined;
|
||||
$scope.selectedType = undefined;
|
||||
$scope.selectedFavorites = false;
|
||||
$scope.selectedAll = false;
|
||||
}
|
||||
|
||||
function fixLayout() {
|
||||
if ($.AdminLTE && $.AdminLTE.layout) {
|
||||
$timeout(function () {
|
||||
$.AdminLTE.layout.fix();
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
$scope.clearFilters = function () {
|
||||
$scope.folderIdFilter = undefined;
|
||||
|
||||
if ($.AdminLTE && $.AdminLTE.layout) {
|
||||
$timeout(function () {
|
||||
$.AdminLTE.layout.fix();
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.folderFilter = function (folder) {
|
||||
return $scope.folderIdFilter === undefined || folder.id === $scope.folderIdFilter;
|
||||
$scope.cipherFilter = function () {
|
||||
return function (cipher) {
|
||||
return !$scope.filter || $scope.filter(cipher);
|
||||
};
|
||||
};
|
||||
|
||||
$scope.unselectAll = function () {
|
||||
selectAll(false);
|
||||
};
|
||||
|
||||
$scope.selectFolder = function (folder, $event) {
|
||||
var checkbox = $($event.currentTarget).closest('.box').find('input[name="loginSelection"]');
|
||||
checkbox.prop('checked', true);
|
||||
$scope.selectAll = function () {
|
||||
selectAll(true);
|
||||
};
|
||||
|
||||
$scope.select = function ($event) {
|
||||
var checkbox = $($event.currentTarget).closest('tr').find('input[name="loginSelection"]');
|
||||
var checkbox = $($event.currentTarget).closest('tr').find('input[name="cipherSelection"]');
|
||||
checkbox.prop('checked', !checkbox.prop('checked'));
|
||||
};
|
||||
|
||||
@@ -380,18 +443,18 @@
|
||||
return self.indexOf(value) === index;
|
||||
}
|
||||
|
||||
function getSelectedLogins() {
|
||||
return $('input[name="loginSelection"]:checked').map(function () {
|
||||
function getSelectedCiphers() {
|
||||
return $('input[name="cipherSelection"]:checked').map(function () {
|
||||
return $(this).val();
|
||||
}).get().filter(distinct);
|
||||
}
|
||||
|
||||
function selectAll(select) {
|
||||
$('input[name="loginSelection"]').prop('checked', select);
|
||||
$('input[name="cipherSelection"]').prop('checked', select);
|
||||
}
|
||||
|
||||
$scope.bulkMove = function () {
|
||||
var ids = getSelectedLogins();
|
||||
var ids = getSelectedCiphers();
|
||||
if (ids.length === 0) {
|
||||
alert('You have not selected anything.');
|
||||
return;
|
||||
@@ -399,8 +462,8 @@
|
||||
|
||||
var modal = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/vault/views/vaultMoveLogins.html',
|
||||
controller: 'vaultMoveLoginsController',
|
||||
templateUrl: 'app/vault/views/vaultMoveCiphers.html',
|
||||
controller: 'vaultMoveCiphersController',
|
||||
size: 'sm',
|
||||
resolve: {
|
||||
ids: function () { return ids; }
|
||||
@@ -409,58 +472,59 @@
|
||||
|
||||
modal.result.then(function (folderId) {
|
||||
for (var i = 0; i < ids.length; i++) {
|
||||
var login = $filter('filter')($rootScope.vaultLogins, { id: ids[i] });
|
||||
if (login.length) {
|
||||
login[0].folderId = folderId;
|
||||
var cipher = $filter('filter')($rootScope.vaultCiphers, { id: ids[i] });
|
||||
if (cipher.length) {
|
||||
cipher[0].folderId = folderId;
|
||||
}
|
||||
}
|
||||
|
||||
selectAll(false);
|
||||
sortScopedLoginData();
|
||||
sortScopedCipherData();
|
||||
toastr.success('Items have been moved!');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.bulkDelete = function () {
|
||||
var ids = getSelectedLogins();
|
||||
var ids = getSelectedCiphers();
|
||||
if (ids.length === 0) {
|
||||
alert('You have not selected anything.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('Are you sure you want to delete the selected logins (total: ' + ids.length + ')?')) {
|
||||
if (!confirm('Are you sure you want to delete the selected items (total: ' + ids.length + ')?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.bulkActionLoading = true;
|
||||
$scope.actionLoading = true;
|
||||
apiService.ciphers.delMany({ ids: ids }, function () {
|
||||
$analytics.eventTrack('Bulk Deleted Logins');
|
||||
$analytics.eventTrack('Bulk Deleted Items');
|
||||
|
||||
for (var i = 0; i < ids.length; i++) {
|
||||
var login = $filter('filter')($rootScope.vaultLogins, { id: ids[i] });
|
||||
if (login.length && login[0].edit) {
|
||||
removeLoginFromScopes(login[0]);
|
||||
var cipher = $filter('filter')($rootScope.vaultCiphers, { id: ids[i] });
|
||||
if (cipher.length && cipher[0].edit) {
|
||||
removeCipherFromScopes(cipher[0]);
|
||||
}
|
||||
}
|
||||
|
||||
selectAll(false);
|
||||
$scope.bulkActionLoading = false;
|
||||
$scope.actionLoading = false;
|
||||
toastr.success('Items have been deleted!');
|
||||
}, function () {
|
||||
toastr.error('An error occurred.');
|
||||
$scope.bulkActionLoading = false;
|
||||
}, function (e) {
|
||||
var errors = validationService.parseErrors(e);
|
||||
toastr.error(errors.length ? errors[0] : 'An error occurred.');
|
||||
$scope.actionLoading = false;
|
||||
});
|
||||
};
|
||||
|
||||
function removeLoginFromScopes(login) {
|
||||
var index = $rootScope.vaultLogins.indexOf(login);
|
||||
function removeCipherFromScopes(cipher) {
|
||||
var index = $rootScope.vaultCiphers.indexOf(cipher);
|
||||
if (index > -1) {
|
||||
$rootScope.vaultLogins.splice(index, 1);
|
||||
$rootScope.vaultCiphers.splice(index, 1);
|
||||
}
|
||||
|
||||
index = $scope.logins.indexOf(login);
|
||||
index = $scope.ciphers.indexOf(cipher);
|
||||
if (index > -1) {
|
||||
$scope.logins.splice(index, 1);
|
||||
$scope.ciphers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
180
src/app/vault/vaultEditCipherController.js
Normal file
180
src/app/vault/vaultEditCipherController.js
Normal file
@@ -0,0 +1,180 @@
|
||||
angular
|
||||
.module('bit.vault')
|
||||
|
||||
.controller('vaultEditCipherController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService,
|
||||
passwordService, cipherId, $analytics, $rootScope, authService, $uibModal, constants, $filter) {
|
||||
$analytics.eventTrack('vaultEditCipherController', { category: 'Modal' });
|
||||
$scope.folders = $rootScope.vaultFolders;
|
||||
$scope.cipher = {};
|
||||
$scope.readOnly = false;
|
||||
$scope.constants = constants;
|
||||
|
||||
authService.getUserProfile().then(function (profile) {
|
||||
$scope.useTotp = profile.premium;
|
||||
return apiService.ciphers.get({ id: cipherId }).$promise;
|
||||
}).then(function (cipher) {
|
||||
$scope.cipher = cipherService.decryptCipher(cipher);
|
||||
$scope.readOnly = !$scope.cipher.edit;
|
||||
$scope.useTotp = $scope.useTotp || $scope.cipher.organizationUseTotp;
|
||||
setUriMatchValues();
|
||||
});
|
||||
|
||||
$scope.save = function (model) {
|
||||
if ($scope.readOnly) {
|
||||
$scope.savePromise = apiService.ciphers.putPartial({ id: cipherId }, {
|
||||
folderId: model.folderId,
|
||||
favorite: model.favorite
|
||||
}, function (response) {
|
||||
$analytics.eventTrack('Partially Edited Cipher');
|
||||
$uibModalInstance.close({
|
||||
action: 'partialEdit',
|
||||
data: {
|
||||
id: cipherId,
|
||||
favorite: model.favorite,
|
||||
folderId: model.folderId && model.folderId !== '' ? model.folderId : null
|
||||
}
|
||||
});
|
||||
}).$promise;
|
||||
}
|
||||
else {
|
||||
var cipher = cipherService.encryptCipher(model, $scope.cipher.type);
|
||||
$scope.savePromise = apiService.ciphers.put({ id: cipherId }, cipher, function (cipherResponse) {
|
||||
$analytics.eventTrack('Edited Cipher');
|
||||
var decCipher = cipherService.decryptCipherPreview(cipherResponse);
|
||||
$uibModalInstance.close({
|
||||
action: 'edit',
|
||||
data: decCipher
|
||||
});
|
||||
}).$promise;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.generatePassword = function () {
|
||||
if (!$scope.cipher.login.password || confirm('Are you sure you want to overwrite the current password?')) {
|
||||
$analytics.eventTrack('Generated Password From Edit');
|
||||
$scope.cipher.login.password = passwordService.generatePassword({ length: 14, special: true });
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addUri = function () {
|
||||
if (!$scope.cipher.login) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$scope.cipher.login.uris) {
|
||||
$scope.cipher.login.uris = [];
|
||||
}
|
||||
|
||||
$scope.cipher.login.uris.push({
|
||||
uri: null,
|
||||
match: null,
|
||||
matchValue: null
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeUri = function (uri) {
|
||||
if (!$scope.cipher.login || !$scope.cipher.login.uris) {
|
||||
return;
|
||||
}
|
||||
|
||||
var index = $scope.cipher.login.uris.indexOf(uri);
|
||||
if (index > -1) {
|
||||
$scope.cipher.login.uris.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.uriMatchChanged = function (uri) {
|
||||
if ((!uri.matchValue && uri.matchValue !== 0) || uri.matchValue === '') {
|
||||
uri.match = null;
|
||||
}
|
||||
else {
|
||||
uri.match = parseInt(uri.matchValue);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addField = function () {
|
||||
if (!$scope.cipher.fields) {
|
||||
$scope.cipher.fields = [];
|
||||
}
|
||||
|
||||
$scope.cipher.fields.push({
|
||||
type: constants.fieldType.text.toString(),
|
||||
name: null,
|
||||
value: null
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeField = function (field) {
|
||||
var index = $scope.cipher.fields.indexOf(field);
|
||||
if (index > -1) {
|
||||
$scope.cipher.fields.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.toggleFavorite = function () {
|
||||
$scope.cipher.favorite = !$scope.cipher.favorite;
|
||||
};
|
||||
|
||||
$scope.clipboardSuccess = function (e) {
|
||||
e.clearSelection();
|
||||
selectPassword(e);
|
||||
};
|
||||
|
||||
$scope.clipboardError = function (e, password) {
|
||||
if (password) {
|
||||
selectPassword(e);
|
||||
}
|
||||
alert('Your web browser does not support easy clipboard copying. Copy it manually instead.');
|
||||
};
|
||||
|
||||
$scope.folderSort = function (item) {
|
||||
if (!item.id) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return item.name.toLowerCase();
|
||||
};
|
||||
|
||||
function selectPassword(e) {
|
||||
var target = $(e.trigger).parent().prev();
|
||||
if (target.attr('type') === 'text') {
|
||||
target.select();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.delete = function () {
|
||||
if (!confirm('Are you sure you want to delete this item (' + $scope.cipher.name + ')?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
apiService.ciphers.del({ id: $scope.cipher.id }, function () {
|
||||
$analytics.eventTrack('Deleted Cipher From Edit');
|
||||
$uibModalInstance.close({
|
||||
action: 'delete',
|
||||
data: $scope.cipher.id
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
|
||||
$scope.showUpgrade = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/views/premiumRequired.html',
|
||||
controller: 'premiumRequiredController'
|
||||
});
|
||||
};
|
||||
|
||||
function setUriMatchValues() {
|
||||
if ($scope.cipher.login && $scope.cipher.login.uris) {
|
||||
for (var i = 0; i < $scope.cipher.login.uris.length; i++) {
|
||||
$scope.cipher.login.uris[i].matchValue =
|
||||
$scope.cipher.login.uris[i].match || $scope.cipher.login.uris[i].match === 0 ?
|
||||
$scope.cipher.login.uris[i].match.toString() : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,132 +0,0 @@
|
||||
angular
|
||||
.module('bit.vault')
|
||||
|
||||
.controller('vaultEditLoginController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService,
|
||||
passwordService, loginId, $analytics, $rootScope, authService, $uibModal) {
|
||||
$analytics.eventTrack('vaultEditLoginController', { category: 'Modal' });
|
||||
$scope.folders = $rootScope.vaultFolders;
|
||||
$scope.login = {};
|
||||
$scope.readOnly = false;
|
||||
|
||||
authService.getUserProfile().then(function (profile) {
|
||||
$scope.useTotp = profile.premium;
|
||||
return apiService.ciphers.get({ id: loginId }).$promise;
|
||||
}).then(function (login) {
|
||||
$scope.login = cipherService.decryptLogin(login);
|
||||
$scope.readOnly = !$scope.login.edit;
|
||||
$scope.useTotp = $scope.useTotp || $scope.login.organizationUseTotp;
|
||||
});
|
||||
|
||||
$scope.save = function (model) {
|
||||
if ($scope.readOnly) {
|
||||
$scope.savePromise = apiService.ciphers.putPartial({ id: loginId }, {
|
||||
folderId: model.folderId,
|
||||
favorite: model.favorite
|
||||
}, function (response) {
|
||||
$analytics.eventTrack('Partially Edited Login');
|
||||
$uibModalInstance.close({
|
||||
action: 'partialEdit',
|
||||
data: {
|
||||
id: loginId,
|
||||
favorite: model.favorite,
|
||||
folderId: model.folderId && model.folderId !== '' ? model.folderId : null
|
||||
}
|
||||
});
|
||||
}).$promise;
|
||||
}
|
||||
else {
|
||||
var login = cipherService.encryptLogin(model);
|
||||
$scope.savePromise = apiService.ciphers.put({ id: loginId }, login, function (loginResponse) {
|
||||
$analytics.eventTrack('Edited Login');
|
||||
var decLogin = cipherService.decryptLogin(loginResponse);
|
||||
$uibModalInstance.close({
|
||||
action: 'edit',
|
||||
data: decLogin
|
||||
});
|
||||
}).$promise;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.generatePassword = function () {
|
||||
if (!$scope.login.password || confirm('Are you sure you want to overwrite the current password?')) {
|
||||
$analytics.eventTrack('Generated Password From Edit');
|
||||
$scope.login.password = passwordService.generatePassword({ length: 12, special: true });
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addField = function () {
|
||||
if (!$scope.login.fields) {
|
||||
$scope.login.fields = [];
|
||||
}
|
||||
|
||||
$scope.login.fields.push({
|
||||
type: '0',
|
||||
name: null,
|
||||
value: null
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeField = function (field) {
|
||||
var index = $scope.login.fields.indexOf(field);
|
||||
if (index > -1) {
|
||||
$scope.login.fields.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.toggleFavorite = function () {
|
||||
$scope.login.favorite = !$scope.login.favorite;
|
||||
};
|
||||
|
||||
$scope.clipboardSuccess = function (e) {
|
||||
e.clearSelection();
|
||||
selectPassword(e);
|
||||
};
|
||||
|
||||
$scope.clipboardError = function (e, password) {
|
||||
if (password) {
|
||||
selectPassword(e);
|
||||
}
|
||||
alert('Your web browser does not support easy clipboard copying. Copy it manually instead.');
|
||||
};
|
||||
|
||||
$scope.folderSort = function (item) {
|
||||
if (!item.id) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return item.name.toLowerCase();
|
||||
};
|
||||
|
||||
function selectPassword(e) {
|
||||
var target = $(e.trigger).parent().prev();
|
||||
if (target.attr('type') === 'text') {
|
||||
target.select();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.delete = function () {
|
||||
if (!confirm('Are you sure you want to delete this login (' + $scope.login.name + ')?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
apiService.ciphers.del({ id: $scope.login.id }, function () {
|
||||
$analytics.eventTrack('Deleted Login From Edit');
|
||||
$uibModalInstance.close({
|
||||
action: 'delete',
|
||||
data: $scope.login.id
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
|
||||
$scope.showUpgrade = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/views/premiumRequired.html',
|
||||
controller: 'premiumRequiredController'
|
||||
});
|
||||
};
|
||||
});
|
||||
@@ -1,15 +1,15 @@
|
||||
angular
|
||||
.module('bit.vault')
|
||||
|
||||
.controller('vaultMoveLoginsController', function ($scope, apiService, $uibModalInstance, ids, $analytics,
|
||||
$rootScope) {
|
||||
$analytics.eventTrack('vaultMoveLoginsController', { category: 'Modal' });
|
||||
.controller('vaultMoveCiphersController', function ($scope, apiService, $uibModalInstance, ids, $analytics,
|
||||
$rootScope, $filter) {
|
||||
$analytics.eventTrack('vaultMoveCiphersController', { category: 'Modal' });
|
||||
$scope.folders = $rootScope.vaultFolders;
|
||||
$scope.count = ids.length;
|
||||
|
||||
$scope.save = function () {
|
||||
$scope.savePromise = apiService.ciphers.moveMany({ ids: ids, folderId: $scope.folderId }, function () {
|
||||
$analytics.eventTrack('Bulk Moved Logins');
|
||||
$analytics.eventTrack('Bulk Moved Ciphers');
|
||||
$uibModalInstance.close($scope.folderId || null);
|
||||
}).$promise;
|
||||
};
|
||||
@@ -1,11 +1,11 @@
|
||||
angular
|
||||
.module('bit.vault')
|
||||
|
||||
.controller('vaultShareLoginController', function ($scope, apiService, $uibModalInstance, authService, cipherService,
|
||||
loginId, $analytics, $state, cryptoService, $q, toastr) {
|
||||
$analytics.eventTrack('vaultShareLoginController', { category: 'Modal' });
|
||||
.controller('vaultShareCipherController', function ($scope, apiService, $uibModalInstance, authService, cipherService,
|
||||
cipherId, $analytics, $state, cryptoService, $q, toastr) {
|
||||
$analytics.eventTrack('vaultShareCipherController', { category: 'Modal' });
|
||||
$scope.model = {};
|
||||
$scope.login = {};
|
||||
$scope.cipher = {};
|
||||
$scope.collections = [];
|
||||
$scope.selectedCollections = {};
|
||||
$scope.organizations = [];
|
||||
@@ -14,13 +14,13 @@
|
||||
$scope.loading = true;
|
||||
$scope.readOnly = false;
|
||||
|
||||
apiService.ciphers.get({ id: loginId }).$promise.then(function (login) {
|
||||
$scope.readOnly = !login.Edit;
|
||||
if (login.Edit) {
|
||||
$scope.login = cipherService.decryptLogin(login);
|
||||
apiService.ciphers.get({ id: cipherId }).$promise.then(function (cipher) {
|
||||
$scope.readOnly = !cipher.Edit;
|
||||
if (cipher.Edit) {
|
||||
$scope.cipher = cipherService.decryptCipher(cipher);
|
||||
}
|
||||
|
||||
return login.Edit;
|
||||
return cipher.Edit;
|
||||
}).then(function (canEdit) {
|
||||
$scope.loading = false;
|
||||
if (!canEdit) {
|
||||
@@ -110,8 +110,9 @@
|
||||
|
||||
var errorOnUpload = false;
|
||||
var attachmentSharePromises = [];
|
||||
if ($scope.login.attachments) {
|
||||
for (var i = 0; i < $scope.login.attachments.length; i++) {
|
||||
if ($scope.cipher.attachments) {
|
||||
for (var i = 0; i < $scope.cipher.attachments.length; i++) {
|
||||
/* jshint ignore:start */
|
||||
(function (attachment) {
|
||||
var promise = cipherService.downloadAndDecryptAttachment(null, attachment, false)
|
||||
.then(function (decData) {
|
||||
@@ -127,7 +128,7 @@
|
||||
fd.append('data', blob, encFilename);
|
||||
|
||||
return apiService.ciphers.postShareAttachment({
|
||||
id: loginId,
|
||||
id: cipherId,
|
||||
attachmentId: attachment.id,
|
||||
orgId: model.organizationId
|
||||
}, fd).$promise;
|
||||
@@ -135,20 +136,22 @@
|
||||
errorOnUpload = true;
|
||||
});
|
||||
attachmentSharePromises.push(promise);
|
||||
})($scope.login.attachments[i]);
|
||||
})($scope.cipher.attachments[i]);
|
||||
/* jshint ignore:end */
|
||||
}
|
||||
}
|
||||
|
||||
var returnedCollectionIds = null;
|
||||
$scope.submitPromise = $q.all(attachmentSharePromises).then(function () {
|
||||
if (errorOnUpload) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.login.organizationId = model.organizationId;
|
||||
$scope.cipher.organizationId = model.organizationId;
|
||||
|
||||
var request = {
|
||||
collectionIds: [],
|
||||
cipher: cipherService.encryptLogin($scope.login, null, true)
|
||||
cipher: cipherService.encryptCipher($scope.cipher, $scope.cipher.type, null, true)
|
||||
};
|
||||
|
||||
for (var id in $scope.selectedCollections) {
|
||||
@@ -157,11 +160,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
return apiService.ciphers.putShare({ id: loginId }, request).$promise;
|
||||
returnedCollectionIds = request.collectionIds;
|
||||
return apiService.ciphers.putShare({ id: cipherId }, request).$promise;
|
||||
}).then(function (response) {
|
||||
$analytics.eventTrack('Shared Login');
|
||||
toastr.success('Login has been shared.');
|
||||
$uibModalInstance.close(model.organizationId);
|
||||
$analytics.eventTrack('Shared Cipher');
|
||||
toastr.success('Item has been shared.');
|
||||
$uibModalInstance.close({
|
||||
orgId: model.organizationId,
|
||||
collectionIds: returnedCollectionIds
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,233 +0,0 @@
|
||||
angular
|
||||
.module('bit.vault')
|
||||
|
||||
.controller('vaultSharedController', function ($scope, apiService, cipherService, $analytics, $q, $localStorage,
|
||||
$uibModal, $filter, $rootScope, authService, cryptoService) {
|
||||
$scope.logins = [];
|
||||
$scope.collections = [];
|
||||
$scope.loading = true;
|
||||
|
||||
$scope.$on('$viewContentLoaded', function () {
|
||||
var collectionPromise = apiService.collections.listMe({ writeOnly: false }, function (collections) {
|
||||
var decCollections = [];
|
||||
|
||||
for (var i = 0; i < collections.Data.length; i++) {
|
||||
var decCollection = cipherService.decryptCollection(collections.Data[i], null, true);
|
||||
decCollection.collapsed = $localStorage.collapsedCollections &&
|
||||
decCollection.id in $localStorage.collapsedCollections;
|
||||
decCollections.push(decCollection);
|
||||
}
|
||||
|
||||
$scope.collections = decCollections;
|
||||
}).$promise;
|
||||
|
||||
var cipherPromise = apiService.ciphers.listDetails({}, function (ciphers) {
|
||||
var decLogins = [];
|
||||
|
||||
for (var i = 0; i < ciphers.Data.length; i++) {
|
||||
if (ciphers.Data[i].Type === 1) {
|
||||
var decLogin = cipherService.decryptLoginPreview(ciphers.Data[i]);
|
||||
decLogins.push(decLogin);
|
||||
}
|
||||
}
|
||||
|
||||
if (decLogins.length) {
|
||||
$scope.collections.push({
|
||||
id: null,
|
||||
name: 'Unassigned',
|
||||
collapsed: $localStorage.collapsedCollections && 'unassigned' in $localStorage.collapsedCollections
|
||||
});
|
||||
}
|
||||
|
||||
$scope.logins = decLogins;
|
||||
}).$promise;
|
||||
|
||||
$q.all([collectionPromise, cipherPromise]).then(function () {
|
||||
$scope.loading = false;
|
||||
});
|
||||
});
|
||||
|
||||
$scope.clipboardError = function (e) {
|
||||
alert('Your web browser does not support easy clipboard copying. ' +
|
||||
'Edit the login and copy it manually instead.');
|
||||
};
|
||||
|
||||
$scope.attachments = function (login) {
|
||||
authService.getUserProfile().then(function (profile) {
|
||||
return {
|
||||
isPremium: profile.premium,
|
||||
orgUseStorage: login.organizationId && !!profile.organizations[login.organizationId].maxStorageGb
|
||||
};
|
||||
}).then(function (perms) {
|
||||
if (login.organizationId && !perms.orgUseStorage) {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/views/paidOrgRequired.html',
|
||||
controller: 'paidOrgRequiredController',
|
||||
resolve: {
|
||||
orgId: function () { return login.organizationId; }
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!login.organizationId && !perms.isPremium) {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/views/premiumRequired.html',
|
||||
controller: 'premiumRequiredController'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!login.organizationId && !cryptoService.getEncKey()) {
|
||||
toastr.error('You cannot use this feature until you update your encryption key.', 'Feature Unavailable');
|
||||
return;
|
||||
}
|
||||
|
||||
var attachmentModel = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/vault/views/vaultAttachments.html',
|
||||
controller: 'vaultAttachmentsController',
|
||||
resolve: {
|
||||
loginId: function () { return login.id; }
|
||||
}
|
||||
});
|
||||
|
||||
attachmentModel.result.then(function (hasAttachments) {
|
||||
login.hasAttachments = hasAttachments;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.filterByCollection = function (collection) {
|
||||
return function (cipher) {
|
||||
if (!cipher.collectionIds || !cipher.collectionIds.length) {
|
||||
return collection.id === null;
|
||||
}
|
||||
|
||||
return cipher.collectionIds.indexOf(collection.id) > -1;
|
||||
};
|
||||
};
|
||||
|
||||
$scope.collectionSort = function (item) {
|
||||
if (!item.id) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return item.name.toLowerCase();
|
||||
};
|
||||
|
||||
$scope.collapseExpand = function (collection) {
|
||||
if (!$localStorage.collapsedCollections) {
|
||||
$localStorage.collapsedCollections = {};
|
||||
}
|
||||
|
||||
var id = collection.id || 'unassigned';
|
||||
|
||||
if (id in $localStorage.collapsedCollections) {
|
||||
delete $localStorage.collapsedCollections[id];
|
||||
}
|
||||
else {
|
||||
$localStorage.collapsedCollections[id] = true;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.editLogin = function (login) {
|
||||
var editModel = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/vault/views/vaultEditLogin.html',
|
||||
controller: 'vaultEditLoginController',
|
||||
resolve: {
|
||||
loginId: function () { return login.id; }
|
||||
}
|
||||
});
|
||||
|
||||
editModel.result.then(function (returnVal) {
|
||||
var rootLogin = findRootLogin(login) || {};
|
||||
|
||||
if (returnVal.action === 'edit') {
|
||||
login.folderId = rootLogin.folderId = returnVal.data.folderId;
|
||||
login.name = rootLogin.name = returnVal.data.name;
|
||||
login.username = rootLogin.username = returnVal.data.username;
|
||||
login.password = rootLogin.password = returnVal.data.password;
|
||||
login.favorite = rootLogin.favorite = returnVal.data.favorite;
|
||||
}
|
||||
else if (returnVal.action === 'partialEdit') {
|
||||
login.folderId = rootLogin.folderId = returnVal.data.folderId;
|
||||
login.favorite = rootLogin.favorite = returnVal.data.favorite;
|
||||
}
|
||||
else if (returnVal.action === 'delete') {
|
||||
var index = $scope.logins.indexOf(login);
|
||||
if (index > -1) {
|
||||
$scope.logins.splice(index, 1);
|
||||
}
|
||||
|
||||
removeRootLogin(rootLogin);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.editCollections = function (login) {
|
||||
var modal = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/vault/views/vaultLoginCollections.html',
|
||||
controller: 'vaultLoginCollectionsController',
|
||||
resolve: {
|
||||
loginId: function () { return login.id; }
|
||||
}
|
||||
});
|
||||
|
||||
modal.result.then(function (response) {
|
||||
if (response.collectionIds) {
|
||||
login.collectionIds = response.collectionIds;
|
||||
// TODO: if there are no collectionIds now, it is possible that the user no longer has access to this login
|
||||
// which means it should be removed by calling removeRootLogin(findRootLogin(login))
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeLogin = function (login, collection) {
|
||||
if (!confirm('Are you sure you want to remove this login (' + login.name + ') from the ' +
|
||||
'collection (' + collection.name + ') ?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var request = {
|
||||
collectionIds: []
|
||||
};
|
||||
|
||||
for (var i = 0; i < login.collectionIds.length; i++) {
|
||||
if (login.collectionIds[i] !== collection.id) {
|
||||
request.collectionIds.push(login.collectionIds[i]);
|
||||
}
|
||||
}
|
||||
|
||||
apiService.ciphers.putCollections({ id: login.id }, request).$promise.then(function (response) {
|
||||
$analytics.eventTrack('Removed From Collection');
|
||||
login.collectionIds = request.collectionIds;
|
||||
// TODO: if there are no collectionIds now, it is possible that the user no longer has access to this login
|
||||
// which means it should be removed by calling removeRootLogin(findRootLogin(login))
|
||||
});
|
||||
};
|
||||
|
||||
function findRootLogin(login) {
|
||||
if ($rootScope.vaultLogins) {
|
||||
var rootLogins = $filter('filter')($rootScope.vaultLogins, { id: login.id });
|
||||
if (rootLogins && rootLogins.length) {
|
||||
return rootLogins[0];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function removeRootLogin(rootLogin) {
|
||||
if (rootLogin && rootLogin.id) {
|
||||
var index = $rootScope.vaultLogins.indexOf(rootLogin);
|
||||
if (index > -1) {
|
||||
$rootScope.vaultLogins.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,9 +1,8 @@
|
||||
<section class="content-header">
|
||||
<div class="btn-group pull-right">
|
||||
<button type="button" class="btn btn-link dropdown-toggle" data-toggle="dropdown"
|
||||
ng-disabled="bulkActionLoading">
|
||||
<i class="fa fa-refresh fa-spin" ng-show="bulkActionLoading"></i>
|
||||
Bulk Actions <span class="caret"></span>
|
||||
<button type="button" class="btn btn-link dropdown-toggle" data-toggle="dropdown" ng-disabled="actionLoading">
|
||||
<i class="fa fa-refresh fa-spin" ng-show="actionLoading"></i>
|
||||
Actions <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
@@ -17,6 +16,11 @@
|
||||
</a>
|
||||
</li>
|
||||
<li role="separator" class="divider"></li>
|
||||
<li>
|
||||
<a href="#" stop-click ng-click="selectAll()">
|
||||
<i class="fa fa-fw fa-check-square-o"></i> Select All
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" stop-click ng-click="unselectAll()">
|
||||
<i class="fa fa-fw fa-minus-square-o"></i> Unselect All
|
||||
@@ -26,165 +30,59 @@
|
||||
</div>
|
||||
<h1>
|
||||
My Vault
|
||||
<small>
|
||||
<span ng-pluralize
|
||||
count="vaultFolders.length > 0 ? vaultFolders.length - 1 : 0"
|
||||
<small class="visible-md-inline visible-lg-inline">
|
||||
<span ng-pluralize count="vaultFolders.length > 0 ? vaultFolders.length - 1 : 0"
|
||||
when="{'1': '{} folder', 'other': '{} folders'}"></span>,
|
||||
<span ng-pluralize count="logins.length" when="{'1': '{} login', 'other': '{} logins'}"></span>
|
||||
<span ng-pluralize count="vaultCollections.length"
|
||||
when="{'1': '{} collection', 'other': '{} collections'}"></span>, &
|
||||
<span ng-pluralize count="ciphers.length" when="{'1': '{} item', 'other': '{} items'}"></span>
|
||||
</small>
|
||||
</h1>
|
||||
</section>
|
||||
<section class="content">
|
||||
<div ng-show="loading && !vaultFolders.length">
|
||||
<div ng-show="loadingCiphers">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
<div class="box box-primary" ng-class="{'collapsed-box': favoriteCollapsed}" style="margin-bottom: 40px;"
|
||||
ng-show="vaultFolders.length && folderIdFilter === undefined && (!main.searchVaultText || favoriteLogins.length)">
|
||||
<div class="box" ng-show="!loadingCiphers">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">
|
||||
<i class="fa fa-star"></i>
|
||||
Favorites
|
||||
<small ng-pluralize count="favoriteLogins.length" when="{'1': '{} login', 'other': '{} logins'}"></small>
|
||||
<i class="fa {{selectedIcon}}"></i>
|
||||
{{selectedFolder ? selectedFolder.name : selectedCollection ? selectedCollection.name : selectedTitle}}
|
||||
<small ng-pluralize count="filteredCiphers.length" when="{'1': '{} item', 'other': '{} items'}"></small>
|
||||
</h3>
|
||||
<div class="box-tools">
|
||||
<div class="box-tools" ng-if="selectedFolder && selectedFolder.id">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-box-tool dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-cog"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li>
|
||||
<a href="#" stop-click ng-click="addLogin(null, true)">
|
||||
<i class="fa fa-fw fa-plus-circle"></i> Add Login
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button type="button" class="btn btn-box-tool" data-widget="collapse" title="Collapse/Expand"
|
||||
ng-click="collapseExpand(null, true)">
|
||||
<i class="fa" ng-class="{'fa-minus': !favoriteCollapsed, 'fa-plus': favoriteCollapsed}"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-body" ng-class="{'no-padding': favoriteLogins.length}">
|
||||
<div ng-show="!favoriteLogins.length">
|
||||
<p>No favorite logins.</p>
|
||||
<button type="button" ng-click="addLogin(null, true)" class="btn btn-default btn-flat">Add a Login</button>
|
||||
</div>
|
||||
<div class="table-responsive" ng-show="favoriteLogins.length">
|
||||
<table class="table table-striped table-hover table-vmiddle">
|
||||
<tbody>
|
||||
<tr ng-repeat="login in favoriteLogins = (logins | filter: { favorite: true } |
|
||||
filter: (main.searchVaultText || '')) track by login.id">
|
||||
<td style="width: 70px;">
|
||||
<div class="btn-group" data-append-to="body">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-cog"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="#" stop-click ng-click="editLogin(login)">
|
||||
<i class="fa fa-fw fa-pencil"></i> Edit
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" stop-click ng-click="attachments(login)">
|
||||
<i class="fa fa-fw fa-paperclip"></i> Attachments
|
||||
</a>
|
||||
</li>
|
||||
<li ng-show="!login.organizationId">
|
||||
<a href="#" stop-click ng-click="share(login)">
|
||||
<i class="fa fa-fw fa-share-alt"></i> Share
|
||||
</a>
|
||||
</li>
|
||||
<li ng-show="login.organizationId && login.edit">
|
||||
<a href="#" stop-click ng-click="collections(login)">
|
||||
<i class="fa fa-fw fa-cubes"></i> Collections
|
||||
</a>
|
||||
</li>
|
||||
<li ng-show="login.password">
|
||||
<a href="#" stop-click ngclipboard ngclipboard-error="clipboardError(e)"
|
||||
data-clipboard-text="{{login.password}}">
|
||||
<i class="fa fa-fw fa-clipboard"></i> Copy Password
|
||||
</a>
|
||||
</li>
|
||||
<li ng-show="login.edit">
|
||||
<a href="#" stop-click ng-click="deleteLogin(login)" class="text-red">
|
||||
<i class="fa fa-fw fa-trash"></i> Delete
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
<td class="action-select">
|
||||
<input type="checkbox" value="{{::login.id}}" name="loginSelection" />
|
||||
</td>
|
||||
<td ng-click="select($event)">
|
||||
<a href="#" stop-click ng-click="editLogin(login)" stop-prop>{{login.name}}</a>
|
||||
<i class="fa fa-share-alt text-muted" title="Shared" ng-if="login.organizationId"
|
||||
stop-prop></i>
|
||||
<i class="fa fa-paperclip text-muted" title="Attachments" ng-if="login.hasAttachments"
|
||||
stop-prop></i><br />
|
||||
<span class="text-sm text-muted" stop-prop>{{login.username}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" ng-class="{'collapsed-box': folder.collapsed}"
|
||||
ng-repeat="folder in filteredVaultFolders = (vaultFolders | filter: folderFilter) track by folder.id"
|
||||
ng-show="vaultFolders.length && (!main.searchVaultText || folderLogins.length)">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">
|
||||
<i class="fa" ng-class="{'fa-folder-open': folder.id !== null, 'fa-folder-open-o': folder.id === null}"></i>
|
||||
{{folder.name}}
|
||||
<small ng-pluralize count="folderLogins.length" when="{'1': '{} login', 'other': '{} logins'}"></small>
|
||||
</h3>
|
||||
<div class="box-tools">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-box-tool dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-cog"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li>
|
||||
<a href="#" stop-click ng-click="addLogin(folder)">
|
||||
<i class="fa fa-fw fa-plus-circle"></i> Add Login
|
||||
</a>
|
||||
</li>
|
||||
<li ng-show="folder.id">
|
||||
<a href="#" stop-click ng-click="editFolder(folder)">
|
||||
<a href="#" stop-click ng-click="editFolder(selectedFolder)">
|
||||
<i class="fa fa-fw fa-pencil"></i> Edit Folder
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" stop-click ng-click="selectFolder(folder, $event)">
|
||||
<i class="fa fa-fw fa-check-square-o"></i> Select All
|
||||
</a>
|
||||
</li>
|
||||
<li ng-show="canDeleteFolder(folder)">
|
||||
<a href="#" stop-click ng-click="deleteFolder(folder)" class="text-red">
|
||||
<a href="#" stop-click ng-click="deleteFolder(selectedFolder)" class="text-red">
|
||||
<i class="fa fa-fw fa-trash"></i> Delete Folder
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button type="button" class="btn btn-box-tool" data-widget="collapse" title="Collapse/Expand"
|
||||
ng-click="collapseExpand(folder)">
|
||||
<i class="fa" ng-class="{'fa-minus': !folder.collapsed, 'fa-plus': folder.collapsed}"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-body" ng-class="{'no-padding': folderLogins.length}">
|
||||
<div ng-show="!folderLogins.length">
|
||||
<p>No logins in this folder.</p>
|
||||
<button type="button" ng-click="addLogin(folder)" class="btn btn-default btn-flat">Add a Login</button>
|
||||
<div class="box-body" ng-class="{'no-padding': filteredCiphers.length}">
|
||||
<div ng-show="!filteredCiphers.length">
|
||||
<p>No items to list.</p>
|
||||
<button type="button" ng-click="addCipher()" class="btn btn-default btn-flat"
|
||||
ng-if="!selectedCollection">
|
||||
Add an Item
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-responsive" ng-show="folderLogins.length">
|
||||
<div class="table-responsive" ng-show="filteredCiphers.length">
|
||||
<table class="table table-striped table-hover table-vmiddle">
|
||||
<tbody>
|
||||
<tr ng-repeat="login in folderLogins = (logins | filter: { folderId: folder.id } |
|
||||
filter: (main.searchVaultText || '')) track by login.id">
|
||||
<tr ng-repeat="cipher in filteredCiphers = (ciphers | filter: cipherFilter() |
|
||||
filter: (searchVaultText || '')) track by cipher.id">
|
||||
<td style="width: 70px;">
|
||||
<div class="btn-group" data-append-to="body">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
@@ -192,33 +90,33 @@
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="#" stop-click ng-click="editLogin(login)">
|
||||
<a href="#" stop-click ng-click="editCipher(cipher)">
|
||||
<i class="fa fa-fw fa-pencil"></i> Edit
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" stop-click ng-click="attachments(login)">
|
||||
<a href="#" stop-click ng-click="attachments(cipher)">
|
||||
<i class="fa fa-fw fa-paperclip"></i> Attachments
|
||||
</a>
|
||||
</li>
|
||||
<li ng-show="!login.organizationId">
|
||||
<a href="#" stop-click ng-click="share(login)">
|
||||
<li ng-show="!cipher.organizationId">
|
||||
<a href="#" stop-click ng-click="share(cipher)">
|
||||
<i class="fa fa-fw fa-share-alt"></i> Share
|
||||
</a>
|
||||
</li>
|
||||
<li ng-show="login.organizationId && login.edit">
|
||||
<a href="#" stop-click ng-click="collections(login)">
|
||||
<li ng-show="cipher.organizationId && cipher.edit">
|
||||
<a href="#" stop-click ng-click="editCollections(cipher)">
|
||||
<i class="fa fa-fw fa-cubes"></i> Collections
|
||||
</a>
|
||||
</li>
|
||||
<li ng-show="login.password">
|
||||
<li ng-show="cipher.meta.password">
|
||||
<a href="#" stop-click ngclipboard ngclipboard-error="clipboardError(e)"
|
||||
data-clipboard-text="{{login.password}}">
|
||||
data-clipboard-text="{{cipher.meta.password}}">
|
||||
<i class="fa fa-fw fa-clipboard"></i> Copy Password
|
||||
</a>
|
||||
</li>
|
||||
<li ng-show="login.edit">
|
||||
<a href="#" stop-click ng-click="deleteLogin(login)" class="text-red">
|
||||
<li ng-show="cipher.edit">
|
||||
<a href="#" stop-click ng-click="deleteCipher(cipher)" class="text-red">
|
||||
<i class="fa fa-fw fa-trash"></i> Delete
|
||||
</a>
|
||||
</li>
|
||||
@@ -226,17 +124,22 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="action-select" ng-click="select($event)">
|
||||
<input type="checkbox" value="{{::login.id}}" name="loginSelection" stop-prop />
|
||||
<input type="checkbox" value="{{::cipher.id}}" name="cipherSelection" stop-prop />
|
||||
</td>
|
||||
<td class="vault-icon" ng-click="select($event)">
|
||||
<i class="fa fa-fw fa-lg {{::cipher.icon}}" ng-if="!cipher.meta.image"></i>
|
||||
<img alt="" ng-if="cipher.meta.image" ng-src="{{cipher.meta.image}}"
|
||||
fallback-src="images/fa-globe.png" />
|
||||
</td>
|
||||
<td ng-click="select($event)">
|
||||
<a href="#" stop-click ng-click="editLogin(login)" stop-prop>{{login.name}}</a>
|
||||
<i class="fa fa-star text-muted" title="Favorite" ng-show="login.favorite" stop-prop></i>
|
||||
<i class="fa fa-share-alt text-muted" title="Shared" ng-show="login.organizationId"
|
||||
<a href="#" stop-click ng-click="editCipher(cipher)" stop-prop>{{cipher.name}}</a>
|
||||
<i class="fa fa-star text-muted" title="Favorite" ng-show="cipher.favorite" stop-prop></i>
|
||||
<i class="fa fa-share-alt text-muted" title="Shared" ng-show="cipher.organizationId"
|
||||
stop-prop></i>
|
||||
<i class="fa fa-paperclip text-muted" title="Attachments" ng-if="login.hasAttachments"
|
||||
<i class="fa fa-paperclip text-muted" title="Attachments" ng-if="cipher.hasAttachments"
|
||||
stop-prop></i>
|
||||
<br />
|
||||
<span class="text-sm text-muted" stop-prop>{{login.username}}</span>
|
||||
<span class="text-sm text-muted" stop-prop>{{cipher.subTitle}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -247,26 +150,86 @@
|
||||
</section>
|
||||
<aside class="control-sidebar control-sidebar-light">
|
||||
<div class="tab-content">
|
||||
<form class="search-form">
|
||||
<label for="search" class="sr-only">Search</label>
|
||||
<div class="form-group has-feedback">
|
||||
<input type="search" id="search" class="form-control" placeholder="Search my vault..."
|
||||
ng-model="searchVaultText" />
|
||||
<span class="fa fa-search form-control-feedback" aria-hidden="true"></span>
|
||||
</div>
|
||||
</form>
|
||||
<ul class="control-sidebar-menu">
|
||||
<li>
|
||||
<a href="#" stop-click ng-click="clearFilters()">
|
||||
Clear All Filters
|
||||
<li ng-class="{active: selectedAll}">
|
||||
<a href="#" stop-click ng-click="filterAll()">
|
||||
<i class="fa fa-th fa-fw"></i> All Items
|
||||
</a>
|
||||
</li>
|
||||
<li ng-class="{active: selectedFavorites}">
|
||||
<a href="#" stop-click ng-click="filterFavorites()">
|
||||
<i class="fa fa-star fa-fw"></i> Favorites
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h3 class="control-sidebar-heading">
|
||||
<i class="fa fa-folder fa-fw"></i> Folders
|
||||
</h3>
|
||||
<div ng-show="loading && !vaultFolders.length">
|
||||
<h3 class="control-sidebar-heading">Types</h3>
|
||||
<div class="control-sidebar-section">
|
||||
<ul class="control-sidebar-menu">
|
||||
<li ng-class="{active: constants.cipherType.login === selectedType}">
|
||||
<a href="#" stop-click ng-click="filterType(constants.cipherType.login)">
|
||||
<i class="fa fa-globe fa-fw"></i> Login
|
||||
</a>
|
||||
</li>
|
||||
<li ng-class="{active: constants.cipherType.card === selectedType}">
|
||||
<a href="#" stop-click ng-click="filterType(constants.cipherType.card)">
|
||||
<i class="fa fa-credit-card fa-fw"></i> Card
|
||||
</a>
|
||||
</li>
|
||||
<li ng-class="{active: constants.cipherType.identity === selectedType}">
|
||||
<a href="#" stop-click ng-click="filterType(constants.cipherType.identity)">
|
||||
<i class="fa fa-id-card-o fa-fw"></i> Identity
|
||||
</a>
|
||||
</li>
|
||||
<li ng-class="{active: constants.cipherType.secureNote === selectedType}">
|
||||
<a href="#" stop-click ng-click="filterType(constants.cipherType.secureNote)">
|
||||
<i class="fa fa-sticky-note-o fa-fw"></i> Secure Note
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h3 class="control-sidebar-heading">Folders</h3>
|
||||
<div ng-show="loadingGroupings">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
<ul class="control-sidebar-menu" ng-show="!loading && vaultFolders.length">
|
||||
<li ng-repeat="folder in vaultFolders track by folder.id">
|
||||
<a href="#" stop-click ng-click="filterFolder(folder)">
|
||||
<i class="fa fa-check" ng-if="folder.id === folderIdFilter"></i>
|
||||
{{folder.name}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="control-sidebar-section" ng-show="!loadingGroupings">
|
||||
<ul class="control-sidebar-menu">
|
||||
<li ng-repeat="folder in vaultFolders | orderBy: [groupingSort] track by folder.id"
|
||||
ng-class="{active: selectedFolder && folder.id === selectedFolder.id}">
|
||||
<a href="#" stop-click ng-click="filterFolder(folder)">
|
||||
<i class="fa fa-caret-right fa-fw"></i>
|
||||
{{folder.name}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h3 class="control-sidebar-heading">Collections</h3>
|
||||
<div ng-show="loadingGroupings">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
<div ng-show="!loadingGroupings && !vaultCollections.length">
|
||||
<p>No collections are being shared with you. <i class="fa fa-frown-o"></i></p>
|
||||
<a ui-sref="backend.user.settingsCreateOrg" class="btn btn-default btn-lint">
|
||||
Create an Organization
|
||||
</a>
|
||||
</div>
|
||||
<div class="control-sidebar-section" ng-show="!loadingGroupings && vaultCollections.length">
|
||||
<ul class="control-sidebar-menu">
|
||||
<li ng-repeat="collection in vaultCollections | orderBy: [groupingSort] track by collection.id"
|
||||
ng-class="{active: selectedCollection && collection.id === selectedCollection.id}">
|
||||
<a href="#" stop-click ng-click="filterCollection(collection)">
|
||||
<i class="fa fa-caret-right fa-fw"></i>
|
||||
{{collection.name}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
616
src/app/vault/views/vaultAddCipher.html
Normal file
616
src/app/vault/views/vaultAddCipher.html
Normal file
@@ -0,0 +1,616 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title"><i class="fa fa-plus-circle"></i> Add New Item</h4>
|
||||
</div>
|
||||
<form name="form" ng-submit="form.$valid && save()" api-form="savePromise" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
|
||||
<h4>Errors have occurred</h4>
|
||||
<ul>
|
||||
<li ng-repeat="e in form.$errors">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group">
|
||||
<label for="type">What type of item is this?</label>
|
||||
<select id="type" name="Type" ng-model="selectedType" class="form-control" ng-change="typeChanged()">
|
||||
<option value="{{constants.cipherType.login}}">Login</option>
|
||||
<option value="{{constants.cipherType.card}}">Card</option>
|
||||
<option value="{{constants.cipherType.identity}}">Identity</option>
|
||||
<option value="{{constants.cipherType.secureNote}}">Secure Note</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="name">Name</label> <span>*</span>
|
||||
<input type="text" id="name" name="Name" ng-model="cipher.name" class="form-control" required api-field />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6" ng-if="!hideFolders">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="folder">Folder</label>
|
||||
<select id="folder" name="FolderId" ng-model="cipher.folderId" class="form-control" api-field>
|
||||
<option ng-repeat="folder in folders | orderBy: folderSort" value="{{folder.id}}">{{folder.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="cipher.type === constants.cipherType.login">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="username">Username</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="username" name="Login.Username" ng-model="cipher.login.username"
|
||||
class="form-control monospaced" ng-readonly="readOnly" api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy Username" tooltip-placement="left">
|
||||
<button class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)"
|
||||
data-clipboard-target="#username">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group" show-errors style="margin-bottom: 5px;">
|
||||
<div class="pull-right password-options">
|
||||
<i class="fa fa-lg fa-refresh" uib-tooltip="Generate Password" tooltip-placement="left"
|
||||
ng-click="generatePassword()" ng-show="!readOnly"></i>
|
||||
<i class="fa fa-lg fa-eye" uib-tooltip="Toggle Password" tooltip-placement="left"
|
||||
password-viewer="#password"></i>
|
||||
</div>
|
||||
<label for="password">Password</label>
|
||||
<div class="input-group">
|
||||
<input type="password" id="password" name="Login.Password" ng-model="cipher.login.password"
|
||||
class="form-control monospaced" ng-readonly="readOnly" api-field
|
||||
autocomplete="new-password" />
|
||||
<span class="input-group-btn" uib-tooltip="Copy Password" tooltip-placement="left">
|
||||
<button class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-success="clipboardSuccess(e)" ngclipboard-error="clipboardError(e, true)"
|
||||
data-clipboard-text="{{cipher.login.password}}">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div password-meter="cipher.login.password" password-meter-username="cipher.login.username"
|
||||
outer-class="xs" class="password-meter"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="totp">Authenticator Key (TOTP)</label>
|
||||
<input type="text" id="totp" name="Login.Totp" ng-model="cipher.login.totp"
|
||||
class="form-control monospaced" ng-readonly="readOnly" api-field />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 totp-col">
|
||||
<div totp="cipher.login.totp" id="verification-code" ng-if="useTotp"></div>
|
||||
<div ng-if="!useTotp">
|
||||
<a href="#" stop-click ng-click="showUpgrade()"><img src="images/totp-countdown.png" alt="" /></a>
|
||||
<span class="label label-info clickable" ng-click="showUpgrade()">
|
||||
{{fromOrg ? 'UPGRADE' : 'PREMIUM'}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-repeat="u in cipher.login.uris" ng-if="cipher.login.uris && cipher.login.uris.length">
|
||||
<div class="row">
|
||||
<div class="col-sm-7">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="uri{{$index}}">URI {{$index + 1}}</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="uri{{$index}}" name="Login.Uris[{{$index}}].Uri"
|
||||
ng-model="u.uri" class="form-control"
|
||||
placeholder="http://..." ng-readonly="readOnly" api-field />
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default btn-flat" type="button" uib-tooltip="Copy URI"
|
||||
tooltip-placement="left" ngclipboard ngclipboard-error="clipboardError(e)"
|
||||
data-clipboard-target="#uri{{$index}}">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
<a href="{{u.uri}}" target="_blank" class="btn btn-default btn-flat"
|
||||
uib-tooltip="Go To Website" tooltip-placement="left">
|
||||
<i class="fa fa-share"></i>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="form-group">
|
||||
<label for="uri_match_{{$index}}">Match Detection</label>
|
||||
<select id="uri_match_{{$index}}" name="Login.Uris[{{$index}}].Match"
|
||||
class="form-control" ng-model="u.matchValue" ng-change="uriMatchChanged(u)">
|
||||
<option value="">Default</option>
|
||||
<option value="0">Base domain</option>
|
||||
<option value="1">Host</option>
|
||||
<option value="2">Starts with</option>
|
||||
<option value="4">Regular Expression</option>
|
||||
<option value="3">Exact</option>
|
||||
<option value="5">Never</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<br class="hidden-xs" />
|
||||
<a href="#" ng-click="removeUri(u)" stop-click>
|
||||
<i class="fa fa-window-close-o fa-lg"></i>
|
||||
<span class="visible-xs-inline">Remove URI</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="visible-xs-block" />
|
||||
</div>
|
||||
<a href="#" ng-click="addUri()" stop-click>
|
||||
<i class="fa fa-plus-circle"></i> New URI
|
||||
</a>
|
||||
<br /><br />
|
||||
</div>
|
||||
<div ng-if="cipher.type === constants.cipherType.card">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="name">Cardholder Name</label>
|
||||
<input type="text" id="cardholderName" name="Card.CarholderName" ng-readonly="readOnly"
|
||||
ng-model="cipher.card.cardholderName" class="form-control" api-field />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="cardBrand">Brand</label>
|
||||
<select id="cardBrand" name="Card.Brand" ng-model="cipher.card.brand" class="form-control"
|
||||
ng-readonly="readOnly" api-field>
|
||||
<option value="">-- Select --</option>
|
||||
<option value="Visa">Visa</option>
|
||||
<option value="Mastercard">Mastercard</option>
|
||||
<option value="Amex">American Express</option>
|
||||
<option value="Discover">Discover</option>
|
||||
<option value="Diners Club">Diners Club</option>
|
||||
<option value="JCB">JCB</option>
|
||||
<option value="Maestro">Maestro</option>
|
||||
<option value="UnionPay">UnionPay</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="cardNumber">Number</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="cardNumber" name="Card.Number" ng-model="cipher.card.number"
|
||||
class="form-control" ng-readonly="readOnly" api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy Number" tooltip-placement="left">
|
||||
<button class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)" data-clipboard-target="#cardNumber">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="cardExpMonth">Expiration Month</label>
|
||||
<select id="cardExpMonth" name="Card.ExpMonth" ng-model="cipher.card.expMonth"
|
||||
ng-readonly="readOnly" class="form-control" api-field>
|
||||
<option value="">-- Select --</option>
|
||||
<option value="1">01 - January</option>
|
||||
<option value="2">02 - February</option>
|
||||
<option value="3">03 - March</option>
|
||||
<option value="4">04 - April</option>
|
||||
<option value="5">05 - May</option>
|
||||
<option value="6">06 - June</option>
|
||||
<option value="7">07 - July</option>
|
||||
<option value="8">08 - August</option>
|
||||
<option value="9">09 - September</option>
|
||||
<option value="10">10 - October</option>
|
||||
<option value="11">11 - November</option>
|
||||
<option value="12">12 - December</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="cardExpYear">Expiration Year</label>
|
||||
<input type="text" id="cardExpYear" name="Card.ExpYear" ng-readonly="readOnly"
|
||||
ng-model="cipher.card.expYear" class="form-control" api-field placeholder="ex. 2019" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="cardCode">Security Code</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="cardCode" name="Card.Code" ng-model="cipher.card.code"
|
||||
class="form-control" ng-readonly="readOnly" api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy Code" tooltip-placement="left">
|
||||
<button class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)" data-clipboard-target="#cardCode">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="cipher.type === constants.cipherType.identity">
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="identityTitle">Title</label>
|
||||
<select id="identityTitle" name="Identity.Title" ng-model="cipher.identity.title" class="form-control"
|
||||
ng-readonly="readOnly" api-field>
|
||||
<option value="">-- Select --</option>
|
||||
<option value="Mr">Mr</option>
|
||||
<option value="Mrs">Mrs</option>
|
||||
<option value="Ms">Ms</option>
|
||||
<option value="Dr">Dr</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="identityFirstName">First Name</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="identityFirstName" name="Identity.FirstName"
|
||||
ng-model="cipher.identity.firstName" class="form-control" ng-readonly="readOnly" api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy First Name" tooltip-placement="left">
|
||||
<button class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityFirstName">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="identityMiddleName">Middle Name</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="identityMiddleName" name="Identity.FirstName"
|
||||
ng-model="cipher.identity.middleName" class="form-control" ng-readonly="readOnly" api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy Middle Name" tooltip-placement="left">
|
||||
<button class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityMiddleName">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="identityLastName">Last Name</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="identityLastName" name="Identity.LastName"
|
||||
ng-model="cipher.identity.lastName" class="form-control" ng-readonly="readOnly" api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy Last Name" tooltip-placement="left">
|
||||
<button class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityLastName">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="identityUsername">Username</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="identityUsername" name="Identity.Username"
|
||||
ng-model="cipher.identity.username" class="form-control" ng-readonly="readOnly" api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy Username" tooltip-placement="left">
|
||||
<button class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityUsername">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="identityCompany">Company</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="identityCompany" name="Identity.Company"
|
||||
ng-model="cipher.identity.company" class="form-control" ng-readonly="readOnly" api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy Company" tooltip-placement="left">
|
||||
<button class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityCompany">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="identitySSN">Social Security Number</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="identitySSN" name="Identity.SSN"
|
||||
ng-model="cipher.identity.ssn" class="form-control" ng-readonly="readOnly" api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy SSN" tooltip-placement="left">
|
||||
<button class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identitySSN">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="identityPassportNumber">Passport Number</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="identityPassportNumber" name="Identity.PassportNumber"
|
||||
ng-model="cipher.identity.passportNumber" class="form-control" ng-readonly="readOnly"
|
||||
api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy Passport Number" tooltip-placement="left">
|
||||
<button class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityPassportNumber">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="identityLicenseNumber">License Number</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="identityLicenseNumber" name="Identity.LicenseNumber"
|
||||
ng-model="cipher.identity.licenseNumber" class="form-control" ng-readonly="readOnly"
|
||||
api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy License Number" tooltip-placement="left">
|
||||
<button class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityLicenseNumber">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="identityEmail">Email</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="identityEmail" name="Identity.Email"
|
||||
ng-model="cipher.identity.email" class="form-control" ng-readonly="readOnly" api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy Email" tooltip-placement="left">
|
||||
<button class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityEmail">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="identityPhone">Phone</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="identityPhone" name="Identity.Phone"
|
||||
ng-model="cipher.identity.phone" class="form-control" ng-readonly="readOnly" api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy Phone" tooltip-placement="left">
|
||||
<button class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityPhone">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="identityAddress1">Address 1</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="identityAddress1" name="Identity.Address1"
|
||||
ng-model="cipher.identity.address1" class="form-control" ng-readonly="readOnly" api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy Address 1" tooltip-placement="left">
|
||||
<button class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityAddress1">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="identityAddress2">Address 2</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="identityAddress2" name="Identity.Address2"
|
||||
ng-model="cipher.identity.address2" class="form-control" ng-readonly="readOnly" api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy Address 2" tooltip-placement="left">
|
||||
<button class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityAddress2">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="identityAddress3">Address 3</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="identityAddress3" name="Identity.Address3"
|
||||
ng-model="cipher.identity.address3" class="form-control" ng-readonly="readOnly" api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy Address 3" tooltip-placement="left">
|
||||
<button class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityAddress3">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="identityCity">City / Town</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="identityCity" name="Identity.City"
|
||||
ng-model="cipher.identity.city" class="form-control" ng-readonly="readOnly" api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy City/Town" tooltip-placement="left">
|
||||
<button class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityCity">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="identityState">State / Province</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="identityState" name="Identity.State"
|
||||
ng-model="cipher.identity.state" class="form-control" ng-readonly="readOnly" api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy State/Province" tooltip-placement="left">
|
||||
<button class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityState">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="identityPostalCode">Zip / Postal Code</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="identityPostalCode" name="Identity.PostalCode"
|
||||
ng-model="cipher.identity.postalCode" class="form-control" ng-readonly="readOnly" api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy Zip/Postal Code" tooltip-placement="left">
|
||||
<button class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityPostalCode">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="identityCountry">Country</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="identityCountry" name="Identity.Country"
|
||||
ng-model="cipher.identity.country" class="form-control" ng-readonly="readOnly" api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy Country" tooltip-placement="left">
|
||||
<button class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)" data-clipboard-target="#identityCountry">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="cipher.type === constants.cipherType.secureNote">
|
||||
<!-- Nothing for now -->
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="notes">Notes</label>
|
||||
<textarea id="notes" name="Notes" class="form-control" ng-model="cipher.notes" api-field
|
||||
ng-class="{'big-textarea': cipher.type === constants.cipherType.secureNote}"></textarea>
|
||||
</div>
|
||||
<hr />
|
||||
<h4><i class="fa fa-list-ul"></i> Custom Fields</h4>
|
||||
<div ng-repeat="field in cipher.fields">
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<div class="form-group">
|
||||
<label for="field_name{{$index}}">Name</label>
|
||||
<input type="text" id="field_name{{$index}}" name="Field.Name{{$index}}" class="form-control" ng-model="field.name" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="form-group">
|
||||
<label for="field_type{{$index}}">Type</label>
|
||||
<select id="field_type{{$index}}" name="Field.Type{{$index}}" class="form-control" ng-model="field.type">
|
||||
<option value="0">Text</option>
|
||||
<option value="1">Hidden</option>
|
||||
<option value="2">Boolean</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<div class="form-group">
|
||||
<div class="pull-right password-options" ng-if="field.type === '1'">
|
||||
<i class="fa fa-lg fa-eye" uib-tooltip="Toggle Visibility" tooltip-placement="left"
|
||||
password-viewer="#field_value{{$index}}"></i>
|
||||
</div>
|
||||
<label for="field_value{{$index}}">Value</label>
|
||||
<div class="input-group" ng-if="field.type !== '2'">
|
||||
<input ng-attr-type="{{field.type === '0' ? 'text' : 'password'}}" id="field_value{{$index}}"
|
||||
name="Field.Value{{$index}}" class="form-control"
|
||||
ng-class="{'monospaced': field.type !== '0'}" ng-model="field.value" />
|
||||
<span class="input-group-btn" uib-tooltip="Copy Value" tooltip-placement="left">
|
||||
<button class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-success="clipboardSuccess(e)" ngclipboard-error="clipboardError(e, true)"
|
||||
data-clipboard-text="{{field.value}}">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div ng-if="field.type === '2'">
|
||||
<input type="checkbox" id="field_value{{$index}}" name="Field.Value{{$index}}" ng-model="field.value"
|
||||
data-ng-true-value="'true'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<br class="hidden-xs" />
|
||||
<a href="#" ng-click="removeField(field)" stop-click>
|
||||
<i class="fa fa-window-close-o fa-lg"></i>
|
||||
<span class="visible-xs-inline">Remove Custom Field</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="visible-xs-block" />
|
||||
</div>
|
||||
<a href="#" ng-click="addField()" stop-click>
|
||||
<i class="fa fa-plus-circle"></i> New Custom Field
|
||||
</a>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="form.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="form.$loading"></i>Submit
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
|
||||
<button type="button" ng-if="!hideFavorite" class="btn btn-link pull-right" ng-click="toggleFavorite()"
|
||||
uib-tooltip="Toggle Favorite" tooltip-placement="left">
|
||||
<i class="fa fa-lg" ng-class="cipher.favorite ? 'fa-star' : 'fa-star-o'"></i>
|
||||
<span class="sr-only">Toggle Favorite</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,171 +0,0 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="addLoginModelLabel"><i class="fa fa-globe"></i> Add New Login</h4>
|
||||
</div>
|
||||
<form name="addLoginForm" ng-submit="addLoginForm.$valid && save(login)" api-form="savePromise" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<div class="callout callout-danger validation-errors" ng-show="addLoginForm.$errors">
|
||||
<h4>Errors have occurred</h4>
|
||||
<ul>
|
||||
<li ng-repeat="e in addLoginForm.$errors">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="name">Name</label> <span>*</span>
|
||||
<input type="text" id="name" name="Name" ng-model="login.name" class="form-control" required api-field />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6" ng-if="!hideFolders">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="folder">Folder</label>
|
||||
<select id="folder" name="FolderId" ng-model="login.folderId" class="form-control" api-field>
|
||||
<option ng-repeat="folder in folders | orderBy: folderSort" value="{{folder.id}}">{{folder.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="uri">URI</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="uri" ng-model="login.uri" name="Uri" class="form-control" placeholder="http://..." api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy URI" tooltip-placement="left">
|
||||
<button tabindex="-1" class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)"
|
||||
data-clipboard-target="#uri">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="username">Username</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="username" name="Username" ng-model="login.username" class="form-control" api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy Username" tooltip-placement="left">
|
||||
<button tabindex="-1" class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-error="clipboardError(e)"
|
||||
data-clipboard-target="#username">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group" show-errors style="margin-bottom: 5px;">
|
||||
<div class="pull-right password-options">
|
||||
<i class="fa fa-lg fa-refresh" uib-tooltip="Generate Password" tooltip-placement="left" ng-click="generatePassword()"></i>
|
||||
<i class="fa fa-lg fa-eye" uib-tooltip="Toggle Password" tooltip-placement="left" password-viewer="#password"></i>
|
||||
</div>
|
||||
<label for="password">Password</label>
|
||||
<div class="input-group">
|
||||
<input tabindex="-1" type="text" id="password-text" value="{{login.password}}" style="margin-left: -9999px;" />
|
||||
<input type="password" id="password" name="Password" ng-model="login.password" class="form-control" api-field />
|
||||
<span class="input-group-btn" uib-tooltip="Copy Password" tooltip-placement="left">
|
||||
<button tabindex="-1" class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-success="clipboardSuccess(e)"
|
||||
ngclipboard-error="clipboardError(e, true)"
|
||||
data-clipboard-target="#password-text">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div password-meter="login.password" password-meter-username="login.username"
|
||||
outer-class="xs" class="password-meter"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group" show-errors>
|
||||
<label for="totp">Authenticator Key (TOTP)</label>
|
||||
<input type="text" id="totp" name="Totp" ng-model="login.totp" class="form-control"
|
||||
ng-readonly="readOnly" api-field />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 totp-col">
|
||||
<div totp="login.totp" id="verification-code" ng-if="useTotp"></div>
|
||||
<div ng-if="!useTotp">
|
||||
<a href="#" stop-click ng-click="showUpgrade()"><img src="images/totp-countdown.png" alt="" /></a>
|
||||
<span class="label label-info clickable" ng-click="showUpgrade()">{{fromOrg ? 'UPGRADE' : 'PREMIUM'}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="notes">Notes</label>
|
||||
<textarea id="notes" name="Notes" class="form-control" ng-model="login.notes" api-field></textarea>
|
||||
</div>
|
||||
<hr />
|
||||
<h4><i class="fa fa-list-ul"></i> Custom Fields</h4>
|
||||
<div ng-repeat="field in login.fields">
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<div class="form-group">
|
||||
<label for="field_name{{$index}}">Name</label>
|
||||
<input type="text" id="field_name{{$index}}" name="Field.Name{{$index}}" class="form-control" ng-model="field.name" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="form-group">
|
||||
<label for="field_type{{$index}}">Type</label>
|
||||
<select id="field_type{{$index}}" name="Field.Type{{$index}}" class="form-control" ng-model="field.type">
|
||||
<option value="0">Text</option>
|
||||
<option value="1">Hidden</option>
|
||||
<option value="2">Boolean</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<div class="form-group">
|
||||
<div class="pull-right password-options" ng-if="field.type === '1'">
|
||||
<i class="fa fa-lg fa-eye" uib-tooltip="Toggle Visibility" tooltip-placement="left"
|
||||
password-viewer="#field_value{{$index}}"></i>
|
||||
</div>
|
||||
<label for="field_value{{$index}}">Value</label>
|
||||
<div class="input-group" ng-if="field.type !== '2'">
|
||||
<input ng-attr-type="{{field.type === '0' ? 'text' : 'password'}}" id="field_value{{$index}}"
|
||||
name="Field.Value{{$index}}" class="form-control" ng-model="field.value" />
|
||||
<span class="input-group-btn" uib-tooltip="Copy Value" tooltip-placement="left">
|
||||
<button class="btn btn-default btn-flat" type="button" ngclipboard
|
||||
ngclipboard-success="clipboardSuccess(e)" ngclipboard-error="clipboardError(e, true)"
|
||||
data-clipboard-text="{{field.value}}">
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div ng-if="field.type === '2'">
|
||||
<input type="checkbox" id="field_value{{$index}}" name="Field.Value{{$index}}" ng-model="field.value"
|
||||
data-ng-true-value="'true'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<br class="hidden-xs" />
|
||||
<a href="#" ng-click="removeField(field)" stop-click>
|
||||
<i class="fa fa-window-close-o fa-lg"></i>
|
||||
<span class="visible-xs-inline">Remove Custom Field</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="visible-xs-block" />
|
||||
</div>
|
||||
<a href="#" ng-click="addField()" stop-click>
|
||||
<i class="fa fa-plus-circle"></i> New Custom Field
|
||||
</a>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="addLoginForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="addLoginForm.$loading"></i>Submit
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
|
||||
<button type="button" ng-if="!hideFavorite" class="btn btn-link pull-right" ng-click="toggleFavorite()"
|
||||
uib-tooltip="Toggle Favorite" tooltip-placement="left">
|
||||
<i class="fa fa-lg" ng-class="login.favorite ? 'fa-star' : 'fa-star-o'"></i>
|
||||
<span class="sr-only">Toggle Favorite</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user