Browse Source

add .editorconfig and auto format

pull/342/head
Guillaume Vincent 7 years ago
parent
commit
cb68eff07f
34 changed files with 1859 additions and 1849 deletions
  1. +17
    -0
      .editorconfig
  2. +20
    -20
      index.html
  3. +2
    -7
      src/LessPass.scss
  4. +23
    -23
      src/LessPass.vue
  5. +21
    -21
      src/api/password.js
  6. +21
    -21
      src/api/user.js
  7. +41
    -41
      src/components/DeleteButton.vue
  8. +55
    -55
      src/components/Fingerprint.vue
  9. +57
    -57
      src/components/MasterPassword.vue
  10. +80
    -80
      src/components/Menu.vue
  11. +52
    -52
      src/components/Message.vue
  12. +138
    -138
      src/components/Options.vue
  13. +9
    -9
      src/components/RemoveAutoComplete.vue
  14. +35
    -35
      src/domain/url-parser.js
  15. +4
    -4
      src/main.js
  16. +10
    -10
      src/router.js
  17. +36
    -36
      src/services/message.js
  18. +7
    -7
      src/services/tooltip.js
  19. +43
    -43
      src/store/actions.js
  20. +6
    -6
      src/store/getters.js
  21. +22
    -22
      src/store/index.js
  22. +65
    -65
      src/store/mutations.js
  23. +47
    -47
      src/views/ConfigureOptions.vue
  24. +147
    -147
      src/views/Login.vue
  25. +245
    -245
      src/views/PasswordGenerator.vue
  26. +48
    -48
      src/views/PasswordReset.vue
  27. +83
    -83
      src/views/PasswordResetConfirm.vue
  28. +105
    -107
      src/views/Passwords.vue
  29. +35
    -35
      test/api.password.js
  30. +28
    -28
      test/api.user.js
  31. +43
    -43
      test/store.getters.js
  32. +222
    -222
      test/store.mutations.js
  33. +60
    -60
      test/url-parser.js
  34. +32
    -32
      webpack.config.js

+ 17
- 0
.editorconfig View File

@@ -0,0 +1,17 @@
# editorconfig.org

root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

[*.py]
indent_size = 4

+ 20
- 20
index.html View File

@@ -1,30 +1,30 @@
<!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/lesspass.min.css">
<style>
div.center {
width: 470px;
max-width: 100%;
display: block;
margin-left: auto;
margin-right: auto;
}
<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/lesspass.min.css">
<style>
div.center {
width: 470px;
max-width: 100%;
display: block;
margin-left: auto;
margin-right: auto;
}

@media (min-width: 544px) {
#lesspass {
margin-top: 1em;
}
}
</style>
@media (min-width: 544px) {
#lesspass {
margin-top: 1em;
}
}
</style>
</head>
<body>
<div class="center">
<div id="lesspass" class="m-x-auto"></div>
<div id="lesspass" class="m-x-auto"></div>
</div>
<script src="dist/lesspass.min.js"></script>
</body>


+ 2
- 7
src/LessPass.scss View File

@@ -1,11 +1,9 @@
@import "~bootstrap/scss/variables";
@import "~bootstrap/scss/mixins";
@import "~bootstrap/scss/custom";

// Reset and dependencies
@import "~bootstrap/scss/normalize";
//@import "~bootstrap/scss/print";

// Core CSS
@import "~bootstrap/scss/reboot";
@import "~bootstrap/scss/type";
@@ -15,7 +13,6 @@
@import "~bootstrap/scss/tables";
@import "~bootstrap/scss/forms";
@import "~bootstrap/scss/buttons";

// Components
@import "~bootstrap/scss/transitions";
@import "~bootstrap/scss/dropdown";
@@ -35,13 +32,11 @@
@import "~bootstrap/scss/list-group";
@import "~bootstrap/scss/responsive-embed";
@import "~bootstrap/scss/close";

// Components w/ JavaScript
//@import "~bootstrap/scss/modal";
//@import "~bootstrap/scss/tooltip";
//@import "~bootstrap/scss/popover";
//@import "~bootstrap/scss/carousel";

// Utility classes
@import "~bootstrap/scss/utilities";
@import '~font-awesome/css/font-awesome.css';
@@ -53,7 +48,7 @@
}

@media (max-width: 419px) {
#lesspass{
#lesspass {
border: none;
}
}
@@ -64,4 +59,4 @@

button, .pointer {
cursor: pointer;
}
}

+ 23
- 23
src/LessPass.vue View File

@@ -1,29 +1,29 @@
<template>
<div id="lesspass" class="card" style="max-width: 420px;">
<lesspass-menu></lesspass-menu>
<lesspass-message></lesspass-message>
<div class="card-block">
<router-view></router-view>
</div>
<div id="lesspass" class="card" style="max-width: 420px;">
<lesspass-menu></lesspass-menu>
<lesspass-message></lesspass-message>
<div class="card-block">
<router-view></router-view>
</div>
</div>
</template>
<script type="text/ecmascript-6">
import './LessPass.scss';
import Menu from './components/Menu.vue';
import Message from './components/Message.vue';
import {mapGetters} from 'vuex';
import './LessPass.scss';
import Menu from './components/Menu.vue';
import Message from './components/Message.vue';
import {mapGetters} from 'vuex';

export default {
name: 'LessPass',
components: {
'lesspass-menu': Menu,
'lesspass-message': Message
},
computed: mapGetters(['version']),
created(){
this.$store.dispatch('cleanMessage');
this.$store.dispatch('loadPasswordFirstTime');
this.$store.dispatch('refreshToken');
}
export default {
name: 'LessPass',
components: {
'lesspass-menu': Menu,
'lesspass-message': Message
},
computed: mapGetters(['version']),
created(){
this.$store.dispatch('cleanMessage');
this.$store.dispatch('loadPasswordFirstTime');
this.$store.dispatch('refreshToken');
}
</script>
}
</script>

+ 21
- 21
src/api/password.js View File

@@ -1,25 +1,25 @@
import axios from 'axios';

export default {
addAuthorizationHeader(config) {
return {
...config,
headers: {Authorization: `JWT ${config.token}`}
};
},
all(config) {
return axios.get('/api/passwords/', this.addAuthorizationHeader(config));
},
create(resource, config) {
return axios.post('/api/passwords/', resource, this.addAuthorizationHeader(config));
},
read(resource, config) {
return axios.get('/api/passwords/' + resource.id + '/', this.addAuthorizationHeader(config));
},
update(resource, config) {
return axios.put('/api/passwords/' + resource.id + '/', resource, this.addAuthorizationHeader(config));
},
delete(resource, config) {
return axios.delete('/api/passwords/' + resource.id + '/', this.addAuthorizationHeader(config));
}
addAuthorizationHeader(config) {
return {
...config,
headers: {Authorization: `JWT ${config.token}`}
};
},
all(config) {
return axios.get('/api/passwords/', this.addAuthorizationHeader(config));
},
create(resource, config) {
return axios.post('/api/passwords/', resource, this.addAuthorizationHeader(config));
},
read(resource, config) {
return axios.get('/api/passwords/' + resource.id + '/', this.addAuthorizationHeader(config));
},
update(resource, config) {
return axios.put('/api/passwords/' + resource.id + '/', resource, this.addAuthorizationHeader(config));
},
delete(resource, config) {
return axios.delete('/api/passwords/' + resource.id + '/', this.addAuthorizationHeader(config));
}
}

+ 21
- 21
src/api/user.js View File

@@ -1,25 +1,25 @@
import axios from 'axios';

export default {
login(user, config) {
return axios.post('/api/tokens/auth/', user, config).then(response => {
return response.data;
});
},
register(user, config) {
return axios.post('/api/auth/register/', user, config).then(response => {
return response.data;
});
},
resetPassword(email, config) {
return axios.post('/api/auth/password/reset/', email, config);
},
confirmResetPassword(password, config) {
return axios.post('/api/auth/password/reset/confirm/', password, config);
},
requestNewToken(token, config){
return axios.post('/api/tokens/refresh/', token, config).then(response => {
return response.data.token;
});
}
login(user, config) {
return axios.post('/api/tokens/auth/', user, config).then(response => {
return response.data;
});
},
register(user, config) {
return axios.post('/api/auth/register/', user, config).then(response => {
return response.data;
});
},
resetPassword(email, config) {
return axios.post('/api/auth/password/reset/', email, config);
},
confirmResetPassword(password, config) {
return axios.post('/api/auth/password/reset/confirm/', password, config);
},
requestNewToken(token, config){
return axios.post('/api/tokens/refresh/', token, config).then(response => {
return response.data.token;
});
}
}

+ 41
- 41
src/components/DeleteButton.vue View File

@@ -1,47 +1,47 @@
<template>
<div id="delete-button">
<div class="form-group has-danger" v-if="confirm">
<button type="button" class="btn btn-danger btn-sm" v-on:click.prevent="confirmDelete">
{{ confirmButton }}
</button>
<button type="button" class="btn btn-secondary btn-sm" v-on:click.prevent="confirm=false">
{{ cancelButton }}
</button>
<div class="form-control-feedback">{{ confirmText }}</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm"
v-if="!confirm"
v-on:click.prevent="confirm=true">
<i class="fa fa-trash fa-fw"></i>
</button>
<div id="delete-button">
<div class="form-group has-danger" v-if="confirm">
<button type="button" class="btn btn-danger btn-sm" v-on:click.prevent="confirmDelete">
{{ confirmButton }}
</button>
<button type="button" class="btn btn-secondary btn-sm" v-on:click.prevent="confirm=false">
{{ cancelButton }}
</button>
<div class="form-control-feedback">{{ confirmText }}</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm"
v-if="!confirm"
v-on:click.prevent="confirm=true">
<i class="fa fa-trash fa-fw"></i>
</button>
</div>
</template>
<script type="text/ecmascript-6">
export default {
data() {
return {
confirm: false
}
},
props: {
confirmText: {
type: String,
default: 'Are you sure you want to delete this?'
},
confirmButton: {
type: String,
default: 'Sure delete it'
},
cancelButton: {
type: String,
default: 'Oups no!'
}
},
methods: {
confirmDelete(){
this.confirm = false;
this.$emit('remove');
}
}
export default {
data() {
return {
confirm: false
}
},
props: {
confirmText: {
type: String,
default: 'Are you sure you want to delete this?'
},
confirmButton: {
type: String,
default: 'Sure delete it'
},
cancelButton: {
type: String,
default: 'Oups no!'
}
},
methods: {
confirmDelete(){
this.confirm = false;
this.$emit('remove');
}
}
}
</script>

+ 55
- 55
src/components/Fingerprint.vue View File

@@ -1,18 +1,18 @@
<style>
#fingerprint {
min-width: 90px;
text-align: center;
background-color: transparent;
color: white;
}
#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;
}
#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">
@@ -27,49 +27,49 @@
</template>

<script type="text/ecmascript-6">
import LessPass from 'lesspass';
import LessPass from 'lesspass';

export default {
data(){
return {
icon1: '',
icon2: '',
icon3: '',
color1: '',
color2: '',
color3: ''
}
},
props: ['fingerprint'],
watch: {
fingerprint(newFingerprint) {
if (!newFingerprint) {
return;
}
LessPass.createFingerprint(newFingerprint).then(sha256 => {
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];
}
export default {
data(){
return {
icon1: '',
icon2: '',
icon3: '',
color1: '',
color2: '',
color3: ''
}
},
props: ['fingerprint'],
watch: {
fingerprint(newFingerprint) {
if (!newFingerprint) {
return;
}
LessPass.createFingerprint(newFingerprint).then(sha256 => {
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>

+ 57
- 57
src/components/MasterPassword.vue View File

@@ -1,64 +1,64 @@
<template>
<div id="masterPassword" class="inner-addon left-addon input-group">
<label for="password" class="sr-only">Master Password</label>
<i class="fa fa-lock"></i>
<input id="password"
name="password"
ref="password"
type="password"
class="form-control"
placeholder="Master password"
autocorrect="off"
autocapitalize="off"
v-model="password"
v-on:input="updatePassword($event.target.value)"
v-on:keyup.enter="triggerEnterMethod">
<fingerprint v-bind:fingerprint="fingerprint" v-on:click.native="togglePasswordType($refs.password)">
</fingerprint>
</div>
<div id="masterPassword" class="inner-addon left-addon input-group">
<label for="password" class="sr-only">Master Password</label>
<i class="fa fa-lock"></i>
<input id="password"
name="password"
ref="password"
type="password"
class="form-control"
placeholder="Master password"
autocorrect="off"
autocapitalize="off"
v-model="password"
v-on:input="updatePassword($event.target.value)"
v-on:keyup.enter="triggerEnterMethod">
<fingerprint v-bind:fingerprint="fingerprint" v-on:click.native="togglePasswordType($refs.password)">
</fingerprint>
</div>
</template>
<script type="text/ecmascript-6">
import debounce from 'lodash.debounce';
import Fingerprint from './Fingerprint.vue';
import debounce from 'lodash.debounce';
import Fingerprint from './Fingerprint.vue';

export default {
components: {
Fingerprint
},
props: ['value', 'keyupEnter'],
data(){
return {
fingerprint: '',
password: this.value
}
},
watch: {
'value': function (password) {
this.password = password;
this.updatePassword(password);
}
},
methods: {
updatePassword: function (password) {
this.fingerprint = Math.random().toString(36).substring(7);
this.showRealFingerprint(password);
this.$emit('input', password)
},
showRealFingerprint: debounce(function (password) {
this.fingerprint = password;
}, 500),
togglePasswordType(element){
if (element.type === 'password') {
element.type = 'text';
} else {
element.type = 'password';
}
},
triggerEnterMethod(){
if (typeof this.keyupEnter !== 'undefined' && this.password) {
this.keyupEnter()
}
}
export default {
components: {
Fingerprint
},
props: ['value', 'keyupEnter'],
data(){
return {
fingerprint: '',
password: this.value
}
},
watch: {
'value': function(password) {
this.password = password;
this.updatePassword(password);
}
},
methods: {
updatePassword: function(password) {
this.fingerprint = Math.random().toString(36).substring(7);
this.showRealFingerprint(password);
this.$emit('input', password)
},
showRealFingerprint: debounce(function(password) {
this.fingerprint = password;
}, 500),
togglePasswordType(element){
if (element.type === 'password') {
element.type = 'text';
} else {
element.type = 'password';
}
},
triggerEnterMethod(){
if (typeof this.keyupEnter !== 'undefined' && this.password) {
this.keyupEnter()
}
}
}
}
</script>

+ 80
- 80
src/components/Menu.vue View File

@@ -1,97 +1,97 @@
<style>
#menu .white-link, #menu .text-white {
color: inherit;
}
#menu .white-link, #menu .text-white {
color: inherit;
}

#menu .white-link:hover, #menu .white-link:focus, #menu .white-link:active {
text-decoration: none;
color: inherit;
}
#menu .white-link:hover, #menu .white-link:focus, #menu .white-link:active {
text-decoration: none;
color: inherit;
}

.card-inverse {
background-color: #333;
border-color: #333;
}
.card-inverse {
background-color: #333;
border-color: #333;
}
</style>
<template>
<div id="menu">
<div class="card-header" v-bind:class="{ 'card-inverse': isGuest}">
<div class="row">
<div class="col-3">
<span v-on:click="fullReload()" class="white-link pointer">LessPass</span>
</div>
<div class="col-9 text-right">
<div id="menu">
<div class="card-header" v-bind:class="{ 'card-inverse': isGuest}">
<div class="row">
<div class="col-3">
<span v-on:click="fullReload()" class="white-link pointer">LessPass</span>
</div>
<div class="col-9 text-right">
<span class="text-white" v-if="saved && isAuthenticated">
<small><i class="fa fa-lg fa-check pl-3" aria-hidden="true"></i> saved</small>
</span>
<span v-on:click="saveOrUpdatePassword()" class="white-link"
v-if="!saved && isAuthenticated && $store.state.password.site !== ''">
<span v-on:click="saveOrUpdatePassword()" class="white-link"
v-if="!saved && isAuthenticated && $store.state.password.site !== ''">
<i class="fa fa-lg fa-save pointer"></i>
</span>
<span class="white-link btn-copy pl-3" v-bind:data-clipboard-text="passwordURL"
v-if="$store.state.password.site !== ''">
<span class="white-link btn-copy pl-3" v-bind:data-clipboard-text="passwordURL"
v-if="$store.state.password.site !== ''">
<i class="fa fa-lg fa-share-alt pointer"></i>
</span>
<router-link class="white-link pl-3" :to="{ name: 'configureOptions'}">
<i class="fa fa-lg fa-cog" aria-hidden="true"></i>
</router-link>
<router-link class="white-link pl-3" :to="{ name: 'passwords'}" v-if="isAuthenticated">
<i class="fa fa-lg fa-key" aria-hidden="true"></i>
</router-link>
<button class="white-link btn btn-link p-0 m-0 pl-3" type="button" v-if="isAuthenticated"
v-on:click="logout">
<i class="fa fa-lg fa-sign-out" aria-hidden="true"></i>
</button>
<router-link class="white-link pl-3" :to="{ name: 'login'}" v-if="isGuest">
<i class="fa fa-lg fa-user-secret pointer" aria-hidden="true"></i>
</router-link>
</div>
</div>
<router-link class="white-link pl-3" :to="{ name: 'configureOptions'}">
<i class="fa fa-lg fa-cog" aria-hidden="true"></i>
</router-link>
<router-link class="white-link pl-3" :to="{ name: 'passwords'}" v-if="isAuthenticated">
<i class="fa fa-lg fa-key" aria-hidden="true"></i>
</router-link>
<button class="white-link btn btn-link p-0 m-0 pl-3" type="button" v-if="isAuthenticated"
v-on:click="logout">
<i class="fa fa-lg fa-sign-out" aria-hidden="true"></i>
</button>
<router-link class="white-link pl-3" :to="{ name: 'login'}" v-if="isGuest">
<i class="fa fa-lg fa-user-secret pointer" aria-hidden="true"></i>
</router-link>
</div>
</div>
</div>
</div>
</template>
<script type="text/ecmascript-6">
import {mapGetters} from 'vuex';
import Clipboard from 'clipboard';
import {showTooltip} from '../services/tooltip';
import {mapGetters} from 'vuex';
import Clipboard from 'clipboard';
import {showTooltip} from '../services/tooltip';

export default {
data(){
return {
saved: false
}
},
created(){
const clipboard = new Clipboard('.btn-copy');
clipboard.on('success', event => {
if (event.text) {
showTooltip(event.trigger, 'copied !');
}
});
},
methods: {
fullReload(){
this.$store.dispatch('savePassword', {password: this.defaultPassword});
this.$router.push({name: 'home'});
},
logout(){
this.$store.dispatch('logout');
this.$router.push({name: 'home'});
},
saveOrUpdatePassword(){
this.$store.dispatch('saveOrUpdatePassword');
this.saved = true;
setTimeout(() => {
this.saved = false;
}, 3000);
}
},
computed: mapGetters([
'isAuthenticated',
'isGuest',
'password',
'defaultPassword',
'passwordURL'
])
}
</script>
export default {
data(){
return {
saved: false
}
},
created(){
const clipboard = new Clipboard('.btn-copy');
clipboard.on('success', event => {
if (event.text) {
showTooltip(event.trigger, 'copied !');
}
});
},
methods: {
fullReload(){
this.$store.dispatch('savePassword', {password: this.defaultPassword});
this.$router.push({name: 'home'});
},
logout(){
this.$store.dispatch('logout');
this.$router.push({name: 'home'});
},
saveOrUpdatePassword(){
this.$store.dispatch('saveOrUpdatePassword');
this.saved = true;
setTimeout(() => {
this.saved = false;
}, 3000);
}
},
computed: mapGetters([
'isAuthenticated',
'isGuest',
'password',
'defaultPassword',
'passwordURL'
])
}
</script>

+ 52
- 52
src/components/Message.vue View File

@@ -1,66 +1,66 @@
<style>
.fade-enter-active {
transition: opacity .5s
}
.fade-enter-active {
transition: opacity .5s
}

.fade-leave-active {
transition: opacity 2s
}
.fade-leave-active {
transition: opacity 2s
}

.fade-enter, .fade-leave-to {
opacity: 0
}
.fade-enter, .fade-leave-to {
opacity: 0
}

#message {
position: absolute;
top: 49px;
left: 0;
right: 0;
z-index: 20;
}
#message {
position: absolute;
top: 49px;
left: 0;
right: 0;
z-index: 20;
}

.close-notification{
float: right;
position: absolute;
top:0;
right: 1em;
cursor: pointer;
}
.close-notification {
float: right;
position: absolute;
top: 0;
right: 1em;
cursor: pointer;
}
</style>
<template>
<div id="message" v-on:click="keepMessage">
<transition name="fade">
<div v-if="message.text">
<div class="card-header text-white"
v-bind:class="{ 'card-warning': message.status==='warning', 'card-danger': message.status==='error', 'card-success': message.status==='success' }">
<div class="row">
<div class="col-12">
<small>{{message.text}}</small>
<span class="close-notification" v-on:click="hideMessage">
<div id="message" v-on:click="keepMessage">
<transition name="fade">
<div v-if="message.text">
<div class="card-header text-white"
v-bind:class="{ 'card-warning': message.status==='warning', 'card-danger': message.status==='error', 'card-success': message.status==='success' }">
<div class="row">
<div class="col-12">
<small>{{message.text}}</small>
<span class="close-notification" v-on:click="hideMessage">
<i class="fa fa-close"></i>
</span>
</div>
</div>
</div>
</div>
</transition>
</div>
</div>
</div>
</div>
</transition>
</div>
</template>
<script type="text/ecmascript-6">
import {mapGetters} from 'vuex';
import message from '../services/message';
import {mapGetters} from 'vuex';
import message from '../services/message';

export default {
computed: mapGetters([
'message'
]),
methods: {
keepMessage(){
message.keepMessage();
},
hideMessage(){
message.hideMessage();
}
}
export default {
computed: mapGetters([
'message'
]),
methods: {
keepMessage(){
message.keepMessage();
},
hideMessage(){
message.hideMessage();
}
}
</script>
}
</script>

+ 138
- 138
src/components/Options.vue View File

@@ -1,164 +1,164 @@
<style>
#options input[type="number"] {
-moz-appearance: textfield;
}
#options input[type="number"] {
-moz-appearance: textfield;
}

#options input[type="number"]::-webkit-outer-spin-button,
#options input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
#options input[type="number"]::-webkit-outer-spin-button,
#options input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
</style>
<template>
<div id="options">
<div class="form-group row">
<div class="col-12">
<div class="row">
<div class="col">
<label for="types">Advanced options</label>
</div>
</div>
<div id="types" class="row">
<div class="col-3">
<button type="button" class="btn btn-block btn-sm px-0"
v-bind:class="{'btn-primary':options.lowercase===true && options.version===2,'btn-warning':options.lowercase===true && options.version===1,'btn-secondary':options.lowercase===false}"
v-on:click="options.lowercase=!options.lowercase">
a-z
</button>
</div>
<div class="col-3">
<button type="button" class="btn btn-block btn-sm px-0"
v-bind:class="{'btn-primary':options.uppercase===true && options.version===2,'btn-warning':options.uppercase===true && options.version===1,'btn-secondary':options.uppercase===false}"
v-on:click="options.uppercase=!options.uppercase">
A-Z
</button>
</div>
<div class="col-3">
<button type="button" class="btn btn-block btn-sm px-0"
v-bind:class="{'btn-primary':options.numbers===true && options.version===2,'btn-warning':options.numbers===true && options.version===1,'btn-secondary':options.numbers===false}"
v-on:click="options.numbers=!options.numbers">
0-9
</button>
</div>
<div class="col-3">
<button type="button" class="btn btn-block btn-sm px-0"
v-bind:class="{'btn-primary':options.symbols===true && options.version===2,'btn-warning':options.symbols===true && options.version===1,'btn-secondary':options.symbols===false}"
v-on:click="options.symbols=!options.symbols">
%!@
</button>
</div>
</div>
</div>
<div id="options">
<div class="form-group row">
<div class="col-12">
<div class="row">
<div class="col">
<label for="types">Advanced options</label>
</div>
</div>
<div id="types" class="row">
<div class="col-3">
<button type="button" class="btn btn-block btn-sm px-0"
v-bind:class="{'btn-primary':options.lowercase===true && options.version===2,'btn-warning':options.lowercase===true && options.version===1,'btn-secondary':options.lowercase===false}"
v-on:click="options.lowercase=!options.lowercase">
a-z
</button>
</div>
<div class="col-3">
<button type="button" class="btn btn-block btn-sm px-0"
v-bind:class="{'btn-primary':options.uppercase===true && options.version===2,'btn-warning':options.uppercase===true && options.version===1,'btn-secondary':options.uppercase===false}"
v-on:click="options.uppercase=!options.uppercase">
A-Z
</button>
</div>
<div class="col-3">
<button type="button" class="btn btn-block btn-sm px-0"
v-bind:class="{'btn-primary':options.numbers===true && options.version===2,'btn-warning':options.numbers===true && options.version===1,'btn-secondary':options.numbers===false}"
v-on:click="options.numbers=!options.numbers">
0-9
</button>
</div>
<div class="col-3">
<button type="button" class="btn btn-block btn-sm px-0"
v-bind:class="{'btn-primary':options.symbols===true && options.version===2,'btn-warning':options.symbols===true && options.version===1,'btn-secondary':options.symbols===false}"
v-on:click="options.symbols=!options.symbols">
%!@
</button>
</div>
</div>
<div class="form-group row mb-0">
<div class="col-6 col-sm-4 mb-3 mb-sm-0">
<label for="passwordLength">Length</label>
<div class="input-group input-group-sm">
</div>
</div>
<div class="form-group row mb-0">
<div class="col-6 col-sm-4 mb-3 mb-sm-0">
<label for="passwordLength">Length</label>
<div class="input-group input-group-sm">
<span class="input-group-btn" v-on:click="options.length-=1">
<button class="btn btn-secondary" type="button"><i class="fa fa-minus"></i></button>
</span>
<input id="passwordLength" class="form-control form-control-sm" type="number"
v-model="options.length" min="5" max="35">
<span class="input-group-btn" v-on:click="options.length+=1">
<input id="passwordLength" class="form-control form-control-sm" type="number"
v-model="options.length" min="5" max="35">
<span class="input-group-btn" v-on:click="options.length+=1">
<button class="btn btn-secondary" type="button"><i class="fa fa-plus"></i></button>
</span>
</div>
</div>
<div class="col-6 col-sm-4 mb-3 mb-sm-0">
<label for="passwordCounter"
class="hint--top hint--medium"
aria-label="Increment counter field to change generated password without changing your master password.">Counter</label>
<div class="input-group input-group-sm">
</div>
</div>
<div class="col-6 col-sm-4 mb-3 mb-sm-0">
<label for="passwordCounter"
class="hint--top hint--medium"
aria-label="Increment counter field to change generated password without changing your master password.">Counter</label>
<div class="input-group input-group-sm">
<span class="input-group-btn" v-on:click="options.counter-=1">
<button class="btn btn-secondary" type="button"><i class="fa fa-minus"></i></button>
</span>
<input id="passwordCounter" class="form-control form-control-sm" type="number"
v-model="options.counter" min="1">
<span class="input-group-btn" v-on:click="options.counter+=1">
<input id="passwordCounter" class="form-control form-control-sm" type="number"
v-model="options.counter" min="1">
<span class="input-group-btn" v-on:click="options.counter+=1">
<button class="btn btn-secondary" type="button"><i class="fa fa-plus"></i></button>
</span>
</div>
</div>
<div class="clearfix hidden-sm-up"></div>
<div class="col-12 col-sm-4">
<div class="row hidden-sm-down">
<div class="col">
<label>Version</label>
</div>
</div>
<div class="row no-gutters">
<div class="col-6">
<button type="button" class="btn btn-block btn-sm border-right-0"
v-bind:class="{'btn-primary':options.version===2,'btn-secondary':options.version!==2}"
v-on:click="setVersion(2)">
v<span class="hidden-sm-up">ersion </span>2
</button>
</div>
</div>
<div class="clearfix hidden-sm-up"></div>
<div class="col-12 col-sm-4">
<div class="row hidden-sm-down">
<div class="col">
<label>Version</label>
</div>
</div>
<div class="row no-gutters">
<div class="col-6">
<button type="button" class="btn btn-block btn-sm border-right-0"
v-bind:class="{'btn-primary':options.version===2,'btn-secondary':options.version!==2}"
v-on:click="setVersion(2)">
v<span class="hidden-sm-up">ersion </span>2
</button>

</div>
<div class="col-6">
<button type="button"
class="btn btn-block btn-sm border-left-0"
v-bind:class="{'btn-warning':options.version===1,'btn-secondary':options.version!==1}"
v-on:click="setVersion(1)">
v<span class="hidden-sm-up">ersion </span>1
</button>
</div>
</div>
</div>
</div>
<div class="col-6">
<button type="button"
class="btn btn-block btn-sm border-left-0"
v-bind:class="{'btn-warning':options.version===1,'btn-secondary':options.version!==1}"
v-on:click="setVersion(1)">
v<span class="hidden-sm-up">ersion </span>1
</button>
</div>
</div>
</div>
</div>
</div>
</template>

<script type="text/ecmascript-6">
import Message from '../services/message';
import Message from '../services/message';

export default {
name: 'options',
props: {
password: {
type: Object,
required: true
}
},
data() {
return {
options: {
uppercase: this.password.uppercase,
lowercase: this.password.lowercase,
numbers: this.password.numbers,
symbols: this.password.symbols,
length: this.password.length,
counter: this.password.counter,
version: this.password.version
}
};
},
watch: {
options: {
handler: function(newOptions) {
if (newOptions.version === 1) {
const dayBeforeOnlyV2 = this.getDayBeforeOnlyV2();
const message = `Version 1 is deprecated and will be removed in ${dayBeforeOnlyV2} days. We strongly advise you to migrate your passwords to version 2.`;
Message.error(message);
}
export default {
name: 'options',
props: {
password: {
type: Object,
required: true
}
},
data() {
return {
options: {
uppercase: this.password.uppercase,
lowercase: this.password.lowercase,
numbers: this.password.numbers,
symbols: this.password.symbols,
length: this.password.length,
counter: this.password.counter,
version: this.password.version
}
};
},
watch: {
options: {
handler: function(newOptions) {
if (newOptions.version === 1) {
const dayBeforeOnlyV2 = this.getDayBeforeOnlyV2();
const message = `Version 1 is deprecated and will be removed in ${dayBeforeOnlyV2} days. We strongly advise you to migrate your passwords to version 2.`;
Message.error(message);
}

this.$emit('optionsUpdated', newOptions)
},
deep: true
}
this.$emit('optionsUpdated', newOptions)
},
methods: {
setVersion(value){
this.options.length = value === 1 ? 12 : 16;
this.options.version = value;
this.$store.dispatch('saveVersion', {version: value});
},
getDayBeforeOnlyV2(){
const oneDay = 24 * 60 * 60 * 1000;
const now = new Date();
const onlyV2DefaultDate = new Date(2017, 4, 10);
return Math.round(Math.abs((now.getTime() - onlyV2DefaultDate.getTime()) / (oneDay)));
},
}
deep: true
}
},
methods: {
setVersion(value){
this.options.length = value === 1 ? 12 : 16;
this.options.version = value;
this.$store.dispatch('saveVersion', {version: value});
},
getDayBeforeOnlyV2(){
const oneDay = 24 * 60 * 60 * 1000;
const now = new Date();
const onlyV2DefaultDate = new Date(2017, 4, 10);
return Math.round(Math.abs((now.getTime() - onlyV2DefaultDate.getTime()) / (oneDay)));
},
}
}
</script>

+ 9
- 9
src/components/RemoveAutoComplete.vue View File

@@ -1,10 +1,10 @@
<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>
<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>

+ 35
- 35
src/domain/url-parser.js View File

@@ -1,45 +1,45 @@
'use strict';

export function getDomainName(urlStr) {
if (typeof urlStr === 'undefined') {
return '';
}
var matchesDomainName = urlStr.match(/^(?:https?\:\/\/)([^\/?#]+)(?:[\/?#]|$)/i);
return matchesDomainName && matchesDomainName[1];
if (typeof urlStr === 'undefined') {
return '';
}
var matchesDomainName = urlStr.match(/^(?:https?\:\/\/)([^\/?#]+)(?:[\/?#]|$)/i);
return matchesDomainName && matchesDomainName[1];
}

export function getSite() {
return new Promise(resolve => {
if (typeof chrome !== 'undefined' && typeof chrome.tabs !== 'undefined' && typeof chrome.tabs.query !== 'undefined') {
chrome.tabs.query({active: true, currentWindow: true}, tabs => {
const url = tabs[0].url;
resolve({
site: getDomainName(url),
url: url
});
});
} else {
resolve('');
}
});
return new Promise(resolve => {
if (typeof chrome !== 'undefined' && typeof chrome.tabs !== 'undefined' && typeof chrome.tabs.query !== 'undefined') {
chrome.tabs.query({active: true, currentWindow: true}, tabs => {
const url = tabs[0].url;
resolve({
site: getDomainName(url),
url: url
});
});
} else {
resolve('');
}
});
}

export function getPasswordFromUrlQuery(query) {
const password = {};
['uppercase', 'lowercase', 'numbers', 'symbols'].forEach(booleanishQuery => {
if (booleanishQuery in query) {
password[booleanishQuery] = (query[booleanishQuery].toLowerCase() === "true" || query[booleanishQuery].toLowerCase() === "1");
}
});
['site', 'login'].forEach(stringQuery => {
if (stringQuery in query) {
password[stringQuery] = query[stringQuery]
}
});
['length', 'counter', 'version'].forEach(intQuery => {
if (intQuery in query) {
password[intQuery] = parseInt(query[intQuery], 10)
}
});
return password;
const password = {};
['uppercase', 'lowercase', 'numbers', 'symbols'].forEach(booleanishQuery => {
if (booleanishQuery in query) {
password[booleanishQuery] = (query[booleanishQuery].toLowerCase() === "true" || query[booleanishQuery].toLowerCase() === "1");
}
});
['site', 'login'].forEach(stringQuery => {
if (stringQuery in query) {
password[stringQuery] = query[stringQuery]
}
});
['length', 'counter', 'version'].forEach(intQuery => {
if (intQuery in query) {
password[intQuery] = parseInt(query[intQuery], 10)
}
});
return password;
}

+ 4
- 4
src/main.js View File

@@ -7,8 +7,8 @@ import router from './router';
sync(store, router);

new Vue({
el: '#lesspass',
store,
router,
render: h => h(LessPass)
el: '#lesspass',
store,
router,
render: h => h(LessPass)
});

+ 10
- 10
src/router.js View File

@@ -11,18 +11,18 @@ import Passwords from './views/Passwords.vue';
Vue.use(VueRouter);

const routes = [
{path: '/', name: 'home', component: PasswordGenerator},
{path: '/login', name: 'login', component: Login},
{path: '/passwords/', name: 'passwords', component: Passwords},
{path: '/options/default/', name: 'configureOptions', component: ConfigureOptions},
{path: '/passwords/:id', name: 'password', component: PasswordGenerator},
{path: '/password/reset', name: 'passwordReset', component: PasswordReset},
{path: '/password/reset/confirm/:uid/:token', name: 'passwordResetConfirm', component: PasswordResetConfirm},
{path: '*', redirect: '/'}
{path: '/', name: 'home', component: PasswordGenerator},
{path: '/login', name: 'login', component: Login},
{path: '/passwords/', name: 'passwords', component: Passwords},
{path: '/options/default/', name: 'configureOptions', component: ConfigureOptions},
{path: '/passwords/:id', name: 'password', component: PasswordGenerator},
{path: '/password/reset', name: 'passwordReset', component: PasswordReset},
{path: '/password/reset/confirm/:uid/:token', name: 'passwordResetConfirm', component: PasswordResetConfirm},
{path: '*', redirect: '/'}
];

const router = new VueRouter({
routes
routes
});

export default router;
export default router;

+ 36
- 36
src/services/message.js View File

@@ -1,40 +1,40 @@
import Store from '../store';

export default {
timeout: 0,
deleteMessage: true,
success(text){
const message = {text, status: 'success'};
Store.dispatch('displayMessage', {message});
this.autoHideMessage(text);
},
warning(text){
const message = {text, status: 'warning'};
Store.dispatch('displayMessage', {message});
this.autoHideMessage(text);
},
error(text){
const message = {text, status: 'error'};
Store.dispatch('displayMessage', {message});
this.autoHideMessage(text);
},
autoHideMessage(text){
clearTimeout(this.timeout);
this.deleteMessage = true;
const duration = Math.min(Math.max(text.length * 100, 3000), 8000);
this.timeout = setTimeout(() => {
if (this.deleteMessage) {
Store.dispatch('cleanMessage');
}
}, duration);
},
keepMessage(){
this.deleteMessage = false;
},
hideMessage(){
timeout: 0,
deleteMessage: true,
success(text){
const message = {text, status: 'success'};
Store.dispatch('displayMessage', {message});
this.autoHideMessage(text);
},
warning(text){
const message = {text, status: 'warning'};
Store.dispatch('displayMessage', {message});
this.autoHideMessage(text);
},
error(text){
const message = {text, status: 'error'};
Store.dispatch('displayMessage', {message});
this.autoHideMessage(text);
},
autoHideMessage(text){
clearTimeout(this.timeout);
this.deleteMessage = true;
const duration = Math.min(Math.max(text.length * 100, 3000), 8000);
this.timeout = setTimeout(() => {
if (this.deleteMessage) {
Store.dispatch('cleanMessage');
},
displayGenericError(){
this.error('Oops! Something went wrong. Retry in a few minutes.');
}
}
}
}, duration);
},
keepMessage(){
this.deleteMessage = false;
},
hideMessage(){
Store.dispatch('cleanMessage');
},
displayGenericError(){
this.error('Oops! Something went wrong. Retry in a few minutes.');
}
}

+ 7
- 7
src/services/tooltip.js View File

@@ -1,8 +1,8 @@
export function showTooltip(elem, msg) {
var classNames = elem.className;
elem.setAttribute('class', classNames + ' hint--right');
elem.setAttribute('aria-label', msg);
setTimeout(function () {
elem.setAttribute('class', classNames);
}, 2000);
}
var classNames = elem.className;
elem.setAttribute('class', classNames + ' hint--right');
elem.setAttribute('aria-label', msg);
setTimeout(function() {
elem.setAttribute('class', classNames);
}, 2000);
}

+ 43
- 43
src/store/actions.js View File

@@ -4,90 +4,90 @@ import User from '../api/user';
import * as types from './mutation-types'

export const loadPasswordFirstTime = ({commit}) => {
commit(types.LOAD_PASSWORD_FIRST_TIME);
commit(types.LOAD_PASSWORD_FIRST_TIME);
};

export const refreshToken = ({commit, state}) => {
const token = state.token;
if (token) {
User.requestNewToken({token}, {baseURL: state.baseURL})
.then(newToken => commit(types.SET_TOKEN, {token: newToken}))
.catch(() => commit(types.LOGOUT));
}
const token = state.token;
if (token) {
User.requestNewToken({token}, {baseURL: state.baseURL})
.then(newToken => commit(types.SET_TOKEN, {token: newToken}))
.catch(() => commit(types.LOGOUT));
}
};

export const loadPasswordForSite = ({commit}, payload) => {
commit(types.LOAD_PASSWORD_FOR_SITE, payload);
commit(types.LOAD_PASSWORD_FOR_SITE, payload);
};

export const saveDefaultPassword = ({commit}, payload) => {
commit(types.SET_DEFAULT_PASSWORD, payload);
commit(types.SET_DEFAULT_PASSWORD, payload);
};

export const passwordGenerated = ({commit}) => {
commit(types.PASSWORD_GENERATED);
commit(types.PASSWORD_GENERATED);
};

export const savePassword = ({commit}, payload) => {
commit(types.SET_PASSWORD, payload);
commit(types.SET_PASSWORD, payload);
};

export const saveVersion = ({commit}, payload) => {
commit(types.SET_VERSION, payload);
commit(types.SET_VERSION, payload);
};

export const login = ({commit}, payload) => {
commit(types.SET_BASE_URL, payload);
commit(types.SET_TOKEN, payload);
commit(types.LOGIN);
commit(types.SET_BASE_URL, payload);
commit(types.SET_TOKEN, payload);
commit(types.LOGIN);
};

export const logout = ({commit}) => {
commit(types.LOGOUT);
commit(types.LOGOUT);
};

export const getPasswords = ({commit, state}) => {
if (state.authenticated) {
Password.all(state).then(response => commit(types.SET_PASSWORDS, {passwords: response.data.results}));
}
if (state.authenticated) {
Password.all(state).then(response => commit(types.SET_PASSWORDS, {passwords: response.data.results}));
}
};

export const getPassword = ({commit, state}, payload) => {
if (state.authenticated) {
Password.read(payload, state).then(response => commit(types.SET_PASSWORD, {password: response.data}));
}
if (state.authenticated) {
Password.read(payload, state).then(response => commit(types.SET_PASSWORD, {password: response.data}));
}
};

export const saveOrUpdatePassword = ({commit, state}) => {
if (state.password && typeof state.password.id === 'undefined') {
const site = state.password.site;
const login = state.password.login;
if (site || login) {
Password.create(state.password, state)
.then(response => {
savePassword({commit}, {password: response.data});
getPasswords({commit, state});
})
}
} else {
Password.update(state.password, state)
.then(() => {
getPasswords({commit, state});
})
if (state.password && typeof state.password.id === 'undefined') {
const site = state.password.site;
const login = state.password.login;
if (site || login) {
Password.create(state.password, state)
.then(response => {
savePassword({commit}, {password: response.data});
getPasswords({commit, state});
})
}
} else {
Password.update(state.password, state)
.then(() => {
getPasswords({commit, state});
})
}
};

export const deletePassword = ({commit, state}, payload) => {
Password.delete(payload, state)
.then(() => {
commit(types.DELETE_PASSWORD, payload);
});
Password.delete(payload, state)
.then(() => {
commit(types.DELETE_PASSWORD, payload);
});
};

export const displayMessage = ({commit}, payload) => {
commit(types.SET_MESSAGE, payload);
commit(types.SET_MESSAGE, payload);
};

export const cleanMessage = ({commit}) => {
commit(types.CLEAN_MESSAGE);
commit(types.CLEAN_MESSAGE);
};

+ 6
- 6
src/store/getters.js View File

@@ -13,12 +13,12 @@ export const baseURL = state => state.baseURL;
export const message = state => state.message;

export const version = state => {
if (state.password === null || state.route.path === '/options/default') {
return state.defaultPassword.version;
}
return state.password.version;
if (state.password === null || state.route.path === '/options/default') {
return state.defaultPassword.version;
}
return state.password.version;
};

export const passwordURL = state => {
return `${state.baseURL}/#/?login=${state.password.login}&site=${state.password.site}&uppercase=${state.password.uppercase}&lowercase=${state.password.lowercase}&numbers=${state.password.numbers}&symbols=${state.password.symbols}&length=${state.password.length}&counter=${state.password.counter}&version=${state.password.version}`;
};
return `${state.baseURL}/#/?login=${state.password.login}&site=${state.password.site}&uppercase=${state.password.uppercase}&lowercase=${state.password.lowercase}&numbers=${state.password.numbers}&symbols=${state.password.symbols}&length=${state.password.length}&counter=${state.password.counter}&version=${state.password.version}`;
};

+ 22
- 22
src/store/index.js View File

@@ -8,31 +8,31 @@ import createPersistedState from 'vuex-persistedstate';
Vue.use(Vuex);

const defaultPassword = {
login: '',
site: '',
uppercase: true,
lowercase: true,
numbers: true,
symbols: true,
length: 16,
counter: 1,
version: 2
login: '',
site: '',
uppercase: true,
lowercase: true,
numbers: true,
symbols: true,
length: 16,
counter: 1,
version: 2
};

const state = {
authenticated: false,
password: defaultPassword,
passwords: [],
defaultPassword: defaultPassword,
lastUse: null,
token: null,
baseURL: 'https://lesspass.com',
authenticated: false,
password: defaultPassword,
passwords: [],
defaultPassword: defaultPassword,
lastUse: null,
token: null,
baseURL: 'https://lesspass.com',
};

export default new Vuex.Store({
state,
getters,
actions,
mutations,
plugins: [createPersistedState({key: 'lesspass'})]
});
state,
getters,
actions,
mutations,
plugins: [createPersistedState({key: 'lesspass'})]
});

+ 65
- 65
src/store/mutations.js View File

@@ -1,70 +1,70 @@
import * as types from './mutation-types';

export default {
[types.LOGIN](state){
state.authenticated = true;
},
[types.SET_TOKEN](state, {token}){
state.token = token;
},
[types.LOGOUT](state){
state.authenticated = false;
state.token = null;
state.passwords = [];
state.password = {...state.defaultPassword};
},
[types.SET_PASSWORD](state, {password}){
[types.LOGIN](state){
state.authenticated = true;
},
[types.SET_TOKEN](state, {token}){
state.token = token;
},
[types.LOGOUT](state){
state.authenticated = false;
state.token = null;
state.passwords = [];
state.password = {...state.defaultPassword};
},
[types.SET_PASSWORD](state, {password}){
state.password = {...password};
},
[types.PASSWORD_GENERATED](state){
state.lastUse = new Date().getTime();
},
[types.SET_DEFAULT_PASSWORD](state, {password}){
state.defaultPassword = {...password};
},
[types.SET_PASSWORDS](state, {passwords}){
state.passwords = passwords;
},
[types.DELETE_PASSWORD](state, {id}){
state.passwords = state.passwords.filter(password => {
return password.id !== id;
});
if (state.password && state.password.id === id) {
state.password = Object.assign({}, state.defaultPassword);
}
},
[types.SET_BASE_URL](state, {baseURL}){
state.baseURL = baseURL;
},
[types.SET_VERSION](state, {version}){
const length = version === 1 ? 12 : 16;
state.password.version = version;
state.password.length = length;
},
[types.LOAD_PASSWORD_FIRST_TIME](state){
const tenMinutesAgo = new Date().getTime() - 60 * 1000;
if (tenMinutesAgo > state.lastUse) {
state.password = {...state.defaultPassword};
}
},
[types.LOAD_PASSWORD_FOR_SITE](state, {site, url}){
state.password.site = site;
const passwords = state.passwords;
for (let i = 0; i < state.passwords.length; i++) {
const password = passwords[i];
if (password.site.endsWith(site)) {
state.password = {...password};
},
[types.PASSWORD_GENERATED](state){
state.lastUse = new Date().getTime();
},
[types.SET_DEFAULT_PASSWORD](state, {password}){
state.defaultPassword = {...password};
},
[types.SET_PASSWORDS](state, {passwords}){
state.passwords = passwords;
},
[types.DELETE_PASSWORD](state, {id}){
state.passwords = state.passwords.filter(password => {
return password.id !== id;
});
if (state.password && state.password.id === id) {
state.password = Object.assign({}, state.defaultPassword);
}
},
[types.SET_BASE_URL](state, {baseURL}){
state.baseURL = baseURL;
},
[types.SET_VERSION](state, {version}){
const length = version === 1 ? 12 : 16;
state.password.version = version;
state.password.length = length;
},
[types.LOAD_PASSWORD_FIRST_TIME](state){
const tenMinutesAgo = new Date().getTime() - 60 * 1000;
if (tenMinutesAgo > state.lastUse) {
state.password = {...state.defaultPassword};
}
},
[types.LOAD_PASSWORD_FOR_SITE](state, {site, url}){
state.password.site = site;
const passwords = state.passwords;
for (let i = 0; i < state.passwords.length; i++) {
const password = passwords[i];
if (password.site.endsWith(site)) {
state.password = {...password};
}
if (typeof url !== 'undefined' && url.includes(password.site)) {
state.password = {...password};
break;
}
}
},
[types.SET_MESSAGE](state, {message}){
state.message = message;
},
[types.CLEAN_MESSAGE](state){
state.message = {text: '', status: 'success'};
},
}
if (typeof url !== 'undefined' && url.includes(password.site)) {
state.password = {...password};
break;
}
}
},
[types.SET_MESSAGE](state, {message}){
state.message = message;
},
[types.CLEAN_MESSAGE](state){
state.message = {text: '', status: 'success'};
},
};

+ 47
- 47
src/views/ConfigureOptions.vue View File

@@ -1,55 +1,55 @@
<template>
<div>
<div class="form-group">
<label for="login">Login</label>
<div class="inner-addon left-addon">
<i class="fa fa-user"></i>
<input id="login"
name="login"
type="text"
class="form-control"
placeholder="Login"
autocomplete="off"
autocorrect="off"
autocapitalize="none"
v-model="defaultOptions.login">
</div>
</div>
<options v-bind:password="defaultOptions" v-on:optionsUpdated="optionsUpdated"></options>
<div class="form-group pt-3">
<button type="button" class="btn btn-sm btn-block hint--top hint--medium"
aria-label="We use local storage to save default options locally. Each time you open the app, those options will be loaded by default."
v-bind:class="{'btn-warning':defaultOptions.version===1,'btn-primary':defaultOptions.version!==1}"
v-on:click="saveOptionsAsDefault">
Save default options locally
</button>
</div>
<div>
<div class="form-group">
<label for="login">Login</label>
<div class="inner-addon left-addon">
<i class="fa fa-user"></i>
<input id="login"
name="login"
type="text"
class="form-control"
placeholder="Login"
autocomplete="off"
autocorrect="off"
autocapitalize="none"
v-model="defaultOptions.login">
</div>
</div>
<options v-bind:password="defaultOptions" v-on:optionsUpdated="optionsUpdated"></options>
<div class="form-group pt-3">
<button type="button" class="btn btn-sm btn-block hint--top hint--medium"
aria-label="We use local storage to save default options locally. Each time you open the app, those options will be loaded by default."
v-bind:class="{'btn-warning':defaultOptions.version===1,'btn-primary':defaultOptions.version!==1}"
v-on:click="saveOptionsAsDefault">
Save default options locally
</button>
</div>
</div>
</template>

<script type="text/ecmascript-6">
import Options from '../components/Options.vue';
import Options from '../components/Options.vue';

export default {
name: 'configure-options-view',
components: {
Options
},
data(){
return {
defaultOptions: {}
}
},
created(){
this.defaultOptions = Object.assign({}, this.$store.state.defaultPassword);
},
methods: {
optionsUpdated(options){
this.defaultOptions = Object.assign({}, this.defaultOptions, options);
},
saveOptionsAsDefault(){
this.$store.dispatch('saveDefaultPassword', {password: this.defaultOptions});
},
}
export default {
name: 'configure-options-view',
components: {
Options
},
data(){
return {
defaultOptions: {}
}
},
created(){
this.defaultOptions = Object.assign({}, this.$store.state.defaultPassword);
},
methods: {
optionsUpdated(options){
this.defaultOptions = Object.assign({}, this.defaultOptions, options);
},
saveOptionsAsDefault(){
this.$store.dispatch('saveDefaultPassword', {password: this.defaultOptions});
},
}
}
</script>

+ 147
- 147
src/views/Login.vue View File

@@ -1,163 +1,163 @@
<style>
#signInButton {
border-right: none;
}
#signInButton {
border-right: none;
}

#registerButton {
border-left: none;
}
#registerButton {
border-left: none;
}
</style>
<template>
<form v-on:submit.prevent="signIn">
<div class="form-group">
<div class="inner-addon left-addon">
<i class="fa fa-globe"></i>
<input id="baseURL"
class="form-control"
type="text"
placeholder="https://lesspass.com"
v-model="baseURL">
</div>
</div>
<div class="form-group row">
<div class="col-12">
<div class="inner-addon left-addon">
<i class="fa fa-user"></i>
<input id="email"
class="form-control"
name="username"
type="email"
placeholder="Email"
required
v-model="email">
</div>
</div>
<form v-on:submit.prevent="signIn">
<div class="form-group">
<div class="inner-addon left-addon">
<i class="fa fa-globe"></i>
<input id="baseURL"
class="form-control"
type="text"
placeholder="https://lesspass.com"
v-model="baseURL">
</div>
</div>
<div class="form-group row">
<div class="col-12">
<div class="inner-addon left-addon">
<i class="fa fa-user"></i>
<input id="email"
class="form-control"
name="username"
type="email"
placeholder="Email"
required
v-model="email">
</div>
<div class="form-group mb-2">
<master-password v-model="password"></master-password>
<label class="custom-control custom-checkbox hint--top hint--medium mb-0"
data-hint="Check me to generate encrypted password for lesspass.com">
<input type="checkbox" class="custom-control-input" v-model="transformMasterPassword">
<span class="custom-control-indicator"></span>
<span class="custom-control-description text-muted">
</div>
</div>
<div class="form-group mb-2">
<master-password v-model="password"></master-password>
<label class="custom-control custom-checkbox hint--top hint--medium mb-0"
data-hint="Check me to generate encrypted password for lesspass.com">
<input type="checkbox" class="custom-control-input" v-model="transformMasterPassword">
<span class="custom-control-indicator"></span>
<span class="custom-control-description text-muted">
encrypt before use
</span>
</label>
</div>
<div class="form-group row no-gutters mb-0">
<div class="col">
<button id="signInButton" class="btn btn-block" type="submit"
v-bind:class="{ 'btn-warning': version===1, 'btn-primary': version===2 }">
Sign In
</button>
</div>
<div class="col">
<button id="registerButton" class="btn btn-secondary btn-block" type="button" v-on:click="register">
Register
</button>
</div>
</div>
<div class="form-group my-0">
<router-link :to="{ name: 'passwordReset'}">
<small>Forgot your password?</small>
</router-link>
</div>
</form>
</label>
</div>
<div class="form-group row no-gutters mb-0">
<div class="col">
<button id="signInButton" class="btn btn-block" type="submit"
v-bind:class="{ 'btn-warning': version===1, 'btn-primary': version===2 }">
Sign In
</button>
</div>
<div class="col">
<button id="registerButton" class="btn btn-secondary btn-block" type="button" v-on:click="register">
Register
</button>
</div>
</div>
<div class="form-group my-0">
<router-link :to="{ name: 'passwordReset'}">
<small>Forgot your password?</small>
</router-link>
</div>
</form>
</template>
<script type="text/ecmascript-6">
import LessPass from 'lesspass';
import User from '../api/user';
import {mapGetters} from 'vuex';
import MasterPassword from '../components/MasterPassword.vue';
import message from '../services/message';
import LessPass from 'lesspass';
import User from '../api/user';
import {mapGetters} from 'vuex';
import MasterPassword from '../components/MasterPassword.vue';
import message from '../services/message';

export default {
data() {
return {
email: '',
password: '',
baseURL: 'https://lesspass.com',
transformMasterPassword: false,
};
},
components: {
MasterPassword
},
computed: {
...mapGetters(['version'])
},
watch: {
password: function() {
this.transformMasterPassword = false;
},
transformMasterPassword: function(transformPassword) {
if (!transformPassword) {
return;
}
const defaultPasswordProfile = {
lowercase: true,
uppercase: true,
numbers: true,
symbols: true,
length: 16,
counter: 1,
version: 2,
};
return LessPass.generatePassword('lesspass.com', this.email, this.password, defaultPasswordProfile).then(generatedPassword => {
this.password = generatedPassword;
});
}
},
methods: {
formIsValid(){
if (!this.email || !this.password || !this.baseURL) {
message.error('LessPass URL, email and password are mandatory');
return false;
}
return true;
},
signIn(){
if (this.formIsValid()) {
const baseURL = this.baseURL;
User.login({email: this.email, password: this.password}, {baseURL})
.then(response => {
this.$store.dispatch('login', {token: response.token, baseURL});
this.$router.push({name: 'home'});
})
.catch(err => {
if (err.response === undefined && baseURL !== "https://lesspass.com") {
message.error('Your LessPass Database is not running');
} else if (err.response.status === 400) {
message.error('The email and password you entered did not match our records. Please double-check and try again.');
} else {
message.displayGenericError();
}
});
export default {
data() {
return {
email: '',
password: '',
baseURL: 'https://lesspass.com',
transformMasterPassword: false,
};
},
components: {
MasterPassword
},
computed: {
...mapGetters(['version'])
},
watch: {
password: function() {
this.transformMasterPassword = false;
},
transformMasterPassword: function(transformPassword) {
if (!transformPassword) {
return;
}
const defaultPasswordProfile = {
lowercase: true,
uppercase: true,
numbers: true,
symbols: true,
length: 16,
counter: 1,
version: 2,
};
return LessPass.generatePassword('lesspass.com', this.email, this.password, defaultPasswordProfile).then(generatedPassword => {
this.password = generatedPassword;
});
}
},
methods: {
formIsValid(){
if (!this.email || !this.password || !this.baseURL) {
message.error('LessPass URL, email and password are mandatory');
return false;
}
return true;
},
signIn(){
if (this.formIsValid()) {
const baseURL = this.baseURL;
User.login({email: this.email, password: this.password}, {baseURL})
.then(response => {
this.$store.dispatch('login', {token: response.token, baseURL});
this.$router.push({name: 'home'});
})
.catch(err => {
if (err.response === undefined && baseURL !== "https://lesspass.com") {
message.error('Your LessPass Database is not running');
} else if (err.response.status === 400) {
message.error('The email and password you entered did not match our records. Please double-check and try again.');
} else {
message.displayGenericError();
}
});
}
},
register(){
if (this.formIsValid()) {
const baseURL = this.baseURL;
User.register({email: this.email, password: this.password}, {baseURL})
.then(() => {
message.success(`Welcome ${this.email}, thank you for signing up.`);
this.signIn();
})
.catch(err => {
if (err.response && typeof err.response.data.email !== 'undefined') {
if (err.response.data.email[0].indexOf('already exists') !== -1) {
message.error('This email is already registered. Want to login or recover your password?');
}
},
register(){
if (this.formIsValid()) {
const baseURL = this.baseURL;
User.register({email: this.email, password: this.password}, {baseURL})
.then(() => {
message.success(`Welcome ${this.email}, thank you for signing up.`);
this.signIn();
})
.catch(err => {
if (err.response && typeof err.response.data.email !== 'undefined') {
if (err.response.data.email[0].indexOf('already exists') !== -1) {
message.error('This email is already registered. Want to login or recover your password?');
}
if (err.response.data.email[0].indexOf('valid email') !== -1) {
message.error('Please enter a valid email');
}
} else {
message.displayGenericError();
}
});
if (err.response.data.email[0].indexOf('valid email') !== -1) {
message.error('Please enter a valid email');
}
}
} else {
message.displayGenericError();
}
});
}
}
}
}
</script>


+ 245
- 245
src/views/PasswordGenerator.vue View File

@@ -1,93 +1,93 @@
<style>
#generated-password {
font-family: Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, sans-serif;
}
#generated-password {
font-family: Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, sans-serif;
}

.inner-addon i {
position: absolute;
padding: 10px;
pointer-events: none;
z-index: 10;
}
.inner-addon i {
position: absolute;
padding: 10px;
pointer-events: none;
z-index: 10;
}

.inner-addon {
position: relative;
}
.inner-addon {
position: relative;
}

.left-addon i {
left: 0;
}
.left-addon i {
left: 0;
}

.right-addon i {
right: 0;
}
.right-addon i {
right: 0;
}

.left-addon input {
padding-left: 30px;
}
.left-addon input {
padding-left: 30px;
}

.right-addon input {
padding-right: 30px;
}
.right-addon input {
padding-right: 30px;
}

div.awesomplete {
display: block;
}
div.awesomplete {
display: block;
}

div.awesomplete > ul {
z-index: 11;
}
div.awesomplete > ul {
z-index: 11;
}
</style>
<template>
<form id="password-generator">
<div class="form-group">
<div class="inner-addon left-addon">
<label for="site" class="sr-only">Site</label>
<i class="fa fa-globe"></i>
<input id="site"
name="site"
type="text"
ref="site"
class="form-control awesomplete"
placeholder="Site"
autocorrect="off"
autocapitalize="none"
v-model="password.site">
</div>
<form id="password-generator">
<div class="form-group">
<div class="inner-addon left-addon">
<label for="site" class="sr-only">Site</label>
<i class="fa fa-globe"></i>
<input id="site"
name="site"
type="text"
ref="site"
class="form-control awesomplete"
placeholder="Site"
autocorrect="off"
autocapitalize="none"
v-model="password.site">
</div>
</div>
<remove-auto-complete></remove-auto-complete>
<div class="form-group">
<div class="inner-addon left-addon">
<label for="login" class="sr-only">Login</label>
<i class="fa fa-user"></i>
<input id="login"
name="login"
type="text"
ref="login"
class="form-control"
placeholder="Login"
autocomplete="off"
autocorrect="off"
autocapitalize="none"
v-model="password.login">
</div>
</div>
<div class="form-group">
<master-password ref="masterPassword" v-model="masterPassword"
:keyupEnter="generatePassword"></master-password>
</div>
<div class="form-group row justify-content-between no-gutters" v-bind:class="{'mb-0':showOptions===false}">
<div class="col col-auto" v-show="!generatedPassword">
<div style="display: inline-block">
<button type="button" class="btn" v-on:click="generatePassword"
v-bind:class="{ 'btn-warning': password.version===1, 'btn-primary': password.version===2 }">
<span v-if="!generatingPassword">Generate</span>
<span v-if="generatingPassword">Generating...</span>
</button>
</div>
<remove-auto-complete></remove-auto-complete>
<div class="form-group">
<div class="inner-addon left-addon">
<label for="login" class="sr-only">Login</label>
<i class="fa fa-user"></i>
<input id="login"
name="login"
type="text"
ref="login"
class="form-control"
placeholder="Login"
autocomplete="off"
autocorrect="off"
autocapitalize="none"
v-model="password.login">
</div>
</div>
<div class="form-group">
<master-password ref="masterPassword" v-model="masterPassword"
:keyupEnter="generatePassword"></master-password>
</div>
<div class="form-group row justify-content-between no-gutters" v-bind:class="{'mb-0':showOptions===false}">
<div class="col col-auto" v-show="!generatedPassword">
<div style="display: inline-block">
<button type="button" class="btn" v-on:click="generatePassword"
v-bind:class="{ 'btn-warning': password.version===1, 'btn-primary': password.version===2 }">
<span v-if="!generatingPassword">Generate</span>
<span v-if="generatingPassword">Generating...</span>
</button>
</div>
</div>
<div class="col-9" v-show="generatedPassword">
<div class="input-group">
</div>
<div class="col-9" v-show="generatedPassword">
<div class="input-group">
<span class="input-group-btn">
<button id="copyPasswordButton" type="button" data-clipboard-text="" class="btn btn-copy"
ref="copyPasswordButton"
@@ -95,191 +95,191 @@
<i class="fa fa-clipboard" aria-hidden="true"></i>
</button>
</span>
<input type="password" id="generated-password" class="form-control" tabindex="-1"
ref="generatedPassword" v-bind:value="generatedPassword"
v-bind:class="{ 'btn-outline-warning': password.version===1, 'btn-outline-primary': password.version===2 }">
<span class="input-group-btn">
<input type="password" id="generated-password" class="form-control" tabindex="-1"
ref="generatedPassword" v-bind:value="generatedPassword"
v-bind:class="{ 'btn-outline-warning': password.version===1, 'btn-outline-primary': password.version===2 }">
<span class="input-group-btn">
<button id="revealGeneratedPassword" type="button" class="btn"
v-on:click="togglePasswordType($refs.generatedPassword)"
v-bind:class="{ 'btn-outline-warning': password.version===1, 'btn-outline-primary': password.version===2 }">
<i class="fa fa-eye" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
<div class="col col-auto">
<button type="button" class="btn btn-secondary" v-on:click="showOptions=!showOptions">
<i class="fa fa-sliders" aria-hidden="true"></i>
</button>
</div>
</div>
<options :password="password" v-on:optionsUpdated="optionsUpdated" v-if="showOptions"></options>
</form>
</div>
<div class="col col-auto">
<button type="button" class="btn btn-secondary" v-on:click="showOptions=!showOptions">
<i class="fa fa-sliders" aria-hidden="true"></i>
</button>
</div>
</div>
<options :password="password" v-on:optionsUpdated="optionsUpdated" v-if="showOptions"></options>
</form>
</template>

<script type="text/ecmascript-6">
import LessPass from 'lesspass';
import {mapGetters} from 'vuex';
import Clipboard from 'clipboard';
import {getSite, getPasswordFromUrlQuery} from '../domain/url-parser';
import RemoveAutoComplete from '../components/RemoveAutoComplete.vue';
import MasterPassword from '../components/MasterPassword.vue';
import Options from '../components/Options.vue';
import {showTooltip} from '../services/tooltip';
import message from '../services/message';
import Awesomplete from 'awesomplete';
import LessPass from 'lesspass';
import {mapGetters} from 'vuex';
import Clipboard from 'clipboard';
import {getSite, getPasswordFromUrlQuery} from '../domain/url-parser';
import RemoveAutoComplete from '../components/RemoveAutoComplete.vue';
import MasterPassword from '../components/MasterPassword.vue';
import Options from '../components/Options.vue';
import {showTooltip} from '../services/tooltip';
import message from '../services/message';
import Awesomplete from 'awesomplete';

function fetchPasswords(store) {
return store.dispatch('getPasswords')
}
function fetchPasswords(store) {
return store.dispatch('getPasswords')
}

export default {
name: 'password-generator-view',
components: {
RemoveAutoComplete,
MasterPassword,
Options
},
computed: mapGetters(['passwords', 'password', 'passwordURL']),
preFetch: fetchPasswords,
beforeMount () {
const query = this.$route.query;
if (Object.keys(query).length >= 9) {
this.$store.dispatch('savePassword', {password: getPasswordFromUrlQuery(query)});
}
export default {
name: 'password-generator-view',
components: {
RemoveAutoComplete,
MasterPassword,
Options
},
computed: mapGetters(['passwords', 'password', 'passwordURL']),
preFetch: fetchPasswords,
beforeMount () {
const query = this.$route.query;
if (Object.keys(query).length >= 9) {
this.$store.dispatch('savePassword', {password: getPasswordFromUrlQuery(query)});
}

const id = this.$route.params.id;
if (id) {
this.$store.dispatch('getPassword', {id});
} else {
fetchPasswords(this.$store);
}
const id = this.$route.params.id;
if (id) {
this.$store.dispatch('getPassword', {id});
} else {
fetchPasswords(this.$store);
}

getSite().then(site => {
if (site) {
this.$store.dispatch('loadPasswordForSite', site);
}
});
getSite().then(site => {
if (site) {
this.$store.dispatch('loadPasswordForSite', site);
}
});

const clipboard = new Clipboard('.btn-copy');
clipboard.on('success', event => {
if (event.text) {
showTooltip(event.trigger, 'copied !');
setTimeout(() => {
this.cleanFormInSeconds(10);
}, 2000);
}
});
},
mounted(){
setTimeout(() => {
this.focusBestInputField();
}, 500);
},
data(){
return {
masterPassword: '',
fingerprint: '',
generatedPassword: '',
cleanTimeout: null,
showOptions: false,
generatingPassword: false
}
},
watch: {
'passwords': function (passwords) {
var site = this.$refs.site;
const self = this;
if (site !== null && passwords.length > 0) {
new Awesomplete(site, {
list: passwords.map(password => {
return {label: password.site + ' ' + password.login, value: password}
}),
replace: function (password) {
self.$store.dispatch('savePassword', {password: password.value});
this.input.value = password.value.site;
self.focusBestInputField();
}
});
}
},
'password.site': function () {
this.cleanErrors();
},
'password.login': function () {
this.cleanErrors();
},
'generatedPassword': function () {
this.cleanFormInSeconds(30);
},
'masterPassword': function () {
this.cleanErrors();
this.cleanFormInSeconds(30);
const clipboard = new Clipboard('.btn-copy');
clipboard.on('success', event => {
if (event.text) {
showTooltip(event.trigger, 'copied !');
setTimeout(() => {
this.cleanFormInSeconds(10);
}, 2000);
}
});
},
mounted(){
setTimeout(() => {
this.focusBestInputField();
}, 500);
},
data(){
return {
masterPassword: '',
fingerprint: '',
generatedPassword: '',
cleanTimeout: null,
showOptions: false,
generatingPassword: false
}
},
watch: {
'passwords': function(passwords) {
var site = this.$refs.site;
const self = this;
if (site !== null && passwords.length > 0) {
new Awesomplete(site, {
list: passwords.map(password => {
return {label: password.site + ' ' + password.login, value: password}
}),
replace: function(password) {
self.$store.dispatch('savePassword', {password: password.value});
this.input.value = password.value.site;
self.focusBestInputField();
}
},
methods: {
togglePasswordType(element){
if (element.type === 'password') {
element.type = 'text';
} else {
element.type = 'password';
}
},
cleanErrors(){
clearTimeout(this.cleanTimeout);
this.generatedPassword = '';
},
cleanFormInSeconds(seconds){
clearTimeout(this.cleanTimeout);
this.cleanTimeout = setTimeout(() => {
this.masterPassword = '';
this.generatedPassword = '';
this.fingerprint = '';
}, 1000 * seconds);
},
generatePassword(){
const site = this.password.site;
const login = this.password.login;
const masterPassword = this.masterPassword;
});
}
},
'password.site': function() {
this.cleanErrors();
},
'password.login': function() {
this.cleanErrors();
},
'generatedPassword': function() {
this.cleanFormInSeconds(30);
},
'masterPassword': function() {
this.cleanErrors();
this.cleanFormInSeconds(30);
}
},
methods: {
togglePasswordType(element){
if (element.type === 'password') {
element.type = 'text';
} else {
element.type = 'password';
}
},
cleanErrors(){
clearTimeout(this.cleanTimeout);
this.generatedPassword = '';
},
cleanFormInSeconds(seconds){
clearTimeout(this.cleanTimeout);
this.cleanTimeout = setTimeout(() => {
this.masterPassword = '';
this.generatedPassword = '';
this.fingerprint = '';
}, 1000 * seconds);
},
generatePassword(){
const site = this.password.site;
const login = this.password.login;
const masterPassword = this.masterPassword;

if (!site && !login || !masterPassword) {
this.showOptions = false;
message.error('Site, login and master password fields are mandatory.');
return;
}
if (!site && !login || !masterPassword) {
this.showOptions = false;
message.error('Site, login and master password fields are mandatory.');
return;
}

this.generatingPassword = true;
this.cleanErrors();
this.fingerprint = this.masterPassword;
this.generatingPassword = true;
this.cleanErrors();
this.fingerprint = this.masterPassword;

const passwordProfile = {
lowercase: this.password.lowercase,
uppercase: this.password.uppercase,
numbers: this.password.numbers,
symbols: this.password.symbols,
length: this.password.length,
counter: this.password.counter,
version: this.password.version,
};
return LessPass.generatePassword(site, login, masterPassword, passwordProfile).then(generatedPassword => {
this.generatingPassword = false;
this.generatedPassword = generatedPassword;
this.$store.dispatch('savePassword', {password: this.password});
this.$store.dispatch('passwordGenerated');
window.document.getElementById('copyPasswordButton').setAttribute('data-clipboard-text', generatedPassword);
});
},
optionsUpdated(options){
this.cleanErrors();
const password = Object.assign({}, this.password, options);
this.$store.dispatch('savePassword', {password});
},
focusBestInputField(){
const site = this.$refs.site;
const login = this.$refs.login;
const masterPassword = this.$refs.masterPassword.$refs.password;
site.value ? (login.value ? masterPassword.focus() : login.focus()) : site.focus();
}
}
const passwordProfile = {
lowercase: this.password.lowercase,
uppercase: this.password.uppercase,
numbers: this.password.numbers,
symbols: this.password.symbols,
length: this.password.length,
counter: this.password.counter,
version: this.password.version,
};
return LessPass.generatePassword(site, login, masterPassword, passwordProfile).then(generatedPassword => {
this.generatingPassword = false;
this.generatedPassword = generatedPassword;
this.$store.dispatch('savePassword', {password: this.password});
this.$store.dispatch('passwordGenerated');
window.document.getElementById('copyPasswordButton').setAttribute('data-clipboard-text', generatedPassword);
});
},
optionsUpdated(options){
this.cleanErrors();
const password = Object.assign({}, this.password, options);
this.$store.dispatch('savePassword', {password});
},
focusBestInputField(){
const site = this.$refs.site;
const login = this.$refs.login;
const masterPassword = this.$refs.masterPassword.$refs.password;
site.value ? (login.value ? masterPassword.focus() : login.focus()) : site.focus();
}
}
}
</script>

+ 48
- 48
src/views/PasswordReset.vue View File

@@ -1,57 +1,57 @@
<template>
<form v-on:submit.prevent="resetPassword">
<div class="form-group row">
<div class="col-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">
</div>
</div>
<form v-on:submit.prevent="resetPassword">
<div class="form-group row">
<div class="col-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">
</div>
<div class="form-group row">
<div class="col-12">
<button id="loginButton" class="btn" type="submit"
v-bind:class="{ 'btn-warning': version===1, 'btn-primary': version===2 }">
Send me a reset link
</button>
</div>
</div>
</form>
</div>
</div>
<div class="form-group row">
<div class="col-12">
<button id="loginButton" class="btn" type="submit"
v-bind:class="{ 'btn-warning': version===1, 'btn-primary': version===2 }">
Send me a reset link
</button>
</div>
</div>
</form>
</template>
<script type="text/ecmascript-6">
import User from '../api/user';
import {mapActions, mapGetters} from 'vuex';
import message from '../services/message';
import User from '../api/user';
import {mapActions, mapGetters} from 'vuex';
import message from '../services/message';

export default {
data() {
return {
email: '',
};
},
computed: {
...mapGetters(['version', 'baseURL'])
},
methods: {
resetPassword(){
if (!this.email) {
message.error(`We need en email to find your account.`);
return;
}
User.resetPassword({email: this.email}, {baseURL: this.baseURL})
.then(() => {
message.success(`If the email address ${this.email} is associated with a LessPass account, you will shortly receive an email from LessPass with instructions on how to reset your password.`)
})
.catch(() => {
message.displayGenericError();
});
}
export default {
data() {
return {
email: '',
};
},
computed: {
...mapGetters(['version', 'baseURL'])
},
methods: {
resetPassword(){
if (!this.email) {
message.error(`We need en email to find your account.`);
return;
}
User.resetPassword({email: this.email}, {baseURL: this.baseURL})
.then(() => {
message.success(`If the email address ${this.email} is associated with a LessPass account, you will shortly receive an email from LessPass with instructions on how to reset your password.`)
})
.catch(() => {
message.displayGenericError();
});
}
}
}
</script>


+ 83
- 83
src/views/PasswordResetConfirm.vue View File

@@ -1,89 +1,89 @@
<template>
<form v-on:submit.prevent="resetPasswordConfirm">
<div class="form-group row" v-if="showError">
<div class="col-12 text-muted text-danger">
{{errorMessage}}
</div>
<form v-on:submit.prevent="resetPasswordConfirm">
<div class="form-group row" v-if="showError">
<div class="col-12 text-muted text-danger">
{{errorMessage}}
</div>
</div>
<div class="form-group row" v-if="successMessage">
<div class="col-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-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 class="form-group row" v-if="successMessage">
<div class="col-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-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-12">
<button id="loginButton" class="btn" type="submit"
v-bind:class="{ 'btn-warning': version===1, 'btn-primary': version===2 }">
Reset my password
</button>
</div>
</div>
</form>
</div>
</div>
<div class="form-group row">
<div class="col-12">
<button id="loginButton" class="btn" type="submit"
v-bind:class="{ 'btn-warning': version===1, 'btn-primary': version===2 }">
Reset my password
</button>
</div>
</div>
</form>
</template>
<script type="text/ecmascript-6">
import User from '../api/user';
import {mapActions, mapGetters} from 'vuex';
import User from '../api/user';
import {mapActions, mapGetters} from 'vuex';

export default {
data() {
return {
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;
}
const resetPassword = {
uid: this.$route.params.uid, token: this.$route.params.token, new_password: this.new_password
};
User.confirmResetPassword(resetPassword)
.then(() => {
this.successMessage = true
})
.catch(err => {
if (err.response.status === 400) {
this.errorMessage = 'This password reset link become invalid.'
}
this.showError = true;
});
export default {
data() {
return {
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;
}
const resetPassword = {
uid: this.$route.params.uid, token: this.$route.params.token, new_password: this.new_password
};
User.confirmResetPassword(resetPassword)
.then(() => {
this.successMessage = true
})
.catch(err => {
if (err.response.status === 400) {
this.errorMessage = 'This password reset link become invalid.'
}
},
computed: {
...mapGetters(['version'])
},
}
</script>
this.showError = true;
});
}
},
computed: {
...mapGetters(['version'])
},
}
</script>

+ 105
- 107
src/views/Passwords.vue View File

@@ -1,123 +1,121 @@
<style>
.fa-none {
display: none
}
.fa-none {
display: none
}

#passwordsList {
min-height: 320px;
}
#passwordsList {
min-height: 320px;
}
</style>
<template>
<div id="passwords">
<div v-if="passwords.length === 0">
<div class="row">
<div class="col">
You don't have any password profile saved in your database.
<router-link :to="{ name: 'home'}">Would you like to create one?</router-link>
</div>
</div>
<div id="passwords">
<div v-if="passwords.length === 0">
<div class="row">
<div class="col">
You don't have any password profile saved in your database.
<router-link :to="{ name: 'home'}">Would you like to create one?</router-link>
</div>
<div v-else>
<div class="row pb-2">
<div class="col">
<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>
<div v-if="filteredPasswords.length === 0">
<div class="row">
<div class="col">
Oops! There are no matches for "{{searchQuery}}". Please try broadening your search.
</div>
</div>
</div>
</div>
<div v-else>
<div class="row pb-2">
<div class="col">
<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>
<div v-if="filteredPasswords.length === 0">
<div class="row">
<div class="col">
Oops! There are no matches for "{{searchQuery}}". Please try broadening your search.
</div>
</div>
</div>
<div v-else>
<div id="passwordsList">
<div class="row py-2" v-for="password in filteredPasswords">
<div class="col-6">
<router-link :to="{ name: 'password', params: { id: password.id }}">
{{password.site}}
</router-link>
<br>
{{password.login}}
</div>
<div v-else>
<div id="passwordsList">
<div class="row py-2" v-for="password in filteredPasswords">
<div class="col-6">
<router-link :to="{ name: 'password', params: { id: password.id }}">
{{password.site}}
</router-link>
<br>
{{password.login}}
</div>
<div class="col-6">
<delete-button class="float-right mt-2"
confirmText="Are you sure you want to delete this password profile?"
confirmButton="Sure"
cancelButton="Oups no!"
v-on:remove="deletePassword(password)">
</delete-button>
</div>
</div>
</div>
<div class="row mt-2" v-if="pagination.number_of_pages > 1">
<div class="col-4">
<i class="fa fa-arrow-left pointer"
v-on:click="pagination.current_page -= 1"
v-bind:class="{'fa-none':pagination.current_page === 1}"></i>
</div>
<div class="col-4 text-center">
{{pagination.current_page}} / {{Math.ceil(passwords.length/pagination.per_page)}}
</div>
<div class="col-4 text-right">
<i class="fa fa-arrow-right pointer"
v-on:click="pagination.current_page += 1"
v-bind:class="{'fa-none':pagination.current_page === pagination.number_of_pages}"></i>
</div>
</div>
<div class="col-6">
<delete-button class="float-right mt-2"
confirmText="Are you sure you want to delete this password profile?"
confirmButton="Sure"
cancelButton="Oups no!"
v-on:remove="deletePassword(password)">
</delete-button>
</div>
</div>
</div>
<div class="row mt-2" v-if="pagination.number_of_pages > 1">
<div class="col-4">
<i class="fa fa-arrow-left pointer"
v-on:click="pagination.current_page -= 1"
v-bind:class="{'fa-none':pagination.current_page === 1}"></i>
</div>
<div class="col-4 text-center">
{{pagination.current_page}} / {{Math.ceil(passwords.length/pagination.per_page)}}
</div>
<div class="col-4 text-right">
<i class="fa fa-arrow-right pointer"
v-on:click="pagination.current_page += 1"
v-bind:class="{'fa-none':pagination.current_page === pagination.number_of_pages}"></i>
</div>
</div>
</div>
</div>
</div>
</template>
<script type="text/ecmascript-6">
import DeleteButton from '../components/DeleteButton.vue';
import {mapGetters} from 'vuex';
import DeleteButton from '../components/DeleteButton.vue';
import {mapGetters} from 'vuex';

function fetchPasswords(store) {
return store.dispatch('getPasswords')
}
function fetchPasswords(store) {
return store.dispatch('getPasswords')
}

export default {
name: 'passwords-view',
data(){
return {
searchQuery: '',
pagination: {
number_of_pages: 1,
per_page: 5,
current_page: 1
},
}
export default {
name: 'passwords-view',
data(){
return {
searchQuery: '',
pagination: {
number_of_pages: 1,
per_page: 5,
current_page: 1
},
components: {DeleteButton},
computed: {
...mapGetters(['passwords']),
filteredPasswords(){
const passwords = this.passwords.filter(password => {
var loginMatch = password.login.match(new RegExp(this.searchQuery, 'i'));
var siteMatch = password.site.match(new RegExp(this.searchQuery, 'i'));
return loginMatch || siteMatch;
});
this.pagination.number_of_pages = Math.ceil(passwords.length / this.pagination.per_page);
return passwords.slice(
this.pagination.current_page * this.pagination.per_page - 5,
this.pagination.current_page * this.pagination.per_page
);
}
},
preFetch: fetchPasswords,
beforeMount () {
fetchPasswords(this.$store);
},
methods: {
deletePassword(password){
return this.$store.dispatch('deletePassword', {id: password.id});
}
}
}
},
components: {DeleteButton},
computed: {
...mapGetters(['passwords']),
filteredPasswords(){
const passwords = this.passwords.filter(password => {
var loginMatch = password.login.match(new RegExp(this.searchQuery, 'i'));
var siteMatch = password.site.match(new RegExp(this.searchQuery, 'i'));
return loginMatch || siteMatch;
});
this.pagination.number_of_pages = Math.ceil(passwords.length / this.pagination.per_page);
return passwords.slice(
this.pagination.current_page * this.pagination.per_page - 5,
this.pagination.current_page * this.pagination.per_page
);
}
},
preFetch: fetchPasswords,
beforeMount () {
fetchPasswords(this.$store);
},
methods: {
deletePassword(password){
return this.$store.dispatch('deletePassword', {id: password.id});
}
}
}
</script>

15

+ 35
- 35
test/api.password.js View File

@@ -7,53 +7,53 @@ const config = {baseURL: 'https://lesspass.com', token: token};
const headers = {reqheaders: {Authorization: `JWT ${token}`}};

test('Passwords.create', t => {
const password = {login: 'text@example.org'};
nock('https://lesspass.com').post('/api/passwords/', password).reply(201, {...password, id: '1'});
return Passwords.create(password, config).then(response => {
const passwordCreated = response.data;
t.is(passwordCreated.id, '1');
t.is(passwordCreated.login, password.login);
});
const password = {login: 'text@example.org'};
nock('https://lesspass.com').post('/api/passwords/', password).reply(201, {...password, id: '1'});
return Passwords.create(password, config).then(response => {
const passwordCreated = response.data;
t.is(passwordCreated.id, '1');
t.is(passwordCreated.login, password.login);
});
});

test('Passwords.create set Authorization header', t => {
const password = {login: 'text@example.org'};
nock('https://lesspass.com', headers).post('/api/passwords/', password).query(true).reply(201, {
id: '1',
...password
});
return Passwords.create(password, config).then(response => {
const passwordCreated = response.data;
t.is(passwordCreated.id, '1');
t.is(passwordCreated.login, password.login);
});
const password = {login: 'text@example.org'};
nock('https://lesspass.com', headers).post('/api/passwords/', password).query(true).reply(201, {
id: '1',
...password
});
return Passwords.create(password, config).then(response => {
const passwordCreated = response.data;
t.is(passwordCreated.id, '1');
t.is(passwordCreated.login, password.login);
});
});

test('Passwords.all', t => {
nock('https://lesspass.com', headers).get('/api/passwords/').query(true).reply(200, {});
return Passwords.all(config).then(response => {
t.is(response.status, 200);
});
nock('https://lesspass.com', headers).get('/api/passwords/').query(true).reply(200, {});
return Passwords.all(config).then(response => {
t.is(response.status, 200);
});
});

test('Passwords.get', t => {
nock('https://lesspass.com', headers).get('/api/passwords/c8e4f983-8ffe-b705-4064-d3b7aa4a4782/').query(true).reply(200, {});
return Passwords.read({id: 'c8e4f983-8ffe-b705-4064-d3b7aa4a4782'}, config).then(response => {
t.is(response.status, 200);
});
nock('https://lesspass.com', headers).get('/api/passwords/c8e4f983-8ffe-b705-4064-d3b7aa4a4782/').query(true).reply(200, {});
return Passwords.read({id: 'c8e4f983-8ffe-b705-4064-d3b7aa4a4782'}, config).then(response => {
t.is(response.status, 200);
});
});

test('Passwords.update', t => {
const password = {id: 'c8e4f983-4064-8ffe-b705-d3b7aa4a4782', login: 'test@example.org'};
nock('https://lesspass.com', headers).put('/api/passwords/c8e4f983-4064-8ffe-b705-d3b7aa4a4782/', password).query(true).reply(200, {});
return Passwords.update(password, config).then(response => {
t.is(response.status, 200);
});
const password = {id: 'c8e4f983-4064-8ffe-b705-d3b7aa4a4782', login: 'test@example.org'};
nock('https://lesspass.com', headers).put('/api/passwords/c8e4f983-4064-8ffe-b705-d3b7aa4a4782/', password).query(true).reply(200, {});
return Passwords.update(password, config).then(response => {
t.is(response.status, 200);
});
});

test('Passwords.delete', t => {
nock('https://lesspass.com', headers).delete('/api/passwords/c8e4f983-8ffe-4064-b705-d3b7aa4a4782/').query(true).reply(204);
return Passwords.delete({id: 'c8e4f983-8ffe-4064-b705-d3b7aa4a4782'}, config).then(response => {
t.is(response.status, 204);
});
});
nock('https://lesspass.com', headers).delete('/api/passwords/c8e4f983-8ffe-4064-b705-d3b7aa4a4782/').query(true).reply(204);
return Passwords.delete({id: 'c8e4f983-8ffe-4064-b705-d3b7aa4a4782'}, config).then(response => {
t.is(response.status, 204);
});
});

+ 28
- 28
test/api.user.js View File

@@ -3,43 +3,43 @@ import nock from 'nock';
import User from '../src/api/user';

test('login', 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 User.login(user, {baseURL: 'https://lesspass.com'}).then(response => {
t.is(response.token, token);
});
const token = '5e0651';
const user = {email: 'test@example.org', password: 'password'};
nock('https://lesspass.com').post('/api/tokens/auth/', user).reply(201, {token});
return User.login(user, {baseURL: 'https://lesspass.com'}).then(response => {
t.is(response.token, token);
});
});

test('register', t => {
const user = {email: 'test@example.org', password: 'password'};
nock('https://lesspass.com').post('/api/auth/register/', user).reply(201, {email: user.email, pk: 1});
return User.register(user, {baseURL: 'https://lesspass.com'}).then(response => {
t.is(response.email, user.email);
});
const user = {email: 'test@example.org', password: 'password'};
nock('https://lesspass.com').post('/api/auth/register/', user).reply(201, {email: user.email, pk: 1});
return User.register(user, {baseURL: 'https://lesspass.com'}).then(response => {
t.is(response.email, user.email);
});
});

test('resetPassword', t => {
var email = 'test@lesspass.com';
nock('https://lesspass.com').post('/api/auth/password/reset/', {email}).reply(204);
t.notThrows(User.resetPassword({email}, {baseURL: 'https://lesspass.com'}));
var email = 'test@lesspass.com';
nock('https://lesspass.com').post('/api/auth/password/reset/', {email}).reply(204);
t.notThrows(User.resetPassword({email}, {baseURL: 'https://lesspass.com'}));
});

test('confirmResetPassword', t => {
var newPassword = {
uid: 'MQ',
token: '5g1-2bd69bd6f6dcd73f8124',
new_password: 'password1'
};
nock('https://lesspass.com').post('/api/auth/password/reset/confirm/', newPassword).reply(204);
t.notThrows(User.confirmResetPassword(newPassword, {baseURL: 'https://lesspass.com'}));
var newPassword = {
uid: 'MQ',
token: '5g1-2bd69bd6f6dcd73f8124',
new_password: 'password1'
};
nock('https://lesspass.com').post('/api/auth/password/reset/confirm/', newPassword).reply(204);
t.notThrows(User.confirmResetPassword(newPassword, {baseURL: 'https://lesspass.com'}));
});

test('refresh token', t => {
const token = '3e3231';
const newToken = 'wibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9eyJzdWIiOiIxMjM0NTY3ODkwIi';
nock('https://lesspass.com').post('/api/tokens/refresh/', {token}).reply(200, {token: newToken});
return User.requestNewToken({token}, {baseURL: 'https://lesspass.com'}).then(refreshedToken => {
t.is(refreshedToken, newToken);
});
});
const token = '3e3231';
const newToken = 'wibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9eyJzdWIiOiIxMjM0NTY3ODkwIi';
nock('https://lesspass.com').post('/api/tokens/refresh/', {token}).reply(200, {token: newToken});
return User.requestNewToken({token}, {baseURL: 'https://lesspass.com'}).then(refreshedToken => {
t.is(refreshedToken, newToken);
});
});

+ 43
- 43
test/store.getters.js View File

@@ -2,59 +2,59 @@ import test from 'ava';
import * as getters from '../src/store/getters';

test('version', t => {
const state = {
route: {path: '/'},
password: {version: 2},
defaultPassword: {version: 1}
};
const version = getters.version(state);
t.is(version, 2);
const state = {
route: {path: '/'},
password: {version: 2},
defaultPassword: {version: 1}
};
const version = getters.version(state);
t.is(version, 2);
});

test('version path equal default options', t => {
const state = {
route: {path: '/options/default'},
password: {version: 2},
defaultPassword: {version: 1}
};
const version = getters.version(state);
t.is(version, 1);
const state = {
route: {path: '/options/default'},
password: {version: 2},
defaultPassword: {version: 1}
};
const version = getters.version(state);
t.is(version, 1);
});

test('version no password', t => {
const state = {
route: {path: '/'},
password: null,
defaultPassword: {version: 1}
};
const version = getters.version(state);
t.is(version, 1);
const state = {
route: {path: '/'},
password: null,
defaultPassword: {version: 1}
};
const version = getters.version(state);
t.is(version, 1);
});

test('passwordURL', t => {
const state = {
password: {
login: "test@example.org",
site: "example.org",
uppercase: true,
lowercase: true,
numbers: true,
symbols: false,
length: 16,
counter: 1,
version: 2
},
baseURL: 'https://lesspass.com'
};
const state = {
password: {
login: "test@example.org",
site: "example.org",
uppercase: true,
lowercase: true,
numbers: true,
symbols: false,
length: 16,
counter: 1,
version: 2
},
baseURL: 'https://lesspass.com'
};

t.is(getters.passwordURL(state), 'https://lesspass.com/#/?login=test@example.org&site=example.org&uppercase=true&lowercase=true&numbers=true&symbols=false&length=16&counter=1&version=2')
t.is(getters.passwordURL(state), 'https://lesspass.com/#/?login=test@example.org&site=example.org&uppercase=true&lowercase=true&numbers=true&symbols=false&length=16&counter=1&version=2')
});

test('message', t => {
const state = {
message: {text: 'error message', status:'error'}
};
const message = getters.message(state);
t.is(message.text, state.message.text);
t.is(message.status, state.message.status);
});
const state = {
message: {text: 'error message', status: 'error'}
};
const message = getters.message(state);
t.is(message.text, state.message.text);
t.is(message.status, state.message.status);
});

+ 222
- 222
test/store.mutations.js View File

@@ -4,294 +4,294 @@ import mutations from '../src/store/mutations';
import * as types from '../src/store/mutation-types';

test('LOGOUT', t => {
const LOGOUT = mutations[types.LOGOUT];
const state = {
authenticated: true
};
LOGOUT(state);
t.false(state.authenticated);
const LOGOUT = mutations[types.LOGOUT];
const state = {
authenticated: true
};
LOGOUT(state);
t.false(state.authenticated);
});

test('LOGOUT clean user personal info', t => {
const LOGOUT = mutations[types.LOGOUT];
const state = {
token: '123456',
password: {counter: 2},
passwords: [{id: '1', site: 'test@example.org'}],
defaultPassword: {counter: 1},
};
LOGOUT(state);
t.true(state.token === null);
t.is(state.passwords.length, 0);
t.is(state.password.counter, 1);
const LOGOUT = mutations[types.LOGOUT];
const state = {
token: '123456',
password: {counter: 2},
passwords: [{id: '1', site: 'test@example.org'}],
defaultPassword: {counter: 1},
};
LOGOUT(state);
t.true(state.token === null);
t.is(state.passwords.length, 0);
t.is(state.password.counter, 1);
});

test('LOGIN', t => {
const LOGIN = mutations[types.LOGIN];
const state = {authenticated: false};
LOGIN(state);
t.true(state.authenticated);
const LOGIN = mutations[types.LOGIN];
const state = {authenticated: false};
LOGIN(state);
t.true(state.authenticated);
});

test('SET_TOKEN', t => {
const token = '123456';
const SET_TOKEN = mutations[types.SET_TOKEN];
const state = {token: null};
SET_TOKEN(state, {token});
t.is(state.token, token);
const token = '123456';
const SET_TOKEN = mutations[types.SET_TOKEN];
const state = {token: null};
SET_TOKEN(state, {token});
t.is(state.token, token);
});

test('SET_PASSWORD', t => {
const SET_PASSWORD = mutations[types.SET_PASSWORD];
const state = {password: null};
SET_PASSWORD(state, {password: {uppercase: true, version: 2}});
t.is(state.password.version, 2);
t.true(state.password.uppercase);
const SET_PASSWORD = mutations[types.SET_PASSWORD];
const state = {password: null};
SET_PASSWORD(state, {password: {uppercase: true, version: 2}});
t.is(state.password.version, 2);
t.true(state.password.uppercase);
});

test('SET_PASSWORD dont change lastUse date', t => {
const SET_PASSWORD = mutations[types.SET_PASSWORD];
const now = 1485989236000;
const time = new Date(now);
timekeeper.freeze(time);
const state = {lastUse: null, password: null};
SET_PASSWORD(state, {password: {}});
t.true(state.lastUse === null);
timekeeper.reset();
const SET_PASSWORD = mutations[types.SET_PASSWORD];
const now = 1485989236000;
const time = new Date(now);
timekeeper.freeze(time);
const state = {lastUse: null, password: null};
SET_PASSWORD(state, {password: {}});
t.true(state.lastUse === null);
timekeeper.reset();
});

test('PASSWORD_GENERATED change lastUse date', t => {
const PASSWORD_GENERATED = mutations[types.PASSWORD_GENERATED];
const now = 1485989236000;
const time = new Date(now);
timekeeper.freeze(time);
const state = {lastUse: null};
PASSWORD_GENERATED(state);
t.is(now, state.lastUse);
timekeeper.reset();
const PASSWORD_GENERATED = mutations[types.PASSWORD_GENERATED];
const now = 1485989236000;
const time = new Date(now);
timekeeper.freeze(time);
const state = {lastUse: null};
PASSWORD_GENERATED(state);
t.is(now, state.lastUse);
timekeeper.reset();
});

test('SET_PASSWORD immutable', t => {
const SET_PASSWORD = mutations[types.SET_PASSWORD];
const state = {};
const password = {version: 2};
SET_PASSWORD(state, {password});
password.version = 1;
t.is(state.password.version, 2);
const SET_PASSWORD = mutations[types.SET_PASSWORD];
const state = {};
const password = {version: 2};
SET_PASSWORD(state, {password});
password.version = 1;
t.is(state.password.version, 2);
});

test('SET_DEFAULT_PASSWORD', t => {
const SET_DEFAULT_PASSWORD = mutations[types.SET_DEFAULT_PASSWORD];
const state = {
defaultPassword: {
site: '',
login: '',
uppercase: true,
lowercase: true,
numbers: true,
symbols: true,
length: 16,
counter: 1,
version: 2
}
};
SET_DEFAULT_PASSWORD(state, {password: {symbols: false, length: 30}});
t.is(state.defaultPassword.length, 30);
t.false(state.defaultPassword.symbols);
const SET_DEFAULT_PASSWORD = mutations[types.SET_DEFAULT_PASSWORD];
const state = {
defaultPassword: {
site: '',
login: '',
uppercase: true,
lowercase: true,
numbers: true,
symbols: true,
length: 16,
counter: 1,
version: 2
}
};
SET_DEFAULT_PASSWORD(state, {password: {symbols: false, length: 30}});
t.is(state.defaultPassword.length, 30);
t.false(state.defaultPassword.symbols);
});

test('SET_PASSWORDS', t => {
const SET_PASSWORDS = mutations[types.SET_PASSWORDS];
const state = {
passwords: []
};
SET_PASSWORDS(state, {passwords: [{site: 'site1'}, {site: 'site2'}]});
t.is(state.passwords[0].site, 'site1');
t.is(state.passwords[1].site, 'site2');
const SET_PASSWORDS = mutations[types.SET_PASSWORDS];
const state = {
passwords: []
};
SET_PASSWORDS(state, {passwords: [{site: 'site1'}, {site: 'site2'}]});
t.is(state.passwords[0].site, 'site1');
t.is(state.passwords[1].site, 'site2');
});

test('DELETE_PASSWORD', t => {
const DELETE_PASSWORD = mutations[types.DELETE_PASSWORD];
const state = {
passwords: [{id: '1', site: 'site1'}, {id: '2', site: 'site2'}]
};
t.is(state.passwords.length, 2);
DELETE_PASSWORD(state, {id: '1'});
t.is(state.passwords.length, 1);
const DELETE_PASSWORD = mutations[types.DELETE_PASSWORD];
const state = {
passwords: [{id: '1', site: 'site1'}, {id: '2', site: 'site2'}]
};
t.is(state.passwords.length, 2);
DELETE_PASSWORD(state, {id: '1'});
t.is(state.passwords.length, 1);
});

test('DELETE_PASSWORD clean password with default password if same id', t => {
const DELETE_PASSWORD = mutations[types.DELETE_PASSWORD];
const state = {
passwords: [{id: '1', length: 30}, {id: '2', length: 16}],
password: {id: '1', length: 30},
defaultPassword: {length: 16}
};
DELETE_PASSWORD(state, {id: '1'});
t.is(state.password.length, 16);
const DELETE_PASSWORD = mutations[types.DELETE_PASSWORD];
const state = {
passwords: [{id: '1', length: 30}, {id: '2', length: 16}],
password: {id: '1', length: 30},
defaultPassword: {length: 16}
};
DELETE_PASSWORD(state, {id: '1'});
t.is(state.password.length, 16);
});

test('SET_BASE_URL', t => {
const SET_BASE_URL = mutations[types.SET_BASE_URL];
const state = {
baseURL: 'https://lesspass.com'
};
const baseURL = 'https://example.org';
SET_BASE_URL(state, {baseURL: baseURL});
t.is(state.baseURL, baseURL);
const SET_BASE_URL = mutations[types.SET_BASE_URL];
const state = {
baseURL: 'https://lesspass.com'
};
const baseURL = 'https://example.org';
SET_BASE_URL(state, {baseURL: baseURL});
t.is(state.baseURL, baseURL);
});

test('SET_VERSION', t => {
const SET_VERSION = mutations[types.SET_VERSION];
const state = {
password: {version: 2},
};
SET_VERSION(state, {version: 1});
t.is(state.password.version, 1);
const SET_VERSION = mutations[types.SET_VERSION];
const state = {
password: {version: 2},
};
SET_VERSION(state, {version: 1});
t.is(state.password.version, 1);
});

test('SET_VERSION 1 should modify length to 12', t => {
const SET_VERSION = mutations[types.SET_VERSION];
const state = {
password: {length: 16, version: 2},
};
SET_VERSION(state, {version: 1});
t.is(state.password.length, 12);
const SET_VERSION = mutations[types.SET_VERSION];
const state = {
password: {length: 16, version: 2},
};
SET_VERSION(state, {version: 1});
t.is(state.password.length, 12);
});

test('SET_VERSION 2 should modify length to 16', t => {
const SET_VERSION = mutations[types.SET_VERSION];
const state = {
password: {length: 12, version: 1},
};
SET_VERSION(state, {version: 2});
t.is(state.password.length, 16);
const SET_VERSION = mutations[types.SET_VERSION];
const state = {
password: {length: 12, version: 1},
};
SET_VERSION(state, {version: 2});
t.is(state.password.length, 16);
});

test('LOAD_PASSWORD_FIRST_TIME 30 seconds after last use', t => {
const now = 1485989236000;
const time = new Date(now);
timekeeper.freeze(time);
const thirtySecondBefore = now - 30 * 1000;
const state = {
lastUse: thirtySecondBefore,
password: {
login: 'test@example.org',
length: 30
},
defaultPassword: {
login: '',
length: 16
}
};
const LOAD_PASSWORD_FIRST_TIME = mutations[types.LOAD_PASSWORD_FIRST_TIME];
LOAD_PASSWORD_FIRST_TIME(state);
t.is(state.password.login, 'test@example.org');
t.is(state.password.length, 30);
timekeeper.reset();
const now = 1485989236000;
const time = new Date(now);
timekeeper.freeze(time);
const thirtySecondBefore = now - 30 * 1000;
const state = {
lastUse: thirtySecondBefore,
password: {
login: 'test@example.org',
length: 30
},
defaultPassword: {
login: '',
length: 16
}
};
const LOAD_PASSWORD_FIRST_TIME = mutations[types.LOAD_PASSWORD_FIRST_TIME];
LOAD_PASSWORD_FIRST_TIME(state);
t.is(state.password.login, 'test@example.org');
t.is(state.password.length, 30);
timekeeper.reset();
});

test('LOAD_PASSWORD_FIRST_TIME more than 1 minute after last use', t => {
const now = 1485989236000;
const time = new Date(now);
timekeeper.freeze(time);
const oneMinuteAndOneSecond = now - 61 * 1000;
const state = {
lastUse: oneMinuteAndOneSecond,
password: {
login: 'test@example.org',
length: 30
},
defaultPassword: {
login: '',
length: 16
}
};
const LOAD_PASSWORD_FIRST_TIME = mutations[types.LOAD_PASSWORD_FIRST_TIME];
LOAD_PASSWORD_FIRST_TIME(state);
t.is(state.password.login, '');
t.is(state.password.length, 16);
timekeeper.reset();
const now = 1485989236000;
const time = new Date(now);
timekeeper.freeze(time);
const oneMinuteAndOneSecond = now - 61 * 1000;
const state = {
lastUse: oneMinuteAndOneSecond,
password: {
login: 'test@example.org',
length: 30
},
defaultPassword: {
login: '',
length: 16
}
};
const LOAD_PASSWORD_FIRST_TIME = mutations[types.LOAD_PASSWORD_FIRST_TIME];
LOAD_PASSWORD_FIRST_TIME(state);
t.is(state.password.login, '');
t.is(state.password.length, 16);
timekeeper.reset();
});

test('LOAD_PASSWORD_FIRST_TIME last use null', t => {
const time = new Date(1485989236000);
timekeeper.freeze(time);
const state = {
lastUse: null,
password: {
site: '',
version: 1
},
defaultPassword: {
site: '',
version: 2
}
};
const LOAD_PASSWORD_FIRST_TIME = mutations[types.LOAD_PASSWORD_FIRST_TIME];
LOAD_PASSWORD_FIRST_TIME(state);
t.is(state.password.version, 2);
timekeeper.reset();
const time = new Date(1485989236000);
timekeeper.freeze(time);
const state = {
lastUse: null,
password: {
site: '',
version: 1
},
defaultPassword: {
site: '',
version: 2
}
};
const LOAD_PASSWORD_FIRST_TIME = mutations[types.LOAD_PASSWORD_FIRST_TIME];
LOAD_PASSWORD_FIRST_TIME(state);
t.is(state.password.version, 2);
timekeeper.reset();
});

test('LOAD_PASSWORD_FOR_SITE', t => {
const state = {
password: {
site: ''
},
passwords: [
{id: '1', site: 'www.example.org'},
{id: '2', site: 'www.google.com'}
]
};
const LOAD_PASSWORD_FOR_SITE = mutations[types.LOAD_PASSWORD_FOR_SITE];
LOAD_PASSWORD_FOR_SITE(state, {site: 'www.google.com'});
t.is(state.password.id, '2');
t.is(state.password.site, 'www.google.com');
const state = {
password: {
site: ''
},
passwords: [
{id: '1', site: 'www.example.org'},
{id: '2', site: 'www.google.com'}
]
};
const LOAD_PASSWORD_FOR_SITE = mutations[types.LOAD_PASSWORD_FOR_SITE];
LOAD_PASSWORD_FOR_SITE(state, {site: 'www.google.com'});
t.is(state.password.id, '2');
t.is(state.password.site, 'www.google.com');
});

test('LOAD_PASSWORD_FOR_SITE no passwords', t => {
const state = {
password: {
site: ''
},
passwords: []
};
const LOAD_PASSWORD_FOR_SITE = mutations[types.LOAD_PASSWORD_FOR_SITE];
LOAD_PASSWORD_FOR_SITE(state, {site: 'account.google.com'});
t.false('id' in state.password);
t.is(state.password.site, 'account.google.com');
const state = {
password: {
site: ''
},
passwords: []
};
const LOAD_PASSWORD_FOR_SITE = mutations[types.LOAD_PASSWORD_FOR_SITE];
LOAD_PASSWORD_FOR_SITE(state, {site: 'account.google.com'});
t.false('id' in state.password);
t.is(state.password.site, 'account.google.com');
});

test('LOAD_PASSWORD_FOR_SITE multiple accounts matching criteria', t => {
const state = {
password: {
site: ''
},
passwords: [
{id: '1', site: 'www.example.org'},
{id: '2', site: 'www.google.com'},
{id: '3', site: 'account.google.com'},
]
};
const LOAD_PASSWORD_FOR_SITE = mutations[types.LOAD_PASSWORD_FOR_SITE];
LOAD_PASSWORD_FOR_SITE(state, {site: 'www.google.com', url: 'https://www.google.com'});
t.is(state.password.id, '2');
t.is(state.password.site, 'www.google.com');
const state = {
password: {
site: ''
},
passwords: [
{id: '1', site: 'www.example.org'},
{id: '2', site: 'www.google.com'},
{id: '3', site: 'account.google.com'},
]
};
const LOAD_PASSWORD_FOR_SITE = mutations[types.LOAD_PASSWORD_FOR_SITE];
LOAD_PASSWORD_FOR_SITE(state, {site: 'www.google.com', url: 'https://www.google.com'});
t.is(state.password.id, '2');
t.is(state.password.site, 'www.google.com');
});

test('SET_MESSAGE', t => {
const SET_MESSAGE = mutations[types.SET_MESSAGE];
const state = {};
SET_MESSAGE(state, {message: {text: 'success message', status: 'success'}});
t.is(state.message.text, 'success message');
t.is(state.message.status, 'success');
const SET_MESSAGE = mutations[types.SET_MESSAGE];
const state = {};
SET_MESSAGE(state, {message: {text: 'success message', status: 'success'}});
t.is(state.message.text, 'success message');
t.is(state.message.status, 'success');
});

test('CLEAN_MESSAGE', t => {
const CLEAN_MESSAGE = mutations[types.CLEAN_MESSAGE];
const state = {message: {text: 'error message', status: 'error'}};
CLEAN_MESSAGE(state);
t.is(state.message.text, '');
t.is(state.message.status, 'success');
});
const CLEAN_MESSAGE = mutations[types.CLEAN_MESSAGE];
const state = {message: {text: 'error message', status: 'error'}};
CLEAN_MESSAGE(state);
t.is(state.message.text, '');
t.is(state.message.status, 'success');
});

+ 60
- 60
test/url-parser.js View File

@@ -2,72 +2,72 @@ import test from 'ava';
import * as urlParser from '../src/domain/url-parser';

test('urlParser.getDomainName', t => {
t.is('lesspass.com', urlParser.getDomainName('https://lesspass.com/#!/'));
t.is('lesspass.com', urlParser.getDomainName('https://lesspass.com/api/'));
t.is('api.lesspass.com', urlParser.getDomainName('https://api.lesspass.com/'));
t.is('lesspass.com', urlParser.getDomainName('http://lesspass.com'));
t.is('stackoverflow.com', urlParser.getDomainName('http://stackoverflow.com/questions/3689423/google-chrome-plugin-how-to-get-domain-from-url-tab-url'));
t.is('v4-alpha.getbootstrap.com', urlParser.getDomainName('http://v4-alpha.getbootstrap.com/components/buttons/'));
t.is('accounts.google.com', urlParser.getDomainName('https://accounts.google.com/ServiceLogin?service=mail&passive=true&rm=false&continue=https://mail.google.com/mail/&ss=1&scc=1&ltmpl=default&ltmplcache=2&emr=1&osid=1#identifier'));
t.is('www.netflix.com', urlParser.getDomainName('https://www.netflix.com/browse'));
t.is('www.bbc.co.uk', urlParser.getDomainName('https://www.bbc.co.uk'));
t.is('192.168.1.1:10443', urlParser.getDomainName('https://192.168.1.1:10443/webapp/'));
t.is('', urlParser.getDomainName(undefined));
t.is('lesspass.com', urlParser.getDomainName('https://lesspass.com/#!/'));
t.is('lesspass.com', urlParser.getDomainName('https://lesspass.com/api/'));
t.is('api.lesspass.com', urlParser.getDomainName('https://api.lesspass.com/'));
t.is('lesspass.com', urlParser.getDomainName('http://lesspass.com'));
t.is('stackoverflow.com', urlParser.getDomainName('http://stackoverflow.com/questions/3689423/google-chrome-plugin-how-to-get-domain-from-url-tab-url'));
t.is('v4-alpha.getbootstrap.com', urlParser.getDomainName('http://v4-alpha.getbootstrap.com/components/buttons/'));
t.is('accounts.google.com', urlParser.getDomainName('https://accounts.google.com/ServiceLogin?service=mail&passive=true&rm=false&continue=https://mail.google.com/mail/&ss=1&scc=1&ltmpl=default&ltmplcache=2&emr=1&osid=1#identifier'));
t.is('www.netflix.com', urlParser.getDomainName('https://www.netflix.com/browse'));
t.is('www.bbc.co.uk', urlParser.getDomainName('https://www.bbc.co.uk'));
t.is('192.168.1.1:10443', urlParser.getDomainName('https://192.168.1.1:10443/webapp/'));
t.is('', urlParser.getDomainName(undefined));
});

test('get current tab', t => {
const url = 'https://example.org';
global.chrome = {
tabs: {
query(a, callback){
callback([{url}])
}
}
};
return urlParser.getSite().then(response => {
t.is(response.url, url);
t.is(response.site, 'example.org')
});
const url = 'https://example.org';
global.chrome = {
tabs: {
query(a, callback){
callback([{url}])
}
}
};
return urlParser.getSite().then(response => {
t.is(response.url, url);
t.is(response.site, 'example.org')
});
});

test('getPasswordFromUrlQuery', t => {
const query = {
login: "test@example.org",
site: "example.org",
uppercase: "true",
lowercase: "true",
numbers: "true",
symbols: "false",
length: "16",
counter: "1",
version: "2"
};
const expectedPassword = {
login: "test@example.org",
site: "example.org",
uppercase: true,
lowercase: true,
numbers: true,
symbols: false,
length: 16,
counter: 1,
version: 2
};
t.deepEqual(urlParser.getPasswordFromUrlQuery(query), expectedPassword);
const query = {
login: "test@example.org",
site: "example.org",
uppercase: "true",
lowercase: "true",
numbers: "true",
symbols: "false",
length: "16",
counter: "1",
version: "2"
};
const expectedPassword = {
login: "test@example.org",
site: "example.org",
uppercase: true,
lowercase: true,
numbers: true,
symbols: false,
length: 16,
counter: 1,
version: 2
};
t.deepEqual(urlParser.getPasswordFromUrlQuery(query), expectedPassword);
});

test('getPasswordFromUrlQuery booleanish', t => {
const query = {
uppercase: "true",
lowercase: "TrUe",
numbers: "1",
symbols: "0",
};
const expectedPassword = {
uppercase: true,
lowercase: true,
numbers: true,
symbols: false,
};
t.deepEqual(urlParser.getPasswordFromUrlQuery(query), expectedPassword);
});
const query = {
uppercase: "true",
lowercase: "TrUe",
numbers: "1",
symbols: "0",
};
const expectedPassword = {
uppercase: true,
lowercase: true,
numbers: true,
symbols: false,
};
t.deepEqual(urlParser.getPasswordFromUrlQuery(query), expectedPassword);
});

+ 32
- 32
webpack.config.js View File

@@ -4,41 +4,41 @@ const ExtractTextPlugin = require('extract-text-webpack-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = {
entry: {
app: './src/main.js',
},
output: {
path: path.resolve(__dirname, './dist'),
publicPath: '/dist/',
filename: 'lesspass.min.js'
},
module: {
rules: [
{test: /\.vue$/, loader: 'vue-loader'},
{test: /\.js$/, include: [path.resolve(__dirname, './src')], loader: 'babel-loader'},
{test: /\.json/, loader: 'json-loader'},
{test: /\.(png|jpg|jpeg|gif)$/, loader: 'file-loader?name=[name].[ext]'},
{test: /\.scss$/, loader: ExtractTextPlugin.extract({fallback: 'style-loader', use: 'css-loader!sass-loader', publicPath: ''})},
{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('lesspass.min.css')
entry: {
app: './src/main.js',
},
output: {
path: path.resolve(__dirname, './dist'),
publicPath: '/dist/',
filename: 'lesspass.min.js'
},
module: {
rules: [
{test: /\.vue$/, loader: 'vue-loader'},
{test: /\.js$/, include: [path.resolve(__dirname, './src')], loader: 'babel-loader'},
{test: /\.json/, loader: 'json-loader'},
{test: /\.(png|jpg|jpeg|gif)$/, loader: 'file-loader?name=[name].[ext]'},
{test: /\.scss$/, loader: ExtractTextPlugin.extract({fallback: 'style-loader', use: 'css-loader!sass-loader', publicPath: ''})},
{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('lesspass.min.css')
]
};

if (process.env.NODE_ENV === 'production') {
module.exports.devtool = false;
module.exports.plugins = (module.exports.plugins || []).concat([
new OptimizeCssAssetsPlugin(),
new webpack.optimize.UglifyJsPlugin({
output: {comments: false},
compress: {warnings: false}
})
]);
module.exports.devtool = false;
module.exports.plugins = (module.exports.plugins || []).concat([
new OptimizeCssAssetsPlugin(),
new webpack.optimize.UglifyJsPlugin({
output: {comments: false},
compress: {warnings: false}
})
]);
}


Loading…
Cancel
Save