mirror of
https://github.com/bitwarden/web
synced 2025-12-06 00:03:28 +00:00
Compare commits
220 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
192eb7e7c9 | ||
|
|
48a36649fa | ||
|
|
6b83b53401 | ||
|
|
c953c2b93f | ||
|
|
7cdce165fb | ||
|
|
b393064f26 | ||
|
|
31bf22063e | ||
|
|
cd9a43f359 | ||
|
|
c326e03eb2 | ||
|
|
f0a986aa04 | ||
|
|
4225da355d | ||
|
|
14bac6a744 | ||
|
|
05cc9b45e6 | ||
|
|
dba596bf35 | ||
|
|
db39d58ea8 | ||
|
|
c0f38216ef | ||
|
|
3643222b3c | ||
|
|
551217ea38 | ||
|
|
24bf1363ab | ||
|
|
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 | ||
|
|
c7802940b1 | ||
|
|
f7b60febe9 | ||
|
|
6c93a63c06 | ||
|
|
c44a638644 | ||
|
|
0d3fead0f3 | ||
|
|
5ba4b37610 | ||
|
|
44a2d071ae | ||
|
|
3b22764368 | ||
|
|
11336da6df | ||
|
|
a0e5591f8e | ||
|
|
e952073c3c | ||
|
|
9bdd0d116a | ||
|
|
05c8a39e6d | ||
|
|
8fa6ff48cf | ||
|
|
7a31783ea4 | ||
|
|
96585b183d | ||
|
|
f81e7b02dc | ||
|
|
f7fbdf2081 | ||
|
|
06a877c755 | ||
|
|
30abd52189 | ||
|
|
6af0e62976 | ||
|
|
84a36a18d6 | ||
|
|
595cf6c375 | ||
|
|
4262e2cc1d | ||
|
|
c134986bbf | ||
|
|
d9981e1d71 | ||
|
|
2b6d7ec361 | ||
|
|
aaa91e50b7 | ||
|
|
ff9030e7af |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -199,5 +199,4 @@ FakesAssemblies/
|
||||
*.opt
|
||||
|
||||
# Other
|
||||
project.lock.json
|
||||
src/js/*.min.js
|
||||
11
Dockerfile
11
Dockerfile
@@ -1,10 +1,15 @@
|
||||
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
|
||||
|
||||
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,10 +1,10 @@
|
||||
[](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" />
|
||||
<img src="https://i.imgur.com/WB8J2bt.png" alt="" />
|
||||
|
||||
# Build/Run
|
||||
|
||||
|
||||
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!
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.26228.9
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "bitwarden-web", ".", "{25BEDEF4-2CAF-445A-807D-63C17FF85694}"
|
||||
ProjectSection(WebsiteProperties) = preProject
|
||||
TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.6.1"
|
||||
Debug.AspNetCompiler.VirtualPath = "/localhost_15509"
|
||||
Debug.AspNetCompiler.PhysicalPath = "."
|
||||
Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_15509\"
|
||||
Debug.AspNetCompiler.Updateable = "true"
|
||||
Debug.AspNetCompiler.ForceOverwrite = "true"
|
||||
Debug.AspNetCompiler.FixedNames = "false"
|
||||
Debug.AspNetCompiler.Debug = "True"
|
||||
Release.AspNetCompiler.VirtualPath = "/localhost_15509"
|
||||
Release.AspNetCompiler.PhysicalPath = "."
|
||||
Release.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_15509\"
|
||||
Release.AspNetCompiler.Updateable = "true"
|
||||
Release.AspNetCompiler.ForceOverwrite = "true"
|
||||
Release.AspNetCompiler.FixedNames = "false"
|
||||
Release.AspNetCompiler.Debug = "False"
|
||||
VWDPort = "15509"
|
||||
SlnRelativePath = "."
|
||||
DefaultWebSiteLanguage = "Visual C#"
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{25BEDEF4-2CAF-445A-807D-63C17FF85694}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{25BEDEF4-2CAF-445A-807D-63C17FF85694}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
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 be3a93a581
@@ -1,5 +1,39 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
# Setup
|
||||
|
||||
GROUPNAME="bitwarden"
|
||||
USERNAME="bitwarden"
|
||||
|
||||
LUID=${LOCAL_UID:-0}
|
||||
LGID=${LOCAL_GID:-0}
|
||||
|
||||
# Step down from host root to well-known nobody/nogroup user
|
||||
|
||||
if [ $LUID -eq 0 ]
|
||||
then
|
||||
LUID=65534
|
||||
fi
|
||||
if [ $LGID -eq 0 ]
|
||||
then
|
||||
LGID=65534
|
||||
fi
|
||||
|
||||
# Create user and group
|
||||
|
||||
groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||
|
||||
groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1
|
||||
useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||
|
||||
usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1
|
||||
mkhomedir_helper $USERNAME
|
||||
|
||||
# The rest...
|
||||
|
||||
chown -R $USERNAME:$GROUPNAME /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:$GROUPNAME /app
|
||||
chown -R $USERNAME:$GROUPNAME /bitwarden_server
|
||||
|
||||
exec gosu $USERNAME:$GROUPNAME dotnet /bitwarden_server/Server.dll \
|
||||
/contentRoot=/app /webRoot=. /serveUnknown=false
|
||||
|
||||
33
gulpfile.js
33
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('.'));
|
||||
});
|
||||
|
||||
@@ -291,7 +293,7 @@ gulp.task('browserify:cc', function () {
|
||||
});
|
||||
|
||||
gulp.task('dist:clean', function (cb) {
|
||||
return rimraf(paths.dist, cb);
|
||||
return rimraf(paths.dist + '**/*', cb);
|
||||
});
|
||||
|
||||
gulp.task('dist:move', function () {
|
||||
@@ -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.15.0",
|
||||
"version": "1.27.0",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,18 +117,18 @@ angular
|
||||
}
|
||||
|
||||
var keys = Object.keys(twoFactorProviders);
|
||||
var cleanedProviders = [];
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var provider = $filter('filter')(constants.twoFactorProviderInfo, {
|
||||
type: keys[i],
|
||||
active: true,
|
||||
requiresUsb: false
|
||||
});
|
||||
if (provider.length) {
|
||||
cleanedProviders.push(twoFactorProviders[keys[i]]);
|
||||
if (!provider.length) {
|
||||
delete twoFactorProviders[keys[i]];
|
||||
}
|
||||
}
|
||||
return cleanedProviders;
|
||||
|
||||
return twoFactorProviders;
|
||||
}
|
||||
|
||||
// ref: https://stackoverflow.com/questions/11381673/detecting-a-mobile-browser
|
||||
@@ -216,8 +216,9 @@ angular
|
||||
function init() {
|
||||
stopU2fCheck = true;
|
||||
var params;
|
||||
if ($scope.twoFactorProvider === constants.twoFactorProvider.duo) {
|
||||
params = $scope.twoFactorProviders[constants.twoFactorProvider.duo];
|
||||
if ($scope.twoFactorProvider === constants.twoFactorProvider.duo ||
|
||||
$scope.twoFactorProvider === constants.twoFactorProvider.organizationDuo) {
|
||||
params = $scope.twoFactorProviders[$scope.twoFactorProvider];
|
||||
|
||||
$window.Duo.init({
|
||||
host: params.Host,
|
||||
|
||||
@@ -29,6 +29,13 @@ angular
|
||||
};
|
||||
$scope.readOnlyEmail = stateParams.email !== null;
|
||||
|
||||
var registerOrgUserId = null;
|
||||
var registerToken = null;
|
||||
if(stateParams.returnState && stateParams.returnState.params &&
|
||||
stateParams.returnState.name === 'frontend.organizationAccept') {
|
||||
registerOrgUserId = stateParams.returnState.params.organizationUserId || null;
|
||||
registerToken = stateParams.returnState.params.token || null;
|
||||
}
|
||||
|
||||
$timeout(function () {
|
||||
if ($scope.model.email) {
|
||||
@@ -73,7 +80,9 @@ angular
|
||||
keys: {
|
||||
publicKey: result.publicKey,
|
||||
encryptedPrivateKey: result.privateKeyEnc
|
||||
}
|
||||
},
|
||||
token: registerToken,
|
||||
organizationUserId: registerOrgUserId
|
||||
};
|
||||
|
||||
return apiService.accounts.register(request).$promise;
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
|
||||
$scope.providers = [];
|
||||
|
||||
if (providers.hasOwnProperty(constants.twoFactorProvider.organizationDuo)) {
|
||||
add(constants.twoFactorProvider.organizationDuo);
|
||||
}
|
||||
if (providers.hasOwnProperty(constants.twoFactorProvider.authenticator)) {
|
||||
add(constants.twoFactorProvider.authenticator);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
<div class="form-group has-feedback" show-errors>
|
||||
<label for="code" class="sr-only">Code</label>
|
||||
<input type="text" id="code" name="Code" class="form-control" placeholder="Verification code"
|
||||
ng-model="token" required api-field />
|
||||
ng-model="token" required api-field autocomplete="off" autocorrect="off" autocapitalize="off"
|
||||
spellcheck="false" />
|
||||
<span class="fa fa-lock form-control-feedback"></span>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -48,7 +49,8 @@
|
||||
<p class="login-box-msg">
|
||||
Complete logging in with YubiKey.
|
||||
</p>
|
||||
<form name="twoFactorForm" ng-submit="twoFactorForm.$valid && twoFactor(token)" api-form="twoFactorPromise">
|
||||
<form name="twoFactorForm" ng-submit="twoFactorForm.$valid && twoFactor(token)" api-form="twoFactorPromise"
|
||||
autocomplete="off">
|
||||
<div class="callout callout-danger validation-errors" ng-show="twoFactorForm.$errors">
|
||||
<h4>Errors have occurred</h4>
|
||||
<ul>
|
||||
@@ -61,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">
|
||||
@@ -80,11 +83,13 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div ng-if="twoFactorProvider === twoFactorProviderConstants.duo">
|
||||
<div ng-if="twoFactorProvider === twoFactorProviderConstants.duo ||
|
||||
twoFactorProvider === twoFactorProviderConstants.organizationDuo">
|
||||
<p class="login-box-msg">
|
||||
Complete logging in with Duo.
|
||||
</p>
|
||||
<form name="twoFactorForm" ng-submit="twoFactorForm.$valid && twoFactor(token)" api-form="twoFactorPromise">
|
||||
<form name="twoFactorForm" ng-submit="twoFactorForm.$valid && twoFactor(token)" api-form="twoFactorPromise"
|
||||
autocomplete="off">
|
||||
<div class="callout callout-danger validation-errors" ng-show="twoFactorForm.$errors">
|
||||
<h4>Errors have occurred</h4>
|
||||
<ul>
|
||||
@@ -115,7 +120,7 @@
|
||||
<p class="login-box-msg">
|
||||
Complete logging in with FIDO U2F.
|
||||
</p>
|
||||
<form name="twoFactorForm" api-form="twoFactorPromise">
|
||||
<form name="twoFactorForm" api-form="twoFactorPromise" autocomplete="off">
|
||||
<div class="callout callout-danger validation-errors" ng-show="twoFactorForm.$errors">
|
||||
<h4>Errors have occurred</h4>
|
||||
<ul>
|
||||
|
||||
@@ -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,20 +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({
|
||||
urlParam: 'access_token',
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -34,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;
|
||||
});
|
||||
@@ -57,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) {
|
||||
@@ -102,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',
|
||||
@@ -156,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
|
||||
@@ -173,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' }
|
||||
@@ -197,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', {
|
||||
@@ -353,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;
|
||||
|
||||
@@ -26,21 +26,88 @@ angular.module('bit')
|
||||
duo: 2,
|
||||
authenticator: 0,
|
||||
email: 1,
|
||||
remember: 5
|
||||
remember: 5,
|
||||
organizationDuo: 6
|
||||
},
|
||||
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,
|
||||
name: 'Authenticator App',
|
||||
description: 'Use an authenticator app (such as Authy or Google Authenticator) to generate time-based ' +
|
||||
'verification codes.',
|
||||
'verification codes.',
|
||||
enabled: false,
|
||||
active: true,
|
||||
free: true,
|
||||
image: 'authapp.png',
|
||||
displayOrder: 0,
|
||||
priority: 1,
|
||||
requiresUsb: false
|
||||
requiresUsb: false,
|
||||
organization: false
|
||||
},
|
||||
{
|
||||
type: 3,
|
||||
@@ -51,7 +118,8 @@ angular.module('bit')
|
||||
image: 'yubico.png',
|
||||
displayOrder: 1,
|
||||
priority: 3,
|
||||
requiresUsb: true
|
||||
requiresUsb: true,
|
||||
organization: false
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
@@ -62,7 +130,8 @@ angular.module('bit')
|
||||
image: 'duo.png',
|
||||
displayOrder: 2,
|
||||
priority: 2,
|
||||
requiresUsb: false
|
||||
requiresUsb: false,
|
||||
organization: false
|
||||
},
|
||||
{
|
||||
type: 4,
|
||||
@@ -73,7 +142,8 @@ angular.module('bit')
|
||||
image: 'fido.png',
|
||||
displayOrder: 3,
|
||||
priority: 4,
|
||||
requiresUsb: true
|
||||
requiresUsb: true,
|
||||
organization: false
|
||||
},
|
||||
{
|
||||
type: 1,
|
||||
@@ -85,7 +155,21 @@ angular.module('bit')
|
||||
image: 'gmail.png',
|
||||
displayOrder: 4,
|
||||
priority: 0,
|
||||
requiresUsb: false
|
||||
requiresUsb: false,
|
||||
organization: false
|
||||
},
|
||||
{
|
||||
type: 6,
|
||||
name: 'Duo (Organization)',
|
||||
description: 'Verify with Duo Security for your organization using the Duo Mobile app, SMS, ' +
|
||||
'phone call, or U2F security key.',
|
||||
enabled: false,
|
||||
active: true,
|
||||
image: 'duo.png',
|
||||
displayOrder: 1,
|
||||
priority: 10,
|
||||
requiresUsb: false,
|
||||
organization: true
|
||||
}
|
||||
],
|
||||
plans: {
|
||||
@@ -95,14 +179,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) {
|
||||
|
||||
});
|
||||
@@ -4,6 +4,7 @@ angular
|
||||
.controller('mainController', function ($scope, $state, authService, appSettings, toastr, $window, $document,
|
||||
cryptoService, $uibModal, apiService) {
|
||||
var vm = this;
|
||||
vm.skinClass = appSettings.selfHosted ? 'skin-blue-light' : 'skin-blue';
|
||||
vm.bodyClass = '';
|
||||
vm.usingControlSidebar = vm.openControlSidebar = false;
|
||||
vm.searchVaultText = null;
|
||||
@@ -48,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 () {
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
angular
|
||||
.module('bit.global')
|
||||
|
||||
.controller('sideNavController', function ($scope, $state, authService, toastr, $analytics, constants) {
|
||||
.controller('sideNavController', function ($scope, $state, authService, toastr, $analytics, constants, appSettings) {
|
||||
$scope.$state = $state;
|
||||
$scope.params = $state.params;
|
||||
$scope.orgs = [];
|
||||
$scope.name = '';
|
||||
|
||||
if(appSettings.selfHosted) {
|
||||
$scope.orgIconBgColor = '#ffffff';
|
||||
$scope.orgIconBorder = '3px solid #a0a0a0';
|
||||
$scope.orgIconTextColor = '#333333';
|
||||
}
|
||||
else {
|
||||
$scope.orgIconBgColor = '#2c3b41';
|
||||
$scope.orgIconBorder = '3px solid #1a2226';
|
||||
$scope.orgIconTextColor = '#ffffff';
|
||||
}
|
||||
|
||||
authService.getUserProfile().then(function (userProfile) {
|
||||
$scope.name = userProfile.extended && userProfile.extended.name ?
|
||||
userProfile.extended.name : userProfile.email;
|
||||
@@ -40,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');
|
||||
};
|
||||
});
|
||||
@@ -2,16 +2,47 @@
|
||||
.module('bit.organization')
|
||||
|
||||
.controller('organizationSettingsController', function ($scope, $state, apiService, toastr, authService, $uibModal,
|
||||
$analytics, appSettings) {
|
||||
$analytics, appSettings, constants, $filter) {
|
||||
$scope.selfHosted = appSettings.selfHosted;
|
||||
$scope.model = {};
|
||||
$scope.twoStepProviders = $filter('filter')(constants.twoFactorProviderInfo, { organization: true });
|
||||
$scope.use2fa = false;
|
||||
|
||||
$scope.$on('$viewContentLoaded', function () {
|
||||
apiService.organizations.get({ id: $state.params.orgId }, function (org) {
|
||||
apiService.organizations.get({ id: $state.params.orgId }).$promise.then(function (org) {
|
||||
$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
|
||||
};
|
||||
|
||||
$scope.use2fa = org.Use2fa;
|
||||
if (org.Use2fa) {
|
||||
return apiService.twoFactor.listOrganization({ orgId: $state.params.orgId }).$promise;
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}).then(function (response) {
|
||||
if (!response || !response.Data) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < response.Data.length; i++) {
|
||||
if (!response.Data[i].Enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var provider = $filter('filter')($scope.twoStepProviders, { type: response.Data[i].Type });
|
||||
if (provider.length) {
|
||||
provider[0].enabled = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,6 +59,22 @@
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
$scope.import = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/tools/views/toolsImport.html',
|
||||
controller: 'organizationSettingsImportController'
|
||||
});
|
||||
};
|
||||
|
||||
$scope.export = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/tools/views/toolsExport.html',
|
||||
controller: 'organizationSettingsExportController'
|
||||
});
|
||||
};
|
||||
|
||||
$scope.delete = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
@@ -35,4 +82,30 @@
|
||||
controller: 'organizationDeleteController'
|
||||
});
|
||||
};
|
||||
|
||||
$scope.edit = function (provider) {
|
||||
if (provider.type === constants.twoFactorProvider.organizationDuo) {
|
||||
typeName = 'Duo';
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
var modal = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/settings/views/settingsTwoStep' + typeName + '.html',
|
||||
controller: 'settingsTwoStep' + typeName + 'Controller',
|
||||
resolve: {
|
||||
enabled: function () { return provider.enabled; },
|
||||
orgId: function () { return $state.params.orgId; }
|
||||
}
|
||||
});
|
||||
|
||||
modal.result.then(function (enabled) {
|
||||
if (enabled || enabled === false) {
|
||||
// do not adjust when undefined or null
|
||||
provider.enabled = enabled;
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
155
src/app/organization/organizationSettingsExportController.js
Normal file
155
src/app/organization/organizationSettingsExportController.js
Normal file
@@ -0,0 +1,155 @@
|
||||
angular
|
||||
.module('bit.organization')
|
||||
|
||||
.controller('organizationSettingsExportController', function ($scope, apiService, $uibModalInstance, cipherService,
|
||||
$q, toastr, $analytics, $state, constants) {
|
||||
$analytics.eventTrack('organizationSettingsExportController', { category: 'Modal' });
|
||||
$scope.export = function (model) {
|
||||
$scope.startedExport = true;
|
||||
var decCiphers = [],
|
||||
decCollections = [];
|
||||
|
||||
var collectionsPromise = apiService.collections.listOrganization({ orgId: $state.params.orgId },
|
||||
function (collections) {
|
||||
decCollections = cipherService.decryptCollections(collections.Data, $state.params.orgId, true);
|
||||
}).$promise;
|
||||
|
||||
var ciphersPromise = apiService.ciphers.listOrganizationDetails({ organizationId: $state.params.orgId },
|
||||
function (ciphers) {
|
||||
decCiphers = cipherService.decryptCiphers(ciphers.Data);
|
||||
}).$promise;
|
||||
|
||||
$q.all([collectionsPromise, ciphersPromise]).then(function () {
|
||||
if (!decCiphers.length) {
|
||||
toastr.error('Nothing to export.', 'Error!');
|
||||
$scope.close();
|
||||
return;
|
||||
}
|
||||
|
||||
var i;
|
||||
var collectionsDict = {};
|
||||
for (i = 0; i < decCollections.length; i++) {
|
||||
collectionsDict[decCollections[i].id] = decCollections[i];
|
||||
}
|
||||
|
||||
try {
|
||||
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
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(exportCiphers);
|
||||
var csvBlob = new Blob([csvString]);
|
||||
|
||||
// IE hack. ref http://msdn.microsoft.com/en-us/library/ie/hh779016.aspx
|
||||
if (window.navigator.msSaveOrOpenBlob) {
|
||||
window.navigator.msSaveBlob(csvBlob, makeFileName());
|
||||
}
|
||||
else {
|
||||
var a = window.document.createElement('a');
|
||||
a.href = window.URL.createObjectURL(csvBlob, { type: 'text/plain' });
|
||||
a.download = makeFileName();
|
||||
document.body.appendChild(a);
|
||||
// IE: "Access is denied".
|
||||
// ref: https://connect.microsoft.com/IE/feedback/details/797361/ie-10-treats-blob-url-as-cross-origin-and-denies-access
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
$analytics.eventTrack('Exported Organization Data');
|
||||
toastr.success('Your data has been exported. Check your browser\'s downloads folder.', 'Success!');
|
||||
$scope.close();
|
||||
}
|
||||
catch (err) {
|
||||
toastr.error('Something went wrong. Please try again.', 'Error!');
|
||||
$scope.close();
|
||||
}
|
||||
}, function () {
|
||||
toastr.error('Something went wrong. Please try again.', 'Error!');
|
||||
$scope.close();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
|
||||
function makeFileName() {
|
||||
var now = new Date();
|
||||
var dateString =
|
||||
now.getFullYear() + '' + padNumber(now.getMonth() + 1, 2) + '' + padNumber(now.getDate(), 2) +
|
||||
padNumber(now.getHours(), 2) + '' + padNumber(now.getMinutes(), 2) +
|
||||
padNumber(now.getSeconds(), 2);
|
||||
|
||||
return 'bitwarden_org_export_' + dateString + '.csv';
|
||||
}
|
||||
|
||||
function padNumber(number, width, paddingCharacter) {
|
||||
paddingCharacter = paddingCharacter || '0';
|
||||
number = number + '';
|
||||
return number.length >= width ? number : new Array(width - number.length + 1).join(paddingCharacter) + number;
|
||||
}
|
||||
});
|
||||
129
src/app/organization/organizationSettingsImportController.js
Normal file
129
src/app/organization/organizationSettingsImportController.js
Normal file
@@ -0,0 +1,129 @@
|
||||
angular
|
||||
.module('bit.organization')
|
||||
|
||||
.controller('organizationSettingsImportController', function ($scope, $state, apiService, $uibModalInstance, cipherService,
|
||||
toastr, importService, $analytics, $sce, validationService, cryptoService) {
|
||||
$analytics.eventTrack('organizationSettingsImportController', { category: 'Modal' });
|
||||
$scope.model = { source: '' };
|
||||
$scope.source = {};
|
||||
$scope.splitFeatured = false;
|
||||
|
||||
$scope.options = [
|
||||
{
|
||||
id: 'bitwardencsv',
|
||||
name: 'Bitwarden (csv)',
|
||||
featured: true,
|
||||
sort: 1,
|
||||
instructions: $sce.trustAsHtml('Export using the web vault (vault.bitwarden.com). ' +
|
||||
'Log into the web vault and navigate to your organization\'s admin area. Then to go ' +
|
||||
'"Settings" > "Tools" > "Export".')
|
||||
},
|
||||
{
|
||||
id: 'lastpass',
|
||||
name: 'LastPass (csv)',
|
||||
featured: true,
|
||||
sort: 2,
|
||||
instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' +
|
||||
'<a target="_blank" href="https://help.bitwarden.com/article/import-from-lastpass/">' +
|
||||
'https://help.bitwarden.com/article/import-from-lastpass/</a>')
|
||||
}
|
||||
];
|
||||
|
||||
$scope.setSource = function () {
|
||||
for (var i = 0; i < $scope.options.length; i++) {
|
||||
if ($scope.options[i].id === $scope.model.source) {
|
||||
$scope.source = $scope.options[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
$scope.setSource();
|
||||
|
||||
$scope.import = function (model, form) {
|
||||
if (!model.source || model.source === '') {
|
||||
validationService.addError(form, 'source', 'Select the format of the import file.', true);
|
||||
return;
|
||||
}
|
||||
|
||||
var file = document.getElementById('file').files[0];
|
||||
if (!file && (!model.fileContents || model.fileContents === '')) {
|
||||
validationService.addError(form, 'file', 'Select the import file or copy/paste the import file contents.', true);
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.processing = true;
|
||||
importService.importOrg(model.source, file || model.fileContents, importSuccess, importError);
|
||||
};
|
||||
|
||||
function importSuccess(collections, ciphers, collectionRelationships) {
|
||||
if (!collections.length && !ciphers.length) {
|
||||
importError('Nothing was imported.');
|
||||
return;
|
||||
}
|
||||
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.encryptCiphers(ciphers, cryptoService.getOrgKey($state.params.orgId)),
|
||||
collectionRelationships: collectionRelationships
|
||||
}, function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
$state.go('backend.org.vault', { orgId: $state.params.orgId }).then(function () {
|
||||
$analytics.eventTrack('Imported Org Data', { label: $scope.model.source });
|
||||
toastr.success('Data has been successfully imported into your vault.', 'Import Success');
|
||||
});
|
||||
}, importError);
|
||||
}
|
||||
|
||||
function cipherIsBadData(cipher) {
|
||||
return (cipher.name === null || cipher.name === '--') &&
|
||||
(cipher.login && (cipher.login.password === null || cipher.login.password === ''));
|
||||
}
|
||||
|
||||
function importError(error) {
|
||||
$analytics.eventTrack('Import Org Data Failed', { label: $scope.model.source });
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
|
||||
if (error) {
|
||||
var data = error.data;
|
||||
if (data && data.ValidationErrors) {
|
||||
var message = '';
|
||||
for (var key in data.ValidationErrors) {
|
||||
if (!data.ValidationErrors.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var i = 0; i < data.ValidationErrors[key].length; i++) {
|
||||
message += (key + ': ' + data.ValidationErrors[key][i] + ' ');
|
||||
}
|
||||
}
|
||||
|
||||
if (message !== '') {
|
||||
toastr.error(message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (data && data.Message) {
|
||||
toastr.error(data.Message);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
toastr.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
toastr.error('Something went wrong. Try again.', 'Oh No!');
|
||||
}
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
});
|
||||
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,66 +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.logins.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.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.logins.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,102 @@
|
||||
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';
|
||||
if (col.id) {
|
||||
$scope.filter = function (c) {
|
||||
return c.collectionIds && c.collectionIds.indexOf(col.id) > -1;
|
||||
};
|
||||
}
|
||||
else {
|
||||
$scope.filter = function (c) {
|
||||
return !c.collectionIds || c.collectionIds.length === 0;
|
||||
};
|
||||
}
|
||||
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,81 +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.logins.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.logins.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.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>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{{add ? 'Add Seats' : 'Remove Seats'}}
|
||||
</h4>
|
||||
</div>
|
||||
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise">
|
||||
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<div class="callout callout-default" ng-show="add">
|
||||
<h4><i class="fa fa-dollar"></i> Note About Charges</h4>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<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> Change Plan</h4>
|
||||
</div>
|
||||
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise">
|
||||
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
You can <a href="https://bitwarden.com/contact/" target="_blank">contact us</a>
|
||||
if you would like to change your plan. Please ensure that you have an active payment
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
Verify Bank Account
|
||||
</h4>
|
||||
</div>
|
||||
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise">
|
||||
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Enter the two micro-deposit amounts from your bank account. Both amounts will be less than $1.00 each.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<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-cubes"></i> Add New Collection</h4>
|
||||
</div>
|
||||
<form name="form" ng-submit="form.$valid && submit(model)" api-form="submitPromise">
|
||||
<form name="form" ng-submit="form.$valid && submit(model)" api-form="submitPromise" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<div class="callout callout-default">
|
||||
<h4><i class="fa fa-info-circle"></i> Note</h4>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<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-cubes"></i> Edit Collection</h4>
|
||||
</div>
|
||||
<form name="form" ng-submit="form.$valid && submit(collection)" api-form="submitPromise">
|
||||
<form name="form" ng-submit="form.$valid && submit(collection)" api-form="submitPromise" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<div class="callout callout-default">
|
||||
<h4><i class="fa fa-info-circle"></i> Note</h4>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<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-sitemap"></i> Add New Group</h4>
|
||||
</div>
|
||||
<form name="form" ng-submit="form.$valid && submit(model)" api-form="submitPromise">
|
||||
<form name="form" ng-submit="form.$valid && submit(model)" api-form="submitPromise" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<div class="callout callout-default">
|
||||
<h4><i class="fa fa-info-circle"></i> Note</h4>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<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-sitemap"></i> Edit Group</h4>
|
||||
</div>
|
||||
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise">
|
||||
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<div class="callout callout-default">
|
||||
<h4><i class="fa fa-info-circle"></i> Note</h4>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<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-user"></i> Edit User <small>{{email}}</small></h4>
|
||||
</div>
|
||||
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise">
|
||||
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
|
||||
<h4>Errors have occurred</h4>
|
||||
|
||||
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>
|
||||
@@ -2,7 +2,7 @@
|
||||
<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-sitemap"></i> Edit User Groups <small>{{orgUser.email}}</small></h4>
|
||||
</div>
|
||||
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise">
|
||||
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
|
||||
<h4>Errors have occurred</h4>
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
<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-user"></i> Invite User</h4>
|
||||
</div>
|
||||
<form name="inviteForm" ng-submit="inviteForm.$valid && submit(model)" api-form="submitPromise">
|
||||
<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>
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">General</h3>
|
||||
</div>
|
||||
<form role="form" name="generalForm" ng-submit="generalForm.$valid && generalSave()" api-form="generalPromise">
|
||||
<form role="form" name="generalForm" ng-submit="generalForm.$valid && generalSave()" api-form="generalPromise"
|
||||
autocomplete="off">
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-9">
|
||||
@@ -24,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">
|
||||
@@ -49,6 +63,51 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="box box-default" ng-if="use2fa">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Two-step Login Providers</h3>
|
||||
</div>
|
||||
<div class="box-body no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover table-vmiddle">
|
||||
<tbody>
|
||||
<tr ng-repeat="provider in twoStepProviders | orderBy: 'displayOrder'">
|
||||
<td style="width: 120px; height: 75px;" align="center">
|
||||
<a href="#" stop-click ng-click="edit(provider)">
|
||||
<img alt="{{::provider.name}}" ng-src="{{'images/two-factor/' + provider.image}}" />
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" stop-click ng-click="edit(provider)">{{::provider.name}}</a>
|
||||
<div class="text-muted text-sm">{{::provider.description}}</div>
|
||||
</td>
|
||||
<td style="width: 100px;" class="text-right">
|
||||
<span class="label label-full"
|
||||
ng-class="{ 'label-success': provider.enabled, 'label-default': !provider.enabled }">
|
||||
{{provider.enabled ? 'Enabled' : 'Disabled'}}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box box-default">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Import/Export</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<p>
|
||||
Quickly import logins, collections, and other data. You can also export all of your organization's
|
||||
vault data in <code>.csv</code> format.
|
||||
</p>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button class="btn btn-default btn-flat" type="button" ng-click="import()">Import Data</button>
|
||||
<button class="btn btn-default btn-flat" type="button" ng-click="export()">Export Data</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box box-danger">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Danger Zone</h3>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<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-cubes"></i> Collections <small>{{cipher.name}}</small></h4>
|
||||
</div>
|
||||
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise">
|
||||
<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,22 +1,11 @@
|
||||
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;
|
||||
|
||||
_service.logins = $resource(_apiUri + '/logins/:id', {}, {
|
||||
get: { method: 'GET', params: { id: '@id' } },
|
||||
getAdmin: { url: _apiUri + '/logins/:id/admin', method: 'GET', params: { id: '@id' } },
|
||||
list: { method: 'GET', params: {} },
|
||||
post: { method: 'POST', params: {} },
|
||||
postAdmin: { url: _apiUri + '/logins/admin', method: 'POST', params: {} },
|
||||
put: { method: 'POST', params: { id: '@id' } },
|
||||
putAdmin: { url: _apiUri + '/logins/:id/admin', method: 'POST', params: { id: '@id' } },
|
||||
del: { url: _apiUri + '/logins/:id/delete', method: 'POST', params: { id: '@id' } }
|
||||
});
|
||||
|
||||
_service.folders = $resource(_apiUri + '/folders/:id', {}, {
|
||||
get: { method: 'GET', params: { id: '@id' } },
|
||||
list: { method: 'GET', params: {} },
|
||||
@@ -27,12 +16,16 @@
|
||||
|
||||
_service.ciphers = $resource(_apiUri + '/ciphers/:id', {}, {
|
||||
get: { method: 'GET', params: { id: '@id' } },
|
||||
getAdmin: { url: _apiUri + '/ciphers/:id/admin', method: 'GET', params: { id: '@id' } },
|
||||
getDetails: { url: _apiUri + '/ciphers/:id/details', method: 'GET', params: { id: '@id' } },
|
||||
list: { method: 'GET', params: { includeFolders: false, includeShared: true } },
|
||||
listDetails: { url: _apiUri + '/ciphers/details', method: 'GET', params: {} },
|
||||
list: { method: 'GET', params: {} },
|
||||
listOrganizationDetails: { url: _apiUri + '/ciphers/organization-details', method: 'GET', params: {} },
|
||||
post: { method: 'POST', params: {} },
|
||||
postAdmin: { url: _apiUri + '/ciphers/admin', method: 'POST', params: {} },
|
||||
put: { method: 'POST', params: { id: '@id' } },
|
||||
putAdmin: { url: _apiUri + '/ciphers/:id/admin', method: 'POST', params: { id: '@id' } },
|
||||
'import': { url: _apiUri + '/ciphers/import', method: 'POST', params: {} },
|
||||
favorite: { url: _apiUri + '/ciphers/:id/favorite', method: 'POST', params: { id: '@id' } },
|
||||
importOrg: { url: _apiUri + '/ciphers/import-organization?organizationId=:orgId', method: 'POST', params: { orgId: '@orgId' } },
|
||||
putPartial: { url: _apiUri + '/ciphers/:id/partial', method: 'POST', params: { id: '@id' } },
|
||||
putShare: { url: _apiUri + '/ciphers/:id/share', method: 'POST', params: { id: '@id' } },
|
||||
putCollections: { url: _apiUri + '/ciphers/:id/collections', method: 'POST', params: { id: '@id' } },
|
||||
@@ -41,19 +34,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', {}, {
|
||||
@@ -158,9 +159,11 @@
|
||||
|
||||
_service.twoFactor = $resource(_apiUri + '/two-factor', {}, {
|
||||
list: { method: 'GET', params: {} },
|
||||
listOrganization: { url: _apiUri + '/organizations/:orgId/two-factor', method: 'GET', params: { orgId: '@orgId' } },
|
||||
getEmail: { url: _apiUri + '/two-factor/get-email', method: 'POST', params: {} },
|
||||
getU2f: { url: _apiUri + '/two-factor/get-u2f', method: 'POST', params: {} },
|
||||
getDuo: { url: _apiUri + '/two-factor/get-duo', method: 'POST', params: {} },
|
||||
getOrganizationDuo: { url: _apiUri + '/organizations/:orgId/two-factor/get-duo', method: 'POST', params: { orgId: '@orgId' } },
|
||||
getAuthenticator: { url: _apiUri + '/two-factor/get-authenticator', method: 'POST', params: {} },
|
||||
getYubi: { url: _apiUri + '/two-factor/get-yubikey', method: 'POST', params: {} },
|
||||
sendEmail: { url: _apiUri + '/two-factor/send-email', method: 'POST', params: {} },
|
||||
@@ -169,8 +172,10 @@
|
||||
putU2f: { url: _apiUri + '/two-factor/u2f', method: 'POST', params: {} },
|
||||
putAuthenticator: { url: _apiUri + '/two-factor/authenticator', method: 'POST', params: {} },
|
||||
putDuo: { url: _apiUri + '/two-factor/duo', method: 'POST', params: {} },
|
||||
putOrganizationDuo: { url: _apiUri + '/organizations/:orgId/two-factor/duo', method: 'POST', params: { orgId: '@orgId' } },
|
||||
putYubi: { url: _apiUri + '/two-factor/yubikey', method: 'POST', params: {} },
|
||||
disable: { url: _apiUri + '/two-factor/disable', method: 'POST', params: {} },
|
||||
disableOrganization: { url: _apiUri + '/organizations/:orgId/two-factor/disable', method: 'POST', params: { orgId: '@orgId' } },
|
||||
recover: { url: _apiUri + '/two-factor/recover', method: 'POST', params: {} },
|
||||
getRecover: { url: _apiUri + '/two-factor/get-recover', method: 'POST', params: {} }
|
||||
});
|
||||
@@ -184,11 +189,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,9 @@ 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,
|
||||
use2fa: profile.Organizations[i].Use2fa,
|
||||
useTotp: profile.Organizations[i].UseTotp
|
||||
};
|
||||
}
|
||||
@@ -158,8 +161,8 @@ angular
|
||||
cryptoService.setOrgKeys(orgs);
|
||||
_setDeferred.resolve(_userProfile);
|
||||
}
|
||||
}, function () {
|
||||
_setDeferred.reject();
|
||||
}, function (error) {
|
||||
_setDeferred.reject(error);
|
||||
});
|
||||
|
||||
return _setDeferred.promise;
|
||||
@@ -183,6 +186,9 @@ angular
|
||||
maxStorageGb: org.MaxStorageGb,
|
||||
seats: org.Seats,
|
||||
useGroups: org.UseGroups,
|
||||
useDirectory: org.UseDirectory,
|
||||
useEvents: org.UseEvents,
|
||||
use2fa: org.Use2fa,
|
||||
useTotp: org.UseTotp
|
||||
};
|
||||
profile.organizations[o.id] = o;
|
||||
@@ -233,7 +239,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,63 +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 || encryptedLogin;
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -65,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 || encryptedCipher;
|
||||
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";
|
||||
|
||||
@@ -134,6 +276,28 @@ angular
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
_service.decryptFields = function (key, encryptedFields) {
|
||||
var unencryptedFields = [];
|
||||
|
||||
if (encryptedFields) {
|
||||
for (var i = 0; i < encryptedFields.length; i++) {
|
||||
unencryptedFields.push(_service.decryptField(key, encryptedFields[i]));
|
||||
}
|
||||
}
|
||||
|
||||
return unencryptedFields;
|
||||
};
|
||||
|
||||
_service.decryptField = function (key, encryptedField) {
|
||||
if (!encryptedField) throw "encryptedField is undefined or null";
|
||||
|
||||
return {
|
||||
type: encryptedField.Type.toString(),
|
||||
name: encryptedField.Name && encryptedField.Name !== '' ? cryptoService.decrypt(encryptedField.Name, key) : null,
|
||||
value: encryptedField.Value && encryptedField.Value !== '' ? cryptoService.decrypt(encryptedField.Value, key) : null
|
||||
};
|
||||
};
|
||||
|
||||
_service.decryptFolders = function (encryptedFolders) {
|
||||
if (!encryptedFolders) throw "encryptedFolders is undefined or null";
|
||||
|
||||
@@ -159,7 +323,7 @@ angular
|
||||
|
||||
return {
|
||||
id: encryptedFolder.Id,
|
||||
name: _service.decryptProperty(encryptedFolder.Name, null, false)
|
||||
name: _service.decryptProperty(encryptedFolder.Name, null, false, true)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -183,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;
|
||||
}
|
||||
@@ -200,50 +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,
|
||||
uri: !unencryptedLogin.uri || unencryptedLogin.uri === '' ? null : cryptoService.encrypt(unencryptedLogin.uri, key),
|
||||
name: cryptoService.encrypt(unencryptedLogin.name, key),
|
||||
username: !unencryptedLogin.username || unencryptedLogin.username === '' ? null : cryptoService.encrypt(unencryptedLogin.username, key),
|
||||
password: !unencryptedLogin.password || unencryptedLogin.password === '' ? null : cryptoService.encrypt(unencryptedLogin.password, key),
|
||||
notes: !unencryptedLogin.notes || unencryptedLogin.notes === '' ? null : cryptoService.encrypt(unencryptedLogin.notes, key),
|
||||
totp: !unencryptedLogin.totp || unencryptedLogin.totp === '' ? null : cryptoService.encrypt(unencryptedLogin.totp, 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) {
|
||||
@@ -272,6 +495,33 @@ angular
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
_service.encryptFields = function (unencryptedFields, key) {
|
||||
if (!unencryptedFields || !unencryptedFields.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var encFields = [];
|
||||
for (var i = 0; i < unencryptedFields.length; i++) {
|
||||
if (!unencryptedFields[i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
encFields.push(_service.encryptField(unencryptedFields[i], key));
|
||||
}
|
||||
|
||||
return encFields;
|
||||
};
|
||||
|
||||
_service.encryptField = function (unencryptedField, key) {
|
||||
if (!unencryptedField) throw "unencryptedField is undefined or null";
|
||||
|
||||
return {
|
||||
type: parseInt(unencryptedField.type),
|
||||
name: unencryptedField.name ? cryptoService.encrypt(unencryptedField.name, key) : null,
|
||||
value: unencryptedField.value ? cryptoService.encrypt(unencryptedField.value.toString(), key) : null
|
||||
};
|
||||
};
|
||||
|
||||
_service.encryptFolders = function (unencryptedFolders, key) {
|
||||
if (!unencryptedFolders) throw "unencryptedFolders is undefined or null";
|
||||
|
||||
@@ -312,5 +562,9 @@ angular
|
||||
};
|
||||
};
|
||||
|
||||
function encryptProperty(property, key) {
|
||||
return !property || property === '' ? null : cryptoService.encrypt(property, key);
|
||||
}
|
||||
|
||||
return _service;
|
||||
});
|
||||
|
||||
@@ -235,7 +235,7 @@ angular
|
||||
};
|
||||
|
||||
_service.makeKey = function (password, salt) {
|
||||
if (!$window.cryptoShimmed && $window.navigator.userAgent.indexOf('Edge') === -1) {
|
||||
if (_subtle != null && !$window.cryptoShimmed && $window.navigator.userAgent.indexOf('Edge') === -1) {
|
||||
return pbkdf2WC(password, salt, 5000, 256).then(function (keyBuf) {
|
||||
return new SymmetricCryptoKey(bufToB64(keyBuf), true);
|
||||
});
|
||||
@@ -305,7 +305,7 @@ angular
|
||||
throw 'Invalid parameters.';
|
||||
}
|
||||
|
||||
if (!$window.cryptoShimmed && $window.navigator.userAgent.indexOf('Edge') === -1) {
|
||||
if (_subtle != null && !$window.cryptoShimmed && $window.navigator.userAgent.indexOf('Edge') === -1) {
|
||||
var keyBuf = key.getBuffers();
|
||||
return pbkdf2WC(new Uint8Array(keyBuf.key), password, 1, 256).then(function (hashBuf) {
|
||||
return bufToB64(hashBuf);
|
||||
@@ -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.15.0","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.27.0","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,11 +4,16 @@
|
||||
.controller('settingsPremiumController', function ($scope, $state, apiService, toastr, $analytics, authService,
|
||||
constants, $timeout, appSettings, validationService
|
||||
// @if !selfHosted
|
||||
/* jshint ignore:start */
|
||||
, stripe
|
||||
/* jshint ignore:end */
|
||||
// @endif
|
||||
) {
|
||||
authService.getUserProfile().then(function (profile) {
|
||||
if (profile.premium) {
|
||||
var profile = null;
|
||||
|
||||
authService.getUserProfile().then(function (theProfile) {
|
||||
profile = theProfile;
|
||||
if (profile && profile.premium) {
|
||||
return $state.go('backend.user.settingsBilling');
|
||||
}
|
||||
});
|
||||
@@ -63,6 +68,11 @@
|
||||
|
||||
$scope.submit = function (model, form) {
|
||||
if ($scope.selfHosted) {
|
||||
if (profile && !profile.emailVerified) {
|
||||
validationService.addError(form, null, 'Your account\'s email address first must be verified.', true);
|
||||
return;
|
||||
}
|
||||
|
||||
var fileEl = document.getElementById('file');
|
||||
var files = fileEl.files;
|
||||
if (!files || !files.length) {
|
||||
|
||||
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;
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
$scope.model = {
|
||||
key: formatString(_key),
|
||||
qr: 'https://chart.googleapis.com/chart?chs=123x123&chld=L|0&cht=qr&chl=otpauth://totp/' +
|
||||
qr: 'https://chart.googleapis.com/chart?chs=160x160&chld=L|0&cht=qr&chl=otpauth://totp/' +
|
||||
_issuer + ':' + encodeURIComponent(_profile.email) +
|
||||
'%3Fsecret=' + encodeURIComponent(_key) +
|
||||
'%26issuer=' + _issuer
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
.controller('settingsTwoStepController', function ($scope, apiService, toastr, $analytics, constants,
|
||||
$filter, $uibModal, authService) {
|
||||
$scope.providers = constants.twoFactorProviderInfo;
|
||||
$scope.providers = $filter('filter')(constants.twoFactorProviderInfo, { organization: false });
|
||||
$scope.premium = true;
|
||||
|
||||
authService.getUserProfile().then(function (profile) {
|
||||
@@ -60,7 +60,8 @@
|
||||
templateUrl: 'app/settings/views/settingsTwoStep' + typeName + '.html',
|
||||
controller: 'settingsTwoStep' + typeName + 'Controller',
|
||||
resolve: {
|
||||
enabled: function () { return provider.enabled; }
|
||||
enabled: function () { return provider.enabled; },
|
||||
orgId: function () { return null; }
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
.module('bit.settings')
|
||||
|
||||
.controller('settingsTwoStepDuoController', function ($scope, apiService, $uibModalInstance, cryptoService,
|
||||
toastr, $analytics, constants, $timeout) {
|
||||
toastr, $analytics, constants, $timeout, orgId) {
|
||||
$analytics.eventTrack('settingsTwoStepDuoController', { category: 'Modal' });
|
||||
var _masterPasswordHash;
|
||||
|
||||
@@ -20,9 +20,16 @@
|
||||
$scope.auth = function (model) {
|
||||
$scope.authPromise = cryptoService.hashPassword(model.masterPassword).then(function (hash) {
|
||||
_masterPasswordHash = hash;
|
||||
return apiService.twoFactor.getDuo({}, {
|
||||
var requestModel = {
|
||||
masterPasswordHash: _masterPasswordHash
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
if (orgId) {
|
||||
return apiService.twoFactor.getOrganizationDuo({ orgId: orgId }, requestModel).$promise;
|
||||
}
|
||||
else {
|
||||
return apiService.twoFactor.getDuo({}, requestModel).$promise;
|
||||
}
|
||||
}).then(function (apiResponse) {
|
||||
processResult(apiResponse);
|
||||
$scope.authed = true;
|
||||
@@ -43,27 +50,52 @@
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.submitPromise = apiService.twoFactor.disable({}, {
|
||||
masterPasswordHash: _masterPasswordHash,
|
||||
type: constants.twoFactorProvider.duo
|
||||
}, function (response) {
|
||||
$analytics.eventTrack('Disabled Two-step Duo');
|
||||
toastr.success('Duo has been disabled.');
|
||||
$scope.enabled = response.Enabled;
|
||||
$scope.close();
|
||||
}).$promise;
|
||||
if (orgId) {
|
||||
$scope.submitPromise = apiService.twoFactor.disableOrganization({ orgId: orgId }, {
|
||||
masterPasswordHash: _masterPasswordHash,
|
||||
type: constants.twoFactorProvider.organizationDuo
|
||||
}, function (response) {
|
||||
$analytics.eventTrack('Disabled Two-step Organization Duo');
|
||||
toastr.success('Duo has been disabled.');
|
||||
$scope.enabled = response.Enabled;
|
||||
$scope.close();
|
||||
}).$promise;
|
||||
}
|
||||
else {
|
||||
$scope.submitPromise = apiService.twoFactor.disable({}, {
|
||||
masterPasswordHash: _masterPasswordHash,
|
||||
type: constants.twoFactorProvider.duo
|
||||
}, function (response) {
|
||||
$analytics.eventTrack('Disabled Two-step Duo');
|
||||
toastr.success('Duo has been disabled.');
|
||||
$scope.enabled = response.Enabled;
|
||||
$scope.close();
|
||||
}).$promise;
|
||||
}
|
||||
}
|
||||
|
||||
function update(model) {
|
||||
$scope.submitPromise = apiService.twoFactor.putDuo({}, {
|
||||
var requestModel = {
|
||||
integrationKey: model.ikey,
|
||||
secretKey: model.skey,
|
||||
host: model.host,
|
||||
masterPasswordHash: _masterPasswordHash
|
||||
}, function (response) {
|
||||
$analytics.eventTrack('Enabled Two-step Duo');
|
||||
processResult(response);
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
if (orgId) {
|
||||
$scope.submitPromise = apiService.twoFactor.putOrganizationDuo({ orgId: orgId }, requestModel,
|
||||
function (response) {
|
||||
$analytics.eventTrack('Enabled Two-step Organization Duo');
|
||||
processResult(response);
|
||||
}).$promise;
|
||||
}
|
||||
else {
|
||||
$scope.submitPromise = apiService.twoFactor.putDuo({}, requestModel,
|
||||
function (response) {
|
||||
$analytics.eventTrack('Enabled Two-step Duo');
|
||||
processResult(response);
|
||||
}).$promise;
|
||||
}
|
||||
}
|
||||
|
||||
function processResult(response) {
|
||||
@@ -80,7 +112,7 @@
|
||||
closing = true;
|
||||
$uibModalInstance.close($scope.enabled);
|
||||
};
|
||||
|
||||
|
||||
$scope.$on('modal.closing', function (e, reason, closed) {
|
||||
if (closing) {
|
||||
return;
|
||||
|
||||
@@ -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.logins.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
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">General</h3>
|
||||
</div>
|
||||
<form role="form" name="generalForm" ng-submit="generalForm.$valid && generalSave()" api-form="generalPromise">
|
||||
<form role="form" name="generalForm" ng-submit="generalForm.$valid && generalSave()" api-form="generalPromise"
|
||||
autocomplete="off">
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-9">
|
||||
@@ -57,7 +58,7 @@
|
||||
<h3 class="box-title">Master Password</h3>
|
||||
</div>
|
||||
<form role="form" name="masterPasswordForm" ng-submit="masterPasswordForm.$valid && passwordHintSave()"
|
||||
api-form="passwordHintPromise">
|
||||
api-form="passwordHintPromise" autocomplete="off">
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-9">
|
||||
@@ -85,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>
|
||||
@@ -128,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>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<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-globe"></i> {{index ? 'Edit Equivalent Domain' : 'Add Equivalent Domain'}}</h4>
|
||||
</div>
|
||||
<form name="domainAddEditForm" ng-submit="domainAddEditForm.$valid && submit(domainAddEditForm)">
|
||||
<form name="domainAddEditForm" ng-submit="domainAddEditForm.$valid && submit(domainAddEditForm)" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<div class="callout callout-danger validation-errors" ng-show="domainAddEditForm.$errors">
|
||||
<h4>Errors have occurred</h4>
|
||||
|
||||
@@ -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;">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{{add ? 'Add Storage' : 'Remove Storage'}}
|
||||
</h4>
|
||||
</div>
|
||||
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise">
|
||||
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<div class="callout callout-default" ng-show="add">
|
||||
<h4><i class="fa fa-dollar"></i> Note About Charges</h4>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{{existingPaymentMethod ? 'Change Payment Method' : 'Add Payment Method'}}
|
||||
</h4>
|
||||
</div>
|
||||
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise">
|
||||
<form name="form" ng-submit="form.$valid && submit()" api-form="submitPromise" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
|
||||
<h4>Errors have occurred</h4>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
Update License
|
||||
</h4>
|
||||
</div>
|
||||
<form name="form" ng-submit="form.$valid && submit(form)" api-form="submitPromise">
|
||||
<form name="form" ng-submit="form.$valid && submit(form)" api-form="submitPromise" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<div class="callout callout-danger validation-errors" ng-show="form.$errors">
|
||||
<h4>Errors have occurred</h4>
|
||||
|
||||
@@ -30,12 +30,13 @@
|
||||
</div>
|
||||
</form>
|
||||
<form name="changeEmailConfirmForm" ng-submit="changeEmailConfirmForm.$valid && confirm(model)" api-form="confirmPromise"
|
||||
ng-show="tokenSent">
|
||||
ng-show="tokenSent" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<p>We have emailed a verification code to <b>{{model.newEmail}}</b>. Please check your email for this code and enter it below to finalize your the email address change.</p>
|
||||
<div class="callout callout-warning">
|
||||
<h4><i class="fa fa-warning"></i> Warning</h4>
|
||||
Proceeding will log you out of your current session, requiring you to log back in.
|
||||
Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices
|
||||
may continue to remain active for up to one hour.
|
||||
</div>
|
||||
<div class="callout callout-danger validation-errors" ng-show="changeEmailConfirmForm.$errors">
|
||||
<h4>Errors have occurred</h4>
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
<p>We recommend that you change your master password immediately if you believe that your credentials have been compromised.</p>
|
||||
<div class="callout callout-warning">
|
||||
<h4><i class="fa fa-warning"></i> Warning</h4>
|
||||
Proceeding will log you out of your current session, requiring you to log back in.
|
||||
Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices
|
||||
may continue to remain active for up to one hour.
|
||||
</div>
|
||||
<div class="callout callout-danger validation-errors" ng-show="changePasswordForm.$errors">
|
||||
<h4>Errors have occurred</h4>
|
||||
|
||||
@@ -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,9 +4,9 @@
|
||||
<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">
|
||||
<form name="customForm" ng-submit="customForm.$valid && saveCustom()" api-form="customPromise" autocomplete="off">
|
||||
<div class="box box-default">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Custom <span class="hidden-xs">Equivalent Domains</span></h3>
|
||||
@@ -59,7 +59,7 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form name="globalForm" ng-submit="globalForm.$valid && saveGlobal()" api-form="globalPromise">
|
||||
<form name="globalForm" ng-submit="globalForm.$valid && saveGlobal()" api-form="globalPromise" autocomplete="off">
|
||||
<div class="box box-default">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Global <span class="hidden-xs">Equivalent Domains</span></h3>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user