@@ -1,3 +0,0 @@ | |||||
language: node_js | |||||
node_js: | |||||
- 4 |
@@ -0,0 +1,21 @@ | |||||
'use strict'; | |||||
var gulp = require('gulp'); | |||||
gulp.task('lesspass', [], function () { | |||||
return gulp.src(['node_modules/lesspass-pure/dist/**/*']) | |||||
.pipe(gulp.dest('dist/')); | |||||
}); | |||||
gulp.task('images', [], function () { | |||||
return gulp.src(['images/**/*']) | |||||
.pipe(gulp.dest('dist/')); | |||||
}); | |||||
gulp.task('build', [], function () { | |||||
gulp.start('lesspass', 'images'); | |||||
}); | |||||
gulp.task('default', ['build'], function () { | |||||
}); |
@@ -5,10 +5,179 @@ | |||||
<title>LessPass</title> | <title>LessPass</title> | ||||
<meta http-equiv=X-UA-Compatible content="IE=edge,chrome=1"> | <meta http-equiv=X-UA-Compatible content="IE=edge,chrome=1"> | ||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | ||||
<link rel="stylesheet" href="dist/styles.css"> | |||||
<link rel="stylesheet" href="dist/lesspass.css"> | |||||
<style> | |||||
html, body { | |||||
height: 100%; | |||||
width: 100%; | |||||
} | |||||
body { | |||||
background: #008ed6 fixed top; | |||||
background-size: cover; | |||||
margin: 0; | |||||
} | |||||
#navbar__logo { | |||||
height: 2em; | |||||
} | |||||
.white-link { | |||||
color: white; | |||||
} | |||||
.white-link:hover, .white-link:focus, .white-link:active { | |||||
text-decoration: none; | |||||
color: white; | |||||
} | |||||
.jumbotron { | |||||
background-color: transparent; | |||||
padding-left: 0; | |||||
padding-right: 0; | |||||
} | |||||
#hero-text { | |||||
color: #ffffff; | |||||
padding-top: 6em; | |||||
} | |||||
@media (max-width: 544px) { | |||||
#hero__password-generator-block { | |||||
padding: 0; | |||||
} | |||||
.jumbotron { | |||||
padding: 0; | |||||
} | |||||
} | |||||
.feature-image { | |||||
width: 64px; | |||||
} | |||||
* { | |||||
border-radius: 0 !important; | |||||
} | |||||
.white{ | |||||
color:white; | |||||
} | |||||
</style> | |||||
</head> | </head> | ||||
<body> | <body> | ||||
<div id="app"></div> | |||||
<script src="dist/bundle.js"></script> | |||||
<div class="container p-l-0 p-r-0 p-y-2 hidden-sm-down"> | |||||
<div class="col-xs-6"> | |||||
<a class="nav-link white-link" href="/"> | |||||
<img src="dist/logo-white.png" alt="LessPass" id="navbar__logo"> | |||||
</a> | |||||
</div> | |||||
<div class="col-xs-6 text-xs-right"> | |||||
<nav class="nav nav-inline pull-sm-right" style="line-height: 2em"> | |||||
<a class="nav-link white-link" | |||||
href="https://addons.mozilla.org/en-US/firefox/addon/lesspass/"> | |||||
<i class="fa fa-firefox" aria-hidden="true"></i> Firefox Extension | |||||
</a> | |||||
<a class="nav-link white-link" | |||||
href="https://chrome.google.com/webstore/detail/lesspass/lcmbpoclaodbgkbjafnkbbinogcbnjih"> | |||||
<i class="fa fa-chrome" aria-hidden="true"></i> Chrome Extension | |||||
</a> | |||||
</nav> | |||||
</div> | |||||
</div> | |||||
<div class="container"> | |||||
<div class="jumbotron"> | |||||
<div class="row"> | |||||
<div id="hero__password-generator-block" class="col-lg-6 push-lg-6"> | |||||
<div id="lesspass"></div> | |||||
</div> | |||||
<div id="hero-text" class="col-lg-6 pull-lg-6"> | |||||
<h1 class="display-5">Next-Gen Open Source Password Manager</h1> | |||||
<p class="lead"> | |||||
Stop wasting time synchronize your encrypted vault. | |||||
Remember one master password to access your passwords, anywhere, anytime. | |||||
No sync needed. | |||||
</p> | |||||
<p class="lead"> | |||||
<button class="btn btn-secondary">How it works ?</button> | |||||
</p> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="container white"> | |||||
<div class="row"> | |||||
<div class="col-md-6 col-lg-3 m-b-3"> | |||||
<div class="media"> | |||||
<div class="media-left"> | |||||
<img class="media-object feature-image" src="dist/responsive.png" | |||||
alt="Available far and wide"> | |||||
</div> | |||||
<div class="media-body"> | |||||
<h4 class="media-heading">Available everywhere</h4> | |||||
<p> | |||||
LessPass is a web application and works on all devices | |||||
(computer, smartphone, tablet and your smartTV) | |||||
</p> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="col-md-6 col-lg-3 m-b-3"> | |||||
<div class="media"> | |||||
<div class="media-left"> | |||||
<img class="media-object feature-image" src="dist/no-cloud.png" | |||||
alt="No Cloud"> | |||||
</div> | |||||
<div class="media-body"> | |||||
<h4 class="media-heading">No storage</h4> | |||||
<p> | |||||
LessPass regenerates your passwords when you need them. No cloud storage is required | |||||
</p> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="clearfix hidden-lg-up"></div> | |||||
<div class="col-md-6 col-lg-3 m-b-3"> | |||||
<div class="media"> | |||||
<a class="media-left white-link" href="https://github.com/lesspass/lesspass/"> | |||||
<img class="media-object feature-image" src="dist/open-source.png" | |||||
alt="Open Source"> | |||||
</a> | |||||
<div class="media-body"> | |||||
<h4 class="media-heading">Open Source</h4> | |||||
<p> | |||||
LessPass is <strong>open-source</strong>. So its security can be audited. | |||||
Source code is available on | |||||
<a class="white-link" href="https://github.com/lesspass/lesspass/">Github</a> | |||||
</p> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="col-md-6 col-lg-3 m-b-3"> | |||||
<div class="media"> | |||||
<div class="media-left"> | |||||
<img class="media-object feature-image" src="dist/free.png" | |||||
alt="Free"> | |||||
</div> | |||||
<div class="media-body"> | |||||
<h4 class="media-heading">Free</h4> | |||||
<p> | |||||
LessPass is free<br>and always will be | |||||
</p> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="container text-xs-center text-sm-left white m-y-3"> | |||||
<div class="row"> | |||||
<div class="col-xs-12"> | |||||
<small> | |||||
Copyright LessPass <br> | |||||
Created by <a class="white-link" href="https://twitter.com/guillaume20100">Guillaume Vincent</a> | |||||
</small> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<script src="dist/lesspass.js"></script> | |||||
</body> | </body> | ||||
</html> | </html> |
@@ -1,14 +0,0 @@ | |||||
<!DOCTYPE html> | |||||
<html> | |||||
<head> | |||||
<meta charset="utf-8"> | |||||
<title>LessPass</title> | |||||
<meta http-equiv=X-UA-Compatible content="IE=edge,chrome=1"> | |||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |||||
<link rel="stylesheet" href="dist/styles.css"> | |||||
</head> | |||||
<body> | |||||
<div id="app"></div> | |||||
<script src="dist/webextension.js"></script> | |||||
</body> | |||||
</html> |
@@ -7,63 +7,13 @@ | |||||
"homepage": "https://github.com/lesspass/frontend#readme", | "homepage": "https://github.com/lesspass/frontend#readme", | ||||
"bugs": "https://github.com/lesspass/frontend/issues", | "bugs": "https://github.com/lesspass/frontend/issues", | ||||
"scripts": { | "scripts": { | ||||
"dev": "npm install && webpack-dev-server --inline --hot --host 0.0.0.0", | |||||
"build": "rm -rf dist && NODE_ENV=production webpack --display-error-details --progress --hide-modules", | |||||
"test": "ava test --compilers js:babel-register" | |||||
"build": "rm -rf dist && gulp" | |||||
}, | }, | ||||
"dependencies": { | "dependencies": { | ||||
"express": "^4.14.0" | |||||
"express": "^4.14.0", | |||||
"lesspass-pure": "^2.0.1" | |||||
}, | }, | ||||
"devDependencies": { | "devDependencies": { | ||||
"ava": "^0.16.0", | |||||
"axios": "^0.14.0", | |||||
"babel-core": "^6.17.0", | |||||
"babel-loader": "^6.2.5", | |||||
"babel-plugin-transform-runtime": "^6.15.0", | |||||
"babel-polyfill": "^6.16.0", | |||||
"babel-preset-es2015": "^6.16.0", | |||||
"babel-preset-stage-2": "^6.17.0", | |||||
"babel-register": "^6.16.3", | |||||
"babel-runtime": "^6.11.6", | |||||
"bootstrap": "^4.0.0-alpha.4", | |||||
"clipboard": "^1.5.12", | |||||
"css-loader": "^0.25.0", | |||||
"extract-text-webpack-plugin": "^1.0.1", | |||||
"file-loader": "^0.9.0", | |||||
"font-awesome": "^4.6.3", | |||||
"hint.css": "^2.3.2", | |||||
"jquery": "^3.1.1", | |||||
"json-loader": "^0.5.4", | |||||
"jwt-decode": "^2.1.0", | |||||
"lesspass": "^4.0.4", | |||||
"lodash.debounce": "^4.0.8", | |||||
"moment": "^2.15.0", | |||||
"nock": "^8.1.0", | |||||
"pilou": "^0.1.4", | |||||
"style-loader": "^0.13.1", | |||||
"tether": "^1.3.7", | |||||
"url-loader": "^0.5.7", | |||||
"vue": "^2.0.1", | |||||
"vue-loader": "^9.5.1", | |||||
"vue-router": "^2.0.0", | |||||
"vuex": "^2.0.0", | |||||
"webpack": "^1.13.2", | |||||
"webpack-dev-server": "^1.16.2" | |||||
}, | |||||
"babel": { | |||||
"presets": [ | |||||
"es2015", | |||||
"stage-2" | |||||
], | |||||
"plugins": [ | |||||
"transform-runtime" | |||||
], | |||||
"comments": false | |||||
}, | |||||
"ava": { | |||||
"require": [ | |||||
"babel-register" | |||||
], | |||||
"babel": "inherit" | |||||
"gulp": "^3.9.1" | |||||
} | } | ||||
} | } |
@@ -1,29 +1,6 @@ | |||||
[![Build Status](https://travis-ci.org/lesspass/frontend.svg?branch=master)](https://travis-ci.org/lesspass/frontend) | |||||
# LessPass frontend | # LessPass frontend | ||||
frontend application for [lesspass.com](https://lesspass.com) | |||||
- vuejs | |||||
- vue-router | |||||
- vue-i18n | |||||
- ava and xo for tests | |||||
- webpack | |||||
- ES6 | |||||
## Tests | |||||
run frontend tests | |||||
cd frontend | |||||
npm install | |||||
npm test | |||||
## Build | |||||
Frontend application for [lesspass.com](https://lesspass.com) | |||||
npm run build | |||||
see [LessPass](https://github.com/lesspass/lesspass) project | see [LessPass](https://github.com/lesspass/lesspass) project |
@@ -1,10 +0,0 @@ | |||||
html, body { | |||||
height: 100%; | |||||
width: 100%; | |||||
} | |||||
body { | |||||
background: #008ed6 fixed top; | |||||
background-size: cover; | |||||
margin: 0; | |||||
} |
@@ -1,23 +0,0 @@ | |||||
<template> | |||||
<div> | |||||
<navigation-bar></navigation-bar> | |||||
<hero></hero> | |||||
<features class="m-t-3 m-b-2"></features> | |||||
<lesspass-footer class="m-y-2"></lesspass-footer> | |||||
</div> | |||||
</template> | |||||
<script type="text/ecmascript-6"> | |||||
import NavigationBar from './components/NavigationBar'; | |||||
import Hero from './components/Hero'; | |||||
import Features from './components/Features'; | |||||
import Footer from './components/Footer'; | |||||
export default { | |||||
components: { | |||||
'navigation-bar': NavigationBar, | |||||
'hero': Hero, | |||||
'features': Features, | |||||
'lesspass-footer': Footer, | |||||
} | |||||
} | |||||
</script> |
@@ -1,88 +0,0 @@ | |||||
import axios from 'axios'; | |||||
export default class Auth { | |||||
constructor(storage) { | |||||
this.user = { | |||||
authenticated: false | |||||
}; | |||||
this.storage = storage; | |||||
} | |||||
isAuthenticated() { | |||||
const token = this.storage.getToken(); | |||||
if (token.stillValid()) { | |||||
this.user.authenticated = true; | |||||
return true; | |||||
} | |||||
this.user.authenticated = false; | |||||
return false; | |||||
} | |||||
isGuest() { | |||||
return !this.isAuthenticated() | |||||
} | |||||
logout() { | |||||
return new Promise(resolve => { | |||||
this.storage.clear(); | |||||
this.user.authenticated = false; | |||||
resolve(); | |||||
}); | |||||
} | |||||
login(user, baseURL) { | |||||
const config = this.storage.json(); | |||||
if (baseURL) { | |||||
config.baseURL = baseURL; | |||||
} | |||||
return Auth._requestToken(user, config).then(token => { | |||||
this.storage.saveToken(token) | |||||
}) | |||||
} | |||||
static _requestToken(user, config = {}) { | |||||
return axios.post('/api/tokens/auth/', user, config).then(response => { | |||||
return response.data.token; | |||||
}); | |||||
} | |||||
refreshToken() { | |||||
const config = this.storage.json(); | |||||
const token = this.storage.getToken(); | |||||
return Auth._requestNewToken({token: token.name}, config).then(token => { | |||||
this.storage.saveToken(token) | |||||
}); | |||||
} | |||||
static _requestNewToken(token, config = {}) { | |||||
return axios.post('/api/tokens/refresh/', token, config).then(response => { | |||||
return response.data.token; | |||||
}); | |||||
} | |||||
register(user, baseURL) { | |||||
const config = this.storage.json(); | |||||
if (baseURL) { | |||||
config.baseURL = baseURL; | |||||
} | |||||
return axios.post('/api/auth/register/', user, config).then(response => { | |||||
return response.data; | |||||
}); | |||||
} | |||||
resetPassword(email, baseURL) { | |||||
const config = this.storage.json(); | |||||
if (baseURL) { | |||||
config.baseURL = baseURL; | |||||
} | |||||
return axios.post('/api/auth/password/reset/', email, config); | |||||
} | |||||
confirmResetPassword(password, baseURL) { | |||||
const config = this.storage.json(); | |||||
if (baseURL) { | |||||
config.baseURL = baseURL; | |||||
} | |||||
return axios.post('/api/auth/password/reset/confirm/', password, config); | |||||
} | |||||
} |
@@ -1,40 +0,0 @@ | |||||
import pilou from 'pilou'; | |||||
import {TOKEN_KEY} from './storage'; | |||||
export default class HTTP { | |||||
constructor(resourceName, storage) { | |||||
this.storage = storage; | |||||
this.resource = pilou(resourceName); | |||||
} | |||||
getRequestConfig() { | |||||
const config = this.storage.json(); | |||||
return { | |||||
baseURL: config.baseURL, | |||||
headers: {Authorization: `JWT ${config[TOKEN_KEY]}`} | |||||
}; | |||||
} | |||||
create(resource) { | |||||
return this.resource.create(resource, this.getRequestConfig()); | |||||
} | |||||
all(params = {}) { | |||||
const config = this.getRequestConfig(); | |||||
config.params = params; | |||||
return this.resource.all(config); | |||||
} | |||||
get(resource) { | |||||
return this.resource.get(resource, this.getRequestConfig()); | |||||
} | |||||
update(resource) { | |||||
return this.resource.update({id: resource.id}, resource, this.getRequestConfig()); | |||||
} | |||||
remove(resource) { | |||||
return this.resource.delete(resource, this.getRequestConfig()); | |||||
} | |||||
} |
@@ -1,42 +0,0 @@ | |||||
export const LOCAL_STORAGE_KEY = 'lesspass'; | |||||
export const TOKEN_KEY = 'jwt'; | |||||
import Token from './token'; | |||||
export default class Storage { | |||||
constructor(storage = window.localStorage) { | |||||
this.storage = storage; | |||||
} | |||||
_getLocalStorage() { | |||||
return JSON.parse(this.storage.getItem(LOCAL_STORAGE_KEY) || '{}') | |||||
} | |||||
json() { | |||||
const defaultStorage = { | |||||
baseURL: 'https://lesspass.com' | |||||
}; | |||||
const localStorage = this._getLocalStorage(); | |||||
return Object.assign(defaultStorage, localStorage); | |||||
} | |||||
save(data) { | |||||
const newData = Object.assign(this._getLocalStorage(), data); | |||||
this.storage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newData)); | |||||
} | |||||
clear() { | |||||
this.storage.clear(); | |||||
} | |||||
getToken() { | |||||
const storage = this.json(); | |||||
if (TOKEN_KEY in storage) { | |||||
return new Token(storage[TOKEN_KEY]); | |||||
} | |||||
return new Token(); | |||||
} | |||||
saveToken(token) { | |||||
this.save({[TOKEN_KEY]: token}) | |||||
} | |||||
} |
@@ -1,43 +0,0 @@ | |||||
import moment from 'moment'; | |||||
import jwtDecode from 'jwt-decode'; | |||||
export const TOKEN_KEY = 'jwt'; | |||||
export default class Token { | |||||
constructor(tokenName) { | |||||
this.name = tokenName | |||||
} | |||||
stillValid(now = moment()) { | |||||
try { | |||||
return this._expirationDateSuperiorTo(now); | |||||
} | |||||
catch (err) { | |||||
return false; | |||||
} | |||||
} | |||||
expiresIn(duration, unit, now = moment()) { | |||||
try { | |||||
const nowPlusDuration = now.add(duration, moment.normalizeUnits(unit)); | |||||
return this._expirationDateInferiorTo(nowPlusDuration); | |||||
} | |||||
catch (err) { | |||||
return false; | |||||
} | |||||
} | |||||
_expirationDateInferiorTo(date) { | |||||
const expireDate = this._getTokenExpirationDate(); | |||||
return expireDate.diff(date) < 0; | |||||
} | |||||
_expirationDateSuperiorTo(date) { | |||||
return !this._expirationDateInferiorTo(date) | |||||
} | |||||
_getTokenExpirationDate() { | |||||
const decodedToken = jwtDecode(this.name); | |||||
return moment(decodedToken.exp * 1000); | |||||
} | |||||
} |
@@ -1,8 +0,0 @@ | |||||
export function showTooltip(elem, msg) { | |||||
var classNames = elem.className; | |||||
elem.setAttribute('class', classNames + ' hint--top'); | |||||
elem.setAttribute('aria-label', msg); | |||||
setTimeout(function () { | |||||
elem.setAttribute('class', classNames); | |||||
}, 2000); | |||||
} |
@@ -1,35 +0,0 @@ | |||||
<style> | |||||
.fa-white { | |||||
color: #ffffff; | |||||
} | |||||
</style> | |||||
<template> | |||||
<div id="delete-button"> | |||||
<button type="button" class="btn btn-danger" v-on:click="confirm"> | |||||
<i class="fa-white fa fa-trash"></i> | |||||
</button> | |||||
</div> | |||||
</template> | |||||
<script type="text/ecmascript-6"> | |||||
export default { | |||||
data() { | |||||
return { | |||||
pending: false | |||||
} | |||||
}, | |||||
props: { | |||||
action: {type: Function, required: true}, | |||||
text: {type: String, required: true}, | |||||
object: {type: Object, required: true} | |||||
}, | |||||
methods: { | |||||
confirm() { | |||||
this.pending = true; | |||||
var response = confirm(this.text); | |||||
if (response == true) { | |||||
this.action(this.object); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
</script> |
@@ -1,80 +0,0 @@ | |||||
<style scoped> | |||||
.container { | |||||
color: #ffffff; | |||||
} | |||||
.feature-image { | |||||
width: 64px; | |||||
} | |||||
.white-link { | |||||
color: white; | |||||
text-decoration: underline; | |||||
} | |||||
</style> | |||||
<template> | |||||
<div class="container"> | |||||
<div class="row"> | |||||
<div class="col-md-6 col-lg-3 m-b-3"> | |||||
<div class="media"> | |||||
<div class="media-left"> | |||||
<img class="media-object feature-image" src="../images/responsive.png" | |||||
alt="Available far and wide"> | |||||
</div> | |||||
<div class="media-body"> | |||||
<h4 class="media-heading">Available everywhere</h4> | |||||
<p> | |||||
LessPass is a web application and works on all devices | |||||
(computer, smartphone, tablet and your smartTV) | |||||
</p> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="col-md-6 col-lg-3 m-b-3"> | |||||
<div class="media"> | |||||
<div class="media-left"> | |||||
<img class="media-object feature-image" src="../images/no-cloud.png" | |||||
alt="No Cloud"> | |||||
</div> | |||||
<div class="media-body"> | |||||
<h4 class="media-heading">No storage</h4> | |||||
<p> | |||||
LessPass regenerates your passwords when you need them. No cloud storage is required | |||||
</p> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="clearfix hidden-lg-up"></div> | |||||
<div class="col-md-6 col-lg-3 m-b-3"> | |||||
<div class="media"> | |||||
<a class="media-left white-link" href="https://github.com/lesspass/lesspass/"> | |||||
<img class="media-object feature-image" src="../images/open-source.png" | |||||
alt="Open Source"> | |||||
</a> | |||||
<div class="media-body"> | |||||
<h4 class="media-heading">Open Source</h4> | |||||
<p> | |||||
LessPass is <strong>open-source</strong>. So its security can be audited. | |||||
Source code is available on | |||||
<a class="white-link" href="https://github.com/lesspass/lesspass/">Github</a> | |||||
</p> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="col-md-6 col-lg-3 m-b-3"> | |||||
<div class="media"> | |||||
<div class="media-left"> | |||||
<img class="media-object feature-image" src="../images/free.png" | |||||
alt="Free"> | |||||
</div> | |||||
<div class="media-body"> | |||||
<h4 class="media-heading">Free</h4> | |||||
<p> | |||||
LessPass is free<br>and always will be | |||||
</p> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</template> |
@@ -1,71 +0,0 @@ | |||||
<style> | |||||
#fingerprint { | |||||
min-width: 90px; | |||||
text-align: center; | |||||
background-color: transparent; | |||||
color: white; | |||||
} | |||||
#fingerprint i { | |||||
color: black; | |||||
position: relative; | |||||
padding: 0; | |||||
text-shadow: 1px 1px 0 white; | |||||
font-size: 1.3em; | |||||
} | |||||
</style> | |||||
<template> | |||||
<span class="input-group-btn" v-if="fingerprint"> | |||||
<button id="fingerprint" class="btn" type="button" tabindex="-1"> | |||||
<small class="hint--left" aria-label="master password fingerprint"> | |||||
<i class="fa fa-fw" v-bind:class="[icon1]" v-bind:style="{ color: color1 }"></i> | |||||
<i class="fa fa-fw" v-bind:class="[icon2]" v-bind:style="{ color: color2 }"></i> | |||||
<i class="fa fa-fw" v-bind:class="[icon3]" v-bind:style="{ color: color3 }"></i> | |||||
</small> | |||||
</button> | |||||
</span> | |||||
</template> | |||||
<script type="text/ecmascript-6"> | |||||
const crypto = require('crypto'); | |||||
export default { | |||||
data(){ | |||||
return { | |||||
icon1: '', | |||||
icon2: '', | |||||
icon3: '', | |||||
color1: '', | |||||
color2: '', | |||||
color3: '', | |||||
} | |||||
}, | |||||
props: ['fingerprint'], | |||||
watch: { | |||||
fingerprint: function (newFingerprint) { | |||||
const sha256 = crypto.createHmac('sha256', newFingerprint).digest('hex'); | |||||
const hash1 = sha256.substring(0, 6); | |||||
const hash2 = sha256.substring(6, 12); | |||||
const hash3 = sha256.substring(12, 18); | |||||
this.icon1 = this.getIcon(hash1); | |||||
this.icon2 = this.getIcon(hash2); | |||||
this.icon3 = this.getIcon(hash3); | |||||
this.color1 = this.getColor(hash1); | |||||
this.color2 = this.getColor(hash2); | |||||
this.color3 = this.getColor(hash3); | |||||
} | |||||
}, | |||||
methods: { | |||||
getColor(color) { | |||||
var colors = ['#000000','#074750', '#009191', '#FF6CB6', '#FFB5DA', '#490092','#006CDB','#B66DFF','#6DB5FE','#B5DAFE','#920000','#924900','#DB6D00','#24FE23']; | |||||
var index = parseInt(color, 16) % colors.length; | |||||
return colors[index]; | |||||
}, | |||||
getIcon(hash) { | |||||
var icons = ['fa-hashtag', 'fa-heart', 'fa-hotel', 'fa-university', 'fa-plug', 'fa-ambulance', 'fa-bus', 'fa-car', 'fa-plane', 'fa-rocket', 'fa-ship', 'fa-subway', 'fa-truck', 'fa-jpy', 'fa-eur', 'fa-btc', 'fa-usd', 'fa-gbp', 'fa-archive', 'fa-area-chart', 'fa-bed', 'fa-beer', 'fa-bell', 'fa-binoculars', 'fa-birthday-cake', 'fa-bomb', 'fa-briefcase', 'fa-bug', 'fa-camera', 'fa-cart-plus', 'fa-certificate', 'fa-coffee', 'fa-cloud', 'fa-coffee', 'fa-comment', 'fa-cube', 'fa-cutlery', 'fa-database', 'fa-diamond', 'fa-exclamation-circle', 'fa-eye', 'fa-flag', 'fa-flask', 'fa-futbol-o', 'fa-gamepad', 'fa-graduation-cap']; | |||||
var index = parseInt(hash, 16) % icons.length; | |||||
return icons[index]; | |||||
} | |||||
} | |||||
} | |||||
</script> |
@@ -1,26 +0,0 @@ | |||||
<style scoped> | |||||
.container{ | |||||
color:#ffffff; | |||||
} | |||||
.white-link { | |||||
color: white; | |||||
text-decoration: underline; | |||||
} | |||||
.white-link:hover { | |||||
color: white; | |||||
text-decoration: none; | |||||
} | |||||
</style> | |||||
<template> | |||||
<div class="container text-xs-center text-sm-left"> | |||||
<div class="row"> | |||||
<div class="col-xs-12"> | |||||
<small> | |||||
Copyright LessPass <br> | |||||
Created by <a class="white-link" href="https://twitter.com/guillaume20100">Guillaume Vincent</a> | |||||
</small> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</template> |
@@ -1,53 +0,0 @@ | |||||
<style scoped> | |||||
.jumbotron { | |||||
background-color: transparent; | |||||
padding-left: 0; | |||||
padding-right: 0; | |||||
} | |||||
#hero-text { | |||||
color: #ffffff; | |||||
padding-top: 6em; | |||||
} | |||||
@media (max-width: 544px) { | |||||
#hero__password-generator-block { | |||||
padding: 0; | |||||
} | |||||
.jumbotron { | |||||
padding: 0; | |||||
} | |||||
} | |||||
</style> | |||||
<template> | |||||
<div class="container"> | |||||
<div class="jumbotron"> | |||||
<div class="row"> | |||||
<div id="hero__password-generator-block" class="col-lg-6 push-lg-6"> | |||||
<lesspass></lesspass> | |||||
</div> | |||||
<div id="hero-text" class="col-lg-6 pull-lg-6"> | |||||
<h1 class="display-5">Next-Gen Open Source Password Manager</h1> | |||||
<p class="lead"> | |||||
Stop wasting time synchronize your encrypted vault. | |||||
Remember one master password to access your passwords, anywhere, anytime. | |||||
No sync needed. | |||||
</p> | |||||
<p class="lead"> | |||||
<button class="btn btn-secondary">How it works ?</button> | |||||
</p> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</template> | |||||
<script type="text/ecmascript-6"> | |||||
import LessPass from './LessPass'; | |||||
export default { | |||||
components: { | |||||
'lesspass': LessPass | |||||
} | |||||
} | |||||
</script> |
@@ -1,39 +0,0 @@ | |||||
<style> | |||||
#lesspass .white-link { | |||||
color: white; | |||||
} | |||||
#lesspass .white-link:hover, #lesspass .white-link:focus, #lesspass .white-link:active { | |||||
text-decoration: none; | |||||
color: white; | |||||
} | |||||
#lesspass * { | |||||
border-radius: 0 !important; | |||||
} | |||||
</style> | |||||
<template> | |||||
<div id="lesspass" class="card" style="border:none;"> | |||||
<lesspass-menu></lesspass-menu> | |||||
<div class="card-block" style="min-height: 400px;"> | |||||
<router-view></router-view> | |||||
</div> | |||||
</div> | |||||
</template> | |||||
<script type="text/ecmascript-6"> | |||||
import LessPassMenu from './Menu'; | |||||
export default { | |||||
name: 'LessPass', | |||||
components: { | |||||
'lesspass-menu': LessPassMenu | |||||
}, | |||||
created(){ | |||||
const fiveMinutes = 1000 * 60 * 5; | |||||
this.$store.dispatch('REFRESH_TOKEN'); | |||||
setInterval(()=> { | |||||
this.$store.dispatch('REFRESH_TOKEN'); | |||||
}, fiveMinutes); | |||||
} | |||||
} | |||||
</script> |
@@ -1,100 +0,0 @@ | |||||
<style> | |||||
.card-header-dark { | |||||
background-color: #555; | |||||
border-color: #555; | |||||
color: #FFF; | |||||
} | |||||
.menu-link { | |||||
color: #373a3c; | |||||
text-decoration: none; | |||||
} | |||||
.menu-link:hover, .menu-link:focus, .menu-link:active { | |||||
color: #373a3c; | |||||
text-decoration: none; | |||||
} | |||||
.white-link { | |||||
color: white; | |||||
} | |||||
.white-link:hover, .white-link:focus, .white-link:active { | |||||
text-decoration: none; | |||||
color: white; | |||||
} | |||||
.fa-clickable { | |||||
cursor: pointer; | |||||
} | |||||
</style> | |||||
<template> | |||||
<div id="menu"> | |||||
<div class="card-header" v-show="isAuthenticated"> | |||||
<div class="row"> | |||||
<div class="col-xs-6"> | |||||
<router-link class="menu-link" :to="{ name: 'home'}">LessPass</router-link> | |||||
<span class=" hint--right" aria-label="Save password" | |||||
v-on:click="saveOrUpdatePassword"> | |||||
<i class="fa fa-save m-l-1 fa-clickable" v-if="passwordStatus=='DIRTY'"></i> | |||||
</span> | |||||
<span v-if="passwordStatus=='CREATED'" class="text-success"> | |||||
<i class="fa fa-check m-l-1 text-success"></i> saved | |||||
</span> | |||||
<span v-if="passwordStatus=='UPDATED'" class="text-success"> | |||||
<i class="fa fa-check m-l-1 text-success"></i> updated | |||||
</span> | |||||
</div> | |||||
<div class="col-xs-6 text-xs-right"> | |||||
<div class="btn-group"> | |||||
<button type="button" class="btn dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" | |||||
aria-expanded="false" style="background-color:transparent; padding:0;"> | |||||
{{email}} | |||||
</button> | |||||
<div class="dropdown-menu dropdown-menu-right"> | |||||
<router-link class="dropdown-item" :to="{ name: 'passwords'}">Passwords</router-link> | |||||
<router-link class="dropdown-item" :to="{ name: 'help'}">Help</router-link> | |||||
<div class="dropdown-divider"></div> | |||||
<button class="dropdown-item" type="button" v-on:click="logout">Log out</button> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="card-header card-header-dark" v-show="isGuest"> | |||||
<div class="row"> | |||||
<div class="index-header"> | |||||
<div class="col-xs-6"> | |||||
<router-link class="white-link" :to="{ name: 'home'}">LessPass</router-link> | |||||
</div> | |||||
<div class="col-xs-6 text-xs-right"> | |||||
<router-link class="white-link" :to="{ name: 'login'}"> | |||||
<i class="fa fa-user-secret white" aria-hidden="true"></i> | |||||
</router-link> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</template> | |||||
<script type="text/ecmascript-6"> | |||||
import {mapGetters} from 'vuex'; | |||||
export default { | |||||
methods: { | |||||
logout(){ | |||||
this.$store.dispatch('LOGOUT'); | |||||
this.$router.push({name: 'home'}); | |||||
}, | |||||
saveOrUpdatePassword(){ | |||||
this.$store.dispatch('SAVE_OR_UPDATE_PASSWORD'); | |||||
} | |||||
}, | |||||
computed: mapGetters([ | |||||
'isAuthenticated', | |||||
'isGuest', | |||||
'email', | |||||
'passwordStatus' | |||||
]) | |||||
} | |||||
</script> |
@@ -1,26 +0,0 @@ | |||||
<style scoped> | |||||
#navbar__logo { | |||||
height: 2em; | |||||
} | |||||
</style> | |||||
<template> | |||||
<div class="container p-l-0 p-r-0 p-y-2 hidden-sm-down"> | |||||
<div class="col-xs-6"> | |||||
<router-link class="nav-link white-link" :to="{ name: 'home'}"> | |||||
<img src="../images/logo-white.png" alt="LessPass" id="navbar__logo"> | |||||
</router-link> | |||||
</div> | |||||
<div class="col-xs-6 text-xs-right"> | |||||
<nav class="nav nav-inline pull-sm-right" style="line-height: 2em"> | |||||
<a class="nav-link white-link" | |||||
href="https://addons.mozilla.org/en-US/firefox/addon/lesspass/"> | |||||
<i class="fa fa-firefox" aria-hidden="true"></i> Firefox Extension | |||||
</a> | |||||
<a class="nav-link white-link" | |||||
href="https://chrome.google.com/webstore/detail/lesspass/lcmbpoclaodbgkbjafnkbbinogcbnjih"> | |||||
<i class="fa fa-chrome" aria-hidden="true"></i> Chrome Extension | |||||
</a> | |||||
</nav> | |||||
</div> | |||||
</div> | |||||
</template> |
@@ -1,10 +0,0 @@ | |||||
<template> | |||||
<div style="display: none;"> | |||||
<label for="username"> | |||||
<input type="text" id="username" name="username" autocomplete="username"> | |||||
</label> | |||||
<label for="password"> | |||||
<input type="password" id="password" name="password" autocomplete="current-password"> | |||||
</label> | |||||
</div> | |||||
</template> |
@@ -1,27 +0,0 @@ | |||||
export default class Password { | |||||
constructor(password) { | |||||
this.password = password; | |||||
this.options = { | |||||
uppercase: password.uppercase, | |||||
lowercase: password.lowercase, | |||||
numbers: password.numbers, | |||||
symbols: password.symbols, | |||||
length: password.length, | |||||
counter: password.counter, | |||||
} | |||||
} | |||||
isNewPassword(passwords) { | |||||
let isNew = true; | |||||
passwords.forEach(pwd => { | |||||
if (pwd.site === this.password.site && pwd.login === this.password.login) { | |||||
isNew = false; | |||||
} | |||||
}); | |||||
return isNew; | |||||
} | |||||
json() { | |||||
return this.password | |||||
} | |||||
} |
@@ -1,17 +0,0 @@ | |||||
import 'babel-polyfill'; | |||||
import Vue from 'vue'; | |||||
import 'bootstrap/dist/css/bootstrap.css'; | |||||
import 'font-awesome/css/font-awesome.css'; | |||||
import 'hint.css/hint.css'; | |||||
import './App.css'; | |||||
import LessPass from './components/LessPass'; | |||||
import 'bootstrap/dist/js/bootstrap'; | |||||
import store from './store'; | |||||
import router from './router'; | |||||
new Vue({ | |||||
el: '#app', | |||||
store, | |||||
router, | |||||
render: h => h(LessPass) | |||||
}); |
@@ -1,17 +0,0 @@ | |||||
import 'babel-polyfill'; | |||||
import Vue from 'vue'; | |||||
import 'bootstrap/dist/css/bootstrap.css'; | |||||
import 'font-awesome/css/font-awesome.css'; | |||||
import 'hint.css/hint.css'; | |||||
import './App.css'; | |||||
import App from './App'; | |||||
import 'bootstrap/dist/js/bootstrap'; | |||||
import store from './store'; | |||||
import router from './router'; | |||||
new Vue({ | |||||
el: '#app', | |||||
store, | |||||
router, | |||||
render: h => h(App) | |||||
}); |
@@ -1,27 +0,0 @@ | |||||
import Vue from 'vue'; | |||||
import VueRouter from 'vue-router'; | |||||
import PasswordGenerator from './views/PasswordGenerator'; | |||||
import Login from './views/Login'; | |||||
import Register from './views/Register'; | |||||
import PasswordReset from './views/PasswordReset'; | |||||
import PasswordResetConfirm from './views/PasswordResetConfirm'; | |||||
import Passwords from './views/Passwords'; | |||||
Vue.use(VueRouter); | |||||
const routes = [ | |||||
{path: '/', name: 'home', component: PasswordGenerator}, | |||||
{path: '/login', name: 'login', component: Login}, | |||||
{path: '/register', name: 'register', component: Register}, | |||||
{path: '/passwords/', name: 'passwords', component: Passwords}, | |||||
{path: '/passwords/:id', name: 'password', component: PasswordGenerator}, | |||||
{path: '/password/reset', name: 'passwordReset', component: PasswordReset}, | |||||
{path: '/password/reset/confirm/:uid/:token', name: 'passwordResetConfirm', component: PasswordResetConfirm}, | |||||
]; | |||||
const router = new VueRouter({ | |||||
routes | |||||
}); | |||||
export default router; |
@@ -1,146 +0,0 @@ | |||||
import Vue from 'vue' | |||||
import Vuex from 'vuex' | |||||
import Auth from './api/auth'; | |||||
import HTTP from './api/http'; | |||||
import Storage from './api/storage'; | |||||
import Password from './domain/password'; | |||||
Vue.use(Vuex); | |||||
const storage = new Storage(); | |||||
const auth = new Auth(storage); | |||||
const PasswordsAPI = new HTTP('passwords', storage); | |||||
const defaultPassword = { | |||||
id: '', | |||||
site: '', | |||||
login: '', | |||||
uppercase: true, | |||||
lowercase: true, | |||||
numbers: true, | |||||
symbols: true, | |||||
length: 12, | |||||
counter: 1, | |||||
}; | |||||
const state = { | |||||
authenticated: auth.isAuthenticated(), | |||||
email: '', | |||||
passwordStatus: 'CLEAN', | |||||
passwords: [], | |||||
password: {} | |||||
}; | |||||
const mutations = { | |||||
LOGOUT(state){ | |||||
state.authenticated = false; | |||||
}, | |||||
USER_AUTHENTICATED(state, user){ | |||||
state.authenticated = true; | |||||
state.email = user.email; | |||||
}, | |||||
SET_PASSWORDS(state, passwords){ | |||||
state.passwords = passwords; | |||||
}, | |||||
SET_PASSWORD(state, {password}){ | |||||
state.password = password; | |||||
}, | |||||
DELETE_PASSWORD(state, {id}){ | |||||
var passwords = state.passwords; | |||||
state.passwords = passwords.filter(password => { | |||||
return password.id !== id; | |||||
}); | |||||
if (state.password.id === id) { | |||||
state.password = state.defaultPassword; | |||||
} | |||||
}, | |||||
PASSWORD_CLEAN(state){ | |||||
setTimeout(()=> { | |||||
state.passwordStatus = 'CLEAN'; | |||||
}, 5000); | |||||
}, | |||||
CHANGE_PASSWORD_STATUS(state, status){ | |||||
state.passwordStatus = status; | |||||
}, | |||||
SET_DEFAULT_PASSWORD(state){ | |||||
state.password = Object.assign({}, defaultPassword) | |||||
} | |||||
}; | |||||
const actions = { | |||||
USER_AUTHENTICATED: ({commit}, user) => commit('USER_AUTHENTICATED', user), | |||||
LOGOUT: ({commit}) => { | |||||
auth.logout(); | |||||
commit('LOGOUT'); | |||||
}, | |||||
SAVE_OR_UPDATE_PASSWORD: ({commit, state, dispatch}) => { | |||||
const password = new Password(state.password); | |||||
if (password.isNewPassword(state.passwords)) { | |||||
PasswordsAPI.create(password.json()).then(() => { | |||||
commit('CHANGE_PASSWORD_STATUS', 'CREATED'); | |||||
commit('PASSWORD_CLEAN'); | |||||
dispatch('FETCH_PASSWORDS'); | |||||
}) | |||||
} else { | |||||
PasswordsAPI.update(password.json()).then(() => { | |||||
commit('CHANGE_PASSWORD_STATUS', 'UPDATED'); | |||||
commit('PASSWORD_CLEAN'); | |||||
dispatch('FETCH_PASSWORDS'); | |||||
}) | |||||
} | |||||
}, | |||||
REFRESH_TOKEN: ({commit}) => { | |||||
if (auth.isAuthenticated()) { | |||||
auth.refreshToken().catch(() => { | |||||
commit('LOGOUT'); | |||||
}); | |||||
} | |||||
}, | |||||
PASSWORD_CHANGE({commit}, {password}){ | |||||
commit('SET_PASSWORD', {password}); | |||||
}, | |||||
PASSWORD_GENERATED: ({commit}) => { | |||||
commit('CHANGE_PASSWORD_STATUS', 'DIRTY'); | |||||
}, | |||||
FETCH_PASSWORDS: ({commit}) => { | |||||
if (auth.isAuthenticated()) { | |||||
PasswordsAPI.all().then(response => commit('SET_PASSWORDS', response.data.results)); | |||||
} | |||||
}, | |||||
FETCH_PASSWORD: ({commit}, {id}) => { | |||||
PasswordsAPI.get({id}).then(response => commit('SET_PASSWORD', {password: response.data})); | |||||
}, | |||||
DELETE_PASSWORD: ({commit}, {id}) => { | |||||
PasswordsAPI.remove({id}).then(()=> { | |||||
commit('DELETE_PASSWORD', {id}); | |||||
}); | |||||
}, | |||||
LOAD_DEFAULT_PASSWORD: ({commit})=> { | |||||
commit('SET_DEFAULT_PASSWORD'); | |||||
} | |||||
}; | |||||
const getters = { | |||||
passwords: state => state.passwords, | |||||
password: state => { | |||||
var password = state.password; | |||||
if (Object.keys(password).length === 0) { | |||||
return state.defaultPassword; | |||||
} | |||||
return password; | |||||
}, | |||||
isAuthenticated: state => state.authenticated, | |||||
isGuest: state => !state.authenticated, | |||||
passwordStatus: state => state.passwordStatus, | |||||
email: state => state.email, | |||||
baseURL: state => state.baseURL | |||||
}; | |||||
export default new Vuex.Store({ | |||||
state: Object.assign(state, storage.json()), | |||||
getters, | |||||
actions, | |||||
mutations | |||||
}); |
@@ -1,131 +0,0 @@ | |||||
<template> | |||||
<form v-on:submit.prevent="login"> | |||||
<div class="form-group row" v-if="showError"> | |||||
<div class="col-xs-12 text-muted text-danger"> | |||||
{{ errorMessage }} | |||||
</div> | |||||
</div> | |||||
<div class="form-group row"> | |||||
<div class="col-xs-12"> | |||||
<div class="inner-addon left-addon"> | |||||
<i class="fa fa-user"></i> | |||||
<input id="login" | |||||
class="form-control" | |||||
name="login" | |||||
type="email" | |||||
placeholder="Email" | |||||
v-model="user.email"> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="form-group row"> | |||||
<div class="col-xs-12"> | |||||
<div class="inner-addon left-addon"> | |||||
<i class="fa fa-lock"></i> | |||||
<input id="password" | |||||
name="password" | |||||
type="password" | |||||
class="form-control" | |||||
placeholder="Password" | |||||
v-model="user.password"> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="form-group row"> | |||||
<div class="col-xs-12"> | |||||
<div class="inner-addon left-addon"> | |||||
<i class="fa fa-globe"></i> | |||||
<input class="form-control" type="text" id="baseURL" v-model="$store.state.baseURL"> | |||||
<small id="siteHelp" class="form-text text-muted">You can use your self hosted LessPass | |||||
Database | |||||
</small> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="form-group row"> | |||||
<div class="col-xs-12"> | |||||
<button id="loginButton" class="btn btn-primary" type="submit"> | |||||
Sign In | |||||
</button> | |||||
<router-link class="btn btn-secondary" :to="{ name: 'register'}"> | |||||
Register | |||||
</router-link> | |||||
</div> | |||||
</div> | |||||
<div class="form-group row"> | |||||
<div class="col-xs-12"> | |||||
<router-link :to="{ name: 'passwordReset'}"> | |||||
Forgot you password ? | |||||
</router-link> | |||||
</div> | |||||
</div> | |||||
</form> | |||||
</template> | |||||
<script type="text/ecmascript-6"> | |||||
import Auth from '../api/auth'; | |||||
import Storage from '../api/storage'; | |||||
import {mapGetters} from 'vuex'; | |||||
export default { | |||||
data() { | |||||
const storage = new Storage(); | |||||
const auth = new Auth(storage); | |||||
return { | |||||
auth, | |||||
storage, | |||||
user: { | |||||
email: '', | |||||
password: '' | |||||
}, | |||||
errorMessage: '', | |||||
showError: false | |||||
}; | |||||
}, | |||||
methods: { | |||||
showErrorMessage(errorMessage){ | |||||
this.errorMessage = errorMessage; | |||||
this.showError = true; | |||||
setTimeout(() => { | |||||
this.cleanErrors(); | |||||
}, 6000); | |||||
}, | |||||
cleanErrors(){ | |||||
this.showError = false; | |||||
this.errorMessage = ''; | |||||
}, | |||||
login(){ | |||||
this.cleanErrors(); | |||||
var baseURL = this.baseURL; | |||||
var email = this.user.email; | |||||
if (!email || !this.user.password || !baseURL) { | |||||
this.showErrorMessage('email, password and url are mandatory'); | |||||
return; | |||||
} | |||||
this.auth.login(this.user, baseURL) | |||||
.then(()=> { | |||||
this.storage.save({baseURL: baseURL, email: email}); | |||||
this.$store.dispatch('USER_AUTHENTICATED', {email: email}); | |||||
this.$router.push({name: 'home'}); | |||||
}) | |||||
.catch(err => { | |||||
if (err.response === undefined) { | |||||
if (baseURL === "https://lesspass.com") { | |||||
this.showErrorMessage('Oops! Something went wrong. Retry in a few minutes.'); | |||||
} else { | |||||
this.showErrorMessage('Your LessPass Database is not running'); | |||||
} | |||||
} else if (err.response.status === 400) { | |||||
this.showErrorMessage('Your login or password is not good. Do you have an account ?'); | |||||
} else { | |||||
this.showErrorMessage('Oops! Something went wrong. Retry in a few minutes.') | |||||
} | |||||
}); | |||||
} | |||||
}, | |||||
computed: mapGetters([ | |||||
'baseURL' | |||||
]) | |||||
} | |||||
</script> | |||||
@@ -1,277 +0,0 @@ | |||||
<style> | |||||
#password-generator { | |||||
color: #555; | |||||
} | |||||
.inner-addon i { | |||||
position: absolute; | |||||
padding: 10px; | |||||
pointer-events: none; | |||||
z-index: 10; | |||||
} | |||||
.inner-addon { | |||||
position: relative; | |||||
} | |||||
.left-addon i { | |||||
left: 0; | |||||
} | |||||
.right-addon i { | |||||
right: 0; | |||||
} | |||||
.left-addon input { | |||||
padding-left: 30px; | |||||
} | |||||
.right-addon input { | |||||
padding-right: 30px; | |||||
} | |||||
</style> | |||||
<template> | |||||
<form id="password-generator"> | |||||
<div class="form-group row"> | |||||
<div class="col-xs-12"> | |||||
<div class="inner-addon left-addon"> | |||||
<i class="fa fa-globe"></i> | |||||
<input id="site" | |||||
name="site" | |||||
type="text" | |||||
ref="site" | |||||
class="form-control" | |||||
placeholder="Site" | |||||
list="savedSites" | |||||
autocorrect="off" | |||||
autocapitalize="none" | |||||
v-model="password.site"> | |||||
<datalist id="savedSites"> | |||||
<option v-for="pwd in passwords"> | |||||
{{pwd.site}} | {{pwd.login}} | |||||
</option> | |||||
</datalist> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<remove-auto-complete></remove-auto-complete> | |||||
<div class="form-group row"> | |||||
<div class="col-xs-12"> | |||||
<div class="inner-addon left-addon"> | |||||
<i class="fa fa-user"></i> | |||||
<label for="login" class="sr-only">Login</label> | |||||
<input id="login" | |||||
name="login" | |||||
type="text" | |||||
class="form-control" | |||||
placeholder="Login" | |||||
autocomplete="off" | |||||
autocorrect="off" | |||||
autocapitalize="none" | |||||
v-model="password.login"> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="form-group row"> | |||||
<div class="col-xs-12"> | |||||
<div class="inner-addon left-addon input-group"> | |||||
<label for="masterPassword" class="sr-only">Password</label> | |||||
<i class="fa fa-lock"></i> | |||||
<input id="masterPassword" | |||||
name="masterPassword" | |||||
ref="masterPassword" | |||||
type="password" | |||||
class="form-control" | |||||
placeholder="Master password" | |||||
autocomplete="new-password" | |||||
autocorrect="off" | |||||
autocapitalize="none" | |||||
v-model="masterPassword"> | |||||
<fingerprint :fingerprint="masterPassword" v-on:click.native="showMasterPassword"></fingerprint> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="form-group row"> | |||||
<div class="col-xs-12"> | |||||
<div class="input-group"> | |||||
<label for="generatedPassword" class="sr-only">Password Generated</label> | |||||
<input id="generatedPassword" | |||||
name="generatedPassword" | |||||
type="text" | |||||
class="form-control" | |||||
tabindex="-1" | |||||
readonly | |||||
v-model="generatedPassword"> | |||||
<span class="input-group-btn"> | |||||
<button id="copyPasswordButton" class="btn-copy btn btn-primary" | |||||
:disabled="!generatedPassword" | |||||
type="button" | |||||
v-on:click="cleanFormInSeconds(10)" | |||||
data-clipboard-target="#generatedPassword"> | |||||
<i class="fa fa-clipboard white"></i> Copy | |||||
</button> | |||||
</span> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="form-group row"> | |||||
<div class="col-xs-12"> | |||||
Password options | |||||
<hr style="margin:0;"> | |||||
</div> | |||||
</div> | |||||
<div class="form-group row"> | |||||
<div class="col-xs-12"> | |||||
<label class="form-check-inline"> | |||||
<input class="form-check-input" type="checkbox" id="lowercase" | |||||
v-model="password.lowercase"> abc | |||||
</label> | |||||
<label class="form-check-inline"> | |||||
<input class="form-check-input" type="checkbox" id="uppercase" | |||||
v-model="password.uppercase"> ABC | |||||
</label> | |||||
<label class="form-check-inline"> | |||||
<input class="form-check-input" type="checkbox" id="numbers" | |||||
v-model="password.numbers"> | |||||
123 | |||||
</label> | |||||
<label class="form-check-inline"> | |||||
<input class="form-check-input" type="checkbox" id="symbols" | |||||
v-model="password.symbols"> | |||||
%!@ | |||||
</label> | |||||
</div> | |||||
</div> | |||||
<div class="form-group row"> | |||||
<label for="passwordLength" class="col-xs-3 col-form-label">Length</label> | |||||
<div class="col-xs-3 p-l-0"> | |||||
<input class="form-control" type="number" id="passwordLength" v-model="password.length" | |||||
min="6"> | |||||
</div> | |||||
<label for="passwordCounter" class="col-xs-3 col-form-label">Counter</label> | |||||
<div class="col-xs-3 p-l-0"> | |||||
<input class="form-control" type="number" id="passwordCounter" | |||||
v-model="password.counter" min="1"> | |||||
</div> | |||||
</div> | |||||
</form> | |||||
</template> | |||||
<script type="text/ecmascript-6"> | |||||
import {mapGetters} from 'vuex'; | |||||
import RemoveAutoComplete from '../components/RemoveAutoComplete'; | |||||
import Fingerprint from '../components/Fingerprint'; | |||||
import lesspass from 'lesspass'; | |||||
import Clipboard from 'clipboard'; | |||||
import debounce from 'lodash.debounce'; | |||||
import {showTooltip} from '../api/tooltip'; | |||||
import Password from '../domain/password'; | |||||
function fetchPasswords(store) { | |||||
return store.dispatch('FETCH_PASSWORDS') | |||||
} | |||||
export default { | |||||
name: 'password-generator-view', | |||||
components: { | |||||
RemoveAutoComplete, | |||||
Fingerprint | |||||
}, | |||||
computed: { | |||||
...mapGetters(['passwords', 'password']), | |||||
generatedPassword(){ | |||||
if (!this.encryptedLogin || !this.password.site) { | |||||
this.generatedPassword = ''; | |||||
return; | |||||
} | |||||
return this.generatePassword(); | |||||
} | |||||
}, | |||||
preFetch: fetchPasswords, | |||||
beforeMount () { | |||||
const id = this.$route.params.id; | |||||
if (id) { | |||||
this.$store.dispatch('FETCH_PASSWORD', {id}); | |||||
} else { | |||||
fetchPasswords(this.$store); | |||||
this.$store.dispatch('LOAD_DEFAULT_PASSWORD'); | |||||
} | |||||
var clipboard = new Clipboard('#copyPasswordButton'); | |||||
clipboard.on('success', event => { | |||||
if (event.text) { | |||||
showTooltip(event.trigger, 'copied !'); | |||||
} | |||||
}); | |||||
}, | |||||
data(){ | |||||
return { | |||||
masterPassword: '', | |||||
encryptedLogin: '', | |||||
generatedPassword: '', | |||||
cleanTimeout: null | |||||
} | |||||
}, | |||||
watch: { | |||||
'password.site': function (newValue) { | |||||
const values = newValue.split(" | "); | |||||
if (values.length === 2) { | |||||
const site = values[0]; | |||||
const login = values[1]; | |||||
const passwords = this.passwords; | |||||
for (var i = 0; i < passwords.length; i++) { | |||||
var password = passwords[i]; | |||||
if (password.site === site && password.login === login) { | |||||
this.$store.dispatch('PASSWORD_CHANGE', {password: {...password}}); | |||||
this.$refs.masterPassword.focus(); | |||||
break; | |||||
} | |||||
} | |||||
return site; | |||||
} | |||||
return newValue; | |||||
}, | |||||
'password.login': function () { | |||||
this.encryptedLogin = ''; | |||||
this.encryptLogin(); | |||||
}, | |||||
'masterPassword': function () { | |||||
this.encryptedLogin = ''; | |||||
this.encryptLogin(); | |||||
}, | |||||
'generatedPassword': function () { | |||||
this.cleanFormInSeconds(30); | |||||
} | |||||
}, | |||||
methods: { | |||||
encryptLogin: debounce(function () { | |||||
if (this.password.login && this.masterPassword) { | |||||
lesspass.encryptLogin(this.password.login, this.masterPassword).then(encryptedLogin => { | |||||
this.encryptedLogin = encryptedLogin; | |||||
}); | |||||
} | |||||
}, 500), | |||||
showMasterPassword(){ | |||||
if (this.$refs.masterPassword.type === 'password') { | |||||
this.$refs.masterPassword.type = 'text'; | |||||
} else { | |||||
this.$refs.masterPassword.type = 'password'; | |||||
} | |||||
}, | |||||
generatePassword(){ | |||||
const password = new Password(this.password); | |||||
const generatedPassword = lesspass.renderPassword(this.encryptedLogin, this.password.site, password.options); | |||||
this.$store.dispatch('PASSWORD_GENERATED'); | |||||
return generatedPassword; | |||||
}, | |||||
cleanFormInSeconds(seconds){ | |||||
clearTimeout(this.cleanTimeout); | |||||
this.cleanTimeout = setTimeout(() => { | |||||
this.masterPassword = ''; | |||||
this.encryptedLogin = ''; | |||||
this.generatedPassword = ''; | |||||
}, 1000 * seconds); | |||||
} | |||||
} | |||||
} | |||||
</script> |
@@ -1,80 +0,0 @@ | |||||
<template> | |||||
<form v-on:submit.prevent="resetPassword"> | |||||
<div class="form-group row" v-if="showError"> | |||||
<div class="col-xs-12 text-muted text-danger"> | |||||
Oops! Something went wrong. Retry in a few minutes. | |||||
</div> | |||||
</div> | |||||
<div class="form-group row" v-if="successMessage"> | |||||
<div class="col-xs-12 text-muted text-success"> | |||||
If a matching account was found an email was sent to allow you to reset your password. | |||||
</div> | |||||
</div> | |||||
<div class="form-group row"> | |||||
<div class="col-xs-12"> | |||||
<div class="inner-addon left-addon"> | |||||
<i class="fa fa-user"></i> | |||||
<input id="email" | |||||
class="form-control" | |||||
name="email" | |||||
type="email" | |||||
placeholder="Email" | |||||
v-model="email"> | |||||
<small class="form-text text-muted text-danger"> | |||||
<span v-if="emailRequired">An email is required</span> | |||||
</small> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="form-group row"> | |||||
<div class="col-xs-12"> | |||||
<button id="loginButton" class="btn btn-primary" type="submit"> | |||||
Send me a reset link | |||||
</button> | |||||
</div> | |||||
</div> | |||||
</form> | |||||
</template> | |||||
<script type="text/ecmascript-6"> | |||||
import Auth from '../api/auth'; | |||||
import Storage from '../api/storage'; | |||||
import {mapActions} from 'vuex'; | |||||
export default { | |||||
data() { | |||||
const storage = new Storage(); | |||||
const auth = new Auth(storage); | |||||
return { | |||||
auth, | |||||
storage, | |||||
email: '', | |||||
emailRequired: false, | |||||
showError: false, | |||||
successMessage: false, | |||||
}; | |||||
}, | |||||
methods: { | |||||
cleanErrors(){ | |||||
this.emailRequired = false; | |||||
this.showError = false; | |||||
this.successMessage = false; | |||||
}, | |||||
noErrors(){ | |||||
return !(this.emailRequired || this.showError); | |||||
}, | |||||
resetPassword(){ | |||||
this.cleanErrors(); | |||||
if (!this.email) { | |||||
this.emailRequired = true; | |||||
return; | |||||
} | |||||
this.auth.resetPassword({email: this.email}).then(()=> { | |||||
this.successMessage = true | |||||
}).catch(err => { | |||||
this.showError = true; | |||||
}); | |||||
} | |||||
} | |||||
} | |||||
</script> | |||||
@@ -1,89 +0,0 @@ | |||||
<template> | |||||
<form v-on:submit.prevent="resetPasswordConfirm"> | |||||
<div class="form-group row" v-if="showError"> | |||||
<div class="col-xs-12 text-muted text-danger"> | |||||
{{errorMessage}} | |||||
</div> | |||||
</div> | |||||
<div class="form-group row" v-if="successMessage"> | |||||
<div class="col-xs-12 text-muted text-success"> | |||||
You're password was reset successfully. | |||||
<router-link :to="{ name: 'login'}">Do you want to login ?</router-link> | |||||
</div> | |||||
</div> | |||||
<div class="form-group row"> | |||||
<div class="col-xs-12"> | |||||
<div class="inner-addon left-addon"> | |||||
<i class="fa fa-lock"></i> | |||||
<input id="new-password" | |||||
class="form-control" | |||||
name="new-password" | |||||
type="password" | |||||
autocomplete="new-password" | |||||
placeholder="New Password" | |||||
v-model="new_password"> | |||||
<small class="form-text text-muted text-danger"> | |||||
<span v-if="passwordRequired">A password is required</span> | |||||
</small> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="form-group row"> | |||||
<div class="col-xs-12"> | |||||
<button id="loginButton" class="btn btn-primary" type="submit"> | |||||
Reset my password | |||||
</button> | |||||
</div> | |||||
</div> | |||||
</form> | |||||
</template> | |||||
<script type="text/ecmascript-6"> | |||||
import Auth from '../api/auth'; | |||||
import Storage from '../api/storage'; | |||||
import {mapActions} from 'vuex'; | |||||
export default { | |||||
data() { | |||||
const storage = new Storage(); | |||||
const auth = new Auth(storage); | |||||
return { | |||||
auth, | |||||
storage, | |||||
new_password: '', | |||||
passwordRequired: false, | |||||
showError: false, | |||||
successMessage: false, | |||||
errorMessage: 'Oops! Something went wrong. Retry in a few minutes.' | |||||
}; | |||||
}, | |||||
methods: { | |||||
cleanErrors(){ | |||||
this.passwordRequired = false; | |||||
this.showError = false; | |||||
this.successMessage = false; | |||||
}, | |||||
noErrors(){ | |||||
return !(this.passwordRequired || this.showError); | |||||
}, | |||||
resetPasswordConfirm(){ | |||||
this.cleanErrors(); | |||||
if (!this.new_password) { | |||||
this.passwordRequired = true; | |||||
return; | |||||
} | |||||
this.auth.confirmResetPassword({ | |||||
uid: this.$route.params.uid, | |||||
token: this.$route.params.token, | |||||
new_password: this.new_password | |||||
}).then(()=> { | |||||
this.successMessage = true | |||||
}).catch(err => { | |||||
if(err.response.status === 400){ | |||||
this.errorMessage = 'This password reset link become invalid.' | |||||
} | |||||
this.showError = true; | |||||
}); | |||||
} | |||||
} | |||||
} | |||||
</script> |
@@ -1,85 +0,0 @@ | |||||
<style> | |||||
#passwords { | |||||
max-height: 320px; | |||||
overflow-y: scroll; | |||||
overflow-x: hidden; | |||||
} | |||||
</style> | |||||
<template> | |||||
<div> | |||||
<form> | |||||
<div class="form-group row"> | |||||
<div class="col-sm-7"> | |||||
<div class="inner-addon left-addon"> | |||||
<i class="fa fa-search"></i> | |||||
<input class="form-control" name="search" placeholder="Search" v-model="searchQuery"> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</form> | |||||
<div id="passwords" class="row"> | |||||
<div class="col-xs-12"> | |||||
<table class="table"> | |||||
<tbody> | |||||
<tr v-if="passwords.length === 0"> | |||||
<td> | |||||
You don't have any passwords saved in your database. | |||||
<br> | |||||
<router-link :to="{ name: 'home'}">Would you like to create one ?</router-link> | |||||
</td> | |||||
</tr> | |||||
<tr v-for="password in passwords"> | |||||
<td> | |||||
<router-link :to="{ name: 'password', params: { id: password.id }}"> | |||||
{{password.site}} | |||||
</router-link> | |||||
<br> | |||||
{{password.login}} | |||||
</td> | |||||
<td class="text-xs-right"> | |||||
<delete-button :action="deletePassword" :object="password" | |||||
text="Are you sure you want to delete this password ?"> | |||||
</delete-button> | |||||
</td> | |||||
</tr> | |||||
</tbody> | |||||
</table> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</template> | |||||
<script type="text/ecmascript-6"> | |||||
import DeleteButton from '../components/DeleteButton'; | |||||
import {mapGetters} from 'vuex'; | |||||
function fetchPasswords(store) { | |||||
return store.dispatch('FETCH_PASSWORDS') | |||||
} | |||||
export default { | |||||
name: 'passwords-view', | |||||
data(){ | |||||
return { | |||||
searchQuery: '' | |||||
} | |||||
}, | |||||
components: {DeleteButton}, | |||||
computed: { | |||||
...mapGetters(['passwords', 'email']), | |||||
filteredPasswords(){ | |||||
return this.passwords.filter(password => { | |||||
return password.site.indexOf(this.searchQuery) > -1 || password.login.indexOf(this.searchQuery) > -1 | |||||
}) | |||||
} | |||||
}, | |||||
preFetch: fetchPasswords, | |||||
beforeMount () { | |||||
fetchPasswords(this.$store); | |||||
}, | |||||
methods: { | |||||
deletePassword(password){ | |||||
return this.$store.dispatch('DELETE_PASSWORD', {id: password.id}); | |||||
} | |||||
} | |||||
} | |||||
</script> |
@@ -1,108 +0,0 @@ | |||||
<template> | |||||
<form v-on:submit.prevent="register"> | |||||
<div class="form-group row" v-if="showError"> | |||||
<div class="col-xs-12 text-muted text-danger"> | |||||
Oops! Something went wrong. Retry in a few minutes. | |||||
</div> | |||||
</div> | |||||
<div class="form-group row"> | |||||
<div class="col-xs-12"> | |||||
<div class="inner-addon left-addon"> | |||||
<i class="fa fa-user"></i> | |||||
<input id="email" | |||||
class="form-control" | |||||
name="email" | |||||
type="email" | |||||
placeholder="Email" | |||||
v-model="user.email"> | |||||
<small class="form-text text-muted text-danger"> | |||||
<span v-if="userNameAlreadyExist">Someone already use that username. Do you want to sign in ?</span> | |||||
<span v-if="emailRequired">An email is required</span> | |||||
</small> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="form-group row"> | |||||
<div class="col-xs-12"> | |||||
<div class="inner-addon left-addon"> | |||||
<i class="fa fa-lock"></i> | |||||
<input id="password" | |||||
name="password" | |||||
type="password" | |||||
class="form-control" | |||||
placeholder="Password" | |||||
v-model="user.password"> | |||||
<small class="form-text text-muted"> | |||||
<span v-if="noErrors()" class="text-warning">Do not use your master password here</span> | |||||
<span v-if="passwordRequired" class="text-danger">A password is required</span> | |||||
</small> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="form-group row"> | |||||
<div class="col-xs-12"> | |||||
<button id="loginButton" class="btn btn-primary" type="submit"> | |||||
Register | |||||
</button> | |||||
</div> | |||||
</div> | |||||
</form> | |||||
</template> | |||||
<script type="text/ecmascript-6"> | |||||
import Auth from '../api/auth'; | |||||
import Storage from '../api/storage'; | |||||
import {mapActions} from 'vuex'; | |||||
export default { | |||||
data() { | |||||
const storage = new Storage(); | |||||
const auth = new Auth(storage); | |||||
return { | |||||
auth, | |||||
storage, | |||||
user: { | |||||
email: '', | |||||
password: '' | |||||
}, | |||||
userNameAlreadyExist: false, | |||||
emailRequired: false, | |||||
passwordRequired: false, | |||||
showError: false | |||||
}; | |||||
}, | |||||
methods: { | |||||
cleanErrors(){ | |||||
this.userNameAlreadyExist = false; | |||||
this.emailRequired = false; | |||||
this.passwordRequired = false; | |||||
this.showError = false; | |||||
}, | |||||
noErrors(){ | |||||
return !(this.userNameAlreadyExist || this.emailRequired || this.passwordRequired || this.showError); | |||||
}, | |||||
register(){ | |||||
this.cleanErrors(); | |||||
if (!this.user.email) { | |||||
this.emailRequired = true; | |||||
return; | |||||
} | |||||
if (!this.user.password) { | |||||
this.passwordRequired = true; | |||||
return; | |||||
} | |||||
this.auth.register(this.user, 'https://lesspass.com') | |||||
.then(()=> { | |||||
this.$router.push({name: 'login'}); | |||||
}) | |||||
.catch(err => { | |||||
if (err.response && (err.response.data.email[0].indexOf('already exists') !== -1)) { | |||||
this.userNameAlreadyExist = true | |||||
} else { | |||||
this.showError = true; | |||||
} | |||||
}); | |||||
} | |||||
} | |||||
} | |||||
</script> | |||||
@@ -1,26 +0,0 @@ | |||||
export class LocalStorageMock { | |||||
constructor(storage = {}) { | |||||
this.storage = storage; | |||||
} | |||||
setItem(key, value) { | |||||
this.storage[key] = value || ''; | |||||
} | |||||
getItem(key) { | |||||
return this.storage[key] || null; | |||||
} | |||||
removeItem(key) { | |||||
delete this.storage[key]; | |||||
} | |||||
key(i) { | |||||
const keys = Object.keys(this.storage); | |||||
return keys[i] || null; | |||||
} | |||||
clear() { | |||||
this.storage = {}; | |||||
} | |||||
} |
@@ -1,133 +0,0 @@ | |||||
import test from 'ava'; | |||||
import {LocalStorageMock} from './_helpers'; | |||||
import Auth from '../src/api/auth'; | |||||
import Storage, {LOCAL_STORAGE_KEY} from '../src/api/storage'; | |||||
import nock from 'nock'; | |||||
function AuthFactory(token, localStorage = new LocalStorageMock()) { | |||||
const storage = new Storage(localStorage); | |||||
storage.saveToken(token); | |||||
return new Auth(storage); | |||||
} | |||||
test('request token', t => { | |||||
const token = '5e0651'; | |||||
const user = {email: 'test@example.org', password: 'password'}; | |||||
nock('https://lesspass.com').post('/api/tokens/auth/', user).reply(201, {token}); | |||||
return Auth._requestToken(user, {baseURL: 'https://lesspass.com'}).then(requestedToken => { | |||||
t.is(requestedToken, token); | |||||
}); | |||||
}); | |||||
test('request new token', t => { | |||||
const token = '3e3231'; | |||||
const newToken = 'wibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9eyJzdWIiOiIxMjM0NTY3ODkwIi'; | |||||
nock('https://lesspass.com').post('/api/tokens/refresh/', {token}).reply(200, {token: newToken}); | |||||
return Auth._requestNewToken({token}, {baseURL: 'https://lesspass.com'}).then(refreshedToken => { | |||||
t.is(refreshedToken, newToken); | |||||
}); | |||||
}); | |||||
test('user first connection is guest', t => { | |||||
const storage = new Storage(new LocalStorageMock()); | |||||
const auth = new Auth(storage); | |||||
t.true(auth.isGuest()); | |||||
}); | |||||
test('user return on site before token expire', t => { | |||||
const auth = AuthFactory('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTc1NzkyODQzNH0.KzEBhVgm3xa51jsBklB0Ib9DDwAkvynOnkwLLJoD5AU'); | |||||
t.true(auth.isAuthenticated()); | |||||
t.false(auth.isGuest()); | |||||
}); | |||||
test('user return on site after token expiration', t => { | |||||
const auth = AuthFactory('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU4M30.NmMv7sXjM1dW0eALNXud8LoXknZ0mH14GtnFclwJv0s'); | |||||
t.false(auth.isAuthenticated()); | |||||
t.true(auth.isGuest()); | |||||
t.false(auth.user.authenticated); | |||||
}); | |||||
test('login save token', t => { | |||||
const token = '3e3231'; | |||||
const storage = new LocalStorageMock(); | |||||
const auth = AuthFactory(token, storage); | |||||
const user = { | |||||
email: 'test@lesspass.com', | |||||
password: 'password' | |||||
}; | |||||
nock('https://lesspass.com').post('/api/tokens/auth/', user).reply(201, {token}); | |||||
return auth.login(user).then(() => { | |||||
t.is(JSON.parse(storage.getItem(LOCAL_STORAGE_KEY)).jwt, token); | |||||
}); | |||||
}); | |||||
test('logout user remove token and unauthenticate user', t => { | |||||
const token = '3e3231'; | |||||
const storage = new LocalStorageMock(); | |||||
const auth = AuthFactory(token, storage); | |||||
return auth.logout().then(() => { | |||||
t.falsy(storage.getItem(LOCAL_STORAGE_KEY)); | |||||
}); | |||||
}); | |||||
test('login custom endpoint', t => { | |||||
const token = '3e3231'; | |||||
const storage = new LocalStorageMock(); | |||||
const auth = AuthFactory(token, storage); | |||||
const user = { | |||||
email: 'test@lesspass.com', | |||||
password: 'password' | |||||
}; | |||||
nock('https://test.example.org').post('/api/tokens/auth/', user).reply(201, {token}); | |||||
return auth.login(user, 'https://test.example.org').then(() => { | |||||
t.is(JSON.parse(storage.getItem(LOCAL_STORAGE_KEY)).jwt, token); | |||||
}); | |||||
}); | |||||
test('refresh token', t => { | |||||
const token = '3e3231'; | |||||
const storage = new LocalStorageMock(); | |||||
const auth = AuthFactory(token, storage); | |||||
const newToken = 'wibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9eyJzdWIiOiIxMjM0NTY3ODkwIi'; | |||||
nock('https://lesspass.com').post('/api/tokens/refresh/', {token}).reply(200, {token: newToken}); | |||||
return auth.refreshToken().then(() => { | |||||
t.is(JSON.parse(storage.getItem(LOCAL_STORAGE_KEY)).jwt, newToken); | |||||
}); | |||||
}); | |||||
test('should register a user', t => { | |||||
const user = { | |||||
email: 'test@lesspass.com', | |||||
password: 'password' | |||||
}; | |||||
const localStorage = new LocalStorageMock(); | |||||
const storage = new Storage(localStorage); | |||||
const auth = new Auth(storage); | |||||
nock('https://lesspass.com').post('/api/auth/register/', user).reply(201, {email: user.email, pk: 1}); | |||||
return auth.register(user).then(newUser => { | |||||
t.is(newUser.email, user.email); | |||||
}); | |||||
}); | |||||
test('should reset a password', t => { | |||||
var email = 'test@lesspass.com'; | |||||
const localStorage = new LocalStorageMock(); | |||||
const storage = new Storage(localStorage); | |||||
const auth = new Auth(storage); | |||||
nock('https://lesspass.com').post('/api/auth/password/reset/', {email}).reply(204); | |||||
t.notThrows(auth.resetPassword({email})); | |||||
}); | |||||
test('should confirm reset password', t => { | |||||
var newPassword ={ | |||||
uid: 'MQ', | |||||
token: '5g1-2bd69bd6f6dcd73f8124', | |||||
new_password: 'password1' | |||||
}; | |||||
const localStorage = new LocalStorageMock(); | |||||
const storage = new Storage(localStorage); | |||||
const auth = new Auth(storage); | |||||
nock('https://lesspass.com').post('/api/auth/password/reset/confirm/', newPassword).reply(204); | |||||
t.notThrows(auth.confirmResetPassword(newPassword)); | |||||
}); |
@@ -1,54 +0,0 @@ | |||||
import test from 'ava'; | |||||
import nock from 'nock'; | |||||
import HTTP from '../src/api/http'; | |||||
import {TOKEN_KEY} from '../src/api/token'; | |||||
import {LOCAL_STORAGE_KEY} from '../src/api/storage'; | |||||
import Storage from '../src/api/storage'; | |||||
import {LocalStorageMock} from './_helpers'; | |||||
const storage = new Storage(new LocalStorageMock()); | |||||
const passwords = new HTTP('passwords', storage); | |||||
const token = 'ZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFt'; | |||||
storage.save({baseURL: 'https://lesspass.com', [TOKEN_KEY]: token}); | |||||
/* eslint camelcase: 0 */ | |||||
const foo = { | |||||
name: 'foo' | |||||
}; | |||||
test('should send requests with Authorization header', t => { | |||||
const headers = {reqheaders: {Authorization: `JWT ${token}`}}; | |||||
nock('https://lesspass.com', headers).get('/api/passwords/').query(true).reply(200, {}); | |||||
return passwords.all().then(response => { | |||||
t.is(response.status, 200); | |||||
}); | |||||
}); | |||||
test('should create a foo', t => { | |||||
nock('https://lesspass.com').post('/api/passwords/', foo).reply(201, foo); | |||||
return passwords.create(foo).then(response => { | |||||
const newIncident = response.data; | |||||
t.is(foo.login, newIncident.login); | |||||
}); | |||||
}); | |||||
test('should send requests with Authorization header updated', t => { | |||||
const newToken = 'WV9eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRyd'; | |||||
storage.save({baseURL: 'https://lesspass.com', [TOKEN_KEY]: newToken}); | |||||
const headers = {reqheaders: {Authorization: `JWT ${newToken}`}}; | |||||
nock('https://lesspass.com', headers).get('/api/passwords/').query(true).reply(200, {}); | |||||
return passwords.all().then(response => { | |||||
t.is(response.status, 200); | |||||
}); | |||||
}); | |||||
test('should get all foo with parameters', t => { | |||||
nock('https://lesspass.com').get('/api/passwords/?limit=100&offset=0&search=query&ordering=-created') | |||||
.reply(200, {}); | |||||
const params = {limit: 100, offset: 0, search: 'query', ordering: '-created'}; | |||||
return passwords.all(params).then(response => { | |||||
t.is(response.status, 200); | |||||
}); | |||||
}); |
@@ -1,65 +0,0 @@ | |||||
import test from 'ava'; | |||||
import Password from '../src/domain/password'; | |||||
test('password is new if no passwords', t => { | |||||
const password = new Password({site: 'example.org'}); | |||||
t.true(password.isNewPassword([])) | |||||
}); | |||||
test('password is new if no site matching', t => { | |||||
const password = new Password({site: 'example.org'}); | |||||
t.true(password.isNewPassword([{site: 'ubuntu.org'}])) | |||||
}); | |||||
test('password is new if site match but no login', t => { | |||||
const password = new Password({site: 'example.org', login: 'test'}); | |||||
t.true(password.isNewPassword([{site: 'example.org', login: 'test@example.org'}])) | |||||
}); | |||||
test('password is not new if site and login matching', t => { | |||||
const password = new Password({site: 'example.org', login: 'test'}); | |||||
t.false(password.isNewPassword([{site: 'example.org', login: 'test'}])) | |||||
}); | |||||
test('password options default', t => { | |||||
const password = new Password({ | |||||
site: 'example.org', | |||||
login: 'test', | |||||
uppercase: true, | |||||
lowercase: true, | |||||
numbers: true, | |||||
symbols: true, | |||||
length: 12, | |||||
counter: 1, | |||||
}); | |||||
t.deepEqual(password.options, { | |||||
uppercase: true, | |||||
lowercase: true, | |||||
numbers: true, | |||||
symbols: true, | |||||
length: 12, | |||||
counter: 1, | |||||
}) | |||||
}); | |||||
test('password options', t => { | |||||
const password = new Password({ | |||||
site: 'example.org', | |||||
login: 'test', | |||||
uppercase: false, | |||||
lowercase: true, | |||||
numbers: false, | |||||
symbols: true, | |||||
length: 14, | |||||
counter: 3, | |||||
}); | |||||
t.deepEqual(password.options, { | |||||
uppercase: false, | |||||
lowercase: true, | |||||
numbers: false, | |||||
symbols: true, | |||||
length: 14, | |||||
counter: 3, | |||||
}) | |||||
}); | |||||
@@ -1,39 +0,0 @@ | |||||
import test from 'ava'; | |||||
import {LocalStorageMock} from './_helpers'; | |||||
import Storage, {LOCAL_STORAGE_KEY} from '../src/api/storage'; | |||||
const localStorage = new LocalStorageMock(); | |||||
const storage = new Storage(localStorage); | |||||
test('get default storage', t => { | |||||
t.is(storage.json().baseURL, 'https://lesspass.com'); | |||||
}); | |||||
test('get storage saved in local storage', t => { | |||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify({baseURL: 'https://example.org'})); | |||||
t.is(storage.json().baseURL, 'https://example.org'); | |||||
}); | |||||
test('save storage in local storage', t => { | |||||
storage.save({baseURL: 'https://example.org'}); | |||||
t.is(localStorage.getItem(LOCAL_STORAGE_KEY), '{"baseURL":"https://example.org"}'); | |||||
}); | |||||
test('save storage in local storage', t => { | |||||
storage.save({baseURL: 'https://example.org'}); | |||||
t.is(localStorage.getItem(LOCAL_STORAGE_KEY), '{"baseURL":"https://example.org"}'); | |||||
}); | |||||
test('save storage in local storage merge', t => { | |||||
localStorage.clear(); | |||||
storage.save({a: 'a'}); | |||||
storage.save({b: 'b'}); | |||||
t.is(localStorage.getItem(LOCAL_STORAGE_KEY), '{"a":"a","b":"b"}'); | |||||
}); | |||||
test('storage clear local storage', t => { | |||||
storage.save({a: 'a'}); | |||||
storage.clear(); | |||||
t.is(localStorage.getItem(LOCAL_STORAGE_KEY), null); | |||||
}); | |||||
@@ -1,29 +0,0 @@ | |||||
import test from 'ava'; | |||||
import moment from 'moment'; | |||||
import Token from '../src/api/token'; | |||||
test('token is near the end', t => { | |||||
const token = new Token('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU4M30.NmMv7sXjM1dW0eALNXud8LoXknZ0mH14GtnFclwJv0s'); | |||||
t.true(token.expiresIn(15, 'minutes', moment(1437018283 * 1000))); | |||||
t.false(token.expiresIn(5, 'minutes', moment(1437018283 * 1000))); | |||||
}); | |||||
test('token still valid', t => { | |||||
const token = new Token('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTc1NzkyODQzNH0.KzEBhVgm3xa51jsBklB0Ib9DDwAkvynOnkwLLJoD5AU'); | |||||
t.true(token.stillValid()); | |||||
}); | |||||
test('token still valid check payload date', t => { | |||||
const token = new Token('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU4M30.NmMv7sXjM1dW0eALNXud8LoXknZ0mH14GtnFclwJv0s'); | |||||
t.true(token.stillValid(moment(1437018283 * 1000))); | |||||
}); | |||||
test('token expired', t => { | |||||
const token = new Token('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU4M30.NmMv7sXjM1dW0eALNXud8LoXknZ0mH14GtnFclwJv0s'); | |||||
t.false(token.stillValid()); | |||||
}); | |||||
test('token invalid does not raise an error', t => { | |||||
const token = new Token('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9'); | |||||
t.false(token.stillValid()); | |||||
}); |
@@ -1,68 +0,0 @@ | |||||
var webpack = require('webpack'); | |||||
var path = require('path'); | |||||
var ExtractTextPlugin = require('extract-text-webpack-plugin'); | |||||
module.exports = { | |||||
entry: { | |||||
bundle: './src/main.js', | |||||
lesspass: './src/lesspass.js', | |||||
}, | |||||
output: { | |||||
path: path.resolve(__dirname, './dist'), | |||||
publicPath: '/dist/', | |||||
filename: '[name].js' | |||||
}, | |||||
resolve: { | |||||
extensions: ['', '.js', '.vue'], | |||||
fallback: [path.join(__dirname, 'node_modules')], | |||||
alias: { | |||||
src: path.resolve(__dirname, './src'), | |||||
jquery: 'jquery/src/jquery' | |||||
} | |||||
}, | |||||
resolveLoader: { | |||||
root: path.join(__dirname, 'node_modules') | |||||
}, | |||||
module: { | |||||
loaders: [ | |||||
{test: /\.vue$/, loader: 'vue-loader'}, | |||||
{test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader'}, | |||||
{test: /\.(png|jpg|jpeg|gif)$/, loader: 'file-loader?name=[name].[ext]',}, | |||||
{test: /\.css$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader')}, | |||||
{test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: "url-loader?limit=8192&mimetype=application/font-woff"}, | |||||
{test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: "url-loader?limit=8192&mimetype=application/font-woff"}, | |||||
{test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: "url-loader?limit=8192&mimetype=application/octet-stream"}, | |||||
{test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: "file-loader"}, | |||||
{test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: "url-loader?limit=8192&mimetype=image/svg+xml"}, | |||||
] | |||||
}, | |||||
plugins: [ | |||||
new ExtractTextPlugin('styles.css'), | |||||
new webpack.ProvidePlugin({ | |||||
$: 'jquery', | |||||
jQuery: 'jquery', | |||||
'window.jQuery': 'jquery', | |||||
'window.Tether': 'tether' | |||||
}) | |||||
], | |||||
devtool: '#eval-source-map' | |||||
}; | |||||
if (process.env.NODE_ENV === 'production') { | |||||
module.exports.devtool = false; | |||||
module.exports.plugins = (module.exports.plugins || []).concat([ | |||||
new webpack.optimize.DedupePlugin(), | |||||
new webpack.optimize.OccurrenceOrderPlugin(), | |||||
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), | |||||
new webpack.optimize.UglifyJsPlugin({ | |||||
compress: { | |||||
warnings: true | |||||
}, | |||||
output: { | |||||
comments: false | |||||
}, | |||||
sourceMap: false | |||||
}) | |||||
]); | |||||
} | |||||