@@ -19,9 +19,11 @@ | |||
"@babel/plugin-transform-react-jsx": "^7.3.0", | |||
"@babel/preset-env": "^7.5.4", | |||
"@babel/register": "^7.4.4", | |||
"@vue/cli-plugin-babel": "^4.0.4", | |||
"@vue/cli-service": "^4.0.4", | |||
"@vue/test-utils": "^1.0.0-beta.29", | |||
"babel-core": "7.0.0-bridge.0", | |||
"axios-mock-adapter": "^1.17.0", | |||
"babel-core": "7.0.0-bridge.0", | |||
"babel-loader": "^8.0.6", | |||
"chrome-webstore-upload-cli": "^1.2.0", | |||
"clean-webpack-plugin": "^3.0.0", | |||
@@ -0,0 +1,3 @@ | |||
module.exports = { | |||
presets: ["@vue/cli-plugin-babel/preset"] | |||
}; |
@@ -0,0 +1 @@ | |||
#menu .text-white,#menu .white-link{color:inherit}#menu .white-link:active,#menu .white-link:focus,#menu .white-link:hover{text-decoration:none;color:inherit}.card-inverse{background-color:#333;border-color:#333}.fade-enter-active{transition:opacity .5s}.fade-leave-active{transition:opacity 2s}.fade-enter,.fade-leave-to{opacity:0}#message{position:absolute;top:49px;left:0;right:0;z-index:20}.close-notification{float:right;position:absolute;top:0;right:1em;cursor:pointer}.lesspass--unbordered #lesspass{border:none}.lesspass--full-width #lesspass{max-width:none!important}#lesspass{color:#464646;max-width:420px}.lesspass__inner-box{min-height:240px}@media (max-width:419px){.lesspass__inner-box{border:none}}#lesspass,#lesspass *{border-radius:0!important}.pointer,button{cursor:pointer}.inner-addon i{position:absolute;padding:10px;pointer-events:none;z-index:10}.inner-addon{position:relative}.left-addon i{left:0}.right-addon i{right:0}.left-addon input{padding-left:30px}.right-addon input{padding-right:30px}.border-blue{border-color:#007bff!important}#fingerprint{min-width:90px;text-align:center;background-color:transparent;color:#fff}#fingerprint i{color:#000;position:relative;padding:0;text-shadow:1px 1px 0 #fff;font-size:1.3em}#signInButton{border-right:none}#registerButton{border-left:none}.awesomplete mark{background-color:transparent!important;padding:0;margin:0;color:inherit}#options input[type=number]{-moz-appearance:textfield}#options input[type=number]::-webkit-inner-spin-button,#options input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}#generated-password{font-family:Consolas,Menlo,Monaco,Courier New,monospace,sans-serif}div.awesomplete{display:block}div.awesomplete>ul{z-index:11}.passwordProfile__avatar{display:inline-block;width:2rem;height:2rem;text-align:center;line-height:2rem;margin-right:1em;text-transform:uppercase;color:#fff;font-family:monospace}.passwordProfile{display:flex;cursor:pointer;margin-bottom:1rem;justify-content:space-between;align-items:center}.passwordProfile__info{display:flex;align-items:center;flex-grow:1}.passwordProfile__meta{font-size:.8rem;line-height:1rem;flex-grow:1}#passwords__list{min-height:11rem}#passwords__pagination .pagination{margin-bottom:0}#passwords__pagination .page-link{cursor:pointer} |
@@ -1,16 +1,4 @@ | |||
<!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="shortcut icon" href="dist/favicon.ico" /> | |||
<style> | |||
div.center { | |||
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=/favicon.ico><title>LessPass</title><style>div.center { | |||
max-width: 420px; | |||
display: block; | |||
margin-left: auto; | |||
@@ -21,12 +9,4 @@ | |||
#lesspass { | |||
margin-top: 3em; | |||
} | |||
} | |||
</style> | |||
<link href="lesspass.min.css" rel="stylesheet"></head> | |||
<body> | |||
<div class="center lesspass--full-width"> | |||
<div id="lesspass"></div> | |||
</div> | |||
<script type="text/javascript" src="lesspass.min.js"></script></body> | |||
</html> | |||
}</style><link href=/css/app.c1c55914.css rel=preload as=style><link href=/css/chunk-vendors.194c8e8d.css rel=preload as=style><link href=/js/app.6752e6e6.js rel=preload as=script><link href=/js/chunk-vendors.c12ad38e.js rel=preload as=script><link href=/css/chunk-vendors.194c8e8d.css rel=stylesheet><link href=/css/app.c1c55914.css rel=stylesheet></head><body><noscript><strong>We're sorry but LessPass doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div class="center lesspass--full-width"><div id=lesspass></div></div><script src=/js/chunk-vendors.c12ad38e.js></script><script src=/js/app.6752e6e6.js></script></body></html> |
@@ -5,23 +5,14 @@ | |||
"license": "GPL-3.0", | |||
"author": "Guillaume Vincent <guillaume@oslab.fr>", | |||
"scripts": { | |||
"start": "http-server dist -p 8080", | |||
"build": "rm -rf dist && NODE_ENV=production webpack --mode=production --config webpack.prod.js", | |||
"start": "vue-cli-service serve --port 8000", | |||
"build": "vue-cli-service build", | |||
"build:i18n": "cd scripts && node buildI18n.js", | |||
"dev": "webpack-dev-server --config webpack.dev.js", | |||
"cypress:run": "cypress run", | |||
"cypress:open": "cypress open", | |||
"test": "npm run test:unit && npm run test:e2e", | |||
"test:unit": "jest", | |||
"test:e2e": "start-server-and-test dev http://localhost:8000 cypress:run" | |||
}, | |||
"babel": { | |||
"presets": [ | |||
"@babel/preset-env" | |||
], | |||
"plugins": [ | |||
"@babel/plugin-proposal-object-rest-spread" | |||
] | |||
"test:e2e": "start-server-and-test start http://localhost:8000 cypress:run" | |||
}, | |||
"jest": { | |||
"testPathIgnorePatterns": [ | |||
@@ -37,24 +28,28 @@ | |||
} | |||
}, | |||
"dependencies": { | |||
"@oslab/atob": "0.1.0", | |||
"@oslab/btoa": "0.1.0", | |||
"awesomplete": "^1.1.4", | |||
"@oslab/atob": "^0.1.0", | |||
"@oslab/btoa": "^0.1.0", | |||
"awesomplete": "^1.1.5", | |||
"axios": "^0.19.0", | |||
"balloon-css": "^1.0.2", | |||
"balloon-css": "^1.0.3", | |||
"bootstrap": "^4.3.1", | |||
"copy-text-to-clipboard": "^2.0.0", | |||
"font-awesome": "4.7.0", | |||
"jwt-decode": "2.2.0", | |||
"lesspass": "9.0.2", | |||
"lodash.debounce": "4.0.8", | |||
"lodash.uniqby": "4.7.0", | |||
"copy-text-to-clipboard": "^2.1.1", | |||
"core-js": "^3.1.2", | |||
"font-awesome": "^4.7.0", | |||
"jwt-decode": "^2.2.0", | |||
"lesspass": "^9.0.0", | |||
"lodash": "^4.17.15", | |||
"vue": "^2.6.10", | |||
"vue-polyglot": "^2.0.1", | |||
"vue-router": "^3.0.2", | |||
"vue-router": "^3.1.3", | |||
"vuejs-paginate": "^2.1.0", | |||
"vuex": "^3.1.0", | |||
"vuex": "^3.1.1", | |||
"vuex-persistedstate": "^2.5.4", | |||
"vuex-router-sync": "5.0.0" | |||
} | |||
"vuex-router-sync": "^5.0.0" | |||
}, | |||
"browserslist": [ | |||
"> 1%", | |||
"last 2 versions" | |||
] | |||
} |
@@ -1,14 +1,11 @@ | |||
<!DOCTYPE html> | |||
<html> | |||
<html lang="en"> | |||
<head> | |||
<meta charset="utf-8" /> | |||
<meta charset="utf-8"> | |||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |||
<meta name="viewport" content="width=device-width,initial-scale=1.0"> | |||
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> | |||
<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="shortcut icon" href="dist/favicon.ico" /> | |||
<style> | |||
div.center { | |||
max-width: 420px; | |||
@@ -25,6 +22,9 @@ | |||
</style> | |||
</head> | |||
<body> | |||
<noscript> | |||
<strong>We're sorry but LessPass doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> | |||
</noscript> | |||
<div class="center lesspass--full-width"> | |||
<div id="lesspass"></div> | |||
</div> |
@@ -1,100 +0,0 @@ | |||
@import "~bootstrap/scss/functions"; | |||
@import "~bootstrap/scss/variables"; | |||
@import "~bootstrap/scss/mixins"; | |||
@import "~bootstrap/scss/root"; | |||
@import "~bootstrap/scss/reboot"; | |||
@import "~bootstrap/scss/type"; | |||
@import "~bootstrap/scss/images"; | |||
//@import "~bootstrap/scss/code"; | |||
@import "~bootstrap/scss/grid"; | |||
@import "~bootstrap/scss/tables"; | |||
@import "~bootstrap/scss/forms"; | |||
@import "~bootstrap/scss/buttons"; | |||
@import "~bootstrap/scss/transitions"; | |||
@import "~bootstrap/scss/dropdown"; | |||
@import "~bootstrap/scss/button-group"; | |||
@import "~bootstrap/scss/input-group"; | |||
@import "~bootstrap/scss/custom-forms"; | |||
@import "~bootstrap/scss/nav"; | |||
@import "~bootstrap/scss/navbar"; | |||
@import "~bootstrap/scss/card"; | |||
//@import "~bootstrap/scss/breadcrumb"; | |||
@import "~bootstrap/scss/pagination"; | |||
@import "~bootstrap/scss/badge"; | |||
@import "~bootstrap/scss/jumbotron"; | |||
@import "~bootstrap/scss/alert"; | |||
//@import "~bootstrap/scss/progress"; | |||
@import "~bootstrap/scss/media"; | |||
@import "~bootstrap/scss/list-group"; | |||
@import "~bootstrap/scss/close"; | |||
//@import "~bootstrap/scss/modal"; | |||
//@import "~bootstrap/scss/tooltip"; | |||
//@import "~bootstrap/scss/popover"; | |||
//@import "~bootstrap/scss/carousel"; | |||
@import "~bootstrap/scss/utilities"; | |||
//@import "~bootstrap/scss/print"; | |||
@import '~font-awesome/css/font-awesome.css'; | |||
@import '~balloon-css/balloon.css'; | |||
@import '~awesomplete/awesomplete.css'; | |||
.lesspass--unbordered #lesspass{ | |||
border: none; | |||
} | |||
.lesspass--full-width #lesspass { | |||
max-width: none !important; | |||
} | |||
#lesspass { | |||
color: #464646; | |||
max-width: 420px; | |||
} | |||
.lesspass__inner-box { | |||
min-height: 240px; | |||
} | |||
@media (max-width: 419px) { | |||
.lesspass__inner-box { | |||
border: none; | |||
} | |||
} | |||
#lesspass, #lesspass * { | |||
border-radius: 0 !important; | |||
} | |||
button, .pointer { | |||
cursor: pointer; | |||
} | |||
.inner-addon i { | |||
position: absolute; | |||
padding: 10px; | |||
pointer-events: none; | |||
z-index: 10; | |||
} | |||
.inner-addon { | |||
position: relative; | |||
} | |||
.left-addon i { | |||
left: 0; | |||
} | |||
.right-addon i { | |||
right: 0; | |||
} | |||
.left-addon input { | |||
padding-left: 30px; | |||
} | |||
.right-addon input { | |||
padding-right: 30px; | |||
} | |||
.border-blue { | |||
border-color: $blue !important; | |||
} |
@@ -1,3 +1,68 @@ | |||
<style> | |||
.lesspass--unbordered #lesspass { | |||
border: none; | |||
} | |||
.lesspass--full-width #lesspass { | |||
max-width: none !important; | |||
} | |||
#lesspass { | |||
color: #464646; | |||
max-width: 420px; | |||
} | |||
.lesspass__inner-box { | |||
min-height: 240px; | |||
} | |||
@media (max-width: 419px) { | |||
.lesspass__inner-box { | |||
border: none; | |||
} | |||
} | |||
#lesspass, | |||
#lesspass * { | |||
border-radius: 0 !important; | |||
} | |||
button, | |||
.pointer { | |||
cursor: pointer; | |||
} | |||
.inner-addon i { | |||
position: absolute; | |||
padding: 10px; | |||
pointer-events: none; | |||
z-index: 10; | |||
} | |||
.inner-addon { | |||
position: relative; | |||
} | |||
.left-addon i { | |||
left: 0; | |||
} | |||
.right-addon i { | |||
right: 0; | |||
} | |||
.left-addon input { | |||
padding-left: 30px; | |||
} | |||
.right-addon input { | |||
padding-right: 30px; | |||
} | |||
.border-blue { | |||
border-color: #007bff !important; | |||
} | |||
</style> | |||
<template> | |||
<div id="lesspass" class="card"> | |||
<lesspass-menu></lesspass-menu> | |||
@@ -7,21 +72,21 @@ | |||
</div> | |||
</div> | |||
</template> | |||
<script type="text/ecmascript-6"> | |||
import './LessPass.scss'; | |||
import Menu from './components/Menu.vue'; | |||
import Message from './components/Message.vue'; | |||
export default { | |||
name: 'LessPass', | |||
components: { | |||
'lesspass-menu': Menu, | |||
'lesspass-message': Message | |||
}, | |||
created(){ | |||
this.$store.dispatch('cleanMessage'); | |||
this.$store.dispatch('refreshToken'); | |||
this.$store.dispatch('resetPassword'); | |||
} | |||
<script> | |||
import Menu from "./components/Menu.vue"; | |||
import Message from "./components/Message.vue"; | |||
export default { | |||
name: "lesspass", | |||
components: { | |||
"lesspass-menu": Menu, | |||
"lesspass-message": Message | |||
}, | |||
created() { | |||
this.$store.dispatch("cleanMessage"); | |||
this.$store.dispatch("refreshToken"); | |||
this.$store.dispatch("resetPassword"); | |||
} | |||
}; | |||
</script> | |||
@@ -1,74 +1,71 @@ | |||
<style> | |||
.passwordProfile__avatar { | |||
display: inline-block; | |||
width: 2rem; | |||
height: 2rem; | |||
text-align: center; | |||
line-height: 2rem; | |||
margin-right: 1em; | |||
text-transform: uppercase; | |||
color: white; | |||
font-family: monospace; | |||
} | |||
.passwordProfile__avatar { | |||
display: inline-block; | |||
width: 2rem; | |||
height: 2rem; | |||
text-align: center; | |||
line-height: 2rem; | |||
margin-right: 1em; | |||
text-transform: uppercase; | |||
color: white; | |||
font-family: monospace; | |||
} | |||
</style> | |||
<template> | |||
<div v-bind:style="avatarStyle" | |||
class="passwordProfile__avatar"> | |||
{{firstLetter}} | |||
</div> | |||
<div v-bind:style="avatarStyle" class="passwordProfile__avatar">{{firstLetter}}</div> | |||
</template> | |||
<script type="text/ecmascript-6"> | |||
export default { | |||
name: 'avatar', | |||
props: { | |||
name: { | |||
type: String, | |||
required: true | |||
} | |||
}, | |||
data() { | |||
export default { | |||
name: "avatar", | |||
props: { | |||
name: { | |||
type: String, | |||
required: true | |||
} | |||
}, | |||
data() { | |||
return { | |||
alphabetColors: { | |||
a: "#5A8770", | |||
b: "#B2B7BB", | |||
c: "#6FA9AB", | |||
d: "#F5AF29", | |||
e: "#0088B9", | |||
f: "#F18636", | |||
g: "#D93A37", | |||
h: "#A6B12E", | |||
i: "#5C9BBC", | |||
j: "#F5888D", | |||
k: "#9A89B5", | |||
l: "#407887", | |||
m: "#9A89B5", | |||
n: "#5A8770", | |||
o: "#D33F33", | |||
p: "#A2B01F", | |||
q: "#F0B126", | |||
r: "#0087BF", | |||
s: "#F18636", | |||
t: "#0087BF", | |||
u: "#B2B7BB", | |||
v: "#72ACAE", | |||
w: "#9C8AB4", | |||
x: "#5A8770", | |||
y: "#EEB424", | |||
z: "#407887" | |||
}, | |||
firstLetter: "" | |||
}; | |||
}, | |||
mounted() { | |||
this.firstLetter = this.$props.name.charAt(0); | |||
}, | |||
computed: { | |||
avatarStyle: function() { | |||
return { | |||
alphabetColors: { | |||
a: "#5A8770", | |||
b: "#B2B7BB", | |||
c: "#6FA9AB", | |||
d: "#F5AF29", | |||
e: "#0088B9", | |||
f: "#F18636", | |||
g: "#D93A37", | |||
h: "#A6B12E", | |||
i: "#5C9BBC", | |||
j: "#F5888D", | |||
k: "#9A89B5", | |||
l: "#407887", | |||
m: "#9A89B5", | |||
n: "#5A8770", | |||
o: "#D33F33", | |||
p: "#A2B01F", | |||
q: "#F0B126", | |||
r: "#0087BF", | |||
s: "#F18636", | |||
t: "#0087BF", | |||
u: "#B2B7BB", | |||
v: "#72ACAE", | |||
w: "#9C8AB4", | |||
x: "#5A8770", | |||
y: "#EEB424", | |||
z: "#407887" | |||
}, | |||
firstLetter: '' | |||
} | |||
}, | |||
mounted() { | |||
this.firstLetter = this.$props.name.charAt(0); | |||
}, | |||
computed: { | |||
avatarStyle: function() { | |||
return { | |||
backgroundColor: this.alphabetColors[this.firstLetter] || '#5A8770' | |||
} | |||
} | |||
backgroundColor: this.alphabetColors[this.firstLetter] || "#5A8770" | |||
}; | |||
} | |||
} | |||
}; | |||
</script> | |||
@@ -1,10 +1,10 @@ | |||
<style> | |||
.awesomplete mark { | |||
background-color: transparent !important; | |||
padding: 0; | |||
margin: 0; | |||
color: inherit; | |||
} | |||
.awesomplete mark { | |||
background-color: transparent !important; | |||
padding: 0; | |||
margin: 0; | |||
color: inherit; | |||
} | |||
</style> | |||
<template> | |||
<div class="inputSite"> | |||
@@ -20,69 +20,74 @@ | |||
autocorrect="off" | |||
autocapitalize="none" | |||
v-bind:placeholder="label" | |||
v-model="site"> | |||
v-model="site" | |||
/> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import Awesomplete from "awesomplete"; | |||
import {getSuggestions} from "../services/url-parser"; | |||
import Awesomplete from "awesomplete"; | |||
import { getSuggestions } from "../services/url-parser"; | |||
export default { | |||
name: "inputSite", | |||
props: { | |||
value: String, | |||
label: String, | |||
passwords: { | |||
type: Array, | |||
default: () => [] | |||
export default { | |||
name: "inputSite", | |||
props: { | |||
value: String, | |||
label: String, | |||
passwords: { | |||
type: Array, | |||
default: () => [] | |||
} | |||
}, | |||
mounted() { | |||
this.awesomplete = new Awesomplete(this.$refs.siteField); | |||
this.awesomplete.item = (element, input) => { | |||
let item = Awesomplete.ITEM(element.value.site, input); | |||
item.innerHTML += ` ${element.value.login}`; | |||
return item; | |||
}; | |||
this.awesomplete.filter = (site, input) => { | |||
return ( | |||
Awesomplete.FILTER_CONTAINS(site, input) || | |||
Awesomplete.FILTER_CONTAINS(input, site) | |||
); | |||
}; | |||
this.awesomplete.data = data => { | |||
return { label: data.site, value: data }; | |||
}; | |||
this.awesomplete.replace = password => { | |||
this.$refs.siteField.value = password.label; | |||
if (password.value.suggestion) { | |||
this.$emit("suggestionSelected", password.value.site); | |||
} else { | |||
this.$emit("passwordProfileSelected", password.value); | |||
} | |||
}, | |||
mounted() { | |||
this.awesomplete = new Awesomplete(this.$refs.siteField); | |||
this.awesomplete.item = (element, input) => { | |||
let item = Awesomplete.ITEM(element.value.site, input); | |||
item.innerHTML += ` ${element.value.login}`; | |||
return item; | |||
}; | |||
this.awesomplete.filter = (site, input) => { | |||
return Awesomplete.FILTER_CONTAINS(site, input) || | |||
Awesomplete.FILTER_CONTAINS(input, site); | |||
}; | |||
this.awesomplete.data = data => { | |||
return {label: data.site, value: data} | |||
}; | |||
this.awesomplete.replace = password => { | |||
this.$refs.siteField.value = password.label; | |||
if (password.value.suggestion) { | |||
this.$emit("suggestionSelected", password.value.site); | |||
} else { | |||
this.$emit("passwordProfileSelected", password.value); | |||
} | |||
}; | |||
this.awesomplete.sort = (a,b) => { | |||
return a.value.site.localeCompare(b.value.site) || | |||
a.value.login.localeCompare(b.value.login); | |||
}; | |||
this.awesomplete.sort = (a, b) => { | |||
return ( | |||
a.value.site.localeCompare(b.value.site) || | |||
a.value.login.localeCompare(b.value.login) | |||
); | |||
}; | |||
}, | |||
computed: { | |||
site: { | |||
get: function() { | |||
return this.value; | |||
}, | |||
set: function(newValue) { | |||
this.$emit("input", newValue); | |||
} | |||
}, | |||
computed: { | |||
site: { | |||
get: function () { | |||
return this.value; | |||
}, | |||
set: function (newValue) { | |||
this.$emit("input", newValue); | |||
} | |||
} | |||
}, | |||
watch: { | |||
site: function (newValue, _) { | |||
const suggestions = getSuggestions(newValue).map(suggestion => { | |||
return {site: suggestion, suggestion: true, login: ''} | |||
}); | |||
this.awesomplete.list = this.passwords.concat(suggestions); | |||
} | |||
}, | |||
methods: {} | |||
}; | |||
} | |||
}, | |||
watch: { | |||
site: function (newValue) { | |||
const suggestions = getSuggestions(newValue).map(suggestion => { | |||
return { site: suggestion, suggestion: true, login: "" }; | |||
}); | |||
this.awesomplete.list = this.passwords.concat(suggestions); | |||
} | |||
}, | |||
methods: {} | |||
}; | |||
</script> |
@@ -1,141 +1,208 @@ | |||
<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> | |||
<div class="masterPassword"> | |||
<div class="input-group inner-addon left-addon"> | |||
<label for="passwordField" class="sr-only"> | |||
{{ label }} | |||
</label> | |||
<label for="passwordField" class="sr-only">{{ label }}</label> | |||
<i class="fa fa-lock"></i> | |||
<input id="passwordField" | |||
name="passwordField" | |||
ref="passwordField" | |||
type="password" | |||
class="form-control" | |||
autocorrect="off" | |||
autocapitalize="none" | |||
v-bind:value="value" | |||
v-bind:placeholder="label" | |||
v-on:input="updateValue($event.target.value)" | |||
v-on:keyup.enter="$emit('keyupEnter')"> | |||
<span class="input-group-btn" | |||
v-if="fingerprint && value" | |||
v-on:click="togglePasswordType"> | |||
<input | |||
id="passwordField" | |||
name="passwordField" | |||
ref="passwordField" | |||
type="password" | |||
class="form-control" | |||
autocorrect="off" | |||
autocapitalize="none" | |||
v-bind:value="value" | |||
v-bind:placeholder="label" | |||
v-on:input="updateValue($event.target.value)" | |||
v-on:keyup.enter="$emit('keyupEnter')" | |||
/> | |||
<span class="input-group-btn" v-if="fingerprint && value" v-on:click="togglePasswordType"> | |||
<button id="fingerprint" class="btn" type="button" tabindex="-1"> | |||
<small> | |||
<i class="fa fa-fw" v-bind:class="[icon1]" v-bind:style="{ color: color1 }"></i> | |||
<i class="fa fa-fw" v-bind:class="[icon2]" v-bind:style="{ color: color2 }"></i> | |||
<i class="fa fa-fw" v-bind:class="[icon3]" v-bind:style="{ color: color3 }"></i> | |||
</small> | |||
<small> | |||
<i class="fa fa-fw" v-bind:class="[icon1]" v-bind:style="{ color: color1 }"></i> | |||
<i class="fa fa-fw" v-bind:class="[icon2]" v-bind:style="{ color: color2 }"></i> | |||
<i class="fa fa-fw" v-bind:class="[icon3]" v-bind:style="{ color: color3 }"></i> | |||
</small> | |||
</button> | |||
</span> | |||
</div> | |||
<button id="encryptMasterPassword__btn" | |||
type="button" | |||
class="btn btn-link btn-sm p-0" | |||
v-if="showEncryptButton" | |||
v-on:click="encryptMasterPassword()" | |||
v-bind:class="{'disabled': email === ''}"> | |||
<button | |||
id="encryptMasterPassword__btn" | |||
type="button" | |||
class="btn btn-link btn-sm p-0" | |||
v-if="showEncryptButton" | |||
v-on:click="encryptMasterPassword()" | |||
v-bind:class="{'disabled': email === ''}" | |||
> | |||
<small>{{ EncryptButtonText }}</small> | |||
</button> | |||
</div> | |||
</template> | |||
<script> | |||
import LessPass from 'lesspass'; | |||
import debounce from 'lodash.debounce'; | |||
import defaultPasswordProfile from '../store/defaultPassword'; | |||
import LessPass from "lesspass"; | |||
import { debounce } from "lodash"; | |||
import defaultPasswordProfile from "../store/defaultPassword"; | |||
export default { | |||
name: 'masterPassword', | |||
props: { | |||
value: String, | |||
label: String, | |||
email: String, | |||
showEncryptButton: { | |||
type: Boolean, | |||
default: false | |||
}, | |||
EncryptButtonText: String | |||
export default { | |||
name: "masterPassword", | |||
props: { | |||
value: String, | |||
label: String, | |||
email: String, | |||
showEncryptButton: { | |||
type: Boolean, | |||
default: false | |||
}, | |||
data(){ | |||
return { | |||
fingerprint: null, | |||
icon1: '', | |||
icon2: '', | |||
icon3: '', | |||
color1: '', | |||
color2: '', | |||
color3: '' | |||
EncryptButtonText: String | |||
}, | |||
data() { | |||
return { | |||
fingerprint: null, | |||
icon1: "", | |||
icon2: "", | |||
icon3: "", | |||
color1: "", | |||
color2: "", | |||
color3: "" | |||
}; | |||
}, | |||
methods: { | |||
updateValue(newPassword) { | |||
const fakePassword = Math.random() | |||
.toString(36) | |||
.substring(7); | |||
this.setFingerprint(fakePassword); | |||
this.showRealFingerprint(newPassword); | |||
this.$refs.passwordField.value = newPassword; | |||
this.$emit("input", newPassword); | |||
}, | |||
togglePasswordType() { | |||
const element = this.$refs.passwordField; | |||
if (element.type === "password") { | |||
element.type = "text"; | |||
} else { | |||
element.type = "password"; | |||
} | |||
}, | |||
methods: { | |||
updateValue(newPassword){ | |||
const fakePassword = Math.random().toString(36).substring(7); | |||
this.setFingerprint(fakePassword); | |||
this.showRealFingerprint(newPassword); | |||
this.$refs.passwordField.value = newPassword; | |||
this.$emit('input', newPassword); | |||
}, | |||
togglePasswordType(){ | |||
const element = this.$refs.passwordField; | |||
if (element.type === 'password') { | |||
element.type = 'text'; | |||
} else { | |||
element.type = 'password'; | |||
} | |||
}, | |||
hidePassword(){ | |||
this.$refs.passwordField.type = 'password'; | |||
}, | |||
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]; | |||
}, | |||
setFingerprint(password){ | |||
LessPass.createFingerprint(password).then(fingerprint => { | |||
this.icon1 = fingerprint[0].icon; | |||
this.color1 = fingerprint[0].color; | |||
hidePassword() { | |||
this.$refs.passwordField.type = "password"; | |||
}, | |||
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]; | |||
}, | |||
setFingerprint(password) { | |||
LessPass.createFingerprint(password).then(fingerprint => { | |||
this.icon1 = fingerprint[0].icon; | |||
this.color1 = fingerprint[0].color; | |||
this.icon2 = fingerprint[1].icon; | |||
this.color2 = fingerprint[1].color; | |||
this.icon2 = fingerprint[1].icon; | |||
this.color2 = fingerprint[1].color; | |||
this.icon3 = fingerprint[2].icon; | |||
this.color3 = fingerprint[2].color; | |||
this.icon3 = fingerprint[2].icon; | |||
this.color3 = fingerprint[2].color; | |||
this.fingerprint = fingerprint; | |||
}); | |||
}, | |||
showRealFingerprint: debounce(function(password) { | |||
this.setFingerprint(password); | |||
}, 500), | |||
encryptMasterPassword(){ | |||
const password = this.$refs.passwordField.value; | |||
return LessPass | |||
.generatePassword('lesspass.com', this.email, password, defaultPasswordProfile) | |||
.then(generatedPassword => { | |||
this.updateValue(generatedPassword); | |||
}); | |||
} | |||
this.fingerprint = fingerprint; | |||
}); | |||
}, | |||
showRealFingerprint: debounce(function(password) { | |||
this.setFingerprint(password); | |||
}, 500), | |||
encryptMasterPassword() { | |||
const password = this.$refs.passwordField.value; | |||
return LessPass.generatePassword( | |||
"lesspass.com", | |||
this.email, | |||
password, | |||
defaultPasswordProfile | |||
).then(generatedPassword => { | |||
this.updateValue(generatedPassword); | |||
}); | |||
} | |||
} | |||
}; | |||
</script> |
@@ -1,38 +1,41 @@ | |||
<style> | |||
.fade-enter-active { | |||
transition: opacity .5s | |||
} | |||
.fade-enter-active { | |||
transition: opacity 0.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="{ 'bg-warning': message.status==='warning', 'bg-danger': message.status==='error', 'bg-success': message.status==='success' }"> | |||
<div | |||
class="card-header text-white" | |||
v-bind:class="{ 'bg-warning': message.status==='warning', 'bg-danger': message.status==='error', 'bg-success': message.status==='success' }" | |||
> | |||
<div class="row"> | |||
<div class="col-12"> | |||
<small>{{message.text}}</small> | |||
@@ -47,20 +50,18 @@ | |||
</div> | |||
</template> | |||
<script type="text/ecmascript-6"> | |||
import {mapState} from 'vuex'; | |||
import message from '../services/message'; | |||
import { mapState } from "vuex"; | |||
import message from "../services/message"; | |||
export default { | |||
computed: mapState([ | |||
'message' | |||
]), | |||
methods: { | |||
keepMessage(){ | |||
message.keepMessage(); | |||
}, | |||
hideMessage(){ | |||
message.hideMessage(); | |||
} | |||
export default { | |||
computed: mapState(["message"]), | |||
methods: { | |||
keepMessage() { | |||
message.keepMessage(); | |||
}, | |||
hideMessage() { | |||
message.hideMessage(); | |||
} | |||
} | |||
}; | |||
</script> |
@@ -109,7 +109,6 @@ | |||
</template> | |||
<script type="text/ecmascript-6"> | |||
import message from '../services/message'; | |||
import {increment, decrement} from "../services/form-validator"; | |||
import { mapState} from 'vuex'; | |||
@@ -1,23 +1,23 @@ | |||
<style> | |||
.passwordProfile { | |||
display: flex; | |||
cursor: pointer; | |||
margin-bottom: 1rem; | |||
justify-content: space-between; | |||
align-items: center; | |||
} | |||
.passwordProfile { | |||
display: flex; | |||
cursor: pointer; | |||
margin-bottom: 1rem; | |||
justify-content: space-between; | |||
align-items: center; | |||
} | |||
.passwordProfile__info { | |||
display: flex; | |||
align-items: center; | |||
flex-grow: 1; | |||
} | |||
.passwordProfile__info { | |||
display: flex; | |||
align-items: center; | |||
flex-grow: 1; | |||
} | |||
.passwordProfile__meta { | |||
font-size: 0.8rem; | |||
line-height: 1rem; | |||
flex-grow: 1; | |||
} | |||
.passwordProfile__meta { | |||
font-size: 0.8rem; | |||
line-height: 1rem; | |||
flex-grow: 1; | |||
} | |||
</style> | |||
<template> | |||
<div class="passwordProfile"> | |||
@@ -25,39 +25,41 @@ | |||
<avatar v-bind:name="password.site"></avatar> | |||
<div class="passwordProfile__meta"> | |||
<b>{{password.site}}</b> | |||
<br> | |||
<br /> | |||
{{password.login}} | |||
</div> | |||
</div> | |||
<div class="passwordProfile__actions"> | |||
<i class="passwordProfile__delete-icon fa fa-trash fa-fw text-danger" | |||
v-on:click="deletePassword()"></i> | |||
<i | |||
class="passwordProfile__delete-icon fa fa-trash fa-fw text-danger" | |||
v-on:click="deletePassword()" | |||
></i> | |||
</div> | |||
</div> | |||
</template> | |||
<script type="text/ecmascript-6"> | |||
import Avatar from './Avatar.vue'; | |||
import Avatar from "./Avatar.vue"; | |||
export default { | |||
name: 'passwordProfile', | |||
props: { | |||
password: { | |||
type: Object, | |||
required: true | |||
} | |||
}, | |||
components: { | |||
Avatar | |||
export default { | |||
name: "passwordProfile", | |||
props: { | |||
password: { | |||
type: Object, | |||
required: true | |||
} | |||
}, | |||
components: { | |||
Avatar | |||
}, | |||
methods: { | |||
deletePassword() { | |||
this.$store.dispatch("deletePassword", { id: this.password.id }); | |||
}, | |||
methods: { | |||
deletePassword() { | |||
this.$store.dispatch('deletePassword', {id: this.password.id}); | |||
}, | |||
setPassword() { | |||
this.$store.dispatch('savePassword', {password: this.password}); | |||
this.$router.push({name: 'home'}); | |||
} | |||
setPassword() { | |||
this.$store.dispatch("savePassword", { password: this.password }); | |||
this.$router.push({ name: "home" }); | |||
} | |||
} | |||
}; | |||
</script> | |||
@@ -1,10 +1,14 @@ | |||
import Vue from "vue"; | |||
import "./images/favicon.ico"; | |||
import LessPass from "./LessPass.vue"; | |||
import Polyglot from "vue-polyglot"; | |||
import { sync } from "vuex-router-sync"; | |||
import LessPass from "./LessPass.vue"; | |||
import store from "./store"; | |||
import router from "./router"; | |||
import Polyglot from "vue-polyglot"; | |||
import "bootstrap/dist/css/bootstrap.css"; | |||
import "font-awesome/css/font-awesome.css"; | |||
import "balloon-css/balloon.css"; | |||
import "awesomplete/awesomplete.css"; | |||
import frLocales from "./i18n/fr.json"; | |||
import esLocales from "./i18n/es.json"; | |||
@@ -31,9 +35,10 @@ Vue.locales({ | |||
sync(store, router); | |||
Vue.config.productionTip = true; | |||
new Vue({ | |||
el: "#lesspass", | |||
store, | |||
router, | |||
render: h => h(LessPass) | |||
}); | |||
}).$mount("#lesspass"); |
@@ -6,14 +6,14 @@ import PasswordGenerator from "./views/PasswordGenerator.vue"; | |||
import PasswordReset from "./views/PasswordReset.vue"; | |||
import PasswordResetConfirm from "./views/PasswordResetConfirm.vue"; | |||
import Passwords from "./views/Passwords.vue"; | |||
import OptionsPage from "./views/OptionsPage.vue"; | |||
import SettingsPage from "./views/Settings.vue"; | |||
Vue.use(VueRouter); | |||
const routes = [ | |||
{ path: "/", name: "home", component: PasswordGenerator }, | |||
{ path: "/login", name: "login", component: Login }, | |||
{ path: "/options", name: "options", component: OptionsPage }, | |||
{ path: "/settings", name: "settings", component: SettingsPage }, | |||
{ path: "/passwords/", name: "passwords", component: Passwords }, | |||
{ path: "/password/reset", name: "passwordReset", component: PasswordReset }, | |||
{ | |||
@@ -6,7 +6,7 @@ export function cleanUrl(url) { | |||
if (!url) { | |||
return ""; | |||
} | |||
var matchesDomainName = url.match(/^(?:https?\:\/\/)([^\/?#]+)(?:[\/?#]|$)/i); | |||
var matchesDomainName = url.match(/^(?:https?:\/\/)([^/?#]+)(?:[/?#]|$)/i); | |||
return matchesDomainName && matchesDomainName[1] ? matchesDomainName[1] : ""; | |||
} | |||
@@ -33,7 +33,7 @@ export const savePassword = ({ commit }, payload) => { | |||
commit(types.SET_PASSWORD, payload); | |||
}; | |||
export const resetPassword = ({ commit, state }) => { | |||
export const resetPassword = ({ commit }) => { | |||
commit(types.RESET_PASSWORD); | |||
}; | |||
@@ -1,37 +1,41 @@ | |||
<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" | |||
type="text" | |||
class="form-control" | |||
autocapitalize="none" | |||
v-bind:placeholder="$t('LessPass Database Url')" | |||
v-model="baseURL"> | |||
<input | |||
id="baseURL" | |||
type="text" | |||
class="form-control" | |||
autocapitalize="none" | |||
v-bind:placeholder="$t('LessPass Database Url')" | |||
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" | |||
autocapitalize="none" | |||
v-bind:placeholder="$t('Email')" | |||
required | |||
v-model="email"> | |||
<input | |||
id="email" | |||
class="form-control" | |||
name="username" | |||
type="email" | |||
autocapitalize="none" | |||
v-bind:placeholder="$t('Email')" | |||
required | |||
v-model="email" | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
@@ -41,96 +45,134 @@ | |||
v-bind:label="$t('Master Password')" | |||
v-bind:email="email" | |||
v-bind:showEncryptButton="true" | |||
v-bind:EncryptButtonText="$t('Encrypt my master password')"></master-password> | |||
v-bind:EncryptButtonText="$t('Encrypt my master password')" | |||
></master-password> | |||
</div> | |||
<div class="form-group row no-gutters mb-0"> | |||
<div class="col"> | |||
<button id="signInButton" class="btn btn-primary btn-block"> | |||
{{$t('Sign In')}} | |||
</button> | |||
<button id="signInButton" class="btn btn-primary btn-block">{{$t('Sign In')}}</button> | |||
</div> | |||
<div class="col"> | |||
<button id="registerButton" class="btn btn-secondary btn-block" type="button" v-on:click="register"> | |||
{{$t('Register')}} | |||
</button> | |||
<button | |||
id="registerButton" | |||
class="btn btn-secondary btn-block" | |||
type="button" | |||
v-on:click="register" | |||
>{{$t('Register')}}</button> | |||
</div> | |||
</div> | |||
<div class="form-group mb-0"> | |||
<button id="login__forgot-password-btn" | |||
type="button" | |||
class="btn btn-link btn-sm p-0" | |||
v-on:click="$router.push({name: 'passwordReset'})"> | |||
<button | |||
id="login__forgot-password-btn" | |||
type="button" | |||
class="btn btn-link btn-sm p-0" | |||
v-on:click="$router.push({name: 'passwordReset'})" | |||
> | |||
<small>{{$t('ForgotPassword', 'Forgot your password?')}}</small> | |||
</button> | |||
</div> | |||
</form> | |||
</template> | |||
<script type="text/ecmascript-6"> | |||
import User from '../api/user'; | |||
import MasterPassword from '../components/MasterPassword.vue'; | |||
import message from '../services/message'; | |||
import User from "../api/user"; | |||
import MasterPassword from "../components/MasterPassword.vue"; | |||
import message from "../services/message"; | |||
export default { | |||
data() { | |||
return { | |||
email: '', | |||
password: '', | |||
baseURL: 'https://lesspass.com' | |||
}; | |||
export default { | |||
data() { | |||
return { | |||
email: "", | |||
password: "", | |||
baseURL: "https://lesspass.com" | |||
}; | |||
}, | |||
components: { | |||
MasterPassword | |||
}, | |||
methods: { | |||
formIsValid() { | |||
if (!this.email || !this.password || !this.baseURL) { | |||
message.error( | |||
this.$t( | |||
"LoginFormInvalid", | |||
"LessPass URL, email, and password are mandatory" | |||
) | |||
); | |||
return false; | |||
} | |||
return true; | |||
}, | |||
components: { | |||
MasterPassword | |||
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( | |||
this.$t("DBNotRunning", "Your LessPass Database is not running") | |||
); | |||
} else if (err.response && err.response.status === 400) { | |||
message.error( | |||
this.$t( | |||
"LoginIncorrectError", | |||
"The email and password you entered did not match our records. Please double-check and try again." | |||
) | |||
); | |||
} else { | |||
message.displayGenericError(); | |||
} | |||
}); | |||
} | |||
}, | |||
methods: { | |||
formIsValid(){ | |||
if (!this.email || !this.password || !this.baseURL) { | |||
message.error(this.$t('LoginFormInvalid', '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(this.$t('DBNotRunning', 'Your LessPass Database is not running')); | |||
} else if (err.response && err.response.status === 400) { | |||
message.error(this.$t('LoginIncorrectError', '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( | |||
this.$t( | |||
"WelcomeRegister", | |||
"Welcome {email}, thank you for signing up.", | |||
{ email: this.email } | |||
) | |||
); | |||
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.$t( | |||
"EmailAlreadyExist", | |||
"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(this.$t('WelcomeRegister', 'Welcome {email}, thank you for signing up.', {email: this.email})); | |||
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.$t('EmailAlreadyExist', 'This email is already registered. Want to login or recover your password?')); | |||
} | |||
if (err.response.data.email[0].indexOf('valid email') !== -1) { | |||
message.error(this.$t('EmailInvalid', 'Please enter a valid email')); | |||
} | |||
} else { | |||
message.displayGenericError(); | |||
if (err.response.data.email[0].indexOf("valid email") !== -1) { | |||
message.error( | |||
this.$t("EmailInvalid", "Please enter a valid email") | |||
); | |||
} | |||
}); | |||
} | |||
} else { | |||
message.displayGenericError(); | |||
} | |||
}); | |||
} | |||
} | |||
} | |||
}; | |||
</script> | |||
@@ -14,87 +14,104 @@ div.awesomplete > ul { | |||
<template> | |||
<form id="password-generator" v-on:submit.prevent="generatePassword" novalidate> | |||
<div class="form-group"> | |||
<input-site ref="site" | |||
v-model="password.site" | |||
v-bind:passwords="passwords" | |||
v-bind:label="$t('Site')" | |||
v-on:suggestionSelected="setSite" | |||
v-on:passwordProfileSelected="setPasswordProfile"></input-site> | |||
<input-site | |||
ref="site" | |||
v-model="password.site" | |||
v-bind:passwords="passwords" | |||
v-bind:label="$t('Site')" | |||
v-on:suggestionSelected="setSite" | |||
v-on:passwordProfileSelected="setPasswordProfile" | |||
></input-site> | |||
</div> | |||
<remove-auto-complete></remove-auto-complete> | |||
<div class="form-group"> | |||
<label for="login" class="sr-only">{{ $t('Login') }}</label> | |||
<div class="inner-addon left-addon"> | |||
<i class="fa fa-user"></i> | |||
<input id="login" | |||
type="text" | |||
name="login" | |||
ref="login" | |||
class="form-control" | |||
autocomplete="off" | |||
autocorrect="off" | |||
autocapitalize="none" | |||
v-bind:placeholder="$t('Login')" | |||
v-model="password.login"> | |||
<input | |||
id="login" | |||
type="text" | |||
name="login" | |||
ref="login" | |||
class="form-control" | |||
autocomplete="off" | |||
autocorrect="off" | |||
autocapitalize="none" | |||
v-bind:placeholder="$t('Login')" | |||
v-model="password.login" | |||
/> | |||
</div> | |||
</div> | |||
<div class="form-group"> | |||
<master-password ref="masterPassword" | |||
v-model="masterPassword" | |||
v-on:generatePassword="generatePassword" | |||
v-bind:label="$t('Master Password')"></master-password> | |||
<master-password | |||
ref="masterPassword" | |||
v-model="masterPassword" | |||
v-on:generatePassword="generatePassword" | |||
v-bind:label="$t('Master Password')" | |||
></master-password> | |||
</div> | |||
<div class="form-group" | |||
v-bind:class="{ 'mb-0': !showOptions }"> | |||
<div class="form-group" v-bind:class="{ 'mb-0': !showOptions }"> | |||
<div v-if="!passwordGenerated"> | |||
<button id="generatePassword__btn" | |||
type="submit" | |||
class="btn btn-primary border-blue"> | |||
{{ $t('Generate') }} | |||
</button> | |||
<button type="button" | |||
class="btn btn-secondary pull-right showOptions__btn" | |||
v-show="!passwordGenerated" | |||
v-on:click="showOptions =! showOptions"> | |||
<button | |||
id="generatePassword__btn" | |||
type="submit" | |||
class="btn btn-primary border-blue" | |||
>{{ $t('Generate') }}</button> | |||
<button | |||
type="button" | |||
class="btn btn-secondary pull-right showOptions__btn" | |||
v-show="!passwordGenerated" | |||
v-on:click="showOptions =! showOptions" | |||
> | |||
<i class="fa fa-sliders"></i> | |||
</button> | |||
</div> | |||
<div class="btn-group" v-show="passwordGenerated"> | |||
<div class="input-group"> | |||
<span class="input-group-btn"> | |||
<button id="copyPasswordButton" | |||
class="btn btn-primary border-blue" | |||
type="button" | |||
v-on:click="copyPassword()"> | |||
<button | |||
id="copyPasswordButton" | |||
class="btn btn-primary border-blue" | |||
type="button" | |||
v-on:click="copyPassword()" | |||
> | |||
<i class="fa fa-clipboard"></i> | |||
</button> | |||
</span> | |||
<input id="generated-password" | |||
type="password" | |||
class="form-control" | |||
tabindex="-1" | |||
ref="passwordGenerated" | |||
v-bind:value="passwordGenerated"> | |||
<input | |||
id="generated-password" | |||
type="password" | |||
class="form-control" | |||
tabindex="-1" | |||
ref="passwordGenerated" | |||
v-bind:value="passwordGenerated" | |||
/> | |||
<span class="input-group-btn"> | |||
<button id="revealGeneratedPassword" | |||
type="button" | |||
class="btn btn-secondary" | |||
v-on:click="togglePasswordType($refs.passwordGenerated)"> | |||
<button | |||
id="revealGeneratedPassword" | |||
type="button" | |||
class="btn btn-secondary" | |||
v-on:click="togglePasswordType($refs.passwordGenerated)" | |||
> | |||
<i class="fa fa-eye"></i> | |||
</button> | |||
</span> | |||
<span class="input-group-btn"> | |||
<button id="sharePasswordProfileButton" | |||
type="button" | |||
class="btn btn-secondary" | |||
v-on:click="sharePasswordProfile()"> | |||
<button | |||
id="sharePasswordProfileButton" | |||
type="button" | |||
class="btn btn-secondary" | |||
v-on:click="sharePasswordProfile()" | |||
> | |||
<i class="fa fa-share-alt pointer"></i> | |||
</button> | |||
</span> | |||
<span class="input-group-btn"> | |||
<button type="button" | |||
class="btn btn-secondary showOptions__btn" | |||
v-on:click="showOptions =! showOptions"> | |||
<button | |||
type="button" | |||
class="btn btn-secondary showOptions__btn" | |||
v-on:click="showOptions =! showOptions" | |||
> | |||
<i class="fa fa-sliders"></i> | |||
</button> | |||
</span> | |||
@@ -114,7 +131,6 @@ import InputSite from "../components/InputSite.vue"; | |||
import Options from "../components/Options.vue"; | |||
import { showTooltip, hideTooltip } from "../services/tooltip"; | |||
import message from "../services/message"; | |||
import Awesomplete from "awesomplete"; | |||
import * as urlParser from "../services/url-parser"; | |||
export default { | |||
@@ -159,7 +175,7 @@ export default { | |||
}, | |||
deep: true | |||
}, | |||
masterPassword: function (newMasterPassword, oldMasterPassword) { | |||
masterPassword: function(newMasterPassword) { | |||
this.masterPassword = newMasterPassword; | |||
this.cleanErrors(); | |||
} | |||
@@ -199,7 +215,7 @@ export default { | |||
return; | |||
} | |||
const length = this.password.length; | |||
if (length > 35){ | |||
if (length > 35) { | |||
message.warning( | |||
this.$t( | |||
"LengthDeprecationWarning", | |||
@@ -235,9 +251,8 @@ export default { | |||
if (site && !site.value) return void site.focus(); | |||
if (login && !login.value) return void login.focus(); | |||
masterPassword.$refs.passwordField.focus(); | |||
} | |||
catch(err) { | |||
console.error("Can't focus password field") | |||
} catch (err) { | |||
console.error("Can't focus password field"); | |||
} | |||
}, | |||
copyPassword() { | |||
@@ -248,10 +263,7 @@ export default { | |||
setTimeout(() => hideTooltip(element), 2000); | |||
} else { | |||
message.warning( | |||
this.$t( | |||
"SorryCopy", | |||
"Sorry, copying only works in modern browsers." | |||
) | |||
this.$t("SorryCopy", "Sorry, copying only works in modern browsers.") | |||
); | |||
} | |||
}, | |||
@@ -267,15 +279,12 @@ export default { | |||
setTimeout(() => hideTooltip(element), 2000); | |||
} else { | |||
message.warning( | |||
this.$t( | |||
"SorryCopy", | |||
"Sorry, copying only works in modern browsers." | |||
) | |||
this.$t("SorryCopy", "Sorry, copying only works in modern browsers.") | |||
); | |||
} | |||
}, | |||
setSite(site){ | |||
this.password.site = site | |||
setSite(site) { | |||
this.password.site = site; | |||
}, | |||
setPasswordProfile(passwordProfile) { | |||
this.$store | |||
@@ -1,15 +1,15 @@ | |||
<style> | |||
#passwords__list { | |||
min-height: 11rem; | |||
} | |||
#passwords__list { | |||
min-height: 11rem; | |||
} | |||
#passwords__pagination .pagination { | |||
margin-bottom: 0; | |||
} | |||
#passwords__pagination .pagination { | |||
margin-bottom: 0; | |||
} | |||
#passwords__pagination .page-link { | |||
cursor: pointer; | |||
} | |||
#passwords__pagination .page-link { | |||
cursor: pointer; | |||
} | |||
</style> | |||
<template> | |||
<div id="passwords"> | |||
@@ -18,7 +18,13 @@ | |||
<div class="col"> | |||
<div class="inner-addon left-addon"> | |||
<i class="fa fa-search"></i> | |||
<input class="form-control" type="text" name="search" :placeholder="$t('Search')" v-model="searchQuery"> | |||
<input | |||
class="form-control" | |||
type="text" | |||
name="search" | |||
:placeholder="$t('Search')" | |||
v-model="searchQuery" | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
@@ -28,7 +34,9 @@ | |||
<div class="row"> | |||
<div class="col"> | |||
{{$t('NoPassword', "You don't have any password profile saved in your database.")}} | |||
<router-link :to="{ name: 'home'}">{{$t('CreatePassword', 'Would you like to create one?')}}</router-link> | |||
<router-link | |||
:to="{ name: 'home'}" | |||
>{{$t('CreatePassword', 'Would you like to create one?')}}</router-link> | |||
</div> | |||
</div> | |||
</div> | |||
@@ -44,7 +52,8 @@ | |||
v-bind:password="password" | |||
v-on:deleted="pagination.currentPage=1" | |||
v-for="password in filteredPasswords" | |||
:key="password.id"></password-profile> | |||
:key="password.id" | |||
></password-profile> | |||
</div> | |||
<div id="passwords__pagination" v-if="pagination.pageCount > 1"> | |||
<paginate | |||
@@ -60,51 +69,54 @@ | |||
:prev-link-class="'page-link'" | |||
:next-link-class="'page-link'" | |||
:prev-text="$t('Previous')" | |||
:next-text="$t('Next')"> | |||
</paginate> | |||
:next-text="$t('Next')" | |||
></paginate> | |||
</div> | |||
</div> | |||
</template> | |||
<script type="text/ecmascript-6"> | |||
import PasswordProfile from '../components/PasswordProfile.vue'; | |||
import {mapState} from 'vuex'; | |||
import Paginate from 'vuejs-paginate'; | |||
import PasswordProfile from "../components/PasswordProfile.vue"; | |||
import { mapState } from "vuex"; | |||
import Paginate from "vuejs-paginate"; | |||
export default { | |||
name: 'passwords-view', | |||
data() { | |||
return { | |||
searchQuery: '', | |||
pagination: { | |||
pageCount: 1, | |||
perPage: 4, | |||
currentPage: 1 | |||
}, | |||
export default { | |||
name: "passwords-view", | |||
data() { | |||
return { | |||
searchQuery: "", | |||
pagination: { | |||
pageCount: 1, | |||
perPage: 4, | |||
currentPage: 1 | |||
} | |||
}, | |||
components: { | |||
PasswordProfile, | |||
Paginate | |||
}, | |||
computed: { | |||
...mapState(['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.pageCount = Math.ceil(passwords.length / this.pagination.perPage); | |||
return passwords.slice( | |||
this.pagination.currentPage * this.pagination.perPage - this.pagination.perPage, | |||
this.pagination.currentPage * this.pagination.perPage | |||
}; | |||
}, | |||
components: { | |||
PasswordProfile, | |||
Paginate | |||
}, | |||
computed: { | |||
...mapState(["passwords"]), | |||
filteredPasswords() { | |||
const passwords = this.passwords.filter(password => { | |||
var loginMatch = password.login.match( | |||
new RegExp(this.searchQuery, "i") | |||
); | |||
} | |||
}, | |||
methods: { | |||
setCurrentPage(page) { | |||
this.pagination.currentPage = page; | |||
} | |||
var siteMatch = password.site.match(new RegExp(this.searchQuery, "i")); | |||
return loginMatch || siteMatch; | |||
}); | |||
this.pagination.pageCount = Math.ceil(passwords.length / this.pagination.perPage); | |||
return passwords.slice( | |||
this.pagination.currentPage * this.pagination.perPage - | |||
this.pagination.perPage, | |||
this.pagination.currentPage * this.pagination.perPage | |||
); | |||
} | |||
}, | |||
methods: { | |||
setCurrentPage(page) { | |||
this.pagination.currentPage = page; | |||
} | |||
} | |||
}; | |||
</script> |
@@ -24,7 +24,6 @@ | |||
<script> | |||
import { mapState } from "vuex"; | |||
import { SET_DEFAULT_OPTIONS } from "../store/mutation-types"; | |||
export default { | |||
computed: mapState(["defaultPassword"]), |
@@ -1,55 +0,0 @@ | |||
const path = require("path"); | |||
const webpack = require("webpack"); | |||
const HtmlWebpackPlugin = require("html-webpack-plugin"); | |||
const { CleanWebpackPlugin } = require("clean-webpack-plugin"); | |||
const VueLoaderPlugin = require("vue-loader/lib/plugin"); | |||
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); | |||
const productionMode = process.env.NODE_ENV === "production"; | |||
module.exports = { | |||
entry: "./src/main.js", | |||
resolve: { | |||
modules: [path.resolve(__dirname, "src"), "node_modules"] | |||
}, | |||
plugins: [ | |||
new MiniCssExtractPlugin({ filename: "lesspass.min.css" }), | |||
new CleanWebpackPlugin(), | |||
new HtmlWebpackPlugin({ template: "./src/index.html", inject: "body" }), | |||
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), | |||
new VueLoaderPlugin() | |||
], | |||
output: { | |||
filename: "lesspass.min.js", | |||
path: path.resolve(__dirname, "dist") | |||
}, | |||
module: { | |||
rules: [ | |||
{ | |||
test: /\.vue$/, | |||
loader: "vue-loader" | |||
}, | |||
{ | |||
test: /\.js$/, | |||
exclude: /node_modules\/(?!copy-text-to-clipboard)/, | |||
loader: "babel-loader" | |||
}, | |||
{ | |||
test: /\.(sa|sc|c)ss$/, | |||
use: [ | |||
productionMode ? MiniCssExtractPlugin.loader : "style-loader", | |||
"css-loader", | |||
"sass-loader" | |||
] | |||
}, | |||
{ | |||
test: /\.(png|svg|jpg|gif|ico)$/, | |||
use: ["file-loader?name=[name].[ext]"] | |||
}, | |||
{ | |||
test: /\.(woff|woff2|eot|ttf|otf)$/, | |||
use: ["file-loader"] | |||
}, | |||
{ test: /\.html$/, loader: "raw-loader" } | |||
] | |||
} | |||
}; |
@@ -1,13 +0,0 @@ | |||
const merge = require("webpack-merge"); | |||
const common = require("./webpack.common.js"); | |||
module.exports = merge(common, { | |||
mode: "development", | |||
devtool: "inline-source-map", | |||
devServer: { | |||
host: "0.0.0.0", | |||
port: 8000, | |||
contentBase: "./dist", | |||
historyApiFallback: true | |||
} | |||
}); |
@@ -1,9 +0,0 @@ | |||
const merge = require("webpack-merge"); | |||
const common = require("./webpack.common.js"); | |||
const TerserPlugin = require("terser-webpack-plugin"); | |||
module.exports = merge(common, { | |||
mode: "production", | |||
devtool: "source-map", | |||
optimization: { minimizer: [new TerserPlugin()] } | |||
}); |