@@ -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> | |||
<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"> | |||
<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> | |||
<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> | |||
</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", | |||
"bugs": "https://github.com/lesspass/frontend/issues", | |||
"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": { | |||
"express": "^4.14.0" | |||
"express": "^4.14.0", | |||
"lesspass-pure": "^2.0.1" | |||
}, | |||
"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 | |||
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 |
@@ -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 | |||
}) | |||
]); | |||
} | |||