Browse Source

use lesspass-pure

pull/342/head
Guillaume Vincent 8 years ago
parent
commit
aef30bcf88
52 changed files with 213 additions and 2244 deletions
  1. +0
    -3
      .travis.yml
  2. +0
    -17
      dist/bundle.js
  3. +0
    -0
      dist/demo.gif
  4. +1
    -1
      dist/lesspass.css
  5. +14
    -16
      dist/lesspass.js
  6. +0
    -0
      dist/logo.png
  7. +21
    -0
      gulpfile.js
  8. BIN
      images/demo.gif
  9. +0
    -0
      images/free.png
  10. +0
    -0
      images/logo-white.png
  11. BIN
      images/logo.png
  12. +0
    -0
      images/no-cloud.png
  13. +0
    -0
      images/open-source.png
  14. +0
    -0
      images/responsive.png
  15. +172
    -3
      index.html
  16. +0
    -14
      lesspass.html
  17. +4
    -54
      package.json
  18. +1
    -24
      readme.md
  19. +0
    -10
      src/App.css
  20. +0
    -23
      src/App.vue
  21. +0
    -88
      src/api/auth.js
  22. +0
    -40
      src/api/http.js
  23. +0
    -42
      src/api/storage.js
  24. +0
    -43
      src/api/token.js
  25. +0
    -8
      src/api/tooltip.js
  26. +0
    -35
      src/components/DeleteButton.vue
  27. +0
    -80
      src/components/Features.vue
  28. +0
    -71
      src/components/Fingerprint.vue
  29. +0
    -26
      src/components/Footer.vue
  30. +0
    -53
      src/components/Hero.vue
  31. +0
    -39
      src/components/LessPass.vue
  32. +0
    -100
      src/components/Menu.vue
  33. +0
    -26
      src/components/NavigationBar.vue
  34. +0
    -10
      src/components/RemoveAutoComplete.vue
  35. +0
    -27
      src/domain/password.js
  36. +0
    -17
      src/lesspass.js
  37. +0
    -17
      src/main.js
  38. +0
    -27
      src/router.js
  39. +0
    -146
      src/store.js
  40. +0
    -131
      src/views/Login.vue
  41. +0
    -277
      src/views/PasswordGenerator.vue
  42. +0
    -80
      src/views/PasswordReset.vue
  43. +0
    -89
      src/views/PasswordResetConfirm.vue
  44. +0
    -85
      src/views/Passwords.vue
  45. +0
    -108
      src/views/Register.vue
  46. +0
    -26
      test/_helpers.js
  47. +0
    -133
      test/auth.js
  48. +0
    -54
      test/http.js
  49. +0
    -65
      test/password.js
  50. +0
    -39
      test/storage.js
  51. +0
    -29
      test/token.js
  52. +0
    -68
      webpack.config.js

+ 0
- 3
.travis.yml View File

@@ -1,3 +0,0 @@
language: node_js
node_js:
- 4

+ 0
- 17
dist/bundle.js
File diff suppressed because it is too large
View File


src/images/demo.gif → dist/demo.gif View File


dist/lesspass.css
File diff suppressed because it is too large
View File


+ 14
- 16
dist/lesspass.js
File diff suppressed because it is too large
View File


src/images/logo.png → dist/logo.png View File


+ 21
- 0
gulpfile.js View File

@@ -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 () {

});

BIN
images/demo.gif View File

Before After
Width: 497  |  Height: 452  |  Size: 34 KiB

src/images/free.png → images/free.png View File


src/images/logo-white.png → images/logo-white.png View File


BIN
images/logo.png View File

Before After
Width: 356  |  Height: 80  |  Size: 9.1 KiB

src/images/no-cloud.png → images/no-cloud.png View File


src/images/open-source.png → images/open-source.png View File


src/images/responsive.png → images/responsive.png View File


+ 172
- 3
index.html View File

@@ -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>

+ 0
- 14
lesspass.html View File

@@ -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>

+ 4
- 54
package.json View File

@@ -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
- 24
readme.md View File

@@ -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

+ 0
- 10
src/App.css View File

@@ -1,10 +0,0 @@
html, body {
height: 100%;
width: 100%;
}

body {
background: #008ed6 fixed top;
background-size: cover;
margin: 0;
}

+ 0
- 23
src/App.vue View File

@@ -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>

+ 0
- 88
src/api/auth.js View File

@@ -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);
}
}

+ 0
- 40
src/api/http.js View File

@@ -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());
}
}

+ 0
- 42
src/api/storage.js View File

@@ -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})
}
}

+ 0
- 43
src/api/token.js View File

@@ -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);
}
}

+ 0
- 8
src/api/tooltip.js View File

@@ -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);
}

+ 0
- 35
src/components/DeleteButton.vue View File

@@ -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>

+ 0
- 80
src/components/Features.vue View File

@@ -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>

+ 0
- 71
src/components/Fingerprint.vue View File

@@ -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>

+ 0
- 26
src/components/Footer.vue View File

@@ -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>

+ 0
- 53
src/components/Hero.vue View File

@@ -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>

+ 0
- 39
src/components/LessPass.vue View File

@@ -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>

+ 0
- 100
src/components/Menu.vue View File

@@ -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>

+ 0
- 26
src/components/NavigationBar.vue View File

@@ -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>

+ 0
- 10
src/components/RemoveAutoComplete.vue View File

@@ -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>

+ 0
- 27
src/domain/password.js View File

@@ -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
}
}

+ 0
- 17
src/lesspass.js View File

@@ -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)
});

+ 0
- 17
src/main.js View File

@@ -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)
});

+ 0
- 27
src/router.js View File

@@ -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;

+ 0
- 146
src/store.js View File

@@ -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
});

+ 0
- 131
src/views/Login.vue View File

@@ -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>


+ 0
- 277
src/views/PasswordGenerator.vue View File

@@ -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>

+ 0
- 80
src/views/PasswordReset.vue View File

@@ -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>


+ 0
- 89
src/views/PasswordResetConfirm.vue View File

@@ -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>

+ 0
- 85
src/views/Passwords.vue View File

@@ -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>

+ 0
- 108
src/views/Register.vue View File

@@ -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>


+ 0
- 26
test/_helpers.js View File

@@ -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 = {};
}
}

+ 0
- 133
test/auth.js View File

@@ -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));
});

+ 0
- 54
test/http.js View File

@@ -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);
});
});

+ 0
- 65
test/password.js View File

@@ -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,
})
});


+ 0
- 39
test/storage.js View File

@@ -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);
});


+ 0
- 29
test/token.js View File

@@ -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());
});

+ 0
- 68
webpack.config.js View File

@@ -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
})
]);
}


Loading…
Cancel
Save