mirror of
https://github.com/bitwarden/web
synced 2025-12-06 00:03:28 +00:00
Compare commits
112 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ed69d887f | ||
|
|
f0d440d204 | ||
|
|
3e18f812db | ||
|
|
8cf02fd59a | ||
|
|
06bfab3afa | ||
|
|
71e4697562 | ||
|
|
cf1bffe2f1 | ||
|
|
55a5fd49dc | ||
|
|
3f6637eb8f | ||
|
|
7373e281ac | ||
|
|
52b89455d7 | ||
|
|
bca7592c77 | ||
|
|
f6ab0bfe82 | ||
|
|
012a5c491d | ||
|
|
7666d6136d | ||
|
|
7bdda34f14 | ||
|
|
df21f89fcb | ||
|
|
f3b4cdca8a | ||
|
|
52460bf47b | ||
|
|
e674e7287e | ||
|
|
a20e8b6228 | ||
|
|
8d50e96dab | ||
|
|
1fe673951b | ||
|
|
3df5a9454e | ||
|
|
79fecd6b03 | ||
|
|
f3445c24b9 | ||
|
|
a3150d8505 | ||
|
|
828b5d8703 | ||
|
|
39559e203a | ||
|
|
605bdd0ea0 | ||
|
|
74945e03ce | ||
|
|
c772502af5 | ||
|
|
401d9db0f2 | ||
|
|
2306da94fe | ||
|
|
9f7ed11082 | ||
|
|
fff0efb095 | ||
|
|
9aa61f4bca | ||
|
|
bd70dc5966 | ||
|
|
ac6a3caa8f | ||
|
|
0914776152 | ||
|
|
45d0f43e90 | ||
|
|
7264367fa3 | ||
|
|
022fa34478 | ||
|
|
f7fd28fded | ||
|
|
e01a22de48 | ||
|
|
711c8e63c1 | ||
|
|
f186ec160a | ||
|
|
53fcfd13ee | ||
|
|
c684d66ec0 | ||
|
|
6496f750b0 | ||
|
|
6aaa47cccd | ||
|
|
1f6677d610 | ||
|
|
54b659aff0 | ||
|
|
b9db21309e | ||
|
|
8649c3b2b1 | ||
|
|
6bf6cc365b | ||
|
|
7c2d5448e8 | ||
|
|
a9f2ef7c10 | ||
|
|
f86bce970e | ||
|
|
6cbd618fb8 | ||
|
|
11787193ed | ||
|
|
45aae6810c | ||
|
|
7f6d571ef1 | ||
|
|
908dc4727c | ||
|
|
264759cfa0 | ||
|
|
b5d265526a | ||
|
|
22290eafb8 | ||
|
|
3101e57c36 | ||
|
|
b72a52232d | ||
|
|
a5b8e703fc | ||
|
|
fb26425f17 | ||
|
|
3114e20aef | ||
|
|
a52d2f4b7a | ||
|
|
34e484c377 | ||
|
|
08e8e9ff64 | ||
|
|
ebf55390eb | ||
|
|
c328144a58 | ||
|
|
4b583bea9b | ||
|
|
3f0ca412c6 | ||
|
|
0050b570b4 | ||
|
|
9405be03b0 | ||
|
|
b5b706fe06 | ||
|
|
8313d9fa90 | ||
|
|
93b96b3be7 | ||
|
|
50e6818f2b | ||
|
|
104fb57bd8 | ||
|
|
7ba6b2f00a | ||
|
|
d8e8939eca | ||
|
|
26c5f4049d | ||
|
|
b6c9dba0fc | ||
|
|
8badc1a354 | ||
|
|
15097eb1f0 | ||
|
|
986306f811 | ||
|
|
c5de290dd4 | ||
|
|
21e9e083f5 | ||
|
|
517ea65bc9 | ||
|
|
e7a9699226 | ||
|
|
52b3dfd0e3 | ||
|
|
4c0bde9d87 | ||
|
|
f29bbe4316 | ||
|
|
3c03ed8636 | ||
|
|
71ca4eb84a | ||
|
|
30d19b4ee1 | ||
|
|
7b88d30aa8 | ||
|
|
0ae11cc40c | ||
|
|
dd5cda867d | ||
|
|
2d11bef262 | ||
|
|
5d5d0bfb66 | ||
|
|
1b169f368b | ||
|
|
3375bda789 | ||
|
|
6569fbe6aa | ||
|
|
1d2b82a302 |
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
|
||||
<clear />
|
||||
<add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
20
README.md
20
README.md
@@ -1,7 +1,27 @@
|
||||
[] (https://ci.appveyor.com/project/bitwarden/web) [](https://gitter.im/bitwarden/Lobby)
|
||||
|
||||
# bitwarden Web
|
||||
|
||||
The bitwarden Web project is an AngularJS application that powers the web vault (https://vault.bitwarden.com/).
|
||||
|
||||
# Build/Run
|
||||
|
||||
**Requirements**
|
||||
|
||||
- Node.js
|
||||
- Gulp
|
||||
|
||||
Unless you are running the [Core](https://github.com/bitwarden/core) API locally, you'll probably need to switch the
|
||||
application to target the production API. Open `package.json` and set `production` to `true`.
|
||||
|
||||
Then run the following commands:
|
||||
|
||||
- `npm install`
|
||||
- `gulp build`
|
||||
- `gulp serve`
|
||||
|
||||
You can now access the web vault at `http://localhost:4001`.
|
||||
|
||||
# Contribute
|
||||
|
||||
Code contributions are welcome! Please commit any pull requests against the `master` branch.
|
||||
|
||||
@@ -3,34 +3,37 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 14
|
||||
VisualStudioVersion = 14.0.25420.1
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{860863C9-0436-43D4-840D-FE919C9F6FFC}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{14FE7221-D377-4AD5-9A9E-4541577CF05A}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.gitignore = .gitignore
|
||||
CNAME = CNAME
|
||||
global.json = global.json
|
||||
NuGet.Config = NuGet.Config
|
||||
README.md = README.md
|
||||
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
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Web", "src\Web\Web.xproj", "{0BEBF47C-BA0B-48AC-B48C-718F94084AD5}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{0BEBF47C-BA0B-48AC-B48C-718F94084AD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0BEBF47C-BA0B-48AC-B48C-718F94084AD5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0BEBF47C-BA0B-48AC-B48C-718F94084AD5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0BEBF47C-BA0B-48AC-B48C-718F94084AD5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{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
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{0BEBF47C-BA0B-48AC-B48C-718F94084AD5} = {860863C9-0436-43D4-840D-FE919C9F6FFC}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"projects": [ "src", "test" ],
|
||||
"sdk": {
|
||||
"version": "1.0.0-preview2-003121"
|
||||
}
|
||||
}
|
||||
@@ -8,28 +8,32 @@ var gulp = require('gulp'),
|
||||
uglify = require('gulp-uglify'),
|
||||
ghPages = require('gulp-gh-pages'),
|
||||
less = require('gulp-less'),
|
||||
connect = require('gulp-connect'),
|
||||
ngAnnotate = require('gulp-ng-annotate'),
|
||||
preprocess = require('gulp-preprocess'),
|
||||
runSequence = require('run-sequence'),
|
||||
merge = require('merge-stream'),
|
||||
ngConfig = require('gulp-ng-config'),
|
||||
settings = require('./settings.json'),
|
||||
project = require('./project.json'),
|
||||
project = require('./package.json'),
|
||||
jshint = require('gulp-jshint'),
|
||||
_ = require('lodash');
|
||||
_ = require('lodash'),
|
||||
webpack = require('webpack-stream');
|
||||
|
||||
var paths = {};
|
||||
paths.dist = '../../dist/';
|
||||
paths.webroot = './wwwroot/'
|
||||
paths.dist = './dist/';
|
||||
paths.webroot = './src/'
|
||||
paths.js = paths.webroot + 'js/**/*.js';
|
||||
paths.minJs = paths.webroot + 'js/**/*.min.js';
|
||||
paths.concatJsDest = paths.webroot + 'js/bw.min.js';
|
||||
paths.libDir = paths.webroot + 'lib/';
|
||||
paths.npmDir = 'node_modules/';
|
||||
paths.lessDir = 'less/';
|
||||
paths.lessDir = paths.webroot + 'less/';
|
||||
paths.cssDir = paths.webroot + 'css/';
|
||||
paths.jsDir = paths.webroot + 'js/';
|
||||
|
||||
var randomString = Math.random().toString(36).substring(7);
|
||||
|
||||
gulp.task('lint', function () {
|
||||
return gulp.src(paths.webroot + 'app/**/*.js')
|
||||
.pipe(jshint())
|
||||
@@ -39,7 +43,7 @@ gulp.task('lint', function () {
|
||||
gulp.task('build', function (cb) {
|
||||
return runSequence(
|
||||
'clean',
|
||||
['lib', 'less', 'settings', 'lint'],
|
||||
['lib', 'webpack', 'less', 'settings', 'lint'],
|
||||
cb);
|
||||
});
|
||||
|
||||
@@ -139,10 +143,6 @@ gulp.task('lib', ['clean:lib'], function () {
|
||||
src: paths.npmDir + 'angular-messages/*messages*.js',
|
||||
dest: paths.libDir + 'angular-messages'
|
||||
},
|
||||
{
|
||||
src: [paths.npmDir + 'sjcl/core/cbc.js', paths.npmDir + 'sjcl/core/bitArray.js', paths.npmDir + 'sjcl/sjcl.js'],
|
||||
dest: paths.libDir + 'sjcl'
|
||||
},
|
||||
{
|
||||
src: paths.npmDir + 'ngstorage/*.js',
|
||||
dest: paths.libDir + 'ngstorage'
|
||||
@@ -175,6 +175,33 @@ gulp.task('lib', ['clean:lib'], function () {
|
||||
return merge(tasks);
|
||||
});
|
||||
|
||||
gulp.task('webpack', ['webpack:forge']);
|
||||
|
||||
gulp.task('webpack:forge', function () {
|
||||
var forgeDir = paths.npmDir + '/node-forge/lib/';
|
||||
|
||||
return gulp.src([
|
||||
forgeDir + 'pbkdf2.js',
|
||||
forgeDir + 'aes.js',
|
||||
forgeDir + 'hmac.js',
|
||||
forgeDir + 'sha256.js',
|
||||
forgeDir + 'random.js',
|
||||
forgeDir + 'forge.js'
|
||||
]).pipe(webpack({
|
||||
output: {
|
||||
filename: 'forge.js',
|
||||
library: 'forge',
|
||||
libraryTarget: 'umd'
|
||||
},
|
||||
node: {
|
||||
Buffer: false,
|
||||
process: false,
|
||||
crypto: false,
|
||||
setImmediate: false
|
||||
}
|
||||
})).pipe(gulp.dest(paths.libDir + 'forge'));
|
||||
});
|
||||
|
||||
gulp.task('settings', function () {
|
||||
return config()
|
||||
.pipe(gulp.dest(paths.webroot + 'app'));
|
||||
@@ -187,9 +214,9 @@ function config() {
|
||||
constants: _.merge({}, {
|
||||
appSettings: {
|
||||
version: project.version,
|
||||
environment: project.environment
|
||||
environment: project.production ? 'Production' : 'Development'
|
||||
}
|
||||
}, require('./settings.' + project.environment + '.json') || {})
|
||||
}, require('./settings' + (project.production ? '.Production' : '') + '.json') || {})
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -211,7 +238,7 @@ gulp.task('dist:clean', function (cb) {
|
||||
gulp.task('dist:move', function () {
|
||||
var moves = [
|
||||
{
|
||||
src: '../../CNAME',
|
||||
src: './CNAME',
|
||||
dest: paths.dist
|
||||
},
|
||||
{
|
||||
@@ -261,7 +288,7 @@ gulp.task('dist:css', function () {
|
||||
paths.cssDir + '**/*.css',
|
||||
'!' + paths.cssDir + '**/*.min.css'
|
||||
])
|
||||
.pipe(preprocess({ context: settings }))
|
||||
.pipe(preprocess({ context: { cacheTag: randomString } }))
|
||||
.pipe(cssmin())
|
||||
.pipe(rename({ suffix: '.min' }))
|
||||
.pipe(gulp.dest(paths.dist + 'css'));
|
||||
@@ -277,7 +304,7 @@ gulp.task('dist:js:app', function () {
|
||||
]);
|
||||
|
||||
merge(mainStream, config())
|
||||
.pipe(preprocess({ context: settings }))
|
||||
.pipe(preprocess({ context: { cacheTag: randomString } }))
|
||||
.pipe(concat(paths.dist + '/js/app.min.js'))
|
||||
.pipe(ngAnnotate())
|
||||
.pipe(uglify())
|
||||
@@ -287,8 +314,6 @@ gulp.task('dist:js:app', function () {
|
||||
gulp.task('dist:js:lib', function () {
|
||||
return gulp
|
||||
.src([
|
||||
paths.libDir + 'sjcl/sjcl.js',
|
||||
paths.libDir + 'sjcl/*.js',
|
||||
paths.libDir + 'angulartics/angulartics.js',
|
||||
paths.libDir + '**/*.js',
|
||||
'!' + paths.libDir + '**/*.min.js',
|
||||
@@ -306,7 +331,7 @@ gulp.task('dist:preprocess', function () {
|
||||
.src([
|
||||
paths.dist + '/**/*.html'
|
||||
], { base: '.' })
|
||||
.pipe(preprocess({ context: settings }))
|
||||
.pipe(preprocess({ context: { cacheTag: randomString }}))
|
||||
.pipe(gulp.dest('.'));
|
||||
});
|
||||
|
||||
@@ -322,3 +347,10 @@ gulp.task('deploy', ['dist'], function () {
|
||||
return gulp.src(paths.dist + '**/*')
|
||||
.pipe(ghPages({ cacheDir: paths.dist + '.publish' }));
|
||||
});
|
||||
|
||||
gulp.task('serve', function () {
|
||||
connect.server({
|
||||
port: 4001,
|
||||
root: ['src']
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "bitwarden",
|
||||
"version": "0.0.0",
|
||||
"version": "1.9.1",
|
||||
"production": true,
|
||||
"devDependencies": {
|
||||
"connect": "3.4.1",
|
||||
"lodash": "4.13.1",
|
||||
@@ -14,6 +15,7 @@
|
||||
"gulp-preprocess": "2.0.0",
|
||||
"gulp-ng-annotate": "2.0.0",
|
||||
"gulp-ng-config": "1.3.1",
|
||||
"gulp-connect": "5.0.0",
|
||||
"jshint": "2.9.2",
|
||||
"gulp-jshint": "2.0.1",
|
||||
"rimraf": "2.5.2",
|
||||
@@ -22,7 +24,6 @@
|
||||
"jquery": "2.2.4",
|
||||
"font-awesome": "4.6.3",
|
||||
"bootstrap": "3.3.6",
|
||||
"sjcl": "1.0.3",
|
||||
"angular": "1.5.6",
|
||||
"angular-resource": "1.5.6",
|
||||
"angular-bootstrap-npm": "0.14.3",
|
||||
@@ -40,6 +41,8 @@
|
||||
"clipboard": "1.5.12",
|
||||
"ngclipboard": "1.1.1",
|
||||
"angulartics": "1.1.2",
|
||||
"angulartics-google-analytics": "0.2.1"
|
||||
"angulartics-google-analytics": "0.2.1",
|
||||
"node-forge": "0.7.0",
|
||||
"webpack-stream": "3.2.0"
|
||||
}
|
||||
}
|
||||
6
settings.json
Normal file
6
settings.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"appSettings": {
|
||||
"rememberedEmailCookieName": "bit.rememberedEmail",
|
||||
"apiUri": "http://localhost:4000"
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using System.IO;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
|
||||
namespace Bit.Web
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var host = new WebHostBuilder()
|
||||
.UseKestrel()
|
||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||
.UseIISIntegration()
|
||||
.UseStartup<Startup>()
|
||||
.Build();
|
||||
|
||||
host.Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("Bit.Web")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("bitwarden Web")]
|
||||
[assembly: AssemblyProduct("bitwarden Web")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2016")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("0bebf47c-ba0b-48ac-b48c-718f94084ad5")]
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:4001/",
|
||||
"sslPort": 0
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"Web": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "http://localhost:5001",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Web
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public void ConfigureServices(IServiceCollection services) { }
|
||||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseFileServer();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>0bebf47c-ba0b-48ac-b48c-718f94084ad5</ProjectGuid>
|
||||
<RootNamespace>Bit.Vault</RootNamespace>
|
||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
||||
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
||||
<TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
<DevelopmentServerPort>4001</DevelopmentServerPort>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\DotNet.Web\Microsoft.DotNet.Web.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
@@ -1,47 +0,0 @@
|
||||
@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700,300italic,400italic,600italic);
|
||||
@import "../node_modules/toastr/toastr.less";
|
||||
|
||||
/* Start AdminLTE */
|
||||
|
||||
//Bootstrap Variables & Mixins
|
||||
//The core bootstrap code have not been modified. These files
|
||||
//are included only for reference.
|
||||
@import (reference) "../node_modules/admin-lte/build/bootstrap-less/mixins.less";
|
||||
@import (reference) "../node_modules/admin-lte/build/bootstrap-less/variables.less";
|
||||
//MISC
|
||||
//----
|
||||
@import "../node_modules/admin-lte/build/less/core.less";
|
||||
@import "../node_modules/admin-lte/build/less/variables.less";
|
||||
@import "../node_modules/admin-lte/build/less/mixins.less";
|
||||
//COMPONENTS
|
||||
//-----------
|
||||
@import "../node_modules/admin-lte/build/less/header.less";
|
||||
@import "../node_modules/admin-lte/build/less/sidebar.less";
|
||||
@import "../node_modules/admin-lte/build/less/sidebar-mini.less";
|
||||
@import "../node_modules/admin-lte/build/less/control-sidebar.less";
|
||||
@import "../node_modules/admin-lte/build/less/dropdown.less";
|
||||
@import "../node_modules/admin-lte/build/less/forms.less";
|
||||
@import "../node_modules/admin-lte/build/less/progress-bars.less";
|
||||
@import "../node_modules/admin-lte/build/less/small-box.less";
|
||||
@import "../node_modules/admin-lte/build/less/boxes.less";
|
||||
@import "../node_modules/admin-lte/build/less/info-box.less";
|
||||
@import "../node_modules/admin-lte/build/less/timeline.less";
|
||||
@import "../node_modules/admin-lte/build/less/buttons.less";
|
||||
@import "../node_modules/admin-lte/build/less/callout.less";
|
||||
@import "../node_modules/admin-lte/build/less/alerts.less";
|
||||
@import "../node_modules/admin-lte/build/less/navs.less";
|
||||
@import "../node_modules/admin-lte/build/less/table.less";
|
||||
@import "../node_modules/admin-lte/build/less/labels.less";
|
||||
@import "../node_modules/admin-lte/build/less/modal.less";
|
||||
//PAGES
|
||||
//------
|
||||
@import "../node_modules/admin-lte/build/less/login_and_register.less";
|
||||
@import "../node_modules/admin-lte/build/less/404_500_errors.less";
|
||||
//Miscellaneous
|
||||
//-------------
|
||||
@import "../node_modules/admin-lte/build/less/miscellaneous.less";
|
||||
@import "../node_modules/admin-lte/build/less/print.less";
|
||||
|
||||
/* End AdminLTE */
|
||||
|
||||
@import "../node_modules/admin-lte/build/less/skins/skin-blue.less";
|
||||
@@ -1,56 +0,0 @@
|
||||
{
|
||||
"version": "1.1.0",
|
||||
"environment": "Development",
|
||||
|
||||
"dependencies": {
|
||||
"Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
|
||||
"Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
|
||||
"Microsoft.AspNetCore.StaticFiles": "1.0.0"
|
||||
},
|
||||
|
||||
"tools": {
|
||||
"Microsoft.AspNetCore.Server.IISIntegration.Tools": {
|
||||
"version": "1.0.0-preview2-final",
|
||||
"imports": "portable-net45+win8+dnxcore50"
|
||||
}
|
||||
},
|
||||
|
||||
"frameworks": {
|
||||
"netcoreapp1.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"version": "1.0.0",
|
||||
"type": "platform"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"buildOptions": {
|
||||
"emitEntryPoint": true,
|
||||
"preserveCompilationContext": true
|
||||
},
|
||||
|
||||
"runtimeOptions": {
|
||||
"gcServer": false,
|
||||
"gcConcurrent": true
|
||||
},
|
||||
|
||||
"publishOptions": {
|
||||
"include": [
|
||||
"wwwroot",
|
||||
"Views",
|
||||
"settings.json",
|
||||
"settings.Development.json",
|
||||
"settings.Production.json",
|
||||
"settings.Staging.json",
|
||||
"web.config"
|
||||
]
|
||||
},
|
||||
|
||||
"scripts": {
|
||||
"postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
|
||||
},
|
||||
|
||||
"userSecretsId": "aspnet-Vault-20160519103145"
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"appSettings": {
|
||||
"apiUri": "http://localhost:4000"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"appSettings": {
|
||||
"apiUri": "https://api.bitwarden.com"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"appSettings": {
|
||||
"rememberedEmailCookieName": "bit.rememberedEmail"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<system.webServer>
|
||||
<handlers>
|
||||
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified"/>
|
||||
</handlers>
|
||||
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false"/>
|
||||
</system.webServer>
|
||||
</configuration>
|
||||
@@ -1,70 +0,0 @@
|
||||
/// <autosync enabled="true" />
|
||||
/// <reference path="../gulpfile.js" />
|
||||
/// <reference path="app/accounts/accountsLoginController.js" />
|
||||
/// <reference path="app/accounts/accountsLogoutController.js" />
|
||||
/// <reference path="app/accounts/accountsmodule.js" />
|
||||
/// <reference path="app/accounts/accountspasswordhintcontroller.js" />
|
||||
/// <reference path="app/accounts/accountsRegisterController.js" />
|
||||
/// <reference path="app/apiInterceptor.js" />
|
||||
/// <reference path="app/app.js" />
|
||||
/// <reference path="app/config.js" />
|
||||
/// <reference path="app/directives/apiFieldDirective.js" />
|
||||
/// <reference path="app/directives/apiFormDirective.js" />
|
||||
/// <reference path="app/directives/directivesModule.js" />
|
||||
/// <reference path="app/directives/masterPasswordDirective.js" />
|
||||
/// <reference path="app/directives/pageTitleDirective.js" />
|
||||
/// <reference path="app/directives/passwordmeterdirective.js" />
|
||||
/// <reference path="app/directives/passwordviewerdirective.js" />
|
||||
/// <reference path="app/global/globalModule.js" />
|
||||
/// <reference path="app/global/mainController.js" />
|
||||
/// <reference path="app/global/sideNavController.js" />
|
||||
/// <reference path="app/global/topNavController.js" />
|
||||
/// <reference path="app/services/apiService.js" />
|
||||
/// <reference path="app/services/authService.js" />
|
||||
/// <reference path="app/services/cipherService.js" />
|
||||
/// <reference path="app/services/cryptoService.js" />
|
||||
/// <reference path="app/services/importservice.js" />
|
||||
/// <reference path="app/services/passwordservice.js" />
|
||||
/// <reference path="app/services/servicesModule.js" />
|
||||
/// <reference path="app/services/tokenService.js" />
|
||||
/// <reference path="app/services/validationservice.js" />
|
||||
/// <reference path="app/settings.js" />
|
||||
/// <reference path="app/settings/settingsChangeEmailController.js" />
|
||||
/// <reference path="app/settings/settingsChangePasswordController.js" />
|
||||
/// <reference path="app/settings/settingsController.js" />
|
||||
/// <reference path="app/settings/settingsdeletecontroller.js" />
|
||||
/// <reference path="app/settings/settingsmodule.js" />
|
||||
/// <reference path="app/settings/settingsSessionsController.js" />
|
||||
/// <reference path="app/settings/settingsTwoFactorController.js" />
|
||||
/// <reference path="app/tools/toolsAuditsController.js" />
|
||||
/// <reference path="app/tools/toolsController.js" />
|
||||
/// <reference path="app/tools/toolsExportController.js" />
|
||||
/// <reference path="app/tools/toolsImportController.js" />
|
||||
/// <reference path="app/tools/toolsmodule.js" />
|
||||
/// <reference path="app/vault/vaultAddFolderController.js" />
|
||||
/// <reference path="app/vault/vaultAddSiteController.js" />
|
||||
/// <reference path="app/vault/vaultController.js" />
|
||||
/// <reference path="app/vault/vaultEditFolderController.js" />
|
||||
/// <reference path="app/vault/vaultEditSiteController.js" />
|
||||
/// <reference path="app/vault/vaultmodule.js" />
|
||||
/// <reference path="lib/admin-lte/js/app.js" />
|
||||
/// <reference path="lib/angular/angular.js" />
|
||||
/// <reference path="lib/angular-bootstrap/angular-bootstrap-tpls.js" />
|
||||
/// <reference path="lib/angular-bootstrap-show-errors/showErrors.js" />
|
||||
/// <reference path="lib/angular-cookies/angular-cookies.js" />
|
||||
/// <reference path="lib/angular-jwt/angular-jwt.js" />
|
||||
/// <reference path="lib/angular-md5/angular-md5.js" />
|
||||
/// <reference path="lib/angular-messages/angular-messages.js" />
|
||||
/// <reference path="lib/angular-resource/angular-resource.js" />
|
||||
/// <reference path="lib/angular-toastr/angular-toastr.js" />
|
||||
/// <reference path="lib/angular-toastr/angular-toastr.tpls.js" />
|
||||
/// <reference path="lib/angular-ui-router/angular-ui-router.js" />
|
||||
/// <reference path="lib/bootstrap/js/bootstrap.min.js" />
|
||||
/// <reference path="lib/clipboard/clipboard.js" />
|
||||
/// <reference path="lib/jquery/jquery.js" />
|
||||
/// <reference path="lib/ngclipboard/ngclipboard.js" />
|
||||
/// <reference path="lib/ngstorage/ngStorage.js" />
|
||||
/// <reference path="lib/papaparse/papaparse.js" />
|
||||
/// <reference path="lib/sjcl/bitArray.js" />
|
||||
/// <reference path="lib/sjcl/cbc.js" />
|
||||
/// <reference path="lib/sjcl/sjcl.js" />
|
||||
@@ -1,112 +0,0 @@
|
||||
angular
|
||||
.module('bit.services')
|
||||
|
||||
.factory('authService', function (cryptoService, apiService, tokenService, $q, jwtHelper) {
|
||||
var _service = {},
|
||||
_userProfile = null;
|
||||
|
||||
_service.logIn = function (email, masterPassword) {
|
||||
email = email.toLowerCase();
|
||||
var key = cryptoService.makeKey(masterPassword, email);
|
||||
|
||||
var request = {
|
||||
email: email,
|
||||
masterPasswordHash: cryptoService.hashPassword(masterPassword, key)
|
||||
};
|
||||
|
||||
var deferred = $q.defer();
|
||||
apiService.auth.token(request, function (response) {
|
||||
if (!response || !response.Token) {
|
||||
return;
|
||||
}
|
||||
|
||||
tokenService.setToken(response.Token);
|
||||
cryptoService.setKey(key);
|
||||
_service.setUserProfile(response.Profile);
|
||||
|
||||
deferred.resolve(response);
|
||||
}, function (error) {
|
||||
deferred.reject(error);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
_service.logInTwoFactor = function (code, provider) {
|
||||
var request = {
|
||||
code: code,
|
||||
provider: provider
|
||||
};
|
||||
|
||||
var deferred = $q.defer();
|
||||
apiService.auth.tokenTwoFactor(request, function (response) {
|
||||
if (!response || !response.Token) {
|
||||
return;
|
||||
}
|
||||
|
||||
tokenService.setToken(response.Token);
|
||||
_service.setUserProfile(response.Profile);
|
||||
|
||||
deferred.resolve(response);
|
||||
}, function (error) {
|
||||
deferred.reject(error);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
_service.logOut = function () {
|
||||
tokenService.clearToken();
|
||||
cryptoService.clearKey();
|
||||
_userProfile = null;
|
||||
};
|
||||
|
||||
_service.getUserProfile = function () {
|
||||
if (!_userProfile) {
|
||||
_service.setUserProfile();
|
||||
}
|
||||
|
||||
return _userProfile;
|
||||
};
|
||||
|
||||
_service.setUserProfile = function (profile) {
|
||||
var token = tokenService.getToken();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
var decodedToken = jwtHelper.decodeToken(token);
|
||||
var twoFactor = decodedToken.authmethod === "TwoFactor";
|
||||
|
||||
_userProfile = {
|
||||
id: decodedToken.nameid,
|
||||
email: decodedToken.email,
|
||||
twoFactor: twoFactor
|
||||
};
|
||||
|
||||
if (!twoFactor && profile) {
|
||||
loadProfile(profile);
|
||||
}
|
||||
else if (!twoFactor && !profile) {
|
||||
apiService.accounts.getProfile({}, loadProfile);
|
||||
}
|
||||
};
|
||||
|
||||
function loadProfile(profile) {
|
||||
_userProfile.extended = {
|
||||
name: profile.Name,
|
||||
twoFactorEnabled: profile.TwoFactorEnabled,
|
||||
culture: profile.Culture
|
||||
};
|
||||
}
|
||||
|
||||
_service.isAuthenticated = function () {
|
||||
return _service.getUserProfile() !== null && !_service.getUserProfile().twoFactor;
|
||||
};
|
||||
|
||||
_service.isTwoFactorAuthenticated = function () {
|
||||
return _service.getUserProfile() !== null && _service.getUserProfile().twoFactor;
|
||||
};
|
||||
|
||||
return _service;
|
||||
});
|
||||
@@ -1,112 +0,0 @@
|
||||
angular
|
||||
.module('bit.services')
|
||||
|
||||
.factory('cipherService', function (cryptoService, apiService) {
|
||||
var _service = {};
|
||||
|
||||
_service.decryptSites = function (encryptedSites) {
|
||||
if (!encryptedSites) throw "encryptedSites is undefined or null";
|
||||
|
||||
var unencryptedSites = [];
|
||||
for (var i = 0; i < encryptedSites.length; i++) {
|
||||
unencryptedSites.push(_service.decryptSite(encryptedSites[i]));
|
||||
}
|
||||
|
||||
return unencryptedSites;
|
||||
};
|
||||
|
||||
_service.decryptSite = function (encryptedSite) {
|
||||
if (!encryptedSite) throw "encryptedSite is undefined or null";
|
||||
|
||||
var site = {
|
||||
id: encryptedSite.Id,
|
||||
'type': 1,
|
||||
folderId: encryptedSite.FolderId,
|
||||
favorite: encryptedSite.Favorite,
|
||||
name: cryptoService.decrypt(encryptedSite.Name),
|
||||
uri: encryptedSite.Uri && encryptedSite.Uri !== '' ? cryptoService.decrypt(encryptedSite.Uri) : null,
|
||||
username: encryptedSite.Username && encryptedSite.Username !== '' ? cryptoService.decrypt(encryptedSite.Username) : null,
|
||||
password: encryptedSite.Password && encryptedSite.Password !== '' ? cryptoService.decrypt(encryptedSite.Password) : null,
|
||||
notes: encryptedSite.Notes && encryptedSite.Notes !== '' ? cryptoService.decrypt(encryptedSite.Notes) : null
|
||||
};
|
||||
|
||||
if (encryptedSite.Folder) {
|
||||
site.folder = {
|
||||
name: cryptoService.decrypt(encryptedSite.Folder.Name)
|
||||
};
|
||||
}
|
||||
|
||||
return site;
|
||||
};
|
||||
|
||||
_service.decryptFolders = function (encryptedFolders) {
|
||||
if (!encryptedFolders) throw "encryptedFolders is undefined or null";
|
||||
|
||||
var unencryptedFolders = [];
|
||||
for (var i = 0; i < encryptedFolders.length; i++) {
|
||||
unencryptedFolders.push(_service.decryptFolder(encryptedFolders[i]));
|
||||
}
|
||||
|
||||
return unencryptedFolders;
|
||||
};
|
||||
|
||||
_service.decryptFolder = function (encryptedFolder) {
|
||||
if (!encryptedFolder) throw "encryptedFolder is undefined or null";
|
||||
|
||||
return {
|
||||
id: encryptedFolder.Id,
|
||||
'type': 0,
|
||||
name: cryptoService.decrypt(encryptedFolder.Name)
|
||||
};
|
||||
};
|
||||
|
||||
_service.encryptSites = function (unencryptedSites, key) {
|
||||
if (!unencryptedSites) throw "unencryptedSites is undefined or null";
|
||||
|
||||
var encryptedSites = [];
|
||||
for (var i = 0; i < unencryptedSites.length; i++) {
|
||||
encryptedSites.push(_service.encryptSite(unencryptedSites[i], key));
|
||||
}
|
||||
|
||||
return encryptedSites;
|
||||
};
|
||||
|
||||
_service.encryptSite = function (unencryptedSite, key) {
|
||||
if (!unencryptedSite) throw "unencryptedSite is undefined or null";
|
||||
|
||||
return {
|
||||
id: unencryptedSite.id,
|
||||
'type': 1,
|
||||
folderId: unencryptedSite.folderId === '' ? null : unencryptedSite.folderId,
|
||||
favorite: unencryptedSite.favorite !== null ? unencryptedSite.favorite : false,
|
||||
uri: !unencryptedSite.uri || unencryptedSite.uri === '' ? null : cryptoService.encrypt(unencryptedSite.uri, key),
|
||||
name: cryptoService.encrypt(unencryptedSite.name, key),
|
||||
username: !unencryptedSite.username || unencryptedSite.username === '' ? null : cryptoService.encrypt(unencryptedSite.username, key),
|
||||
password: !unencryptedSite.password || unencryptedSite.password === '' ? null : cryptoService.encrypt(unencryptedSite.password, key),
|
||||
notes: !unencryptedSite.notes || unencryptedSite.notes === '' ? null : cryptoService.encrypt(unencryptedSite.notes, key)
|
||||
};
|
||||
};
|
||||
|
||||
_service.encryptFolders = function (unencryptedFolders, key) {
|
||||
if (!unencryptedFolders) throw "unencryptedFolders is undefined or null";
|
||||
|
||||
var encryptedFolders = [];
|
||||
for (var i = 0; i < unencryptedFolders.length; i++) {
|
||||
encryptedFolders.push(_service.encryptFolder(unencryptedFolders[i], key));
|
||||
}
|
||||
|
||||
return encryptedFolders;
|
||||
};
|
||||
|
||||
_service.encryptFolder = function (unencryptedFolder, key) {
|
||||
if (!unencryptedFolder) throw "unencryptedFolder is undefined or null";
|
||||
|
||||
return {
|
||||
id: unencryptedFolder.id,
|
||||
'type': 0,
|
||||
name: cryptoService.encrypt(unencryptedFolder.name, key)
|
||||
};
|
||||
};
|
||||
|
||||
return _service;
|
||||
});
|
||||
@@ -1,114 +0,0 @@
|
||||
angular
|
||||
.module('bit.services')
|
||||
|
||||
.factory('cryptoService', function ($sessionStorage) {
|
||||
var _service = {},
|
||||
_key,
|
||||
_b64Key,
|
||||
_aes;
|
||||
|
||||
sjcl.beware["CBC mode is dangerous because it doesn't protect message integrity."]();
|
||||
|
||||
_service.setKey = function (key) {
|
||||
_key = key;
|
||||
$sessionStorage.key = sjcl.codec.base64.fromBits(key);
|
||||
};
|
||||
|
||||
_service.getKey = function (b64) {
|
||||
if (b64 && b64 === true && _b64Key) {
|
||||
return _b64Key;
|
||||
}
|
||||
else if (!b64 && _key) {
|
||||
return _key;
|
||||
}
|
||||
|
||||
if ($sessionStorage.key) {
|
||||
_key = sjcl.codec.base64.toBits($sessionStorage.key);
|
||||
}
|
||||
|
||||
if (b64 && b64 === true) {
|
||||
_b64Key = sjcl.codec.base64.fromBits(_key);
|
||||
return _b64Key;
|
||||
}
|
||||
|
||||
return _key;
|
||||
};
|
||||
|
||||
_service.clearKey = function () {
|
||||
_key = _b64Key = _aes = null;
|
||||
delete $sessionStorage.key;
|
||||
};
|
||||
|
||||
_service.makeKey = function (password, salt, b64) {
|
||||
var key = sjcl.misc.pbkdf2(password, salt, 5000, 256, null);
|
||||
|
||||
if (b64 && b64 === true) {
|
||||
return sjcl.codec.base64.fromBits(key);
|
||||
}
|
||||
|
||||
return key;
|
||||
};
|
||||
|
||||
_service.hashPassword = function (password, key) {
|
||||
if (!key) {
|
||||
key = _service.getKey();
|
||||
}
|
||||
|
||||
if (!password || !key) {
|
||||
throw 'Invalid parameters.';
|
||||
}
|
||||
|
||||
var hashBits = sjcl.misc.pbkdf2(key, password, 1, 256, null);
|
||||
return sjcl.codec.base64.fromBits(hashBits);
|
||||
};
|
||||
|
||||
_service.getAes = function () {
|
||||
if (!_aes && _service.getKey()) {
|
||||
_aes = new sjcl.cipher.aes(_service.getKey());
|
||||
}
|
||||
|
||||
return _aes;
|
||||
};
|
||||
|
||||
_service.encrypt = function (plaintextValue, key) {
|
||||
if (!_service.getKey() && !key) {
|
||||
throw 'Encryption key unavailable.';
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
key = _service.getKey();
|
||||
}
|
||||
|
||||
var response = {};
|
||||
var params = {
|
||||
mode: "cbc",
|
||||
iv: sjcl.random.randomWords(4, 10)
|
||||
};
|
||||
|
||||
var ctJson = sjcl.encrypt(key, plaintextValue, params, response);
|
||||
|
||||
var ct = ctJson.match(/"ct":"([^"]*)"/)[1];
|
||||
var iv = sjcl.codec.base64.fromBits(response.iv);
|
||||
|
||||
return iv + "|" + ct;
|
||||
};
|
||||
|
||||
_service.decrypt = function (encValue) {
|
||||
if (!_service.getAes()) {
|
||||
throw 'AES encryption unavailable.';
|
||||
}
|
||||
|
||||
var encPieces = encValue.split('|');
|
||||
if (encPieces.length !== 2) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var ivBits = sjcl.codec.base64.toBits(encPieces[0]);
|
||||
var ctBits = sjcl.codec.base64.toBits(encPieces[1]);
|
||||
|
||||
var decBits = sjcl.mode.cbc.decrypt(_service.getAes(), ctBits, ivBits, null);
|
||||
return sjcl.codec.utf8String.fromBits(decBits);
|
||||
};
|
||||
|
||||
return _service;
|
||||
});
|
||||
@@ -1,321 +0,0 @@
|
||||
angular
|
||||
.module('bit.services')
|
||||
|
||||
.factory('importService', function () {
|
||||
var _service = {};
|
||||
|
||||
_service.import = function (source, file, success, error) {
|
||||
switch (source) {
|
||||
case 'local':
|
||||
importLocal(file, success, error);
|
||||
break;
|
||||
case 'lastpass':
|
||||
importLastPass(file, success, error);
|
||||
break;
|
||||
case 'safeincloudcsv':
|
||||
importSafeInCloudCsv(file, success, error);
|
||||
break;
|
||||
case 'keypassxml':
|
||||
importKeyPassXml(file, success, error);
|
||||
break;
|
||||
default:
|
||||
error();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
function importLocal(file, success, error) {
|
||||
Papa.parse(file, {
|
||||
header: true,
|
||||
complete: function (results) {
|
||||
var folders = [],
|
||||
sites = [],
|
||||
folderRelationships = [];
|
||||
|
||||
angular.forEach(results.data, function (value, key) {
|
||||
if (!value.uri || value.uri === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
var folderIndex = folders.length,
|
||||
siteIndex = sites.length,
|
||||
hasFolder = value.folder && value.folder !== '',
|
||||
addFolder = hasFolder;
|
||||
|
||||
if (hasFolder) {
|
||||
for (var i = 0; i < folders.length; i++) {
|
||||
if (folders[i].name === value.folder) {
|
||||
addFolder = false;
|
||||
folderIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sites.push({
|
||||
favorite: value.favorite !== null ? value.favorite : false,
|
||||
uri: value.uri,
|
||||
username: value.username && value.username !== '' ? value.username : null,
|
||||
password: value.password,
|
||||
notes: value.notes && value.notes !== '' ? value.notes : null,
|
||||
name: value.name
|
||||
});
|
||||
|
||||
if (addFolder) {
|
||||
folders.push({
|
||||
name: value.folder
|
||||
});
|
||||
}
|
||||
|
||||
if (hasFolder) {
|
||||
var relationship = {
|
||||
key: siteIndex,
|
||||
value: folderIndex
|
||||
};
|
||||
folderRelationships.push(relationship);
|
||||
}
|
||||
});
|
||||
|
||||
success(folders, sites, folderRelationships);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function importLastPass(file, success, error) {
|
||||
if (file.type === 'text/html') {
|
||||
var reader = new FileReader();
|
||||
reader.readAsText(file, 'utf-8');
|
||||
reader.onload = function (evt) {
|
||||
var doc = $(evt.target.result);
|
||||
var pre = doc.find('pre');
|
||||
|
||||
if (pre.length === 1) {
|
||||
var csv = pre.text().trim();
|
||||
var results = Papa.parse(csv, { header: true });
|
||||
parseData(results.data);
|
||||
}
|
||||
else {
|
||||
var foundPre = false;
|
||||
for (var i = 0; i < doc.length; i++) {
|
||||
if (doc[i].tagName === 'PRE') {
|
||||
foundPre = true;
|
||||
var csv = doc[i].outerText.trim();
|
||||
var results = Papa.parse(csv, { header: true });
|
||||
parseData(results.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundPre) {
|
||||
error();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = function (evt) {
|
||||
error();
|
||||
};
|
||||
}
|
||||
else {
|
||||
Papa.parse(file, {
|
||||
header: true,
|
||||
complete: function (results) {
|
||||
parseData(results.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function parseData(data) {
|
||||
var folders = [],
|
||||
sites = [],
|
||||
siteRelationships = [];
|
||||
|
||||
angular.forEach(data, function (value, key) {
|
||||
if (!value.url || value.url === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
var folderIndex = folders.length,
|
||||
siteIndex = sites.length,
|
||||
hasFolder = value.grouping && value.grouping !== '' && value.grouping !== '(none)',
|
||||
addFolder = hasFolder;
|
||||
|
||||
if (hasFolder) {
|
||||
for (var i = 0; i < folders.length; i++) {
|
||||
if (folders[i].name === value.grouping) {
|
||||
addFolder = false;
|
||||
folderIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sites.push({
|
||||
favorite: value.fav === '1',
|
||||
uri: value.url,
|
||||
username: value.username && value.username !== '' ? value.username : null,
|
||||
password: value.password,
|
||||
notes: value.extra && value.extra !== '' ? value.extra : null,
|
||||
name: value.name
|
||||
});
|
||||
|
||||
if (addFolder) {
|
||||
folders.push({
|
||||
name: value.grouping
|
||||
});
|
||||
}
|
||||
|
||||
if (hasFolder) {
|
||||
var relationship = {
|
||||
key: siteIndex,
|
||||
value: folderIndex
|
||||
};
|
||||
siteRelationships.push(relationship);
|
||||
}
|
||||
});
|
||||
|
||||
success(folders, sites, siteRelationships);
|
||||
}
|
||||
}
|
||||
|
||||
function importSafeInCloudCsv(file, success, error) {
|
||||
Papa.parse(file, {
|
||||
header: true,
|
||||
complete: function (results) {
|
||||
var folders = [],
|
||||
sites = [],
|
||||
siteRelationships = [];
|
||||
|
||||
angular.forEach(results.data, function (value, key) {
|
||||
sites.push({
|
||||
favorite: false,
|
||||
uri: value.URL && value.URL !== '' ? value.URL : null,
|
||||
username: value.Login && value.Login !== '' ? value.Login : null,
|
||||
password: value.Password,
|
||||
notes: value.Notes && value.Notes !== '' ? value.Notes : null,
|
||||
name: value.Title
|
||||
});
|
||||
});
|
||||
|
||||
success(folders, sites, siteRelationships);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function importKeyPassXml(file, success, error) {
|
||||
var folders = [],
|
||||
sites = [],
|
||||
siteRelationships = [];
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.readAsText(file, 'utf-8');
|
||||
reader.onload = function (evt) {
|
||||
var xmlDoc = $.parseXML(evt.target.result),
|
||||
xml = $(xmlDoc);
|
||||
|
||||
var root = xml.find('Root');
|
||||
if (root.length) {
|
||||
var group = root.find('> Group');
|
||||
if (group.length) {
|
||||
traverse($(group[0]), true, '');
|
||||
success(folders, sites, siteRelationships);
|
||||
}
|
||||
}
|
||||
else {
|
||||
error();
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = function (evt) {
|
||||
error();
|
||||
};
|
||||
|
||||
function traverse(node, isRootNode, groupNamePrefix) {
|
||||
var nodeEntries = [];
|
||||
var folderIndex = folders.length;
|
||||
var groupName = groupNamePrefix;
|
||||
|
||||
if (!isRootNode) {
|
||||
if (groupName !== '') {
|
||||
groupName += ' > ';
|
||||
}
|
||||
groupName += node.find('> Name').text();
|
||||
folders.push({
|
||||
name: groupName
|
||||
});
|
||||
}
|
||||
|
||||
var entries = node.find('> Entry');
|
||||
if (entries.length) {
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
var entry = $(entries[i]);
|
||||
var siteIndex = sites.length;
|
||||
var site = {
|
||||
favorite: false,
|
||||
uri: '',
|
||||
username: '',
|
||||
password: '',
|
||||
notes: '',
|
||||
name: ''
|
||||
};
|
||||
|
||||
var entryStrings = entry.find('> String');
|
||||
for (var j = 0; j < entryStrings.length; j++) {
|
||||
var entryString = $(entryStrings[j]);
|
||||
|
||||
var key = entryString.find('> Key').text();
|
||||
var value = entryString.find('> Value').text();
|
||||
if (value === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case 'URL':
|
||||
site.uri = value;
|
||||
break;
|
||||
case 'UserName':
|
||||
site.username = value;
|
||||
break;
|
||||
case 'Password':
|
||||
site.password = value;
|
||||
break;
|
||||
case 'Title':
|
||||
site.name = value;
|
||||
break;
|
||||
case 'Notes':
|
||||
site.notes = site.notes === null ? value + '\n' : site.notes + value + '\n';
|
||||
break;
|
||||
default:
|
||||
// other custom fields
|
||||
site.notes = site.notes === null ? key + ': ' + value + '\n'
|
||||
: site.notes + key + ': ' + value + '\n';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (site.name === '') {
|
||||
site.name = '--';
|
||||
}
|
||||
|
||||
sites.push(site);
|
||||
|
||||
if (!isRootNode) {
|
||||
siteRelationships.push({
|
||||
key: siteIndex,
|
||||
value: folderIndex
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var groups = node.find('> Group');
|
||||
if (groups.length) {
|
||||
for (var k = 0; k < groups.length; k++) {
|
||||
traverse($(groups[k]), false, groupName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _service;
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
angular
|
||||
.module('bit.services')
|
||||
|
||||
.factory('tokenService', function ($sessionStorage) {
|
||||
var _service = {},
|
||||
_token;
|
||||
|
||||
_service.setToken = function (token) {
|
||||
$sessionStorage.authBearer = token;
|
||||
_token = token;
|
||||
};
|
||||
|
||||
_service.getToken = function () {
|
||||
if (!_token) {
|
||||
_token = $sessionStorage.authBearer;
|
||||
}
|
||||
|
||||
return _token;
|
||||
};
|
||||
|
||||
_service.clearToken = function () {
|
||||
_token = null;
|
||||
delete $sessionStorage.authBearer;
|
||||
};
|
||||
|
||||
return _service;
|
||||
});
|
||||
@@ -1,2 +0,0 @@
|
||||
angular.module("bit")
|
||||
.constant("appSettings", {"rememberedEmailCookieName":"bit.rememberedEmail","version":"1.1.0","environment":"Development","apiUri":"http://localhost:4000"});
|
||||
@@ -1,60 +0,0 @@
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
Settings
|
||||
<small>manage your account</small>
|
||||
</h1>
|
||||
</section>
|
||||
<section class="content">
|
||||
<div class="box box-default">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">General</h3>
|
||||
</div>
|
||||
<form role="form" name="profileForm" ng-submit="profileForm.$valid && save(model)" api-form="savePromise">
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-9">
|
||||
<div class="callout callout-danger validation-errors" ng-show="profileForm.$errors">
|
||||
<h4>Errors have occured</h4>
|
||||
<ul>
|
||||
<li ng-repeat="e in profileForm.$errors">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" name="Name" ng-model="model.name" class="form-control"
|
||||
required api-field />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email - <a href="javascript:void(0)" ng-click="changeEmail()">change</a></label>
|
||||
<input type="text" id="email" ng-model="model.email" class="form-control" readonly />
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="hint">Master Password Hint</label>
|
||||
<input type="text" id="hint" name="MasterPasswordHint" ng-model="model.masterPasswordHint"
|
||||
class="form-control" api-field />
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="culture">Language/Culture</label>
|
||||
<select id="culture" name="Culture" ng-model="model.culture" class="form-control" api-field>
|
||||
<option value="en-US">English (US)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3 settings-photo">
|
||||
<a href="http://www.gravatar.com/" target="_blank">
|
||||
<img src="//www.gravatar.com/avatar/{{ main.userProfile.email | gravatar }}.jpg?s=150&d=mm"
|
||||
class="img-rounded img-responsive" alt="User Image">
|
||||
</a>
|
||||
<a href="http://www.gravatar.com/" target="_blank" class="btn btn-link"
|
||||
analytics-on="click" analytics-event="Clicked Update Photo">Update Photo</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="profileForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="profileForm.$loading"></i>Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,36 +0,0 @@
|
||||
angular
|
||||
.module('bit.tools')
|
||||
|
||||
.controller('toolsImportController', function ($scope, $state, apiService, $uibModalInstance, cryptoService, cipherService, toastr, importService, $analytics) {
|
||||
$analytics.eventTrack('toolsImportController', { category: 'Modal' });
|
||||
$scope.model = { source: 'local' };
|
||||
|
||||
$scope.import = function (model) {
|
||||
$scope.processing = true;
|
||||
var file = document.getElementById('file').files[0];
|
||||
importService.import(model.source, file, importSuccess, importError);
|
||||
};
|
||||
|
||||
function importSuccess(folders, sites, folderRelationships) {
|
||||
apiService.ciphers.import({
|
||||
folders: cipherService.encryptFolders(folders, cryptoService.getKey()),
|
||||
sites: cipherService.encryptSites(sites, cryptoService.getKey()),
|
||||
folderRelationships: folderRelationships
|
||||
}, function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
$state.go('backend.vault').then(function () {
|
||||
$analytics.eventTrack('Imported Data', { label: $scope.model.source });
|
||||
toastr.success('Data has been successfully imported into your vault.', 'Import Success');
|
||||
});
|
||||
}, importError);
|
||||
}
|
||||
|
||||
function importError() {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
toastr.error('Something went wrong. Try again.', 'Oh No!');
|
||||
}
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
Tools
|
||||
<small>helpful utilities</small>
|
||||
</h1>
|
||||
</section>
|
||||
<section class="content">
|
||||
Several tools are available in the menu to the left. More tools coming soon...
|
||||
</section>
|
||||
@@ -1,51 +0,0 @@
|
||||
angular
|
||||
.module('bit.vault')
|
||||
|
||||
.controller('vaultEditSiteController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, passwordService, siteId, folders, $analytics) {
|
||||
$analytics.eventTrack('vaultEditSiteController', { category: 'Modal' });
|
||||
$scope.folders = folders;
|
||||
$scope.site = {};
|
||||
|
||||
apiService.sites.get({ id: siteId }, function (site) {
|
||||
$scope.site = cipherService.decryptSite(site);
|
||||
});
|
||||
|
||||
$scope.save = function (model) {
|
||||
var site = cipherService.encryptSite(model);
|
||||
$scope.savePromise = apiService.sites.put({ id: siteId }, site, function (siteResponse) {
|
||||
$analytics.eventTrack('Edited Site');
|
||||
var decSite = cipherService.decryptSite(siteResponse);
|
||||
$uibModalInstance.close(decSite);
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
$scope.generatePassword = function () {
|
||||
if (!$scope.site.password || confirm('Are you sure you want to overwrite the current password?')) {
|
||||
$analytics.eventTrack('Generated Password From Edit');
|
||||
$scope.site.password = passwordService.generatePassword({ length: 10, 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('cancel');
|
||||
};
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
angular
|
||||
.module('bit.accounts')
|
||||
|
||||
.controller('accountsLoginController', function ($scope, $rootScope, $cookies, apiService, cryptoService, authService, $state, appSettings, $analytics) {
|
||||
.controller('accountsLoginController', function ($scope, $rootScope, $cookies, apiService, cryptoService, authService,
|
||||
$state, appSettings, $analytics) {
|
||||
var rememberedEmail = $cookies.get(appSettings.rememberedEmailCookieName);
|
||||
if (rememberedEmail) {
|
||||
$scope.model = {
|
||||
@@ -10,10 +11,13 @@ angular
|
||||
};
|
||||
}
|
||||
|
||||
var email,
|
||||
masterPassword;
|
||||
|
||||
$scope.login = function (model) {
|
||||
$scope.loginPromise = authService.logIn(model.email, model.masterPassword);
|
||||
|
||||
$scope.loginPromise.then(function () {
|
||||
$scope.loginPromise.then(function (twoFactorProviders) {
|
||||
if (model.rememberEmail) {
|
||||
var cookieExpiration = new Date();
|
||||
cookieExpiration.setFullYear(cookieExpiration.getFullYear() + 10);
|
||||
@@ -27,8 +31,10 @@ angular
|
||||
$cookies.remove(appSettings.rememberedEmailCookieName);
|
||||
}
|
||||
|
||||
var profile = authService.getUserProfile();
|
||||
if (profile.twoFactor) {
|
||||
if (twoFactorProviders && twoFactorProviders.length > 0) {
|
||||
email = model.email;
|
||||
masterPassword = model.masterPassword;
|
||||
|
||||
$analytics.eventTrack('Logged In To Two-step');
|
||||
$state.go('frontend.login.twoFactor');
|
||||
}
|
||||
@@ -40,8 +46,8 @@ angular
|
||||
};
|
||||
|
||||
$scope.twoFactor = function (model) {
|
||||
// Only supporting Authenticator provider for now
|
||||
$scope.twoFactorPromise = authService.logInTwoFactor(model.code, "Authenticator");
|
||||
// Only supporting Authenticator (0) provider for now
|
||||
$scope.twoFactorPromise = authService.logIn(email, masterPassword, model.code, 0);
|
||||
|
||||
$scope.twoFactorPromise.then(function () {
|
||||
$analytics.eventTrack('Logged In From Two-step');
|
||||
21
src/app/accounts/accountsRecoverController.js
Normal file
21
src/app/accounts/accountsRecoverController.js
Normal file
@@ -0,0 +1,21 @@
|
||||
angular
|
||||
.module('bit.accounts')
|
||||
|
||||
.controller('accountsRecoverController', function ($scope, apiService, cryptoService) {
|
||||
$scope.success = false;
|
||||
|
||||
$scope.submit = function (model) {
|
||||
var email = model.email.toLowerCase();
|
||||
var key = cryptoService.makeKey(model.masterPassword, email);
|
||||
|
||||
var request = {
|
||||
email: email,
|
||||
masterPasswordHash: cryptoService.hashPassword(model.masterPassword, key),
|
||||
recoveryCode: model.code.replace(/\s/g, '').toLowerCase()
|
||||
};
|
||||
|
||||
$scope.submitPromise = apiService.accounts.postTwoFactorRecover(request, function () {
|
||||
$scope.success = true;
|
||||
}).$promise;
|
||||
};
|
||||
});
|
||||
@@ -13,7 +13,10 @@
|
||||
<span class="fa fa-lock form-control-feedback"></span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-offset-7 col-xs-5">
|
||||
<div class="col-xs-7">
|
||||
<a ui-sref="frontend.recover">Lost authenticator app?</a>
|
||||
</div>
|
||||
<div class="col-xs-5">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-flat" ng-disabled="twoFactorForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="twoFactorForm.$loading"></i>Log In
|
||||
</button>
|
||||
52
src/app/accounts/views/accountsRecover.html
Normal file
52
src/app/accounts/views/accountsRecover.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<div class="login-box">
|
||||
<div class="login-logo">
|
||||
<i class="fa fa-shield"></i> <b>bit</b>warden
|
||||
</div>
|
||||
<div class="login-box-body">
|
||||
<p class="login-box-msg">Lost your authenticator app?</p>
|
||||
<div class="text-center" ng-show="success">
|
||||
<div class="callout callout-success">
|
||||
Two-step login has been successfully disabled on your account.
|
||||
</div>
|
||||
<a ui-sref="frontend.login.info">Ready to log in?</a>
|
||||
</div>
|
||||
<form name="recoverForm" ng-submit="recoverForm.$valid && submit(model)" ng-show="!success"
|
||||
api-form="submitPromise">
|
||||
<div class="callout callout-danger validation-errors" ng-show="recoverForm.$errors">
|
||||
<h4>Errors have occured</h4>
|
||||
<ul>
|
||||
<li ng-repeat="e in recoverForm.$errors">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group has-feedback" show-errors>
|
||||
<label for="email" class="sr-only">Email</label>
|
||||
<input type="email" id="email" name="Email" class="form-control" placeholder="Email" ng-model="model.email"
|
||||
required api-field />
|
||||
<span class="fa fa-envelope form-control-feedback"></span>
|
||||
</div>
|
||||
<div class="form-group has-feedback" show-errors>
|
||||
<label for="masterPassword" class="sr-only">Master Password</label>
|
||||
<input type="password" id="masterPassword" name="MasterPasswordHash" class="form-control" placeholder="Master Password"
|
||||
ng-model="model.masterPassword"
|
||||
required api-field />
|
||||
<span class="fa fa-lock form-control-feedback"></span>
|
||||
</div>
|
||||
<div class="form-group has-feedback" show-errors>
|
||||
<label for="code" class="sr-only">Recovery code</label>
|
||||
<input type="text" id="code" name="RecoveryCode" class="form-control" placeholder="Recovery code"
|
||||
ng-model="model.code" required api-field />
|
||||
<span class="fa fa-key form-control-feedback"></span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-7">
|
||||
<a ui-sref="frontend.login.info">Ready to log in?</a>
|
||||
</div>
|
||||
<div class="col-xs-5">
|
||||
<button type="submit" class="btn btn-primary btn-block btn-flat" ng-disabled="recoverForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="recoverForm.$loading"></i>Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,11 +2,44 @@ angular
|
||||
.module('bit')
|
||||
|
||||
.config(function ($stateProvider, $urlRouterProvider, $httpProvider, jwtInterceptorProvider, $uibTooltipProvider, toastrConfig) {
|
||||
jwtInterceptorProvider.urlParam = 'access_token';
|
||||
jwtInterceptorProvider.tokenGetter = /*@ngInject*/ function (config, appSettings, tokenService) {
|
||||
if (config.url.indexOf(appSettings.apiUri) === 0) {
|
||||
return tokenService.getToken();
|
||||
jwtInterceptorProvider.urlParam = 'access_token2';
|
||||
var refreshPromise;
|
||||
jwtInterceptorProvider.tokenGetter = /*@ngInject*/ function (config, appSettings, tokenService, apiService, jwtHelper, $q) {
|
||||
if (config.url.indexOf(appSettings.apiUri) !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (refreshPromise) {
|
||||
return refreshPromise;
|
||||
}
|
||||
|
||||
var token = tokenService.getToken();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tokenService.tokenNeedsRefresh(token)) {
|
||||
return token;
|
||||
}
|
||||
|
||||
var refreshToken = tokenService.getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
var deferred = $q.defer();
|
||||
apiService.identity.token({
|
||||
grant_type: 'refresh_token',
|
||||
client_id: 'web',
|
||||
refresh_token: refreshToken
|
||||
}, function (response) {
|
||||
tokenService.setToken(response.access_token);
|
||||
tokenService.setRefreshToken(response.refresh_token);
|
||||
refreshPromise = null;
|
||||
deferred.resolve(response.access_token);
|
||||
});
|
||||
refreshPromise = deferred.promise;
|
||||
return refreshPromise;
|
||||
};
|
||||
|
||||
angular.extend(toastrConfig, {
|
||||
@@ -17,7 +50,9 @@ angular
|
||||
});
|
||||
|
||||
$uibTooltipProvider.options({
|
||||
popupDelay: 600
|
||||
popupDelay: 600,
|
||||
appendToBody: true
|
||||
|
||||
});
|
||||
|
||||
if ($httpProvider.defaults.headers.post) {
|
||||
@@ -41,7 +76,7 @@ angular
|
||||
}
|
||||
})
|
||||
.state('backend.vault', {
|
||||
url: '^/',
|
||||
url: '^/vault',
|
||||
templateUrl: 'app/vault/views/vault.html',
|
||||
controller: 'vaultController',
|
||||
data: { pageTitle: 'My Vault' }
|
||||
@@ -52,6 +87,12 @@ angular
|
||||
controller: 'settingsController',
|
||||
data: { pageTitle: 'Settings' }
|
||||
})
|
||||
.state('backend.settingsDomains', {
|
||||
url: '^/settings/domains',
|
||||
templateUrl: 'app/settings/views/settingsDomains.html',
|
||||
controller: 'settingsDomainsController',
|
||||
data: { pageTitle: 'Domain Settings' }
|
||||
})
|
||||
.state('backend.tools', {
|
||||
url: '^/tools',
|
||||
templateUrl: 'app/tools/views/tools.html',
|
||||
@@ -75,14 +116,14 @@ angular
|
||||
}
|
||||
})
|
||||
.state('frontend.login.info', {
|
||||
url: '^/login',
|
||||
url: '^/',
|
||||
templateUrl: 'app/accounts/views/accountsLoginInfo.html',
|
||||
data: {
|
||||
pageTitle: 'Log In'
|
||||
}
|
||||
})
|
||||
.state('frontend.login.twoFactor', {
|
||||
url: '^/login/two-factor',
|
||||
url: '^/two-factor',
|
||||
templateUrl: 'app/accounts/views/accountsLoginTwoFactor.html',
|
||||
data: {
|
||||
pageTitle: 'Log In (Two Factor)',
|
||||
@@ -105,6 +146,15 @@ angular
|
||||
bodyClass: 'login-page'
|
||||
}
|
||||
})
|
||||
.state('frontend.recover', {
|
||||
url: '^/recover',
|
||||
templateUrl: 'app/accounts/views/accountsRecover.html',
|
||||
controller: 'accountsRecoverController',
|
||||
data: {
|
||||
pageTitle: 'Recover Account',
|
||||
bodyClass: 'login-page'
|
||||
}
|
||||
})
|
||||
.state('frontend.register', {
|
||||
url: '^/register',
|
||||
templateUrl: 'app/accounts/views/accountsRegister.html',
|
||||
@@ -118,7 +168,7 @@ angular
|
||||
.run(function ($rootScope, authService, jwtHelper, tokenService, $state) {
|
||||
$rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
|
||||
if (!toState.data || !toState.data.authorize) {
|
||||
if (authService.isAuthenticated() && !jwtHelper.isTokenExpired(tokenService.getToken())) {
|
||||
if (authService.isAuthenticated()) {
|
||||
event.preventDefault();
|
||||
$state.go('backend.vault');
|
||||
}
|
||||
@@ -126,7 +176,7 @@ angular
|
||||
return;
|
||||
}
|
||||
|
||||
if (!authService.isAuthenticated() || jwtHelper.isTokenExpired(tokenService.getToken())) {
|
||||
if (!authService.isAuthenticated()) {
|
||||
event.preventDefault();
|
||||
authService.logOut();
|
||||
$state.go('frontend.login.info');
|
||||
@@ -40,43 +40,11 @@ angular
|
||||
$state.go('backend.vault');
|
||||
};
|
||||
|
||||
$scope.addSite = function () {
|
||||
$scope.$broadcast('vaultAddSite');
|
||||
$scope.addLogin = function () {
|
||||
$scope.$broadcast('vaultAddLogin');
|
||||
};
|
||||
|
||||
$scope.addFolder = function () {
|
||||
$scope.$broadcast('vaultAddFolder');
|
||||
};
|
||||
|
||||
$scope.changeEmail = function () {
|
||||
$scope.$broadcast('settingsChangeEmail');
|
||||
};
|
||||
|
||||
$scope.changePassword = function () {
|
||||
$scope.$broadcast('settingsChangePassword');
|
||||
};
|
||||
|
||||
$scope.sessions = function () {
|
||||
$scope.$broadcast('settingsSessions');
|
||||
};
|
||||
|
||||
$scope.delete = function () {
|
||||
$scope.$broadcast('settingsDelete');
|
||||
};
|
||||
|
||||
$scope.twoFactor = function () {
|
||||
$scope.$broadcast('settingsTwoFactor');
|
||||
};
|
||||
|
||||
$scope.import = function () {
|
||||
$scope.$broadcast('toolsImport');
|
||||
};
|
||||
|
||||
$scope.export = function () {
|
||||
$scope.$broadcast('toolsExport');
|
||||
};
|
||||
|
||||
$scope.audits = function () {
|
||||
$scope.$broadcast('toolsAudits');
|
||||
};
|
||||
});
|
||||
@@ -1,11 +1,11 @@
|
||||
angular
|
||||
.module('bit.services')
|
||||
|
||||
.factory('apiService', function ($resource, tokenService, appSettings) {
|
||||
.factory('apiService', function ($resource, tokenService, appSettings, $httpParamSerializer) {
|
||||
var _service = {},
|
||||
_apiUri = appSettings.apiUri;
|
||||
|
||||
_service.sites = $resource(_apiUri + '/sites/:id', {}, {
|
||||
_service.logins = $resource(_apiUri + '/sites/:id', {}, {
|
||||
get: { method: 'GET', params: { id: '@id' } },
|
||||
list: { method: 'GET', params: {} },
|
||||
post: { method: 'POST', params: {} },
|
||||
@@ -36,18 +36,36 @@
|
||||
putPassword: { url: _apiUri + '/accounts/password', method: 'POST', params: {} },
|
||||
getProfile: { url: _apiUri + '/accounts/profile', method: 'GET', params: {} },
|
||||
putProfile: { url: _apiUri + '/accounts/profile', method: 'POST', params: {} },
|
||||
getDomains: { url: _apiUri + '/accounts/domains', method: 'GET', params: {} },
|
||||
putDomains: { url: _apiUri + '/accounts/domains', method: 'POST', params: {} },
|
||||
getTwoFactor: { url: _apiUri + '/accounts/two-factor', method: 'GET', params: {} },
|
||||
putTwoFactor: { url: _apiUri + '/accounts/two-factor', method: 'POST', params: {} },
|
||||
postTwoFactorRecover: { url: _apiUri + '/accounts/two-factor-recover', method: 'POST', params: {} },
|
||||
postPasswordHint: { url: _apiUri + '/accounts/password-hint', method: 'POST', params: {} },
|
||||
putSecurityStamp: { url: _apiUri + '/accounts/security-stamp', method: 'POST', params: {} },
|
||||
'import': { url: _apiUri + '/accounts/import', method: 'POST', params: {} },
|
||||
postDelete: { url: _apiUri + '/accounts/delete', method: 'POST', params: {} }
|
||||
});
|
||||
|
||||
_service.auth = $resource(_apiUri + '/auth', {}, {
|
||||
token: { url: _apiUri + '/auth/token', method: 'POST', params: {} },
|
||||
tokenTwoFactor: { url: _apiUri + '/auth/token/two-factor', method: 'POST', params: {} }
|
||||
_service.settings = $resource(_apiUri + '/settings', {}, {
|
||||
getDomains: { url: _apiUri + '/settings/domains', method: 'GET', params: {} },
|
||||
putDomains: { url: _apiUri + '/settings/domains', method: 'POST', params: {} },
|
||||
});
|
||||
|
||||
_service.identity = $resource(_apiUri + '/connect', {}, {
|
||||
token: {
|
||||
url: _apiUri + '/connect/token',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' },
|
||||
transformRequest: transformUrlEncoded,
|
||||
skipAuthorization: true,
|
||||
params: {}
|
||||
}
|
||||
});
|
||||
|
||||
function transformUrlEncoded(data) {
|
||||
return $httpParamSerializer(data);
|
||||
}
|
||||
|
||||
return _service;
|
||||
});
|
||||
93
src/app/services/authService.js
Normal file
93
src/app/services/authService.js
Normal file
@@ -0,0 +1,93 @@
|
||||
angular
|
||||
.module('bit.services')
|
||||
|
||||
.factory('authService', function (cryptoService, apiService, tokenService, $q, jwtHelper) {
|
||||
var _service = {},
|
||||
_userProfile = null;
|
||||
|
||||
_service.logIn = function (email, masterPassword, token, provider) {
|
||||
email = email.toLowerCase();
|
||||
var key = cryptoService.makeKey(masterPassword, email);
|
||||
|
||||
var request = {
|
||||
username: email,
|
||||
password: cryptoService.hashPassword(masterPassword, key),
|
||||
grant_type: 'password',
|
||||
scope: 'api offline_access',
|
||||
client_id: 'web'
|
||||
};
|
||||
|
||||
if (token && typeof (provider) !== 'undefined' && provider !== null) {
|
||||
request.twoFactorToken = token.replace(' ', '');
|
||||
request.twoFactorProvider = provider;
|
||||
}
|
||||
|
||||
// TODO: device information one day?
|
||||
|
||||
var deferred = $q.defer();
|
||||
apiService.identity.token(request, function (response) {
|
||||
if (!response || !response.access_token) {
|
||||
return;
|
||||
}
|
||||
|
||||
tokenService.setToken(response.access_token);
|
||||
tokenService.setRefreshToken(response.refresh_token);
|
||||
cryptoService.setKey(key);
|
||||
deferred.resolve();
|
||||
}, function (error) {
|
||||
if (error.status === 400 && error.data.TwoFactorProviders && error.data.TwoFactorProviders.length) {
|
||||
deferred.resolve(error.data.TwoFactorProviders);
|
||||
}
|
||||
else {
|
||||
deferred.reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
_service.logOut = function () {
|
||||
tokenService.clearToken();
|
||||
tokenService.clearRefreshToken();
|
||||
cryptoService.clearKey();
|
||||
_userProfile = null;
|
||||
};
|
||||
|
||||
_service.getUserProfile = function () {
|
||||
if (!_userProfile) {
|
||||
_service.setUserProfile();
|
||||
}
|
||||
|
||||
return _userProfile;
|
||||
};
|
||||
|
||||
_service.setUserProfile = function () {
|
||||
var token = tokenService.getToken();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
var decodedToken = jwtHelper.decodeToken(token);
|
||||
|
||||
_userProfile = {
|
||||
id: decodedToken.name,
|
||||
email: decodedToken.email
|
||||
};
|
||||
|
||||
apiService.accounts.getProfile({}, loadProfile);
|
||||
};
|
||||
|
||||
function loadProfile(profile) {
|
||||
_userProfile.extended = {
|
||||
name: profile.Name,
|
||||
twoFactorEnabled: profile.TwoFactorEnabled,
|
||||
culture: profile.Culture
|
||||
};
|
||||
}
|
||||
|
||||
_service.isAuthenticated = function () {
|
||||
return tokenService.getToken() !== null;
|
||||
};
|
||||
|
||||
return _service;
|
||||
});
|
||||
112
src/app/services/cipherService.js
Normal file
112
src/app/services/cipherService.js
Normal file
@@ -0,0 +1,112 @@
|
||||
angular
|
||||
.module('bit.services')
|
||||
|
||||
.factory('cipherService', function (cryptoService, apiService) {
|
||||
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;
|
||||
};
|
||||
|
||||
_service.decryptLogin = function (encryptedLogin) {
|
||||
if (!encryptedLogin) throw "encryptedLogin is undefined or null";
|
||||
|
||||
var login = {
|
||||
id: encryptedLogin.Id,
|
||||
'type': 1,
|
||||
folderId: encryptedLogin.FolderId,
|
||||
favorite: encryptedLogin.Favorite,
|
||||
name: cryptoService.decrypt(encryptedLogin.Name),
|
||||
uri: encryptedLogin.Uri && encryptedLogin.Uri !== '' ? cryptoService.decrypt(encryptedLogin.Uri) : null,
|
||||
username: encryptedLogin.Username && encryptedLogin.Username !== '' ? cryptoService.decrypt(encryptedLogin.Username) : null,
|
||||
password: encryptedLogin.Password && encryptedLogin.Password !== '' ? cryptoService.decrypt(encryptedLogin.Password) : null,
|
||||
notes: encryptedLogin.Notes && encryptedLogin.Notes !== '' ? cryptoService.decrypt(encryptedLogin.Notes) : null
|
||||
};
|
||||
|
||||
if (encryptedLogin.Folder) {
|
||||
login.folder = {
|
||||
name: cryptoService.decrypt(encryptedLogin.Folder.Name)
|
||||
};
|
||||
}
|
||||
|
||||
return login;
|
||||
};
|
||||
|
||||
_service.decryptFolders = function (encryptedFolders) {
|
||||
if (!encryptedFolders) throw "encryptedFolders is undefined or null";
|
||||
|
||||
var unencryptedFolders = [];
|
||||
for (var i = 0; i < encryptedFolders.length; i++) {
|
||||
unencryptedFolders.push(_service.decryptFolder(encryptedFolders[i]));
|
||||
}
|
||||
|
||||
return unencryptedFolders;
|
||||
};
|
||||
|
||||
_service.decryptFolder = function (encryptedFolder) {
|
||||
if (!encryptedFolder) throw "encryptedFolder is undefined or null";
|
||||
|
||||
return {
|
||||
id: encryptedFolder.Id,
|
||||
'type': 0,
|
||||
name: cryptoService.decrypt(encryptedFolder.Name)
|
||||
};
|
||||
};
|
||||
|
||||
_service.encryptLogins = function (unencryptedLogins, key) {
|
||||
if (!unencryptedLogins) throw "unencryptedLogins is undefined or null";
|
||||
|
||||
var encryptedLogins = [];
|
||||
for (var i = 0; i < unencryptedLogins.length; i++) {
|
||||
encryptedLogins.push(_service.encryptLogin(unencryptedLogins[i], key));
|
||||
}
|
||||
|
||||
return encryptedLogins;
|
||||
};
|
||||
|
||||
_service.encryptLogin = function (unencryptedLogin, key) {
|
||||
if (!unencryptedLogin) throw "unencryptedLogin is undefined or null";
|
||||
|
||||
return {
|
||||
id: unencryptedLogin.id,
|
||||
'type': 1,
|
||||
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)
|
||||
};
|
||||
};
|
||||
|
||||
_service.encryptFolders = function (unencryptedFolders, key) {
|
||||
if (!unencryptedFolders) throw "unencryptedFolders is undefined or null";
|
||||
|
||||
var encryptedFolders = [];
|
||||
for (var i = 0; i < unencryptedFolders.length; i++) {
|
||||
encryptedFolders.push(_service.encryptFolder(unencryptedFolders[i], key));
|
||||
}
|
||||
|
||||
return encryptedFolders;
|
||||
};
|
||||
|
||||
_service.encryptFolder = function (unencryptedFolder, key) {
|
||||
if (!unencryptedFolder) throw "unencryptedFolder is undefined or null";
|
||||
|
||||
return {
|
||||
id: unencryptedFolder.id,
|
||||
'type': 0,
|
||||
name: cryptoService.encrypt(unencryptedFolder.name, key)
|
||||
};
|
||||
};
|
||||
|
||||
return _service;
|
||||
});
|
||||
153
src/app/services/cryptoService.js
Normal file
153
src/app/services/cryptoService.js
Normal file
@@ -0,0 +1,153 @@
|
||||
angular
|
||||
.module('bit.services')
|
||||
|
||||
.factory('cryptoService', function ($sessionStorage) {
|
||||
var _service = {},
|
||||
_key,
|
||||
_b64Key;
|
||||
|
||||
_service.setKey = function (key) {
|
||||
_key = key;
|
||||
$sessionStorage.key = forge.util.encode64(key);
|
||||
};
|
||||
|
||||
_service.getKey = function (b64) {
|
||||
if (b64 && b64 === true && _b64Key) {
|
||||
return _b64Key;
|
||||
}
|
||||
else if (!b64 && _key) {
|
||||
return _key;
|
||||
}
|
||||
|
||||
if ($sessionStorage.key) {
|
||||
_key = forge.util.decode64($sessionStorage.key);
|
||||
}
|
||||
|
||||
if (b64 && b64 === true) {
|
||||
_b64Key = forge.util.encode64(_key);
|
||||
return _b64Key;
|
||||
}
|
||||
|
||||
return _key;
|
||||
};
|
||||
|
||||
_service.getEncKey = function (key) {
|
||||
key = key || _service.getKey();
|
||||
|
||||
var buffer = forge.util.createBuffer(key);
|
||||
return buffer.getBytes(16);
|
||||
};
|
||||
|
||||
_service.getMacKey = function (key) {
|
||||
key = key || _service.getKey();
|
||||
|
||||
var buffer = forge.util.createBuffer(key);
|
||||
buffer.getBytes(16); // skip first half
|
||||
return buffer.getBytes(16);
|
||||
};
|
||||
|
||||
_service.clearKey = function () {
|
||||
_key = _b64Key = null;
|
||||
delete $sessionStorage.key;
|
||||
};
|
||||
|
||||
_service.makeKey = function (password, salt, b64) {
|
||||
var key = forge.pbkdf2(forge.util.encodeUtf8(password), forge.util.encodeUtf8(salt),
|
||||
5000, 256 / 8, 'sha256');
|
||||
|
||||
if (b64 && b64 === true) {
|
||||
return forge.util.encode64(key);
|
||||
}
|
||||
|
||||
return key;
|
||||
};
|
||||
|
||||
_service.hashPassword = function (password, key) {
|
||||
if (!key) {
|
||||
key = _service.getKey();
|
||||
}
|
||||
|
||||
if (!password || !key) {
|
||||
throw 'Invalid parameters.';
|
||||
}
|
||||
|
||||
var hashBits = forge.pbkdf2(key, forge.util.encodeUtf8(password), 1, 256 / 8, 'sha256');
|
||||
return forge.util.encode64(hashBits);
|
||||
};
|
||||
|
||||
_service.encrypt = function (plaintextValue, key) {
|
||||
if (!_service.getKey() && !key) {
|
||||
throw 'Encryption key unavailable.';
|
||||
}
|
||||
|
||||
// TODO: Turn on whenever ready to support encrypt-then-mac
|
||||
var encKey = null;
|
||||
if (false) {
|
||||
encKey = _service.getEncKey(key);
|
||||
}
|
||||
else {
|
||||
encKey = key || _service.getKey();
|
||||
}
|
||||
|
||||
var buffer = forge.util.createBuffer(plaintextValue, 'utf8');
|
||||
var ivBytes = forge.random.getBytesSync(16);
|
||||
var cipher = forge.cipher.createCipher('AES-CBC', encKey);
|
||||
cipher.start({ iv: ivBytes });
|
||||
cipher.update(buffer);
|
||||
cipher.finish();
|
||||
|
||||
var iv = forge.util.encode64(ivBytes);
|
||||
var ctBytes = cipher.output.getBytes();
|
||||
var ct = forge.util.encode64(ctBytes);
|
||||
var cipherString = iv + '|' + ct;
|
||||
|
||||
// TODO: Turn on whenever ready to support encrypt-then-mac
|
||||
if (false) {
|
||||
var mac = computeMac(ctBytes, ivBytes);
|
||||
cipherString = cipherString + '|' + mac;
|
||||
}
|
||||
|
||||
return cipherString;
|
||||
};
|
||||
|
||||
_service.decrypt = function (encValue) {
|
||||
if (!_service.getKey()) {
|
||||
throw 'AES encryption unavailable.';
|
||||
}
|
||||
|
||||
var encPieces = encValue.split('|');
|
||||
if (encPieces.length !== 2 && encPieces.length !== 3) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var ivBytes = forge.util.decode64(encPieces[0]);
|
||||
var ctBytes = forge.util.decode64(encPieces[1]);
|
||||
|
||||
var computedMac = null;
|
||||
if (encPieces.length === 3) {
|
||||
computedMac = computeMac(ctBytes, ivBytes);
|
||||
if (computedMac !== encPieces[2]) {
|
||||
console.error('MAC failed.');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
var ctBuffer = forge.util.createBuffer(ctBytes);
|
||||
var decipher = forge.cipher.createDecipher('AES-CBC', computedMac ? _service.getEncKey() : _service.getKey());
|
||||
decipher.start({ iv: ivBytes });
|
||||
decipher.update(ctBuffer);
|
||||
decipher.finish();
|
||||
|
||||
return decipher.output.toString('utf8');
|
||||
};
|
||||
|
||||
function computeMac(ct, iv, macKey) {
|
||||
var hmac = forge.hmac.create();
|
||||
hmac.start('sha256', macKey || _service.getMacKey());
|
||||
hmac.update(iv + ct);
|
||||
var mac = hmac.digest();
|
||||
return forge.util.encode64(mac.getBytes());
|
||||
}
|
||||
|
||||
return _service;
|
||||
});
|
||||
2355
src/app/services/importService.js
Normal file
2355
src/app/services/importService.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -97,8 +97,39 @@ angular
|
||||
return password;
|
||||
};
|
||||
|
||||
// EFForg/OpenWireless
|
||||
// ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js
|
||||
function randomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
var rval = 0;
|
||||
var range = max - min;
|
||||
|
||||
var bits_needed = Math.ceil(Math.log2(range));
|
||||
if (bits_needed > 53) {
|
||||
throw new Exception("We cannot generate numbers larger than 53 bits.");
|
||||
}
|
||||
var bytes_needed = Math.ceil(bits_needed / 8);
|
||||
var mask = Math.pow(2, bits_needed) - 1;
|
||||
// 7776 -> (2^13 = 8192) -1 == 8191 or 0x00001111 11111111
|
||||
|
||||
// Create byte array and fill with N random numbers
|
||||
var byteArray = new Uint8Array(bytes_needed);
|
||||
window.crypto.getRandomValues(byteArray);
|
||||
|
||||
var p = (bytes_needed - 1) * 8;
|
||||
for (var i = 0; i < bytes_needed; i++) {
|
||||
rval += byteArray[i] * Math.pow(2, p);
|
||||
p -= 8;
|
||||
}
|
||||
|
||||
// Use & to apply the mask and reduce the number of recursive lookups
|
||||
rval = rval & mask;
|
||||
|
||||
if (rval >= range) {
|
||||
// Integer out of acceptable range
|
||||
return randomInt(min, max);
|
||||
}
|
||||
// Return an integer that falls within the range
|
||||
return min + rval;
|
||||
}
|
||||
|
||||
return _service;
|
||||
63
src/app/services/tokenService.js
Normal file
63
src/app/services/tokenService.js
Normal file
@@ -0,0 +1,63 @@
|
||||
angular
|
||||
.module('bit.services')
|
||||
|
||||
.factory('tokenService', function ($sessionStorage, jwtHelper) {
|
||||
var _service = {},
|
||||
_token = null,
|
||||
_refreshToken = null;
|
||||
|
||||
_service.setToken = function (token) {
|
||||
$sessionStorage.accessToken = token;
|
||||
_token = token;
|
||||
};
|
||||
|
||||
_service.getToken = function () {
|
||||
if (!_token) {
|
||||
_token = $sessionStorage.accessToken;
|
||||
}
|
||||
|
||||
return _token ? _token : null;
|
||||
};
|
||||
|
||||
_service.clearToken = function () {
|
||||
_token = null;
|
||||
delete $sessionStorage.accessToken;
|
||||
};
|
||||
|
||||
_service.setRefreshToken = function (token) {
|
||||
$sessionStorage.refreshToken = token;
|
||||
_refreshToken = token;
|
||||
};
|
||||
|
||||
_service.getRefreshToken = function () {
|
||||
if (!_refreshToken) {
|
||||
_refreshToken = $sessionStorage.refreshToken;
|
||||
}
|
||||
|
||||
return _refreshToken ? _refreshToken : null;
|
||||
};
|
||||
|
||||
_service.clearRefreshToken = function () {
|
||||
_refreshToken = null;
|
||||
delete $sessionStorage.refreshToken;
|
||||
};
|
||||
|
||||
_service.tokenSecondsRemaining = function (token, offsetSeconds) {
|
||||
var d = jwtHelper.getTokenExpirationDate(token);
|
||||
offsetSeconds = offsetSeconds || 0;
|
||||
if (d === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var msRemaining = d.valueOf() - (new Date().valueOf() + (offsetSeconds * 1000));
|
||||
return Math.round(msRemaining / 1000);
|
||||
};
|
||||
|
||||
_service.tokenNeedsRefresh = function (token, minutes) {
|
||||
minutes = minutes || 5; // default 5 minutes
|
||||
var sRemaining = _service.tokenSecondsRemaining(token);
|
||||
return sRemaining < (60 * minutes);
|
||||
};
|
||||
|
||||
return _service;
|
||||
});
|
||||
@@ -14,6 +14,10 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (data && data.ErrorModel) {
|
||||
data = data.ErrorModel;
|
||||
}
|
||||
|
||||
if (!data.ValidationErrors) {
|
||||
if (data.Message) {
|
||||
form.$errors.push(data.Message);
|
||||
2
src/app/settings.js
Normal file
2
src/app/settings.js
Normal file
@@ -0,0 +1,2 @@
|
||||
angular.module("bit")
|
||||
.constant("appSettings", {"rememberedEmailCookieName":"bit.rememberedEmail","apiUri":"https://api.bitwarden.com","version":"1.9.1","environment":"Production"});
|
||||
@@ -0,0 +1,18 @@
|
||||
angular
|
||||
.module('bit.vault')
|
||||
|
||||
.controller('settingsAddEditEquivalentDomainController', function ($scope, $uibModalInstance, $analytics, domainIndex, domains) {
|
||||
$analytics.eventTrack('settingsAddEditEquivalentDomainController', { category: 'Modal' });
|
||||
|
||||
$scope.domains = domains;
|
||||
$scope.index = domainIndex;
|
||||
|
||||
$scope.submit = function (form) {
|
||||
$analytics.eventTrack((domainIndex ? 'Edited' : 'Added') + ' Equivalent Domain');
|
||||
$uibModalInstance.close({ domains: $scope.domains, index: domainIndex });
|
||||
};
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('close');
|
||||
};
|
||||
});
|
||||
@@ -27,10 +27,10 @@
|
||||
$scope.confirm = function (model) {
|
||||
$scope.processing = true;
|
||||
|
||||
var reencryptedSites = [];
|
||||
var sitesPromise = apiService.sites.list({ dirty: false }, function (encryptedSites) {
|
||||
var unencryptedSites = cipherService.decryptSites(encryptedSites.Data);
|
||||
reencryptedSites = cipherService.encryptSites(unencryptedSites, _newKey);
|
||||
var reencryptedLogins = [];
|
||||
var loginsPromise = apiService.logins.list({ dirty: false }, function (encryptedLogins) {
|
||||
var unencryptedLogins = cipherService.decryptLogins(encryptedLogins.Data);
|
||||
reencryptedLogins = cipherService.encryptLogins(unencryptedLogins, _newKey);
|
||||
}).$promise;
|
||||
|
||||
var reencryptedFolders = [];
|
||||
@@ -39,13 +39,13 @@
|
||||
reencryptedFolders = cipherService.encryptFolders(unencryptedFolders, _newKey);
|
||||
}).$promise;
|
||||
|
||||
$q.all([sitesPromise, foldersPromise]).then(function () {
|
||||
$q.all([loginsPromise, foldersPromise]).then(function () {
|
||||
var request = {
|
||||
token: model.token,
|
||||
newEmail: model.newEmail.toLowerCase(),
|
||||
masterPasswordHash: _masterPasswordHash,
|
||||
newMasterPasswordHash: _newMasterPasswordHash,
|
||||
ciphers: reencryptedSites.concat(reencryptedFolders)
|
||||
ciphers: reencryptedLogins.concat(reencryptedFolders)
|
||||
};
|
||||
|
||||
$scope.confirmPromise = apiService.accounts.email(request, function () {
|
||||
@@ -27,10 +27,10 @@
|
||||
var profile = authService.getUserProfile();
|
||||
var newKey = cryptoService.makeKey(model.newMasterPassword, profile.email.toLowerCase());
|
||||
|
||||
var reencryptedSites = [];
|
||||
var sitesPromise = apiService.sites.list({ dirty: false }, function (encryptedSites) {
|
||||
var unencryptedSites = cipherService.decryptSites(encryptedSites.Data);
|
||||
reencryptedSites = cipherService.encryptSites(unencryptedSites, newKey);
|
||||
var reencryptedLogins = [];
|
||||
var loginsPromise = apiService.logins.list({ dirty: false }, function (encryptedLogins) {
|
||||
var unencryptedLogins = cipherService.decryptLogins(encryptedLogins.Data);
|
||||
reencryptedLogins = cipherService.encryptLogins(unencryptedLogins, newKey);
|
||||
}).$promise;
|
||||
|
||||
var reencryptedFolders = [];
|
||||
@@ -39,11 +39,11 @@
|
||||
reencryptedFolders = cipherService.encryptFolders(unencryptedFolders, newKey);
|
||||
}).$promise;
|
||||
|
||||
$q.all([sitesPromise, foldersPromise]).then(function () {
|
||||
$q.all([loginsPromise, foldersPromise]).then(function () {
|
||||
var request = {
|
||||
masterPasswordHash: cryptoService.hashPassword(model.masterPassword),
|
||||
newMasterPasswordHash: cryptoService.hashPassword(model.newMasterPassword, newKey),
|
||||
ciphers: reencryptedSites.concat(reencryptedFolders)
|
||||
ciphers: reencryptedLogins.concat(reencryptedFolders)
|
||||
};
|
||||
|
||||
$scope.savePromise = apiService.accounts.putPassword(request, function () {
|
||||
@@ -2,20 +2,33 @@
|
||||
.module('bit.settings')
|
||||
|
||||
.controller('settingsController', function ($scope, $uibModal, apiService, toastr, authService) {
|
||||
$scope.model = {};
|
||||
$scope.model = {
|
||||
profile: {},
|
||||
twoFactorEnabled: false,
|
||||
email: null
|
||||
};
|
||||
|
||||
apiService.accounts.getProfile({}, function (user) {
|
||||
$scope.model = {
|
||||
name: user.Name,
|
||||
profile: {
|
||||
name: user.Name,
|
||||
masterPasswordHint: user.MasterPasswordHint,
|
||||
culture: user.Culture
|
||||
},
|
||||
email: user.Email,
|
||||
masterPasswordHint: user.MasterPasswordHint,
|
||||
culture: user.Culture,
|
||||
twoFactorEnabled: user.TwoFactorEnabled
|
||||
};
|
||||
});
|
||||
|
||||
$scope.save = function (model) {
|
||||
$scope.savePromise = apiService.accounts.putProfile({}, model, function (profile) {
|
||||
$scope.generalSave = function () {
|
||||
$scope.generalPromise = apiService.accounts.putProfile({}, $scope.model.profile, function (profile) {
|
||||
authService.setUserProfile(profile);
|
||||
toastr.success('Account has been updated.', 'Success!');
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
$scope.passwordHintSave = function () {
|
||||
$scope.passwordHintPromise = apiService.accounts.putProfile({}, $scope.model.profile, function (profile) {
|
||||
authService.setUserProfile(profile);
|
||||
toastr.success('Account has been updated.', 'Success!');
|
||||
}).$promise;
|
||||
@@ -29,34 +42,29 @@
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('settingsChangePassword', function (event, args) {
|
||||
$scope.changePassword();
|
||||
});
|
||||
|
||||
$scope.changeEmail = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/settings/views/settingsChangeEmail.html',
|
||||
controller: 'settingsChangeEmailController',
|
||||
size: 'sm'
|
||||
controller: 'settingsChangeEmailController'
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('settingsChangeEmail', function (event, args) {
|
||||
$scope.changeEmail();
|
||||
});
|
||||
|
||||
$scope.twoFactor = function () {
|
||||
$uibModal.open({
|
||||
var twoFactorModal = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/settings/views/settingsTwoFactor.html',
|
||||
controller: 'settingsTwoFactorController'
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('settingsTwoFactor', function (event, args) {
|
||||
$scope.twoFactor();
|
||||
});
|
||||
twoFactorModal.result.then(function (enabled) {
|
||||
if (enabled === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.model.twoFactorEnabled = enabled;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.sessions = function () {
|
||||
$uibModal.open({
|
||||
@@ -66,20 +74,11 @@
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('settingsSessions', function (event, args) {
|
||||
$scope.sessions();
|
||||
});
|
||||
|
||||
$scope.delete = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/settings/views/settingsDelete.html',
|
||||
controller: 'settingsDeleteController',
|
||||
size: 'sm'
|
||||
controller: 'settingsDeleteController'
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('settingsDelete', function (event, args) {
|
||||
$scope.delete();
|
||||
});
|
||||
});
|
||||
101
src/app/settings/settingsDomainsController.js
Normal file
101
src/app/settings/settingsDomainsController.js
Normal file
@@ -0,0 +1,101 @@
|
||||
angular
|
||||
.module('bit.settings')
|
||||
|
||||
.controller('settingsDomainsController', function ($scope, $state, apiService, toastr, $analytics, $uibModal) {
|
||||
$scope.globalEquivalentDomains = [];
|
||||
$scope.equivalentDomains = [];
|
||||
|
||||
apiService.settings.getDomains({}, function (response) {
|
||||
var i;
|
||||
if (response.EquivalentDomains) {
|
||||
for (i = 0; i < response.EquivalentDomains.length; i++) {
|
||||
$scope.equivalentDomains.push(response.EquivalentDomains[i].join(', '));
|
||||
}
|
||||
}
|
||||
|
||||
if (response.GlobalEquivalentDomains) {
|
||||
for (i = 0; i < response.GlobalEquivalentDomains.length; i++) {
|
||||
$scope.globalEquivalentDomains.push({
|
||||
domains: response.GlobalEquivalentDomains[i].Domains.join(', '),
|
||||
excluded: response.GlobalEquivalentDomains[i].Excluded,
|
||||
key: response.GlobalEquivalentDomains[i].Type
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$scope.toggleExclude = function (globalDomain) {
|
||||
globalDomain.excluded = !globalDomain.excluded;
|
||||
};
|
||||
|
||||
$scope.customize = function (globalDomain) {
|
||||
globalDomain.excluded = true;
|
||||
$scope.equivalentDomains.push(globalDomain.domains);
|
||||
};
|
||||
|
||||
$scope.delete = function (i) {
|
||||
$scope.equivalentDomains.splice(i, 1);
|
||||
};
|
||||
|
||||
$scope.addEdit = function (i) {
|
||||
var addEditModal = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/settings/views/settingsAddEditEquivalentDomain.html',
|
||||
controller: 'settingsAddEditEquivalentDomainController',
|
||||
resolve: {
|
||||
domainIndex: function () { return i; },
|
||||
domains: function () { return i !== null ? $scope.equivalentDomains[i] : null; }
|
||||
}
|
||||
});
|
||||
|
||||
addEditModal.result.then(function (returnObj) {
|
||||
if (returnObj.domains) {
|
||||
returnObj.domains = returnObj.domains.split(' ').join('').split(',').join(', ');
|
||||
}
|
||||
|
||||
if (returnObj.index !== null) {
|
||||
$scope.equivalentDomains[returnObj.index] = returnObj.domains;
|
||||
}
|
||||
else {
|
||||
$scope.equivalentDomains.push(returnObj.domains);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.saveGlobal = function () {
|
||||
$scope.globalPromise = save();
|
||||
};
|
||||
|
||||
$scope.saveCustom = function () {
|
||||
$scope.customPromise = save();
|
||||
};
|
||||
|
||||
var save = function () {
|
||||
var request = {
|
||||
ExcludedGlobalEquivalentDomains: [],
|
||||
EquivalentDomains: []
|
||||
};
|
||||
|
||||
for (var i = 0; i < $scope.globalEquivalentDomains.length; i++) {
|
||||
if ($scope.globalEquivalentDomains[i].excluded) {
|
||||
request.ExcludedGlobalEquivalentDomains.push($scope.globalEquivalentDomains[i].key);
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < $scope.equivalentDomains.length; i++) {
|
||||
request.EquivalentDomains.push($scope.equivalentDomains[i].split(' ').join('').split(','));
|
||||
}
|
||||
|
||||
if (!request.EquivalentDomains.length) {
|
||||
request.EquivalentDomains = null;
|
||||
}
|
||||
|
||||
if (!request.ExcludedGlobalEquivalentDomains.length) {
|
||||
request.ExcludedGlobalEquivalentDomains = null;
|
||||
}
|
||||
|
||||
return apiService.settings.putDomains(request, function (domains) {
|
||||
toastr.success('Domains have been updated.', 'Success!');
|
||||
}).$promise;
|
||||
};
|
||||
});
|
||||
@@ -19,18 +19,31 @@
|
||||
masterPasswordHash: _masterPasswordHash,
|
||||
provider: 0 /* Only authenticator provider for now. */
|
||||
}, function (response) {
|
||||
var key = response.AuthenticatorKey;
|
||||
$scope.twoFactorModel = {
|
||||
enabled: response.TwoFactorEnabled,
|
||||
key: key.replace(/(.{4})/g, '$1 ').trim(),
|
||||
qr: 'https://chart.googleapis.com/chart?chs=120x120&chld=L|0&cht=qr&chl=otpauth://totp/' +
|
||||
_issuer + ':' + encodeURIComponent(_profile.email) +
|
||||
'%3Fsecret=' + encodeURIComponent(key) +
|
||||
'%26issuer=' + _issuer
|
||||
};
|
||||
processResponse(response);
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
function formatString(s) {
|
||||
if (!s) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return s.replace(/(.{4})/g, '$1 ').trim().toUpperCase();
|
||||
}
|
||||
|
||||
function processResponse(response) {
|
||||
var key = response.AuthenticatorKey;
|
||||
$scope.twoFactorModel = {
|
||||
enabled: response.TwoFactorEnabled,
|
||||
key: formatString(key),
|
||||
recovery: formatString(response.TwoFactorRecoveryCode),
|
||||
qr: 'https://chart.googleapis.com/chart?chs=120x120&chld=L|0&cht=qr&chl=otpauth://totp/' +
|
||||
_issuer + ':' + encodeURIComponent(_profile.email) +
|
||||
'%3Fsecret=' + encodeURIComponent(key) +
|
||||
'%26issuer=' + _issuer
|
||||
};
|
||||
}
|
||||
|
||||
$scope.update = function (model) {
|
||||
var currentlyEnabled = $scope.twoFactorModel.enabled;
|
||||
if (currentlyEnabled && !confirm('Are you sure you want to disable two-step login?')) {
|
||||
@@ -39,7 +52,7 @@
|
||||
|
||||
var request = {
|
||||
enabled: !currentlyEnabled,
|
||||
token: model ? model.token : null,
|
||||
token: model.token.replace(' ', ''),
|
||||
masterPasswordHash: _masterPasswordHash
|
||||
};
|
||||
|
||||
@@ -48,18 +61,28 @@
|
||||
$analytics.eventTrack('Enabled Two-step Login');
|
||||
toastr.success('Two-step login has been enabled.');
|
||||
if (_profile.extended) _profile.extended.twoFactorEnabled = true;
|
||||
processResponse(response);
|
||||
$('#token').blur();
|
||||
model.token = null;
|
||||
}
|
||||
else {
|
||||
$analytics.eventTrack('Disabled Two-step Login');
|
||||
toastr.success('Two-step login has been disabled.');
|
||||
if (_profile.extended) _profile.extended.twoFactorEnabled = false;
|
||||
$scope.close();
|
||||
}
|
||||
|
||||
$scope.close();
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
$scope.print = function (printContent) {
|
||||
var w = window.open();
|
||||
w.document.write('<div style="font-size: 18px; text-align: center;"><p>bitwarden two-step login recovery code:</p>' +
|
||||
'<pre>' + printContent + '</pre>');
|
||||
w.print();
|
||||
w.close();
|
||||
};
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
$uibModalInstance.close(!_profile.extended ? null : _profile.extended.twoFactorEnabled);
|
||||
};
|
||||
});
|
||||
128
src/app/settings/views/settings.html
Normal file
128
src/app/settings/views/settings.html
Normal file
@@ -0,0 +1,128 @@
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
Settings
|
||||
<small>manage your account</small>
|
||||
</h1>
|
||||
</section>
|
||||
<section class="content">
|
||||
<div class="box box-default">
|
||||
<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">
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-9">
|
||||
<div class="callout callout-danger validation-errors" ng-show="generalForm.$errors">
|
||||
<h4>Errors have occured</h4>
|
||||
<ul>
|
||||
<li ng-repeat="e in generalForm.$errors">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" name="Name" ng-model="model.profile.name" class="form-control"
|
||||
required api-field />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email - <a href="javascript:void(0)" ng-click="changeEmail()">change</a></label>
|
||||
<input type="text" id="email" ng-model="model.email" class="form-control" readonly />
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="culture">Language/Culture</label>
|
||||
<select id="culture" name="Culture" ng-model="model.profile.culture" class="form-control" api-field>
|
||||
<option value="en-US">English (US)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3 settings-photo">
|
||||
<a href="http://www.gravatar.com/" target="_blank">
|
||||
<img src="//www.gravatar.com/avatar/{{ main.userProfile.email | gravatar }}.jpg?s=150&d=mm"
|
||||
class="img-rounded img-responsive" alt="User Image">
|
||||
</a>
|
||||
<a href="http://www.gravatar.com/" target="_blank" class="btn btn-link"
|
||||
analytics-on="click" analytics-event="Clicked Update Photo">Update Photo</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="generalForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="generalForm.$loading"></i>Save
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="changeEmail()">
|
||||
Change Email
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="box box-default">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Master Password</h3>
|
||||
</div>
|
||||
<form role="form" name="masterPasswordForm" ng-submit="masterPasswordForm.$valid && passwordHintSave()"
|
||||
api-form="passwordHintPromise">
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-9">
|
||||
<div class="callout callout-danger validation-errors" ng-show="masterPasswordForm.$errors">
|
||||
<h4>Errors have occured</h4>
|
||||
<ul>
|
||||
<li ng-repeat="e in masterPasswordForm.$errors">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="hint">Master Password Hint</label>
|
||||
<input type="text" id="hint" name="MasterPasswordHint" ng-model="model.profile.masterPasswordHint"
|
||||
class="form-control" api-field />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="masterPasswordForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="masterPasswordForm.$loading"></i>Save
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="changePassword()">
|
||||
Change Master Password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="box box-default">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Two-step Log In</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<p>
|
||||
Current Status:
|
||||
<span class="label bg-green" ng-show="model.twoFactorEnabled">ENABLED</span>
|
||||
<span class="label bg-gray" ng-show="!model.twoFactorEnabled">DISABLED</span>
|
||||
</p>
|
||||
<p>
|
||||
Two-step login helps keep your account more secure by requiring a code provided by an app on your mobile device
|
||||
while logging in (in addition to your master password).
|
||||
</p>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="twoFactor()">
|
||||
Manage Two-step Log In
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box box-danger">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Danger Zone</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
Careful, these actions are not reversible!
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button type="submit" class="btn btn-default btn-flat" ng-click="sessions()">
|
||||
Deauthorize Sessions
|
||||
</button>
|
||||
<button type="submit" class="btn btn-default btn-flat" ng-click="delete()">
|
||||
Delete Account
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
35
src/app/settings/views/settingsAddEditEquivalentDomain.html
Normal file
35
src/app/settings/views/settingsAddEditEquivalentDomain.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<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-globe"></i> {{index ? 'Edit Equivalent Domain' : 'Add Equivalent Domain'}}</h4>
|
||||
</div>
|
||||
<form name="domainAddEditForm" ng-submit="domainAddEditForm.$valid && submit(domainAddEditForm)">
|
||||
<div class="modal-body">
|
||||
<div class="callout callout-danger validation-errors" ng-show="domainAddEditForm.$errors">
|
||||
<h4>Errors have occured</h4>
|
||||
<ul>
|
||||
<li ng-repeat="e in domainAddEditForm.$errors">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
Enter a list of domains separated by commas.
|
||||
</p>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="name">Domains</label> <span>*</span>
|
||||
<textarea id="domains" name="Domains" ng-model="domains" class="form-control" placeholder="ex. google.com, gmail.com"
|
||||
style="height: 100px;" required></textarea>
|
||||
<p class="help-block">
|
||||
Only "base" domains are allowed. Do not enter subdomains. For example, enter "google.com" instead of
|
||||
"www.google.com".
|
||||
</p>
|
||||
<p class="help-block">
|
||||
You can also enter "androidapp://package.name" to associate an android app with other website domains.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat">
|
||||
Submit
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
94
src/app/settings/views/settingsDomains.html
Normal file
94
src/app/settings/views/settingsDomains.html
Normal file
@@ -0,0 +1,94 @@
|
||||
<section class="content-header">
|
||||
<h1>Domain Rules</h1>
|
||||
</section>
|
||||
<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.
|
||||
</p>
|
||||
<form name="customForm" ng-submit="customForm.$valid && saveCustom()" api-form="customPromise">
|
||||
<div class="box box-default">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Custom Equivalent Domains</h3>
|
||||
<div class="box-tools pull-right">
|
||||
<button type="button" class="btn btn-box-tool" ng-click="addEdit(null)">
|
||||
<i class="fa fa-plus"></i> Add New
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-body no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<tbody ng-if="equivalentDomains.length">
|
||||
<tr ng-repeat="customDomain in equivalentDomains track by $index">
|
||||
<td style="width: 80px; min-width: 80px;">
|
||||
<button type="button" class="btn btn-link btn-table" uib-tooltip="Edit" ng-click="addEdit($index)">
|
||||
<i class="fa fa-lg fa-pencil"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-link btn-table" uib-tooltip="Delete" ng-click="delete($index)">
|
||||
<i class="fa fa-lg fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
<td>{{customDomain}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody ng-if="!equivalentDomains.length">
|
||||
<tr>
|
||||
<td>No domains to list.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="customForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="customForm.$loading"></i>Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form name="globalForm" ng-submit="globalForm.$valid && saveGlobal()" api-form="globalPromise">
|
||||
<div class="box box-default">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Global Equivalent Domains</h3>
|
||||
</div>
|
||||
<div class="box-body no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<tbody ng-if="globalEquivalentDomains.length">
|
||||
<tr ng-repeat="globalDomain in globalEquivalentDomains">
|
||||
<td style="width: 80px; min-width: 80px;">
|
||||
<button type="button" class="btn btn-link btn-table" uib-tooltip="Exclude"
|
||||
ng-if="!globalDomain.excluded" ng-click="toggleExclude(globalDomain)">
|
||||
<i class="fa fa-lg fa-ban"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-link btn-table" uib-tooltip="Include"
|
||||
ng-if="globalDomain.excluded" ng-click="toggleExclude(globalDomain)">
|
||||
<i class="fa fa-lg fa-plus"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-link btn-table" uib-tooltip="Customize"
|
||||
ng-click="customize(globalDomain)">
|
||||
<i class="fa fa-lg fa-cut"></i>
|
||||
</button>
|
||||
</td>
|
||||
<td ng-class="{strike: globalDomain.excluded}">{{globalDomain.domains}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody ng-if="!globalEquivalentDomains.length">
|
||||
<tr>
|
||||
<td>No domains to list.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="globalForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="globalForm.$loading"></i>Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="twoFactorModelLabel"><i class="fa fa-key"></i> Two-step Login</h4>
|
||||
<h4 class="modal-title" id="twoFactorModelLabel"><i class="fa fa-key"></i> Two-step Log In</h4>
|
||||
</div>
|
||||
<form name="authTwoStepForm" ng-submit="authTwoStepForm.$valid && auth(authModel)" api-form="authPromise" ng-if="!twoFactorModel">
|
||||
<div class="modal-body">
|
||||
<p>Current Status: <span class="label bg-green" ng-show="enabled()">ENABLED</span><span class="label bg-gray" ng-show="!enabled()">DISABLED</span></p>
|
||||
<p>Two-step login helps keep your account more secure by requiring a code provided by an app on your mobile device while logging in (in addition to your master password).</p>
|
||||
<p ng-show="enabled()">Two-step login is already enabled on your account. To access your two-step settings enter your master password below.</p>
|
||||
<p ng-show="!enabled()">To get started with two-step login enter your master password below.</p>
|
||||
<p ng-show="enabled()">Two-step log in is already enabled on your account. To access your two-step settings enter your master password below.</p>
|
||||
<p ng-show="!enabled()">To get started with two-step log in enter your master password below.</p>
|
||||
<div class="callout callout-danger validation-errors" ng-show="authTwoStepForm.$errors">
|
||||
<h4>Errors have occured</h4>
|
||||
<ul>
|
||||
@@ -29,7 +27,7 @@
|
||||
<form name="updateTwoStepForm" ng-submit="updateTwoStepForm.$valid && update(updateModel)" api-form="updatePromise" ng-if="twoFactorModel">
|
||||
<div class="modal-body">
|
||||
<div ng-show="enabled()">
|
||||
<p>Two-step login is enabled on your account. Below is the code required by your verification app.</p>
|
||||
<p>Two-step log in is <strong class="text-green">enabled</strong> on your account. Below is the code required by your verification app.</p>
|
||||
<p>Need a two-step verification app? Download one of the following:</p>
|
||||
</div>
|
||||
<div ng-show="!enabled()">
|
||||
@@ -37,7 +35,7 @@
|
||||
<h4>1. Download a two-step verification app</h4>
|
||||
</div>
|
||||
<ul class="fa-ul">
|
||||
<li><i class="fa-li fa fa-apple"></i> iOS devices: <a href="https://itunes.apple.com/en/app/authy/id494168017" target="_blank">Authy for iOS</a></li>
|
||||
<li><i class="fa-li fa fa-apple"></i> iOS devices: <a href="https://itunes.apple.com/us/app/authy/id494168017?mt=8" target="_blank">Authy for iOS</a></li>
|
||||
<li><i class="fa-li fa fa-android"></i> Android devices: <a href="https://play.google.com/store/apps/details?id=com.authy.authy" target="_blank">Authy for Android</a></li>
|
||||
<li><i class="fa-li fa fa-windows"></i> Windows devices: <a href="https://www.microsoft.com/en-us/store/apps/authenticator/9wzdncrfj3rj" target="_blank">Microsoft Authenticator </a></li>
|
||||
</ul>
|
||||
@@ -47,35 +45,49 @@
|
||||
<div class="col-md-4 text-center">
|
||||
<p><img ng-src="{{twoFactorModel.qr}}" alt="QR" class="img-thumbnail" /></p>
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
<div class="col-md-8">
|
||||
<p><strong>Can't scan the code?</strong> You can add the code to your application manually using the following details:</p>
|
||||
<ul class="list-unstyled">
|
||||
<li><strong>Key:</strong> <samp>{{twoFactorModel.key}}</samp></li>
|
||||
<li><strong>Key:</strong> <code>{{twoFactorModel.key}}</code></li>
|
||||
<li><strong>Account:</strong> {{account}}</li>
|
||||
<li><strong>Time based:</strong> Yes</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="!enabled()">
|
||||
<div class="callout callout-danger validation-errors" ng-show="updateTwoStepForm.$errors">
|
||||
<h4>Errors have occured</h4>
|
||||
<ul>
|
||||
<li ng-repeat="e in updateTwoStepForm.$errors">{{e}}</li>
|
||||
</ul>
|
||||
<div ng-show="enabled()">
|
||||
<hr />
|
||||
<div class="callout callout-danger">
|
||||
<h4><i class="fa fa-warning"></i> Recovery Code <i class="fa fa-warning"></i></h4>
|
||||
<p>
|
||||
The recovery code allows you to access your account in the event that you lose your authenticator app.
|
||||
bitwarden support won't be able to assist you if you lose access to your account. We recommend you write down or
|
||||
print the recovery code below and keep it in a safe place.
|
||||
</p>
|
||||
<p><strong>Recovery Code:</strong> <code>{{twoFactorModel.recovery}}</code></p>
|
||||
<button type="button" class="btn btn-default" ng-click="print(twoFactorModel.recovery)">
|
||||
<i class="fa fa-print"></i> Print
|
||||
</button>
|
||||
</div>
|
||||
<h4 style="margin-top: 30px;">3. Enter the resulting verification code from the app</h4>
|
||||
<div class="form-group" show-errors ng-show="!twoFactorModel.enabled">
|
||||
<label for="token" class="sr-only">Verification Code</label>
|
||||
<input type="number" id="token" name="Token" placeholder="Verification Code" ng-model="updateModel.token" class="form-control" ng-required="!twoFactorModel.enabled" api-field />
|
||||
</div>
|
||||
<p>NOTE: After enabling two-step login, you will be required to enter the current code generated by your verification app each time you log in.</p>
|
||||
</div>
|
||||
<div class="callout callout-danger validation-errors" ng-show="updateTwoStepForm.$errors">
|
||||
<h4>Errors have occured</h4>
|
||||
<ul>
|
||||
<li ng-repeat="e in updateTwoStepForm.$errors">{{e}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<hr ng-show="enabled()" />
|
||||
<h4 style="margin-top: 30px;"><span ng-show="enabled()">Want to disable? </span><span ng-show="!enabled()">3. </span>Enter the resulting verification code from the app</h4>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="token" class="sr-only">Verification Code</label>
|
||||
<input type="text" id="token" name="Token" placeholder="Verification Code" ng-model="updateModel.token" class="form-control" required api-field />
|
||||
</div>
|
||||
<p ng-show="!enabled()">NOTE: After enabling two-step log in, you will be required to enter the current code generated by your verification app each time you log in.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="updateTwoStepForm.$loading">
|
||||
<i class="fa fa-refresh fa-spin loading-icon" ng-show="updateTwoStepForm.$loading"></i>
|
||||
<span ng-show="twoFactorModel.enabled">Disable Two-step</span>
|
||||
<span ng-show="!twoFactorModel.enabled">Enable Two-step</span>
|
||||
<span ng-show="enabled()">Disable Two-step</span>
|
||||
<span ng-show="!enabled()">Enable Two-step</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
|
||||
</div>
|
||||
@@ -6,28 +6,18 @@
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/tools/views/toolsImport.html',
|
||||
controller: 'toolsImportController',
|
||||
size: 'sm'
|
||||
controller: 'toolsImportController'
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('toolsImport', function (event, args) {
|
||||
$scope.import();
|
||||
});
|
||||
|
||||
$scope.export = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/tools/views/toolsExport.html',
|
||||
controller: 'toolsExportController',
|
||||
size: 'sm'
|
||||
controller: 'toolsExportController'
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('toolsExport', function (event, args) {
|
||||
$scope.export();
|
||||
});
|
||||
|
||||
$scope.audits = function () {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
@@ -35,8 +25,4 @@
|
||||
controller: 'toolsAuditsController'
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('toolsAudits', function (event, args) {
|
||||
$scope.audits();
|
||||
});
|
||||
});
|
||||
@@ -5,25 +5,25 @@
|
||||
$analytics.eventTrack('toolsExportController', { category: 'Modal' });
|
||||
$scope.export = function (model) {
|
||||
$scope.startedExport = true;
|
||||
apiService.sites.list({ expand: ['folder'] }, function (sites) {
|
||||
apiService.logins.list({ expand: ['folder'] }, function (logins) {
|
||||
try {
|
||||
var decSites = cipherService.decryptSites(sites.Data);
|
||||
var decLogins = cipherService.decryptLogins(logins.Data);
|
||||
|
||||
var exportSites = [];
|
||||
for (var i = 0; i < decSites.length; i++) {
|
||||
var site = {
|
||||
name: decSites[i].name,
|
||||
uri: decSites[i].uri,
|
||||
username: decSites[i].username,
|
||||
password: decSites[i].password,
|
||||
notes: decSites[i].notes,
|
||||
folder: decSites[i].folder ? decSites[i].folder.name : null
|
||||
var exportLogins = [];
|
||||
for (var i = 0; i < decLogins.length; i++) {
|
||||
var login = {
|
||||
name: decLogins[i].name,
|
||||
uri: decLogins[i].uri,
|
||||
username: decLogins[i].username,
|
||||
password: decLogins[i].password,
|
||||
notes: decLogins[i].notes,
|
||||
folder: decLogins[i].folder ? decLogins[i].folder.name : null
|
||||
};
|
||||
|
||||
exportSites.push(site);
|
||||
exportLogins.push(login);
|
||||
}
|
||||
|
||||
var csvString = Papa.unparse(exportSites);
|
||||
var csvString = Papa.unparse(exportLogins);
|
||||
var csvBlob = new Blob([csvString]);
|
||||
if (window.navigator.msSaveOrOpenBlob) { // IE hack. ref http://msdn.microsoft.com/en-us/library/ie/hh779016.aspx
|
||||
window.navigator.msSaveBlob(csvBlob, makeFileName());
|
||||
281
src/app/tools/toolsImportController.js
Normal file
281
src/app/tools/toolsImportController.js
Normal file
@@ -0,0 +1,281 @@
|
||||
angular
|
||||
.module('bit.tools')
|
||||
|
||||
.controller('toolsImportController', function ($scope, $state, apiService, $uibModalInstance, cryptoService, cipherService, toastr, importService, $analytics, $sce) {
|
||||
$analytics.eventTrack('toolsImportController', { category: 'Modal' });
|
||||
$scope.model = { source: 'bitwardencsv' };
|
||||
$scope.source = {};
|
||||
|
||||
$scope.options = [
|
||||
{
|
||||
id: 'bitwardencsv',
|
||||
name: 'bitwarden (csv)',
|
||||
instructions: $sce.trustAsHtml('Export using the web vault (vault.bitwarden.com). ' +
|
||||
'Log into the web vault and navigate to "Tools" > "Export".')
|
||||
},
|
||||
{
|
||||
id: 'lastpass',
|
||||
name: 'LastPass (csv)',
|
||||
instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' +
|
||||
'<a target="_blank" href="https://help.bitwarden.com/getting-started/import-from-lastpass/">' +
|
||||
'https://help.bitwarden.com/getting-started/import-from-lastpass/</a>')
|
||||
},
|
||||
{
|
||||
id: 'chromecsv',
|
||||
name: 'Chrome (csv)',
|
||||
instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' +
|
||||
'<a target="_blank" href="https://help.bitwarden.com/getting-started/import-from-chrome/">' +
|
||||
'https://help.bitwarden.com/getting-started/import-from-chrome/</a>')
|
||||
},
|
||||
{
|
||||
id: 'firefoxpasswordexportercsvxml',
|
||||
name: 'Firefox Password Exporter (xml)',
|
||||
instructions: $sce.trustAsHtml('Use the ' +
|
||||
'<a target="_blank" href="https://addons.mozilla.org/en-US/firefox/addon/password-exporter/">' +
|
||||
'Password Exporter</a> addon for FireFox to export your passwords to a XML file. After installing ' +
|
||||
'the addon, type <code>about:addons</code> in your FireFox navigation bar. Locate the Password Exporter ' +
|
||||
'addon and click the "Options" button. In the dialog that pops up, click the "Export Passwords" button ' +
|
||||
'to save the XML file.')
|
||||
},
|
||||
{
|
||||
id: 'keepass2xml',
|
||||
name: 'KeePass 2 (xml)',
|
||||
instructions: $sce.trustAsHtml('Using the KeePass 2 desktop application, navigate to "File" > "Export" and ' +
|
||||
'select the KeePass XML (2.x) option.')
|
||||
},
|
||||
{
|
||||
id: 'keepassxcsv',
|
||||
name: 'KeePassX (csv)',
|
||||
instructions: $sce.trustAsHtml('Using the KeePassX desktop application, navigate to "Database" > ' +
|
||||
'"Export to CSV file" and save the CSV file.')
|
||||
},
|
||||
{
|
||||
id: 'dashlanecsv',
|
||||
name: 'Dashlane (csv)',
|
||||
instructions: $sce.trustAsHtml('Using the Dashlane desktop application, navigate to "File" > "Export" > ' +
|
||||
'"Unsecured archive (readable) in CSV format" and save the CSV file.')
|
||||
},
|
||||
{
|
||||
id: '1password1pif',
|
||||
name: '1Password (1pif)',
|
||||
instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' +
|
||||
'<a target="_blank" href="https://help.bitwarden.com/getting-started/import-from-1password/">' +
|
||||
'https://help.bitwarden.com/getting-started/import-from-1password/</a>')
|
||||
},
|
||||
{
|
||||
id: '1password6wincsv',
|
||||
name: '1Password 6 Windows (csv)',
|
||||
instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' +
|
||||
'<a target="_blank" href="https://help.bitwarden.com/getting-started/import-from-1password/">' +
|
||||
'https://help.bitwarden.com/getting-started/import-from-1password/</a>')
|
||||
},
|
||||
{
|
||||
id: 'roboformhtml',
|
||||
name: 'RoboForm (html)',
|
||||
instructions: $sce.trustAsHtml('Using the RoboForm Editor desktop application, navigate to "RoboForm" ' +
|
||||
'(top left) > "Print List" > "Logins". When the following print dialog pops up click on the "Save" button ' +
|
||||
'and save the HTML file.')
|
||||
},
|
||||
{
|
||||
id: 'keepercsv',
|
||||
name: 'Keeper (csv)',
|
||||
instructions: $sce.trustAsHtml('Log into the Keeper web vault (keepersecurity.com/vault). Navigate to "Backup" ' +
|
||||
'(top right) and find the "Export to Text File" option. Click "Export Now" to save the TXT/CSV file.')
|
||||
},
|
||||
{
|
||||
id: 'enpasscsv',
|
||||
name: 'Enpass (csv)',
|
||||
instructions: $sce.trustAsHtml('Using the Enpass desktop application, navigate to "File" > "Export" > ' +
|
||||
'"As CSV". Select "Yes" to the warning alert and save the CSV file.')
|
||||
},
|
||||
{
|
||||
id: 'safeincloudxml',
|
||||
name: 'SafeInCloud (xml)',
|
||||
instructions: $sce.trustAsHtml('Using the SaveInCloud desktop application, navigate to "File" > "Export" > ' +
|
||||
'"As XML" and save the XML file.')
|
||||
},
|
||||
{
|
||||
id: 'pwsafexml',
|
||||
name: 'Password Safe (xml)',
|
||||
instructions: $sce.trustAsHtml('Using the Password Safe desktop application, navigate to "File" > ' +
|
||||
'"Export To" > "XML format..." and save the XML file.')
|
||||
},
|
||||
{
|
||||
id: 'stickypasswordxml',
|
||||
name: 'Sticky Password (xml)',
|
||||
instructions: $sce.trustAsHtml('Using the Sticky Password desktop application, navigate to "Menu" ' +
|
||||
'(top right) > "Export" > "Export all". Select the unencrypted format XML option and then the ' +
|
||||
'"Save to file" button. Save the XML file.')
|
||||
},
|
||||
{
|
||||
id: 'msecurecsv',
|
||||
name: 'mSecure (csv)',
|
||||
instructions: $sce.trustAsHtml('Using the mSecure desktop application, navigate to "File" > ' +
|
||||
'"Export" > "CSV File..." and save the CSV file.')
|
||||
},
|
||||
{
|
||||
id: 'truekeycsv',
|
||||
name: 'True Key (csv)',
|
||||
instructions: $sce.trustAsHtml('Using the True Key desktop application, click the gear icon (top right) and ' +
|
||||
'then navigate to "App Settings". Click the "Export" button, enter your password and save the CSV file.')
|
||||
},
|
||||
{
|
||||
id: 'passwordbossjson',
|
||||
name: 'Password Boss (json)',
|
||||
instructions: $sce.trustAsHtml('Using the Password Boss desktop application, navigate to "File" > ' +
|
||||
'"Export data" > "Password Boss JSON - not encrypted" and save the JSON file.')
|
||||
},
|
||||
{
|
||||
id: 'zohovaultcsv',
|
||||
name: 'Zoho Vault (csv)',
|
||||
instructions: $sce.trustAsHtml('Log into the Zoho web vault (vault.zoho.com). Navigate to "Tools" > ' +
|
||||
'"Export Secrets". Select "All Secrets" and click the "Zoho Vault Format CSV" button. Highlight ' +
|
||||
'and copy the data from the textarea. Open a text editor like Notepad and paste the data. Save the ' +
|
||||
'data from the text editor as <code>zoho_export.csv</code>.')
|
||||
},
|
||||
{
|
||||
id: 'splashidcsv',
|
||||
name: 'SplashID (csv)',
|
||||
instructions: $sce.trustAsHtml('Using the SplashID Safe desktop application, click on the SplashID ' +
|
||||
'blue lock logo in the top right corner. Navigate to "Export" > "Export as CSV" and save the CSV file.')
|
||||
},
|
||||
{
|
||||
id: 'passworddragonxml',
|
||||
name: 'Password Dragon (xml)',
|
||||
instructions: $sce.trustAsHtml('Using the Password Dragon desktop application, navigate to "File" > ' +
|
||||
'"Export" > "To XML". In the dialog that pops up select "All Rows" and check all fields. Click ' +
|
||||
'the "Export" button and save the XML file.')
|
||||
},
|
||||
{
|
||||
id: 'padlockcsv',
|
||||
name: 'Padlock (csv)',
|
||||
instructions: $sce.trustAsHtml('Using the Padlock desktop application, click the hamburger icon ' +
|
||||
'in the top left corner and navigate to "Settings". Click the "Export Data" option. Ensure that ' +
|
||||
'the "CSV" option is selected from the dropdown. Highlight and copy the data from the textarea. ' +
|
||||
'Open a text editor like Notepad and paste the data. Save the data from the text editor as ' +
|
||||
'<code>padlock_export.csv</code>.')
|
||||
},
|
||||
{
|
||||
id: 'clipperzhtml',
|
||||
name: 'Clipperz (html)',
|
||||
instructions: $sce.trustAsHtml('Log into the Clipperz web application (clipperz.is/app). Click the ' +
|
||||
'hamburger menu icon in the top right to expand the navigation bar. Navigate to "Data" > ' +
|
||||
'"Export". Click the "download HTML+JSON" button to save the HTML file.')
|
||||
},
|
||||
{
|
||||
id: 'avirajson',
|
||||
name: 'Avira (json)',
|
||||
instructions: $sce.trustAsHtml('Using the Avira browser extension, click your username in the top ' +
|
||||
'right corner and navigate to "Settings". Locate the "Export Data" section and click "Export". ' +
|
||||
'In the dialog that pops up, click the "Export Password Manager Data" button to save the ' +
|
||||
'TXT/JSON file.')
|
||||
},
|
||||
{
|
||||
id: 'saferpasscsv',
|
||||
name: 'SaferPass (csv)',
|
||||
instructions: $sce.trustAsHtml('Using the SaferPass browser extension, click the hamburger icon ' +
|
||||
'in the top left corner and navigate to "Settings". Click the "Export accounts" button to ' +
|
||||
'save the CSV file.')
|
||||
},
|
||||
{
|
||||
id: 'upmcsv',
|
||||
name: 'Universal Password Manager (csv)',
|
||||
instructions: $sce.trustAsHtml('Using the Universal Password Manager desktop application, navigate ' +
|
||||
'to "Database" > "Export" and save the CSV file.')
|
||||
},
|
||||
{
|
||||
id: 'ascendocsv',
|
||||
name: 'Ascendo DataVault (csv)',
|
||||
instructions: $sce.trustAsHtml('Using the Ascendo DataVault desktop application, navigate ' +
|
||||
'to "Tools" > "Export". In the dialog that pops up, select the "All Items (DVX, CSV)" ' +
|
||||
'option. Click the "Ok" button to save the CSV file.')
|
||||
}
|
||||
];
|
||||
|
||||
$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) {
|
||||
$scope.processing = true;
|
||||
var file = document.getElementById('file').files[0];
|
||||
importService.import(model.source, file, importSuccess, importError);
|
||||
};
|
||||
|
||||
function importSuccess(folders, logins, folderRelationships) {
|
||||
if (!folders.length && !logins.length) {
|
||||
importError('Nothing was imported.');
|
||||
return;
|
||||
}
|
||||
else if (logins.length) {
|
||||
var halfway = Math.floor(logins.length / 2);
|
||||
var last = logins.length - 1;
|
||||
if (loginIsBadData(logins[0]) && loginIsBadData(logins[halfway]) && loginIsBadData(logins[last])) {
|
||||
importError('CSV data is not formatted correctly. Please check your import file and try again.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
apiService.ciphers.import({
|
||||
folders: cipherService.encryptFolders(folders, cryptoService.getKey()),
|
||||
logins: cipherService.encryptLogins(logins, cryptoService.getKey()),
|
||||
folderRelationships: folderRelationships
|
||||
}, function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
$state.go('backend.vault').then(function () {
|
||||
$analytics.eventTrack('Imported Data', { label: $scope.model.source });
|
||||
toastr.success('Data has been successfully imported into your vault.', 'Import Success');
|
||||
});
|
||||
}, importError);
|
||||
}
|
||||
|
||||
function loginIsBadData(login) {
|
||||
return (login.name === null || login.name === '--') && (login.password === null || login.password === '');
|
||||
}
|
||||
|
||||
function importError(error) {
|
||||
$analytics.eventTrack('Import 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');
|
||||
};
|
||||
});
|
||||
47
src/app/tools/views/tools.html
Normal file
47
src/app/tools/views/tools.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
Tools
|
||||
<small>helpful utilities</small>
|
||||
</h1>
|
||||
</section>
|
||||
<section class="content">
|
||||
<div class="box box-default">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Import</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
Quickly import your logins and other data from a previous bitwarden export or from another
|
||||
password manager application.
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button class="btn btn-default btn-flat" type="button" ng-click="import()">Import Data</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box box-default">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Export</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
Export all of your vault data in <code>.csv</code> format.
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button class="btn btn-default btn-flat" type="button" ng-click="export()">Export Data</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box box-default">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Password Generator</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
Coming soon!
|
||||
</div>
|
||||
</div>
|
||||
<div class="box box-default">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Audits</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
Coming soon!
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -4,7 +4,12 @@
|
||||
</div>
|
||||
<form name="exportForm" ng-submit="exportForm.$valid && export(model)" api-form="exportPromise" ng-show="!startedExport">
|
||||
<div class="modal-body">
|
||||
<p>Export all of your vault data in <code>.csv</code> format. Enter your master password to continue.</p>
|
||||
<p>Enter your master password to continue.</p>
|
||||
<div class="callout callout-warning">
|
||||
<h4><i class="fa fa-warning"></i> Warning</h4>
|
||||
This export contains your <u>unencrypted</u> data in <code>.csv</code> format. You should not store or send it
|
||||
over unsecure channels (such as email). Delete it immediately after your are done using it.
|
||||
</div>
|
||||
<div class="callout callout-danger validation-errors" ng-show="exportForm.$errors">
|
||||
<h4>Errors have occured</h4>
|
||||
<ul>
|
||||
@@ -5,17 +5,18 @@
|
||||
<form name="importForm" ng-submit="importForm.$valid && import(model)" ng-show="!processing">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="source">Source</label>
|
||||
<select id="source" name="source" class="form-control" ng-model="model.source">
|
||||
<option value="local">bitwarden (csv)</option>
|
||||
<option value="lastpass">LastPass (csv)</option>
|
||||
<option value="safeincloudcsv">SafeInCloud (csv)</option>
|
||||
<option value="keypassxml">KeyPass (xml)</option>
|
||||
<label for="source">1. Select the source of this import file</label>
|
||||
<select id="source" name="source" class="form-control" ng-model="model.source" ng-change="setSource()">
|
||||
<option ng-repeat="option in options" value="{{option.id}}">{{option.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="callout callout-default">
|
||||
<h4><i class="fa fa-info-circle"></i> {{source.name}} Instructions</h4>
|
||||
<div ng-bind-html="source.instructions"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="file">File</label>
|
||||
<input type="file" id="file" name="file" />
|
||||
<label for="file">2. Select the import file</label>
|
||||
<input type="file" id="file" name="file" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -1,27 +1,27 @@
|
||||
angular
|
||||
.module('bit.vault')
|
||||
|
||||
.controller('vaultAddSiteController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, passwordService, folders, selectedFolder, $analytics) {
|
||||
$analytics.eventTrack('vaultAddSiteController', { category: 'Modal' });
|
||||
.controller('vaultAddLoginController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, passwordService, folders, selectedFolder, $analytics) {
|
||||
$analytics.eventTrack('vaultAddLoginController', { category: 'Modal' });
|
||||
$scope.folders = folders;
|
||||
$scope.site = {
|
||||
$scope.login = {
|
||||
folderId: selectedFolder ? selectedFolder.id : null
|
||||
};
|
||||
|
||||
$scope.savePromise = null;
|
||||
$scope.save = function (model) {
|
||||
var site = cipherService.encryptSite(model);
|
||||
$scope.savePromise = apiService.sites.post(site, function (siteResponse) {
|
||||
$analytics.eventTrack('Created Site');
|
||||
var decSite = cipherService.decryptSite(siteResponse);
|
||||
$uibModalInstance.close(decSite);
|
||||
var login = cipherService.encryptLogin(model);
|
||||
$scope.savePromise = apiService.logins.post(login, function (loginResponse) {
|
||||
$analytics.eventTrack('Created Login');
|
||||
var decLogin = cipherService.decryptLogin(loginResponse);
|
||||
$uibModalInstance.close(decLogin);
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
$scope.generatePassword = function () {
|
||||
if (!$scope.site.password || confirm('Are you sure you want to overwrite the current password?')) {
|
||||
if (!$scope.login.password || confirm('Are you sure you want to overwrite the current password?')) {
|
||||
$analytics.eventTrack('Generated Password From Add');
|
||||
$scope.site.password = passwordService.generatePassword({ length: 10, special: true });
|
||||
$scope.login.password = passwordService.generatePassword({ length: 12, special: true });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -37,6 +37,10 @@
|
||||
alert('Your web browser does not support easy clipboard copying. Copy it manually instead.');
|
||||
};
|
||||
|
||||
$scope.folderSort = function (item) {
|
||||
return item.name.toLowerCase();
|
||||
};
|
||||
|
||||
function selectPassword(e) {
|
||||
var target = $(e.trigger).parent().prev();
|
||||
if (target.attr('type') === 'text') {
|
||||
@@ -2,35 +2,35 @@
|
||||
.module('bit.vault')
|
||||
|
||||
.controller('vaultController', function ($scope, $uibModal, apiService, $filter, cryptoService, authService, toastr, cipherService) {
|
||||
$scope.sites = [];
|
||||
$scope.logins = [];
|
||||
$scope.folders = [];
|
||||
|
||||
$scope.loadingSites = true;
|
||||
apiService.sites.list({}, function (sites) {
|
||||
$scope.loadingSites = false;
|
||||
$scope.loadingLogins = true;
|
||||
apiService.logins.list({}, function (logins) {
|
||||
$scope.loadingLogins = false;
|
||||
|
||||
var decSites = [];
|
||||
for (var i = 0; i < sites.Data.length; i++) {
|
||||
var decSite = {
|
||||
id: sites.Data[i].Id,
|
||||
folderId: sites.Data[i].FolderId,
|
||||
favorite: sites.Data[i].Favorite
|
||||
var decLogins = [];
|
||||
for (var i = 0; i < logins.Data.length; i++) {
|
||||
var decLogin = {
|
||||
id: logins.Data[i].Id,
|
||||
folderId: logins.Data[i].FolderId,
|
||||
favorite: logins.Data[i].Favorite
|
||||
};
|
||||
|
||||
try { decSite.name = cryptoService.decrypt(sites.Data[i].Name); }
|
||||
catch (err) { decSite.name = '[error: cannot decrypt]'; }
|
||||
try { decLogin.name = cryptoService.decrypt(logins.Data[i].Name); }
|
||||
catch (err) { decLogin.name = '[error: cannot decrypt]'; }
|
||||
|
||||
if (sites.Data[i].Username) {
|
||||
try { decSite.username = cryptoService.decrypt(sites.Data[i].Username); }
|
||||
catch (err) { decSite.username = '[error: cannot decrypt]'; }
|
||||
if (logins.Data[i].Username) {
|
||||
try { decLogin.username = cryptoService.decrypt(logins.Data[i].Username); }
|
||||
catch (err) { decLogin.username = '[error: cannot decrypt]'; }
|
||||
}
|
||||
|
||||
decSites.push(decSite);
|
||||
decLogins.push(decLogin);
|
||||
}
|
||||
|
||||
$scope.sites = decSites;
|
||||
$scope.logins = decLogins;
|
||||
}, function () {
|
||||
$scope.loadingSites = false;
|
||||
$scope.loadingLogins = false;
|
||||
});
|
||||
|
||||
$scope.loadingFolders = true;
|
||||
@@ -66,56 +66,70 @@
|
||||
return item.name.toLowerCase();
|
||||
};
|
||||
|
||||
$scope.editSite = function (site) {
|
||||
$scope.editLogin = function (login) {
|
||||
var editModel = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/vault/views/vaultEditSite.html',
|
||||
controller: 'vaultEditSiteController',
|
||||
templateUrl: 'app/vault/views/vaultEditLogin.html',
|
||||
controller: 'vaultEditLoginController',
|
||||
resolve: {
|
||||
siteId: function () { return site.id; },
|
||||
loginId: function () { return login.id; },
|
||||
folders: function () { return $scope.folders; }
|
||||
}
|
||||
});
|
||||
|
||||
editModel.result.then(function (editedSite) {
|
||||
var site = $filter('filter')($scope.sites, { id: editedSite.id }, true);
|
||||
if (site && site.length > 0) {
|
||||
site[0].folderId = editedSite.folderId;
|
||||
site[0].name = editedSite.name;
|
||||
site[0].username = editedSite.username;
|
||||
site[0].favorite = editedSite.favorite;
|
||||
editModel.result.then(function (returnVal) {
|
||||
if (returnVal.action === 'edit') {
|
||||
var loginToUpdate = $filter('filter')($scope.logins, { id: returnVal.data.id }, true);
|
||||
|
||||
if (loginToUpdate && loginToUpdate.length > 0) {
|
||||
loginToUpdate[0].folderId = returnVal.data.folderId;
|
||||
loginToUpdate[0].name = returnVal.data.name;
|
||||
loginToUpdate[0].username = returnVal.data.username;
|
||||
loginToUpdate[0].favorite = returnVal.data.favorite;
|
||||
}
|
||||
}
|
||||
else if (returnVal.action === 'delete') {
|
||||
var loginToDelete = $filter('filter')($scope.logins, { id: returnVal.data }, true);
|
||||
if (loginToDelete && loginToDelete.length > 0) {
|
||||
var index = $scope.logins.indexOf(loginToDelete[0]);
|
||||
if (index > -1) {
|
||||
$scope.logins.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$on('vaultAddSite', function (event, args) {
|
||||
$scope.addSite();
|
||||
$scope.$on('vaultAddLogin', function (event, args) {
|
||||
$scope.addLogin();
|
||||
});
|
||||
|
||||
$scope.addSite = function (folder) {
|
||||
$scope.addLogin = function (folder) {
|
||||
var addModel = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: 'app/vault/views/vaultAddSite.html',
|
||||
controller: 'vaultAddSiteController',
|
||||
templateUrl: 'app/vault/views/vaultAddLogin.html',
|
||||
controller: 'vaultAddLoginController',
|
||||
resolve: {
|
||||
folders: function () { return $scope.folders; },
|
||||
selectedFolder: function () { return folder; }
|
||||
}
|
||||
});
|
||||
|
||||
addModel.result.then(function (addedSite) {
|
||||
$scope.sites.push(addedSite);
|
||||
addModel.result.then(function (addedLogin) {
|
||||
$scope.logins.push(addedLogin);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteSite = function (site) {
|
||||
if (!confirm('Are you sure you want to delete this site (' + site.name + ')?')) {
|
||||
$scope.deleteLogin = function (login) {
|
||||
if (!confirm('Are you sure you want to delete this login (' + login.name + ')?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
apiService.sites.del({ id: site.id }, function () {
|
||||
var index = $scope.sites.indexOf(site);
|
||||
$scope.sites.splice(index, 1);
|
||||
apiService.logins.del({ id: login.id }, function () {
|
||||
var index = $scope.logins.indexOf(login);
|
||||
if (index > -1) {
|
||||
$scope.logins.splice(index, 1);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -162,7 +176,9 @@
|
||||
|
||||
apiService.folders.del({ id: folder.id }, function () {
|
||||
var index = $scope.folders.indexOf(folder);
|
||||
$scope.folders.splice(index, 1);
|
||||
if (index > -1) {
|
||||
$scope.folders.splice(index, 1);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -171,7 +187,7 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
var sites = $filter('filter')($scope.sites, { folderId: folder.id });
|
||||
return sites.length === 0;
|
||||
var logins = $filter('filter')($scope.logins, { folderId: folder.id });
|
||||
return logins.length === 0;
|
||||
};
|
||||
});
|
||||
71
src/app/vault/vaultEditLoginController.js
Normal file
71
src/app/vault/vaultEditLoginController.js
Normal file
@@ -0,0 +1,71 @@
|
||||
angular
|
||||
.module('bit.vault')
|
||||
|
||||
.controller('vaultEditLoginController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, passwordService, loginId, folders, $analytics) {
|
||||
$analytics.eventTrack('vaultEditLoginController', { category: 'Modal' });
|
||||
$scope.folders = folders;
|
||||
$scope.login = {};
|
||||
|
||||
apiService.logins.get({ id: loginId }, function (login) {
|
||||
$scope.login = cipherService.decryptLogin(login);
|
||||
});
|
||||
|
||||
$scope.save = function (model) {
|
||||
var login = cipherService.encryptLogin(model);
|
||||
$scope.savePromise = apiService.logins.put({ id: loginId }, login, function (loginResponse) {
|
||||
$analytics.eventTrack('Edited Login');
|
||||
var decLogin = cipherService.decryptLogin(loginResponse);
|
||||
$uibModalInstance.close({
|
||||
action: 'edit',
|
||||
data: decLogin
|
||||
});
|
||||
}).$promise;
|
||||
};
|
||||
|
||||
$scope.generatePassword = function () {
|
||||
if (!$scope.login.password || confirm('Are you sure you want to overwrite the current password?')) {
|
||||
$analytics.eventTrack('Generated Password From Edit');
|
||||
$scope.login.password = passwordService.generatePassword({ length: 12, special: true });
|
||||
}
|
||||
};
|
||||
|
||||
$scope.clipboardSuccess = function (e) {
|
||||
e.clearSelection();
|
||||
selectPassword(e);
|
||||
};
|
||||
|
||||
$scope.clipboardError = function (e, password) {
|
||||
if (password) {
|
||||
selectPassword(e);
|
||||
}
|
||||
alert('Your web browser does not support easy clipboard copying. Copy it manually instead.');
|
||||
};
|
||||
|
||||
$scope.folderSort = function (item) {
|
||||
return item.name.toLowerCase();
|
||||
};
|
||||
|
||||
function selectPassword(e) {
|
||||
var target = $(e.trigger).parent().prev();
|
||||
if (target.attr('type') === 'text') {
|
||||
target.select();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.delete = function () {
|
||||
if (!confirm('Are you sure you want to delete this login (' + $scope.login.name + ')?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
apiService.logins.del({ id: $scope.login.id }, function () {
|
||||
$uibModalInstance.close({
|
||||
action: 'delete',
|
||||
data: $scope.login.id
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.close = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
};
|
||||
});
|
||||
@@ -1,57 +1,60 @@
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
My Vault
|
||||
<small>safe and secure</small>
|
||||
<small>{{folders.length > 0 ? folders.length - 1 : 0}} folders, {{logins.length}} logins</small>
|
||||
</h1>
|
||||
</section>
|
||||
<section class="content">
|
||||
<div ng-show="loadingFolders && !folders.length">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
<div class="box" ng-repeat="folder in folders | orderBy: folderSort" ng-show="folders.length">
|
||||
<div class="box" ng-repeat="folder in folders | orderBy: folderSort" ng-show="folders.length && (!main.searchVaultText || folderLogins.length)">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title"><i class="fa fa-folder-open"></i> {{folder.name}}</h3>
|
||||
<h3 class="box-title"><i class="fa fa-folder-open"></i> {{folder.name}} <small>{{folderLogins.length}} logins</small></h3>
|
||||
<div class="box-tools pull-right">
|
||||
<button type="button" class="btn btn-box-tool" ng-click="addLogin(folder)" uib-tooltip="Add Login">
|
||||
<i class="fa fa-plus"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-box-tool" ng-click="deleteFolder(folder)" ng-show="canDeleteFolder(folder)" uib-tooltip="Delete">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-box-tool" ng-click="editFolder(folder)" ng-show="folder.id" uib-tooltip="Edit">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-box-tool" data-widget="collapse" uib-tooltip="Collapse">
|
||||
<button type="button" class="btn btn-box-tool" data-widget="collapse" uib-tooltip="Collapse/Expand">
|
||||
<i class="fa fa-minus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-body" ng-class="{'no-padding': folderSites.length}">
|
||||
<div ng-show="loadingSites && !folderSites.length">
|
||||
<p>Loading sites...</p>
|
||||
<div class="box-body" ng-class="{'no-padding': folderLogins.length}">
|
||||
<div ng-show="loadingLogins && !folderLogins.length">
|
||||
<p>Loading logins...</p>
|
||||
</div>
|
||||
<div ng-show="!loadingSites && !folderSites.length">
|
||||
<p>No sites in this folder.</p>
|
||||
<button type="button" ng-click="addSite(folder)" class="btn btn-default btn-flat">Add a Site</button>
|
||||
<div ng-show="!loadingLogins && !folderLogins.length">
|
||||
<p>No logins in this folder.</p>
|
||||
<button type="button" ng-click="addLogin(folder)" class="btn btn-default btn-flat">Add a Login</button>
|
||||
</div>
|
||||
<div class="table-responsive" ng-show="folderSites.length">
|
||||
<div class="table-responsive" ng-show="folderLogins.length">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 75px; min-width: 75px;"></th>
|
||||
<th>Site</th>
|
||||
<th>Name</th>
|
||||
<th style="width: 300px;">Username</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="site in folderSites = (sites | filter: { folderId: folder.id } | filter: (main.searchVaultText || '') | orderBy: ['name', 'username'])">
|
||||
<tr ng-repeat="login in folderLogins = (logins | filter: { folderId: folder.id } | filter: (main.searchVaultText || '') | orderBy: ['name', 'username'])">
|
||||
<td>
|
||||
<button type="button" ng-click="deleteSite(site)" class="btn btn-link btn-table" uib-tooltip="Delete"><i class="fa fa-lg fa-trash"></i></button>
|
||||
<button type="button" ng-click="editSite(site)" class="btn btn-link btn-table" uib-tooltip="View/Edit"><i class="fa fa-lg fa-pencil"></i></button>
|
||||
<button type="button" ng-click="deleteLogin(login)" class="btn btn-link btn-table" uib-tooltip="Delete"><i class="fa fa-lg fa-trash"></i></button>
|
||||
<button type="button" ng-click="editLogin(login)" class="btn btn-link btn-table" uib-tooltip="View/Edit"><i class="fa fa-lg fa-pencil"></i></button>
|
||||
</td>
|
||||
<td>{{site.name}} <i class="fa fa-star text-muted" uib-tooltip="Favorite" ng-show="site.favorite"></i></td>
|
||||
<td>{{site.username}}</td>
|
||||
<td>{{login.name}} <i class="fa fa-star text-muted" uib-tooltip="Favorite" ng-show="login.favorite"></i></td>
|
||||
<td>{{login.username}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
@@ -11,7 +11,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group" show-errors>
|
||||
<label for="name">Name</label>
|
||||
<label for="name">Name</label> <span>*</span>
|
||||
<input type="text" id="name" name="Name" ng-model="folder.name" class="form-control" required api-field />
|
||||
</div>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user