@@ -3,4 +3,4 @@ __pycache__/ | |||||
*.py[cod] | *.py[cod] | ||||
# javascript | # javascript | ||||
node_modules | node_modules | ||||
package-lock.json | |||||
package-lock.json |
@@ -30,8 +30,13 @@ jobs: | |||||
- lts/* | - lts/* | ||||
addons: | addons: | ||||
chrome: stable | chrome: stable | ||||
apt: | |||||
packages: | |||||
- libgconf-2-4 | |||||
cache: | cache: | ||||
yarn: true | yarn: true | ||||
directories: | |||||
- ~/.cache | |||||
before_install: | before_install: | ||||
- curl -o- -L https://yarnpkg.com/install.sh | bash | - curl -o- -L https://yarnpkg.com/install.sh | bash | ||||
- export PATH=$HOME/.yarn/bin:$PATH | - export PATH=$HOME/.yarn/bin:$PATH | ||||
@@ -80,6 +85,21 @@ jobs: | |||||
branch: master | branch: master | ||||
- stage: deploy | - stage: deploy | ||||
language: node_js | language: node_js | ||||
name: Deploy lesspass | |||||
node_js: | |||||
- lts/* | |||||
script: skip | |||||
before_deploy: cd packages/lesspass | |||||
deploy: | |||||
provider: npm | |||||
email: guillaume@oslab.fr | |||||
api_key: | |||||
secure: rD1rs7QEA7IX8erpnJStx2AKhS8DpzvFxCA/jw/t9cx40Hk29jTVq1h22Sr6Gitm169cH9/sFzZEqera64w/sC7tsLimf/3aQhx8ia0y8taNxVw6nQTRYhvJazhOEnPEN0hZV8cW12yAUWNjjU19xokJsmbaT9ICVy7LNoRhRubpPO58ICkn8A6fBaz49i+Q/WXAdzdpH5nvu0ffz5Vm2QXD5BC6MZcBn3NQX8sDftUK1lqUNuSJIppTR6cUuZW+RluuiajssxYT7LX5BUgW4vTgf1CcPJ5AlI2JLeytphOc7xEwJBk72vO9eLXQZ8CiSwHCYcz9QGDbPke1xfmlqBwjN/FaCLSk7fi1zEMLbDy0K8b26KUqlOaulPX2hjWsBzfX1TykrusqDxYZ72tIWe2GHPsLGQPEkmhUHlUSPaIMDkYIPydLcvLwS8YObThShCvG6AVmSR3v/25SM3pfb98AtLuKLieTq5kQFOog1HRqLjDZ4CdXFhxzLfoQb9+tXYC/cKsyD4GqVL6CQlfERhKUDMwSu284/6g6pqdVvZ7rzE7oiXgyO9PM+T9vA/j4UeNTahANbSx28AQrWa3n5EQHNOHMSAxv4rnpyDYUTUBrlTKYAUTHSvenKgBr9BKnGjJ4Oa+1jMpjgzw+BJT8smhJTVSQr5XFQoXiAUK2nzg= | |||||
on: | |||||
tags: true | |||||
branch: master | |||||
- stage: deploy | |||||
language: node_js | |||||
name: Deploy lesspass-crypto | name: Deploy lesspass-crypto | ||||
node_js: | node_js: | ||||
- lts/* | - lts/* | ||||
@@ -43,12 +43,17 @@ function replace_versions_in_files { | |||||
sed -i "s/\"version\": \"${1}\"/\"version\": \"${2}\"/g" package.json | sed -i "s/\"version\": \"${1}\"/\"version\": \"${2}\"/g" package.json | ||||
sed -i "s/__version__ = \"${1}\"/__version__ = \"${2}\"/g" cli/lesspass/version.py | sed -i "s/__version__ = \"${1}\"/__version__ = \"${2}\"/g" cli/lesspass/version.py | ||||
sed -i "s/\"version\": \"${1}\"/\"version\": \"${2}\"/g" mobile/package.json | sed -i "s/\"version\": \"${1}\"/\"version\": \"${2}\"/g" mobile/package.json | ||||
sed -i "s/\"version\": \"${1}\"/\"version\": \"${2}\"/g" packages/lesspass/package.json | |||||
sed -i "s/\"lesspass-entropy\": \"${1}\"/\"lesspass-entropy\": \"${2}\"/g" packages/lesspass/package.json | |||||
sed -i "s/\"lesspass-fingerprint\": \"${1}\"/\"lesspass-fingerprint\": \"${2}\"/g" packages/lesspass/package.json | |||||
sed -i "s/\"lesspass-render-password\": \"${1}\"/\"lesspass-render-password\": \"${2}\"/g" packages/lesspass/package.json | |||||
sed -i "s/\"version\": \"${1}\"/\"version\": \"${2}\"/g" packages/lesspass-crypto/package.json | sed -i "s/\"version\": \"${1}\"/\"version\": \"${2}\"/g" packages/lesspass-crypto/package.json | ||||
sed -i "s/\"version\": \"${1}\"/\"version\": \"${2}\"/g" packages/lesspass-entropy/package.json | sed -i "s/\"version\": \"${1}\"/\"version\": \"${2}\"/g" packages/lesspass-entropy/package.json | ||||
sed -i "s/\"lesspass-crypto\": \"${1}\"/\"lesspass-crypto\": \"${2}\"/g" packages/lesspass-entropy/package.json | sed -i "s/\"lesspass-crypto\": \"${1}\"/\"lesspass-crypto\": \"${2}\"/g" packages/lesspass-entropy/package.json | ||||
sed -i "s/\"version\": \"${1}\"/\"version\": \"${2}\"/g" packages/lesspass-fingerprint/package.json | sed -i "s/\"version\": \"${1}\"/\"version\": \"${2}\"/g" packages/lesspass-fingerprint/package.json | ||||
sed -i "s/\"lesspass-crypto\": \"${1}\"/\"lesspass-crypto\": \"${2}\"/g" packages/lesspass-fingerprint/package.json | sed -i "s/\"lesspass-crypto\": \"${1}\"/\"lesspass-crypto\": \"${2}\"/g" packages/lesspass-fingerprint/package.json | ||||
sed -i "s/\"version\": \"${1}\"/\"version\": \"${2}\"/g" packages/lesspass-pure/package.json | sed -i "s/\"version\": \"${1}\"/\"version\": \"${2}\"/g" packages/lesspass-pure/package.json | ||||
sed -i "s/\"lesspass\": \"${1}\"/\"lesspass\": \"${2}\"/g" packages/lesspass-pure/package.json | |||||
sed -i "s/\"version\": \"${1}\"/\"version\": \"${2}\"/g" packages/lesspass-render-password/package.json | sed -i "s/\"version\": \"${1}\"/\"version\": \"${2}\"/g" packages/lesspass-render-password/package.json | ||||
sed -i "s/\"version\": \"${1}\"/\"version\": \"${2}\"/g" packages/lesspass-web-component/package.json | sed -i "s/\"version\": \"${1}\"/\"version\": \"${2}\"/g" packages/lesspass-web-component/package.json | ||||
sed -i "s/\"version\": \"${1}\"/\"version\": \"${2}\"/g" packages/lesspass-web-extension/package.json | sed -i "s/\"version\": \"${1}\"/\"version\": \"${2}\"/g" packages/lesspass-web-extension/package.json | ||||
@@ -12,7 +12,7 @@ | |||||
}, | }, | ||||
"dependencies": { | "dependencies": { | ||||
"express": "^4.17.1", | "express": "^4.17.1", | ||||
"lesspass-pure": "^8.0.1" | |||||
"lesspass-pure": "latest" | |||||
}, | }, | ||||
"devDependencies": { | "devDependencies": { | ||||
"favicons": "^5.4.1", | "favicons": "^5.4.1", | ||||
@@ -13,15 +13,15 @@ | |||||
"test": "jest" | "test": "jest" | ||||
}, | }, | ||||
"dependencies": { | "dependencies": { | ||||
"axios": "0.18.0", | |||||
"axios": "0.19.0", | |||||
"debounce": "1.2.0", | "debounce": "1.2.0", | ||||
"fuse.js": "^3.4.4", | "fuse.js": "^3.4.4", | ||||
"lesspass-fingerprint": "latest", | |||||
"lesspass-render-password": "latest", | |||||
"lesspass-fingerprint": "../packages/lesspass-fingerprint", | |||||
"lesspass-render-password": "../packages/lesspass-render-password", | |||||
"lodash": "^4.17.11", | "lodash": "^4.17.11", | ||||
"memoize-one": "^5.0.4", | "memoize-one": "^5.0.4", | ||||
"react": "16.8.3", | |||||
"react-native": "0.59.4", | |||||
"react": "^16.8.6", | |||||
"react-native": "^0.60.3", | |||||
"react-native-gesture-handler": "^1.1.0", | "react-native-gesture-handler": "^1.1.0", | ||||
"react-native-keychain": "^3.1.1", | "react-native-keychain": "^3.1.1", | ||||
"react-native-paper": "^2.15.2", | "react-native-paper": "^2.15.2", | ||||
@@ -39,8 +39,8 @@ | |||||
"@babel/runtime": "^7.4.3", | "@babel/runtime": "^7.4.3", | ||||
"babel-jest": "^24.7.1", | "babel-jest": "^24.7.1", | ||||
"jest": "^24.7.1", | "jest": "^24.7.1", | ||||
"metro-react-native-babel-preset": "^0.53.1", | |||||
"react-test-renderer": "16.8.3" | |||||
"metro-react-native-babel-preset": "^0.55.0", | |||||
"react-test-renderer": "^16.8.6" | |||||
}, | }, | ||||
"jest": { | "jest": { | ||||
"preset": "react-native" | "preset": "react-native" | ||||
@@ -3,7 +3,7 @@ import { View, NativeModules } from "react-native"; | |||||
import TextInput from "../ui/TextInput"; | import TextInput from "../ui/TextInput"; | ||||
import TouchId from "./TouchId"; | import TouchId from "./TouchId"; | ||||
import Fingerprint from "./Fingerprint"; | import Fingerprint from "./Fingerprint"; | ||||
import fingerprint from "lesspass-fingerprint"; | |||||
import { createFingerprint } from "lesspass-fingerprint"; | |||||
import { debounce } from "lodash"; | import { debounce } from "lodash"; | ||||
export default class MasterPassword extends Component { | export default class MasterPassword extends Component { | ||||
@@ -19,7 +19,7 @@ export default class MasterPassword extends Component { | |||||
for (let i = 64; i > 0; i -= 1) { | for (let i = 64; i > 0; i -= 1) { | ||||
hmacSha256 += poolOfChars[Math.floor(Math.random() * poolOfChars.length)]; | hmacSha256 += poolOfChars[Math.floor(Math.random() * poolOfChars.length)]; | ||||
} | } | ||||
this.setState({ fingerprint: fingerprint(hmacSha256) }); | |||||
this.setState({ fingerprint: createFingerprint(hmacSha256) }); | |||||
}; | }; | ||||
calcFingerprint = masterPassword => { | calcFingerprint = masterPassword => { | ||||
@@ -27,7 +27,7 @@ export default class MasterPassword extends Component { | |||||
NativeModules.LessPass.createFingerprint(masterPassword).then( | NativeModules.LessPass.createFingerprint(masterPassword).then( | ||||
hmacSha256 => { | hmacSha256 => { | ||||
this.setState({ | this.setState({ | ||||
fingerprint: fingerprint(hmacSha256) | |||||
fingerprint: createFingerprint(hmacSha256) | |||||
}); | }); | ||||
} | } | ||||
); | ); | ||||
@@ -1,5 +1,5 @@ | |||||
import { NativeModules } from "react-native"; | import { NativeModules } from "react-native"; | ||||
import renderLessPassPassword from "lesspass-render-password"; | |||||
import { renderPassword } from "lesspass-render-password"; | |||||
export function generatePassword(masterPassword, passwordProfile) { | export function generatePassword(masterPassword, passwordProfile) { | ||||
const { | const { | ||||
@@ -19,6 +19,6 @@ export function generatePassword(masterPassword, passwordProfile) { | |||||
masterPassword, | masterPassword, | ||||
counter.toString(16) | counter.toString(16) | ||||
).then(entropy => { | ).then(entropy => { | ||||
return renderLessPassPassword(entropy, options); | |||||
return renderPassword(entropy, options); | |||||
}); | }); | ||||
} | } |
@@ -13,17 +13,53 @@ | |||||
"packages/*" | "packages/*" | ||||
], | ], | ||||
"devDependencies": { | "devDependencies": { | ||||
"@babel/core": "7.4.0", | |||||
"babel-core": "7.0.0-bridge.0", | |||||
"babel-jest": "23.6.0", | |||||
"babel-preset-react-native": "5.0.2", | |||||
"jest": "23.6.0", | |||||
"karma": "4.0.1", | |||||
"karma-chrome-launcher": "2.2.0", | |||||
"@babel/core": "^7.5.4", | |||||
"@babel/plugin-proposal-object-rest-spread": "^7.5.4", | |||||
"@babel/plugin-transform-object-assign": "^7.2.0", | |||||
"@babel/plugin-transform-react-jsx": "^7.3.0", | |||||
"@babel/preset-env": "^7.5.4", | |||||
"@babel/register": "^7.4.4", | |||||
"axios-mock-adapter": "^1.17.0", | |||||
"babel-loader": "^8.0.6", | |||||
"chrome-webstore-upload-cli": "^1.2.0", | |||||
"clean-webpack-plugin": "^3.0.0", | |||||
"copy-webpack-plugin": "^5.0.3", | |||||
"cross-env": "^5.2.0", | |||||
"css-loader": "^3.0.0", | |||||
"cypress": "^3.4.0", | |||||
"dot-json": "^1.1.0", | |||||
"express": "^4.17.1", | |||||
"file-loader": "^4.0.0", | |||||
"gulp": "^4.0.2", | |||||
"html-webpack-plugin": "^3.2.0", | |||||
"http-server": "^0.11.1", | |||||
"jest": "^24.8.0", | |||||
"jquery": "^3.4.1", | |||||
"karma": "^4.2.0", | |||||
"karma-chrome-launcher": "^3.0.0", | |||||
"karma-mocha": "1.3.0", | "karma-mocha": "1.3.0", | ||||
"karma-webpack": "3.0.5", | |||||
"mocha": "6.0.2", | |||||
"react-test-renderer": "16.8.4", | |||||
"webpack": "4.28.3" | |||||
"karma-webpack": "^4.0.2", | |||||
"mini-css-extract-plugin": "^0.8.0", | |||||
"mocha": "^6.1.4", | |||||
"nock": "^10.0.6", | |||||
"node-sass": "^4.12.0", | |||||
"popper.js": "^1.15.0", | |||||
"postcss-loader": "^3.0.0", | |||||
"prettier": "^1.18.2", | |||||
"raw-loader": "^3.0.0", | |||||
"sass-loader": "^7.1.0", | |||||
"start-server-and-test": "^1.9.1", | |||||
"style-loader": "^0.23.1", | |||||
"timekeeper": "^2.2.0", | |||||
"url-loader": "^2.0.1", | |||||
"vue-loader": "^15.7.0", | |||||
"vue-polyglot-utils": "^0.1.1", | |||||
"vue-template-compiler": "^2.6.10", | |||||
"walk": "^2.3.14", | |||||
"web-ext-submit": "^3.1.0", | |||||
"webpack": "^4.35.3", | |||||
"webpack-cli": "^3.3.6", | |||||
"webpack-dev-server": "^3.7.2", | |||||
"webpack-merge": "^4.2.1" | |||||
} | } | ||||
} | } |
@@ -1,19 +1,17 @@ | |||||
module.exports = function getKarmaConf(config) { | |||||
module.exports = config => { | |||||
config.set({ | config.set({ | ||||
basePath: "..", | basePath: "..", | ||||
frameworks: ["mocha"], | frameworks: ["mocha"], | ||||
files: ["src/index.js", "test/**/*.js"], | files: ["src/index.js", "test/**/*.js"], | ||||
exclude: [], | |||||
preprocessors: { | preprocessors: { | ||||
"**/*.js": ["webpack"] | |||||
"src/index.js": ["webpack"], | |||||
"test/**/*.js": ["webpack"] | |||||
}, | |||||
webpack: {}, | |||||
webpackMiddleware: { | |||||
stats: "errors-only" | |||||
}, | }, | ||||
reporters: ["progress"], | |||||
port: 9876, | |||||
colors: true, | |||||
logLevel: config.LOG_INFO, | |||||
autoWatch: false, | |||||
browsers: ["ChromeHeadless"], | browsers: ["ChromeHeadless"], | ||||
singleRun: true, | |||||
concurrency: Infinity | |||||
singleRun: true | |||||
}); | }); | ||||
}; | }; |
@@ -0,0 +1,96 @@ | |||||
function getColor(color) { | |||||
const colors = [ | |||||
"#000000", | |||||
"#074750", | |||||
"#009191", | |||||
"#FF6CB6", | |||||
"#FFB5DA", | |||||
"#490092", | |||||
"#006CDB", | |||||
"#B66DFF", | |||||
"#6DB5FE", | |||||
"#B5DAFE", | |||||
"#920000", | |||||
"#924900", | |||||
"#DB6D00", | |||||
"#24FE23" | |||||
]; | |||||
const index = parseInt(color, 16) % colors.length; | |||||
return colors[index]; | |||||
} | |||||
function getIcon(hash) { | |||||
const 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" | |||||
]; | |||||
const index = parseInt(hash, 16) % icons.length; | |||||
return icons[index]; | |||||
} | |||||
module.exports = function(hmacSHA256) { | |||||
const fingerprint = []; | |||||
const hash1 = hmacSHA256.substring(0, 6); | |||||
fingerprint.push({ | |||||
color: getColor(hash1), | |||||
icon: getIcon(hash1) | |||||
}); | |||||
const hash2 = hmacSHA256.substring(6, 12); | |||||
fingerprint.push({ | |||||
color: getColor(hash2), | |||||
icon: getIcon(hash2) | |||||
}); | |||||
const hash3 = hmacSHA256.substring(12, 18); | |||||
fingerprint.push({ | |||||
color: getColor(hash3), | |||||
icon: getIcon(hash3) | |||||
}); | |||||
return fingerprint; | |||||
}; |
@@ -1,7 +1,12 @@ | |||||
const crypto = require("crypto"); | const crypto = require("crypto"); | ||||
module.exports = function hmac(digest, string, salt) { | |||||
return new Promise(((resolve) => { | |||||
resolve(crypto.createHmac(digest, string).update(salt || "").digest("hex")); | |||||
})); | |||||
module.exports = function(digest, string, salt) { | |||||
return new Promise(resolve => { | |||||
resolve( | |||||
crypto | |||||
.createHmac(digest, string) | |||||
.update(salt || "") | |||||
.digest("hex") | |||||
); | |||||
}); | |||||
}; | }; |
@@ -1,96 +1,7 @@ | |||||
function getColor(color) { | |||||
const colors = [ | |||||
"#000000", | |||||
"#074750", | |||||
"#009191", | |||||
"#FF6CB6", | |||||
"#FFB5DA", | |||||
"#490092", | |||||
"#006CDB", | |||||
"#B66DFF", | |||||
"#6DB5FE", | |||||
"#B5DAFE", | |||||
"#920000", | |||||
"#924900", | |||||
"#DB6D00", | |||||
"#24FE23" | |||||
]; | |||||
const index = parseInt(color, 16) % colors.length; | |||||
return colors[index]; | |||||
} | |||||
const createFingerprint = require("./fingerprint"); | |||||
const createHmac = require("./hmac"); | |||||
function getIcon(hash) { | |||||
const 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" | |||||
]; | |||||
const index = parseInt(hash, 16) % icons.length; | |||||
return icons[index]; | |||||
} | |||||
module.exports = function getFingerprint(hmacSHA256) { | |||||
const fingerprint = []; | |||||
const hash1 = hmacSHA256.substring(0, 6); | |||||
fingerprint.push({ | |||||
color: getColor(hash1), | |||||
icon: getIcon(hash1) | |||||
}); | |||||
const hash2 = hmacSHA256.substring(6, 12); | |||||
fingerprint.push({ | |||||
color: getColor(hash2), | |||||
icon: getIcon(hash2) | |||||
}); | |||||
const hash3 = hmacSHA256.substring(12, 18); | |||||
fingerprint.push({ | |||||
color: getColor(hash3), | |||||
icon: getIcon(hash3) | |||||
}); | |||||
return fingerprint; | |||||
module.exports = { | |||||
createFingerprint, | |||||
createHmac | |||||
}; | }; |
@@ -1,10 +1,24 @@ | |||||
const assert = require("assert"); | const assert = require("assert"); | ||||
const fingerprint = require("../src"); | |||||
const { createFingerprint, createHmac } = require("../src"); | |||||
describe("api", () => { | describe("api", () => { | ||||
it("createHmac", () => | |||||
createHmac("sha256", "password").then(fingerprint => { | |||||
assert.equal( | |||||
"e56a207acd1e6714735487c199c6f095844b7cc8e5971d86c003a7b6f36ef51e", | |||||
fingerprint | |||||
); | |||||
})); | |||||
it("createHmac and update", () => | |||||
createHmac("sha256", "password", "salt").then(fingerprint => { | |||||
assert.equal( | |||||
"fc328232993ff34ca56631e4a101d60393cad12171997ee0b562bf7852b2fed0", | |||||
fingerprint | |||||
); | |||||
})); | |||||
it("fingerprint is length of 3", () => { | it("fingerprint is length of 3", () => { | ||||
assert.equal( | assert.equal( | ||||
fingerprint( | |||||
createFingerprint( | |||||
"e56a207acd1e6714735487c199c6f095844b7cc8e5971d86c003a7b6f36ef51e" | "e56a207acd1e6714735487c199c6f095844b7cc8e5971d86c003a7b6f36ef51e" | ||||
).length, | ).length, | ||||
3 | 3 | ||||
@@ -26,7 +40,7 @@ describe("api", () => { | |||||
} | } | ||||
]; | ]; | ||||
assert.deepEqual( | assert.deepEqual( | ||||
fingerprint( | |||||
createFingerprint( | |||||
"e56a207acd1e6714735487c199c6f095844b7cc8e5971d86c003a7b6f36ef51e" | "e56a207acd1e6714735487c199c6f095844b7cc8e5971d86c003a7b6f36ef51e" | ||||
), | ), | ||||
expectedFingerprint | expectedFingerprint |
@@ -1,17 +0,0 @@ | |||||
const assert = require("assert"); | |||||
const createHmac = require("../src/hmac"); | |||||
describe("hmac", () => { | |||||
it("createHmac", () => createHmac("sha256", "password").then((fingerprint) => { | |||||
assert.equal( | |||||
"e56a207acd1e6714735487c199c6f095844b7cc8e5971d86c003a7b6f36ef51e", | |||||
fingerprint | |||||
); | |||||
})); | |||||
it("createHmac and update", () => createHmac("sha256", "password", "salt").then((fingerprint) => { | |||||
assert.equal( | |||||
"fc328232993ff34ca56631e4a101d60393cad12171997ee0b562bf7852b2fed0", | |||||
fingerprint | |||||
); | |||||
})); | |||||
}); |
@@ -1,19 +1,17 @@ | |||||
module.exports = function getKarmaConf(config) { | |||||
module.exports = config => { | |||||
config.set({ | config.set({ | ||||
basePath: "..", | basePath: "..", | ||||
frameworks: ["mocha"], | frameworks: ["mocha"], | ||||
files: ["src/index.js", "test/**/*.js"], | files: ["src/index.js", "test/**/*.js"], | ||||
exclude: [], | |||||
preprocessors: { | preprocessors: { | ||||
"**/*.js": ["webpack"] | |||||
"src/index.js": ["webpack"], | |||||
"test/**/*.js": ["webpack"] | |||||
}, | |||||
webpack: {}, | |||||
webpackMiddleware: { | |||||
stats: "errors-only" | |||||
}, | }, | ||||
reporters: ["progress"], | |||||
port: 9876, | |||||
colors: true, | |||||
logLevel: config.LOG_INFO, | |||||
autoWatch: false, | |||||
browsers: ["ChromeHeadless"], | browsers: ["ChromeHeadless"], | ||||
singleRun: true, | |||||
concurrency: Infinity | |||||
singleRun: true | |||||
}); | }); | ||||
}; | }; |
@@ -1 +1,2 @@ | |||||
test/e2e/reports | |||||
cypress/videos | |||||
cypress/screenshots |
@@ -0,0 +1,4 @@ | |||||
{ | |||||
"baseUrl": "http://localhost:8080", | |||||
"projectId": "j4hn15" | |||||
} |
@@ -0,0 +1,5 @@ | |||||
{ | |||||
"name": "Using fixtures to represent data", | |||||
"email": "hello@cypress.io", | |||||
"body": "Fixtures are a great way to mock data for responses to routes" | |||||
} |
@@ -0,0 +1,58 @@ | |||||
describe("Connected Mode", function() { | |||||
it("can save a password profile on connected mode", function() { | |||||
cy.visit("/"); | |||||
cy.get(".fa-sign-in").click(); | |||||
cy.get("#email").type("test@lesspass.com"); | |||||
cy.get("#passwordField").type("test@lesspass.com"); | |||||
cy.get("#encryptMasterPassword__btn").click(); | |||||
cy.get("#fingerprint .fa-university").should("be.visible"); | |||||
cy.get("#fingerprint .fa-btc").should("be.visible"); | |||||
cy.get("#fingerprint .fa-subway").should("be.visible"); | |||||
cy.get("#signInButton").click(); | |||||
cy.get("#siteField").type("lesspass.com"); | |||||
cy.get("#login").type("test@lesspass.com"); | |||||
cy.get("#passwordField").type("test@lesspass.com"); | |||||
cy.get("#generatePassword__btn").click(); | |||||
cy.get("#generated-password").should("have.value", "hjV@\\5ULp3bIs,6B"); | |||||
cy.get(".fa-save").should("be.visible"); | |||||
cy.get(".fa-sign-out").click(); | |||||
cy.get(".fa-save").should("not.be.visible"); | |||||
}); | |||||
it("can log in and log out", function() { | |||||
cy.visit("/"); | |||||
cy.get(".fa-sign-in").click(); | |||||
cy.get("#baseURL").should("have.value", "https://lesspass.com"); | |||||
cy.get("#email").type("test@lesspass.com"); | |||||
cy.get("#passwordField").type("test@lesspass.com"); | |||||
cy.get("#encryptMasterPassword__btn").click(); | |||||
cy.wait(500); | |||||
cy.get("#signInButton").click(); | |||||
cy.get("#siteField").should("be.visible"); | |||||
cy.get(".fa-key").should("be.visible"); | |||||
cy.get(".fa-sign-out").click(); | |||||
cy.get(".fa-key").should("not.be.visible"); | |||||
}); | |||||
it("reset password page", function() { | |||||
cy.visit("/"); | |||||
cy.get(".fa-sign-in").click(); | |||||
cy.get("#login__forgot-password-btn").click(); | |||||
cy.get("#password-reset__reset-password-btn").click(); | |||||
}); | |||||
it("use saved profile", function() { | |||||
cy.visit("/"); | |||||
cy.get(".fa-sign-in").click(); | |||||
cy.get("#baseURL").should("have.value", "https://lesspass.com"); | |||||
cy.get("#email").type("test@lesspass.com"); | |||||
cy.get("#passwordField").type("test@lesspass.com"); | |||||
cy.get("#encryptMasterPassword__btn").click(); | |||||
cy.wait(500); | |||||
cy.get("#signInButton").click(); | |||||
cy.get(".fa-key").click(); | |||||
cy.get(".passwordProfile__meta") | |||||
.first() | |||||
.click(); | |||||
cy.get("#siteField").should("have.value", "example.org"); | |||||
cy.get("#login").should("have.value", "contact@example.org"); | |||||
cy.get(".fa-sign-out").click(); | |||||
}); | |||||
}); |
@@ -0,0 +1,10 @@ | |||||
describe("LessPass", function() { | |||||
it("successfully loads", function() { | |||||
cy.visit("/"); | |||||
}); | |||||
it("should focus site field", function() { | |||||
cy.visit("/"); | |||||
cy.focused() | |||||
.should('have.id', 'siteField') | |||||
}); | |||||
}); |
@@ -0,0 +1,113 @@ | |||||
describe("Password Generation", function() { | |||||
it("can't decrease counter under 0", function() { | |||||
cy.visit("/"); | |||||
cy.get(".showOptions__btn") | |||||
.first() | |||||
.click(); | |||||
cy.get("#decreaseCounter__btn").click(); | |||||
cy.get("#decreaseCounter__btn").click(); | |||||
cy.get("#passwordCounter").should("have.value", "1"); | |||||
}); | |||||
it("should generate the appropriate password", function() { | |||||
function clickAndAssertOption(cy, button, password1, password2) { | |||||
cy.get(button).click(); | |||||
cy.get("#generatePassword__btn").click(); | |||||
cy.get("#generated-password").should("have.value", password1); | |||||
cy.get(button).click(); | |||||
cy.get("#generatePassword__btn").click(); | |||||
cy.get("#generated-password").should("have.value", password2); | |||||
} | |||||
cy.visit("/"); | |||||
cy.get("#siteField").type("lesspass.com"); | |||||
cy.get("#login").type("test@lesspass.com"); | |||||
cy.get("#passwordField").type("test@lesspass.com"); | |||||
cy.wait(500); | |||||
cy.get("#fingerprint .fa-cutlery").should("be.visible"); | |||||
cy.get("#fingerprint .fa-subway").should("be.visible"); | |||||
cy.get("#fingerprint .fa-plane").should("be.visible"); | |||||
cy.get("#generatePassword__btn").click(); | |||||
cy.get("#generated-password").should("have.value", "hjV@\\5ULp3bIs,6B"); | |||||
cy.get(".showOptions__btn") | |||||
.first() | |||||
.click(); | |||||
cy.get("#decreaseLength__btn").click(); | |||||
cy.get("#passwordLength").should("have.value", "15"); | |||||
cy.get("#increaseLength__btn").click(); | |||||
cy.get("#increaseLength__btn").click(); | |||||
cy.get("#passwordLength").should("have.value", "17"); | |||||
cy.get("#generatePassword__btn").click(); | |||||
cy.get("#generated-password").should("have.value", "hj@r\\ULp3Is62@HB~"); | |||||
cy.get("#decreaseLength__btn").click(); | |||||
cy.get("#passwordLength").should("have.value", "16"); | |||||
cy.get("#increaseCounter__btn").click(); | |||||
cy.get("#passwordCounter").should("have.value", "2"); | |||||
cy.get("#generatePassword__btn").click(); | |||||
cy.get("#generated-password").should("have.value", "#wOxv!q;URh:k82("); | |||||
cy.get("#passwordCounter") | |||||
.clear() | |||||
.type("1"); | |||||
clickAndAssertOption( | |||||
cy, | |||||
"#lowercase__btn", | |||||
"^>_9>+}OV?[3[_U,", | |||||
"hjV@\\5ULp3bIs,6B" | |||||
); | |||||
clickAndAssertOption( | |||||
cy, | |||||
"#uppercase__btn", | |||||
"^>_9>+}ov?[3[_u,", | |||||
"hjV@\\5ULp3bIs,6B" | |||||
); | |||||
clickAndAssertOption( | |||||
cy, | |||||
"#numbers__btn", | |||||
'jCmMpNy=T."+u^ZQ', | |||||
"hjV@\\5ULp3bIs,6B" | |||||
); | |||||
clickAndAssertOption( | |||||
cy, | |||||
"#symbols__btn", | |||||
"XAwlOl5mtjGSY6PA", | |||||
"hjV@\\5ULp3bIs,6B" | |||||
); | |||||
}); | |||||
it("should have a min length of 5 and max length of 35", function() { | |||||
cy.visit("/"); | |||||
cy.get(".showOptions__btn") | |||||
.first() | |||||
.click(); | |||||
cy.get("#passwordLength") | |||||
.clear() | |||||
.type("35"); | |||||
cy.get("#increaseLength__btn").click(); | |||||
cy.get("#passwordLength").should("have.value", "35"); | |||||
cy.get("#passwordLength") | |||||
.clear() | |||||
.type("5"); | |||||
cy.get("#decreaseLength__btn").click(); | |||||
cy.get("#passwordLength").should("have.value", "5"); | |||||
}); | |||||
it("should consider counter as string not hex value nrt_328", function() { | |||||
cy.visit("/"); | |||||
cy.get("#siteField").type("site"); | |||||
cy.get("#login").type("login"); | |||||
cy.get("#passwordField").type("test"); | |||||
cy.get(".showOptions__btn") | |||||
.first() | |||||
.click(); | |||||
cy.get("#passwordCounter") | |||||
.clear() | |||||
.type("10"); | |||||
cy.get("#generatePassword__btn").click(); | |||||
cy.get("#generated-password").should("have.value", "XFt0F*,r619:+}[."); | |||||
}); | |||||
it("should generate password when hit enter nrt_266", function() { | |||||
cy.visit("/"); | |||||
cy.get("#siteField").type("lesspass.com"); | |||||
cy.get("#login").type("test@lesspass.com"); | |||||
cy.get("#passwordField").type("test@lesspass.com").type("{enter}"); | |||||
cy.get("#generated-password").should("have.value", "hjV@\\5ULp3bIs,6B"); | |||||
}); | |||||
}); |
@@ -0,0 +1,17 @@ | |||||
// *********************************************************** | |||||
// This example plugins/index.js can be used to load plugins | |||||
// | |||||
// You can change the location of this file or turn off loading | |||||
// the plugins file with the 'pluginsFile' configuration option. | |||||
// | |||||
// You can read more here: | |||||
// https://on.cypress.io/plugins-guide | |||||
// *********************************************************** | |||||
// This function is called when a project is opened or re-opened (e.g. due to | |||||
// the project's config changing) | |||||
module.exports = (on, config) => { | |||||
// `on` is used to hook into various events Cypress emits | |||||
// `config` is the resolved Cypress config | |||||
} |
@@ -0,0 +1,25 @@ | |||||
// *********************************************** | |||||
// This example commands.js shows you how to | |||||
// create various custom commands and overwrite | |||||
// existing commands. | |||||
// | |||||
// For more comprehensive examples of custom | |||||
// commands please read more here: | |||||
// https://on.cypress.io/custom-commands | |||||
// *********************************************** | |||||
// | |||||
// | |||||
// -- This is a parent command -- | |||||
// Cypress.Commands.add("login", (email, password) => { ... }) | |||||
// | |||||
// | |||||
// -- This is a child command -- | |||||
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) | |||||
// | |||||
// | |||||
// -- This is a dual command -- | |||||
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) | |||||
// | |||||
// | |||||
// -- This is will overwrite an existing command -- | |||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) |
@@ -0,0 +1,20 @@ | |||||
// *********************************************************** | |||||
// This example support/index.js is processed and | |||||
// loaded automatically before your test files. | |||||
// | |||||
// This is a great place to put global configuration and | |||||
// behavior that modifies Cypress. | |||||
// | |||||
// You can change the location of this file or turn off | |||||
// automatically serving support files with the | |||||
// 'supportFile' configuration option. | |||||
// | |||||
// You can read more here: | |||||
// https://on.cypress.io/configuration | |||||
// *********************************************************** | |||||
// Import commands.js using ES2015 syntax: | |||||
import './commands' | |||||
// Alternatively you can use CommonJS syntax: | |||||
// require('./commands') |
@@ -9,7 +9,6 @@ | |||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" | content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" | ||||
/> | /> | ||||
<link rel="shortcut icon" href="dist/favicon.ico" /> | <link rel="shortcut icon" href="dist/favicon.ico" /> | ||||
<link rel="stylesheet" href="dist/lesspass.min.css" /> | |||||
<style> | <style> | ||||
div.center { | div.center { | ||||
max-width: 420px; | max-width: 420px; | ||||
@@ -29,6 +28,5 @@ | |||||
<div class="center lesspass--full-width"> | <div class="center lesspass--full-width"> | ||||
<div id="lesspass"></div> | <div id="lesspass"></div> | ||||
</div> | </div> | ||||
<script src="dist/lesspass.min.js"></script> | |||||
<script type="text/javascript" src="lesspass.min.js"></script></body> | <script type="text/javascript" src="lesspass.min.js"></script></body> | ||||
</html> | </html> |
@@ -2336,344 +2336,249 @@ | |||||
clip: auto; | clip: auto; | ||||
} | } | ||||
button[data-balloon] { | |||||
:root { | |||||
--balloon-color: rgba(16, 16, 16, 0.95); | |||||
--balloon-font-size: 12px; | |||||
--balloon-move: 4px; } | |||||
button[aria-label][data-balloon-pos] { | |||||
overflow: visible; } | overflow: visible; } | ||||
[data-balloon] { | |||||
[aria-label][data-balloon-pos] { | |||||
position: relative; | position: relative; | ||||
cursor: pointer; } | cursor: pointer; } | ||||
[data-balloon]:after { | |||||
filter: alpha(opactiy=0); | |||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; | |||||
-moz-opacity: 0; | |||||
-khtml-opacity: 0; | |||||
[aria-label][data-balloon-pos]:after { | |||||
opacity: 0; | opacity: 0; | ||||
pointer-events: none; | pointer-events: none; | ||||
-webkit-transition: all 0.18s ease-out 0.18s; | |||||
-moz-transition: all 0.18s ease-out 0.18s; | |||||
-ms-transition: all 0.18s ease-out 0.18s; | |||||
-o-transition: all 0.18s ease-out 0.18s; | |||||
transition: all 0.18s ease-out 0.18s; | |||||
font-family: sans-serif !important; | |||||
font-weight: normal !important; | |||||
font-style: normal !important; | |||||
text-shadow: none !important; | |||||
font-size: 12px !important; | |||||
background: rgba(17, 17, 17, 0.9); | |||||
border-radius: 4px; | |||||
transition: all .18s ease-out .18s; | |||||
text-indent: 0; | |||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; | |||||
font-weight: normal; | |||||
font-style: normal; | |||||
text-shadow: none; | |||||
font-size: var(--balloon-font-size); | |||||
background: var(--balloon-color); | |||||
border-radius: 2px; | |||||
color: #fff; | color: #fff; | ||||
content: attr(data-balloon); | |||||
content: attr(aria-label); | |||||
padding: .5em 1em; | padding: .5em 1em; | ||||
position: absolute; | position: absolute; | ||||
white-space: nowrap; | white-space: nowrap; | ||||
z-index: 10; } | z-index: 10; } | ||||
[data-balloon]:before { | |||||
background: no-repeat url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba(17, 17, 17, 0.9)%22%20transform%3D%22rotate(0)%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E"); | |||||
background-size: 100% auto; | |||||
width: 18px; | |||||
height: 6px; | |||||
filter: alpha(opactiy=0); | |||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; | |||||
-moz-opacity: 0; | |||||
-khtml-opacity: 0; | |||||
[aria-label][data-balloon-pos]:before { | |||||
width: 0; | |||||
height: 0; | |||||
border: 5px solid transparent; | |||||
border-top-color: var(--balloon-color); | |||||
opacity: 0; | opacity: 0; | ||||
pointer-events: none; | pointer-events: none; | ||||
-webkit-transition: all 0.18s ease-out 0.18s; | |||||
-moz-transition: all 0.18s ease-out 0.18s; | |||||
-ms-transition: all 0.18s ease-out 0.18s; | |||||
-o-transition: all 0.18s ease-out 0.18s; | |||||
transition: all 0.18s ease-out 0.18s; | |||||
content: ''; | |||||
transition: all .18s ease-out .18s; | |||||
content: ""; | |||||
position: absolute; | position: absolute; | ||||
z-index: 10; } | z-index: 10; } | ||||
[data-balloon]:hover:before, [data-balloon]:hover:after, [data-balloon][data-balloon-visible]:before, [data-balloon][data-balloon-visible]:after { | |||||
filter: alpha(opactiy=100); | |||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; | |||||
-moz-opacity: 1; | |||||
-khtml-opacity: 1; | |||||
opacity: 1; | |||||
pointer-events: auto; } | |||||
[data-balloon].font-awesome:after { | |||||
font-family: FontAwesome; } | |||||
[data-balloon][data-balloon-break]:after { | |||||
white-space: pre; } | |||||
[data-balloon][data-balloon-blunt]:before, [data-balloon][data-balloon-blunt]:after { | |||||
-webkit-transition: none; | |||||
-moz-transition: none; | |||||
-ms-transition: none; | |||||
-o-transition: none; | |||||
transition: none; } | |||||
[data-balloon][data-balloon-pos="up"]:after { | |||||
bottom: 100%; | |||||
left: 50%; | |||||
margin-bottom: 11px; | |||||
-webkit-transform: translate(-50%, 10px); | |||||
-moz-transform: translate(-50%, 10px); | |||||
-ms-transform: translate(-50%, 10px); | |||||
transform: translate(-50%, 10px); | |||||
-webkit-transform-origin: top; | |||||
-moz-transform-origin: top; | |||||
-ms-transform-origin: top; | |||||
transform-origin: top; } | |||||
[data-balloon][data-balloon-pos="up"]:before { | |||||
bottom: 100%; | |||||
left: 50%; | |||||
margin-bottom: 5px; | |||||
-webkit-transform: translate(-50%, 10px); | |||||
-moz-transform: translate(-50%, 10px); | |||||
-ms-transform: translate(-50%, 10px); | |||||
transform: translate(-50%, 10px); | |||||
-webkit-transform-origin: top; | |||||
-moz-transform-origin: top; | |||||
-ms-transform-origin: top; | |||||
transform-origin: top; } | |||||
[data-balloon][data-balloon-pos="up"]:hover:after, [data-balloon][data-balloon-pos="up"][data-balloon-visible]:after { | |||||
-webkit-transform: translate(-50%, 0); | |||||
-moz-transform: translate(-50%, 0); | |||||
-ms-transform: translate(-50%, 0); | |||||
transform: translate(-50%, 0); } | |||||
[data-balloon][data-balloon-pos="up"]:hover:before, [data-balloon][data-balloon-pos="up"][data-balloon-visible]:before { | |||||
-webkit-transform: translate(-50%, 0); | |||||
-moz-transform: translate(-50%, 0); | |||||
-ms-transform: translate(-50%, 0); | |||||
transform: translate(-50%, 0); } | |||||
[data-balloon][data-balloon-pos="up-left"]:after { | |||||
bottom: 100%; | |||||
left: 0; | |||||
margin-bottom: 11px; | |||||
-webkit-transform: translate(0, 10px); | |||||
-moz-transform: translate(0, 10px); | |||||
-ms-transform: translate(0, 10px); | |||||
transform: translate(0, 10px); | |||||
-webkit-transform-origin: top; | |||||
-moz-transform-origin: top; | |||||
-ms-transform-origin: top; | |||||
transform-origin: top; } | |||||
[data-balloon][data-balloon-pos="up-left"]:before { | |||||
bottom: 100%; | |||||
left: 5px; | |||||
margin-bottom: 5px; | |||||
-webkit-transform: translate(0, 10px); | |||||
-moz-transform: translate(0, 10px); | |||||
-ms-transform: translate(0, 10px); | |||||
transform: translate(0, 10px); | |||||
-webkit-transform-origin: top; | |||||
-moz-transform-origin: top; | |||||
-ms-transform-origin: top; | |||||
transform-origin: top; } | |||||
[data-balloon][data-balloon-pos="up-left"]:hover:after, [data-balloon][data-balloon-pos="up-left"][data-balloon-visible]:after { | |||||
-webkit-transform: translate(0, 0); | |||||
-moz-transform: translate(0, 0); | |||||
-ms-transform: translate(0, 0); | |||||
transform: translate(0, 0); } | |||||
[data-balloon][data-balloon-pos="up-left"]:hover:before, [data-balloon][data-balloon-pos="up-left"][data-balloon-visible]:before { | |||||
-webkit-transform: translate(0, 0); | |||||
-moz-transform: translate(0, 0); | |||||
-ms-transform: translate(0, 0); | |||||
transform: translate(0, 0); } | |||||
[data-balloon][data-balloon-pos="up-right"]:after { | |||||
bottom: 100%; | |||||
right: 0; | |||||
margin-bottom: 11px; | |||||
-webkit-transform: translate(0, 10px); | |||||
-moz-transform: translate(0, 10px); | |||||
-ms-transform: translate(0, 10px); | |||||
transform: translate(0, 10px); | |||||
-webkit-transform-origin: top; | |||||
-moz-transform-origin: top; | |||||
-ms-transform-origin: top; | |||||
transform-origin: top; } | |||||
[data-balloon][data-balloon-pos="up-right"]:before { | |||||
bottom: 100%; | |||||
right: 5px; | |||||
margin-bottom: 5px; | |||||
-webkit-transform: translate(0, 10px); | |||||
-moz-transform: translate(0, 10px); | |||||
-ms-transform: translate(0, 10px); | |||||
transform: translate(0, 10px); | |||||
-webkit-transform-origin: top; | |||||
-moz-transform-origin: top; | |||||
-ms-transform-origin: top; | |||||
transform-origin: top; } | |||||
[data-balloon][data-balloon-pos="up-right"]:hover:after, [data-balloon][data-balloon-pos="up-right"][data-balloon-visible]:after { | |||||
-webkit-transform: translate(0, 0); | |||||
-moz-transform: translate(0, 0); | |||||
-ms-transform: translate(0, 0); | |||||
transform: translate(0, 0); } | |||||
[data-balloon][data-balloon-pos="up-right"]:hover:before, [data-balloon][data-balloon-pos="up-right"][data-balloon-visible]:before { | |||||
-webkit-transform: translate(0, 0); | |||||
-moz-transform: translate(0, 0); | |||||
-ms-transform: translate(0, 0); | |||||
transform: translate(0, 0); } | |||||
[data-balloon][data-balloon-pos='down']:after { | |||||
left: 50%; | |||||
margin-top: 11px; | |||||
top: 100%; | |||||
-webkit-transform: translate(-50%, -10px); | |||||
-moz-transform: translate(-50%, -10px); | |||||
-ms-transform: translate(-50%, -10px); | |||||
transform: translate(-50%, -10px); } | |||||
[data-balloon][data-balloon-pos='down']:before { | |||||
background: no-repeat url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba(17, 17, 17, 0.9)%22%20transform%3D%22rotate(180 18 6)%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E"); | |||||
background-size: 100% auto; | |||||
width: 18px; | |||||
height: 6px; | |||||
left: 50%; | |||||
margin-top: 5px; | |||||
top: 100%; | |||||
-webkit-transform: translate(-50%, -10px); | |||||
-moz-transform: translate(-50%, -10px); | |||||
-ms-transform: translate(-50%, -10px); | |||||
transform: translate(-50%, -10px); } | |||||
[data-balloon][data-balloon-pos='down']:hover:after, [data-balloon][data-balloon-pos='down'][data-balloon-visible]:after { | |||||
-webkit-transform: translate(-50%, 0); | |||||
-moz-transform: translate(-50%, 0); | |||||
-ms-transform: translate(-50%, 0); | |||||
transform: translate(-50%, 0); } | |||||
[data-balloon][data-balloon-pos='down']:hover:before, [data-balloon][data-balloon-pos='down'][data-balloon-visible]:before { | |||||
-webkit-transform: translate(-50%, 0); | |||||
-moz-transform: translate(-50%, 0); | |||||
-ms-transform: translate(-50%, 0); | |||||
transform: translate(-50%, 0); } | |||||
[data-balloon][data-balloon-pos='down-left']:after { | |||||
left: 0; | |||||
margin-top: 11px; | |||||
top: 100%; | |||||
-webkit-transform: translate(0, -10px); | |||||
-moz-transform: translate(0, -10px); | |||||
-ms-transform: translate(0, -10px); | |||||
transform: translate(0, -10px); } | |||||
[data-balloon][data-balloon-pos='down-left']:before { | |||||
background: no-repeat url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba(17, 17, 17, 0.9)%22%20transform%3D%22rotate(180 18 6)%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E"); | |||||
background-size: 100% auto; | |||||
width: 18px; | |||||
height: 6px; | |||||
left: 5px; | |||||
margin-top: 5px; | |||||
top: 100%; | |||||
-webkit-transform: translate(0, -10px); | |||||
-moz-transform: translate(0, -10px); | |||||
-ms-transform: translate(0, -10px); | |||||
transform: translate(0, -10px); } | |||||
[data-balloon][data-balloon-pos='down-left']:hover:after, [data-balloon][data-balloon-pos='down-left'][data-balloon-visible]:after { | |||||
-webkit-transform: translate(0, 0); | |||||
-moz-transform: translate(0, 0); | |||||
-ms-transform: translate(0, 0); | |||||
transform: translate(0, 0); } | |||||
[data-balloon][data-balloon-pos='down-left']:hover:before, [data-balloon][data-balloon-pos='down-left'][data-balloon-visible]:before { | |||||
-webkit-transform: translate(0, 0); | |||||
-moz-transform: translate(0, 0); | |||||
-ms-transform: translate(0, 0); | |||||
transform: translate(0, 0); } | |||||
[data-balloon][data-balloon-pos='down-right']:after { | |||||
right: 0; | |||||
margin-top: 11px; | |||||
top: 100%; | |||||
-webkit-transform: translate(0, -10px); | |||||
-moz-transform: translate(0, -10px); | |||||
-ms-transform: translate(0, -10px); | |||||
transform: translate(0, -10px); } | |||||
[data-balloon][data-balloon-pos='down-right']:before { | |||||
background: no-repeat url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba(17, 17, 17, 0.9)%22%20transform%3D%22rotate(180 18 6)%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E"); | |||||
background-size: 100% auto; | |||||
width: 18px; | |||||
height: 6px; | |||||
right: 5px; | |||||
margin-top: 5px; | |||||
top: 100%; | |||||
-webkit-transform: translate(0, -10px); | |||||
-moz-transform: translate(0, -10px); | |||||
-ms-transform: translate(0, -10px); | |||||
transform: translate(0, -10px); } | |||||
[data-balloon][data-balloon-pos='down-right']:hover:after, [data-balloon][data-balloon-pos='down-right'][data-balloon-visible]:after { | |||||
-webkit-transform: translate(0, 0); | |||||
-moz-transform: translate(0, 0); | |||||
-ms-transform: translate(0, 0); | |||||
transform: translate(0, 0); } | |||||
[data-balloon][data-balloon-pos='down-right']:hover:before, [data-balloon][data-balloon-pos='down-right'][data-balloon-visible]:before { | |||||
-webkit-transform: translate(0, 0); | |||||
-moz-transform: translate(0, 0); | |||||
-ms-transform: translate(0, 0); | |||||
transform: translate(0, 0); } | |||||
[data-balloon][data-balloon-pos='left']:after { | |||||
margin-right: 11px; | |||||
right: 100%; | |||||
top: 50%; | |||||
-webkit-transform: translate(10px, -50%); | |||||
-moz-transform: translate(10px, -50%); | |||||
-ms-transform: translate(10px, -50%); | |||||
transform: translate(10px, -50%); } | |||||
[data-balloon][data-balloon-pos='left']:before { | |||||
background: no-repeat url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba(17, 17, 17, 0.9)%22%20transform%3D%22rotate(-90 18 18)%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E"); | |||||
background-size: 100% auto; | |||||
width: 6px; | |||||
height: 18px; | |||||
margin-right: 5px; | |||||
right: 100%; | |||||
top: 50%; | |||||
-webkit-transform: translate(10px, -50%); | |||||
-moz-transform: translate(10px, -50%); | |||||
-ms-transform: translate(10px, -50%); | |||||
transform: translate(10px, -50%); } | |||||
[data-balloon][data-balloon-pos='left']:hover:after, [data-balloon][data-balloon-pos='left'][data-balloon-visible]:after { | |||||
-webkit-transform: translate(0, -50%); | |||||
-moz-transform: translate(0, -50%); | |||||
-ms-transform: translate(0, -50%); | |||||
transform: translate(0, -50%); } | |||||
[data-balloon][data-balloon-pos='left']:hover:before, [data-balloon][data-balloon-pos='left'][data-balloon-visible]:before { | |||||
-webkit-transform: translate(0, -50%); | |||||
-moz-transform: translate(0, -50%); | |||||
-ms-transform: translate(0, -50%); | |||||
transform: translate(0, -50%); } | |||||
[data-balloon][data-balloon-pos='right']:after { | |||||
left: 100%; | |||||
margin-left: 11px; | |||||
top: 50%; | |||||
-webkit-transform: translate(-10px, -50%); | |||||
-moz-transform: translate(-10px, -50%); | |||||
-ms-transform: translate(-10px, -50%); | |||||
transform: translate(-10px, -50%); } | |||||
[data-balloon][data-balloon-pos='right']:before { | |||||
background: no-repeat url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba(17, 17, 17, 0.9)%22%20transform%3D%22rotate(90 6 6)%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E"); | |||||
background-size: 100% auto; | |||||
width: 6px; | |||||
height: 18px; | |||||
left: 100%; | |||||
margin-left: 5px; | |||||
top: 50%; | |||||
-webkit-transform: translate(-10px, -50%); | |||||
-moz-transform: translate(-10px, -50%); | |||||
-ms-transform: translate(-10px, -50%); | |||||
transform: translate(-10px, -50%); } | |||||
[data-balloon][data-balloon-pos='right']:hover:after, [data-balloon][data-balloon-pos='right'][data-balloon-visible]:after { | |||||
-webkit-transform: translate(0, -50%); | |||||
-moz-transform: translate(0, -50%); | |||||
-ms-transform: translate(0, -50%); | |||||
transform: translate(0, -50%); } | |||||
[data-balloon][data-balloon-pos='right']:hover:before, [data-balloon][data-balloon-pos='right'][data-balloon-visible]:before { | |||||
-webkit-transform: translate(0, -50%); | |||||
-moz-transform: translate(0, -50%); | |||||
-ms-transform: translate(0, -50%); | |||||
transform: translate(0, -50%); } | |||||
[data-balloon][data-balloon-length='small']:after { | |||||
white-space: normal; | |||||
width: 80px; } | |||||
[data-balloon][data-balloon-length='medium']:after { | |||||
white-space: normal; | |||||
width: 150px; } | |||||
[data-balloon][data-balloon-length='large']:after { | |||||
white-space: normal; | |||||
width: 260px; } | |||||
[data-balloon][data-balloon-length='xlarge']:after { | |||||
white-space: normal; | |||||
width: 380px; } | |||||
@media screen and (max-width: 768px) { | |||||
[data-balloon][data-balloon-length='xlarge']:after { | |||||
white-space: normal; | |||||
width: 90vw; } } | |||||
[data-balloon][data-balloon-length='fit']:after { | |||||
white-space: normal; | |||||
width: 100%; } | |||||
[aria-label]:hover:before, [aria-label]:hover:after, [aria-label][data-balloon-visible]:before, [aria-label][data-balloon-visible]:after, [aria-label]:not([data-balloon-nofocus]):focus:before, [aria-label]:not([data-balloon-nofocus]):focus:after { | |||||
opacity: 1; | |||||
pointer-events: none; } | |||||
[aria-label].font-awesome:after { | |||||
font-family: FontAwesome, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; } | |||||
[aria-label][data-balloon-break]:after { | |||||
white-space: pre; } | |||||
[aria-label][data-balloon-break][data-balloon-length]:after { | |||||
white-space: pre-line; | |||||
word-break: break-word; } | |||||
[aria-label][data-balloon-blunt]:before, [aria-label][data-balloon-blunt]:after { | |||||
transition: none; } | |||||
[aria-label][data-balloon-pos="up"]:after { | |||||
bottom: 100%; | |||||
left: 50%; | |||||
margin-bottom: 10px; | |||||
transform: translate(-50%, var(--balloon-move)); | |||||
transform-origin: top; } | |||||
[aria-label][data-balloon-pos="up"]:before { | |||||
bottom: 100%; | |||||
left: 50%; | |||||
transform: translate(-50%, var(--balloon-move)); | |||||
transform-origin: top; } | |||||
[aria-label][data-balloon-pos="up"]:hover:after, [aria-label][data-balloon-pos="up"][data-balloon-visible]:after { | |||||
transform: translate(-50%, 0); } | |||||
[aria-label][data-balloon-pos="up"]:hover:before, [aria-label][data-balloon-pos="up"][data-balloon-visible]:before { | |||||
transform: translate(-50%, 0); } | |||||
[aria-label][data-balloon-pos="up-left"]:after { | |||||
bottom: 100%; | |||||
left: 0; | |||||
margin-bottom: 10px; | |||||
transform: translate(0, var(--balloon-move)); | |||||
transform-origin: top; } | |||||
[aria-label][data-balloon-pos="up-left"]:before { | |||||
bottom: 100%; | |||||
left: 5px; | |||||
transform: translate(0, var(--balloon-move)); | |||||
transform-origin: top; } | |||||
[aria-label][data-balloon-pos="up-left"]:hover:after, [aria-label][data-balloon-pos="up-left"][data-balloon-visible]:after { | |||||
transform: translate(0, 0); } | |||||
[aria-label][data-balloon-pos="up-left"]:hover:before, [aria-label][data-balloon-pos="up-left"][data-balloon-visible]:before { | |||||
transform: translate(0, 0); } | |||||
[aria-label][data-balloon-pos="up-right"]:after { | |||||
bottom: 100%; | |||||
right: 0; | |||||
margin-bottom: 10px; | |||||
transform: translate(0, var(--balloon-move)); | |||||
transform-origin: top; } | |||||
[aria-label][data-balloon-pos="up-right"]:before { | |||||
bottom: 100%; | |||||
right: 5px; | |||||
transform: translate(0, var(--balloon-move)); | |||||
transform-origin: top; } | |||||
[aria-label][data-balloon-pos="up-right"]:hover:after, [aria-label][data-balloon-pos="up-right"][data-balloon-visible]:after { | |||||
transform: translate(0, 0); } | |||||
[aria-label][data-balloon-pos="up-right"]:hover:before, [aria-label][data-balloon-pos="up-right"][data-balloon-visible]:before { | |||||
transform: translate(0, 0); } | |||||
[aria-label][data-balloon-pos="down"]:after { | |||||
left: 50%; | |||||
margin-top: 10px; | |||||
top: 100%; | |||||
transform: translate(-50%, calc(var(--balloon-move) * -1)); } | |||||
[aria-label][data-balloon-pos="down"]:before { | |||||
width: 0; | |||||
height: 0; | |||||
border: 5px solid transparent; | |||||
border-bottom-color: var(--balloon-color); | |||||
left: 50%; | |||||
top: 100%; | |||||
transform: translate(-50%, calc(var(--balloon-move) * -1)); } | |||||
[aria-label][data-balloon-pos="down"]:hover:after, [aria-label][data-balloon-pos="down"][data-balloon-visible]:after { | |||||
transform: translate(-50%, 0); } | |||||
[aria-label][data-balloon-pos="down"]:hover:before, [aria-label][data-balloon-pos="down"][data-balloon-visible]:before { | |||||
transform: translate(-50%, 0); } | |||||
[aria-label][data-balloon-pos="down-left"]:after { | |||||
left: 0; | |||||
margin-top: 10px; | |||||
top: 100%; | |||||
transform: translate(0, calc(var(--balloon-move) * -1)); } | |||||
[aria-label][data-balloon-pos="down-left"]:before { | |||||
width: 0; | |||||
height: 0; | |||||
border: 5px solid transparent; | |||||
border-bottom-color: var(--balloon-color); | |||||
left: 5px; | |||||
top: 100%; | |||||
transform: translate(0, calc(var(--balloon-move) * -1)); } | |||||
[aria-label][data-balloon-pos="down-left"]:hover:after, [aria-label][data-balloon-pos="down-left"][data-balloon-visible]:after { | |||||
transform: translate(0, 0); } | |||||
[aria-label][data-balloon-pos="down-left"]:hover:before, [aria-label][data-balloon-pos="down-left"][data-balloon-visible]:before { | |||||
transform: translate(0, 0); } | |||||
[aria-label][data-balloon-pos="down-right"]:after { | |||||
right: 0; | |||||
margin-top: 10px; | |||||
top: 100%; | |||||
transform: translate(0, calc(var(--balloon-move) * -1)); } | |||||
[aria-label][data-balloon-pos="down-right"]:before { | |||||
width: 0; | |||||
height: 0; | |||||
border: 5px solid transparent; | |||||
border-bottom-color: var(--balloon-color); | |||||
right: 5px; | |||||
top: 100%; | |||||
transform: translate(0, calc(var(--balloon-move) * -1)); } | |||||
[aria-label][data-balloon-pos="down-right"]:hover:after, [aria-label][data-balloon-pos="down-right"][data-balloon-visible]:after { | |||||
transform: translate(0, 0); } | |||||
[aria-label][data-balloon-pos="down-right"]:hover:before, [aria-label][data-balloon-pos="down-right"][data-balloon-visible]:before { | |||||
transform: translate(0, 0); } | |||||
[aria-label][data-balloon-pos="left"]:after { | |||||
margin-right: 10px; | |||||
right: 100%; | |||||
top: 50%; | |||||
transform: translate(var(--balloon-move), -50%); } | |||||
[aria-label][data-balloon-pos="left"]:before { | |||||
width: 0; | |||||
height: 0; | |||||
border: 5px solid transparent; | |||||
border-left-color: var(--balloon-color); | |||||
right: 100%; | |||||
top: 50%; | |||||
transform: translate(var(--balloon-move), -50%); } | |||||
[aria-label][data-balloon-pos="left"]:hover:after, [aria-label][data-balloon-pos="left"][data-balloon-visible]:after { | |||||
transform: translate(0, -50%); } | |||||
[aria-label][data-balloon-pos="left"]:hover:before, [aria-label][data-balloon-pos="left"][data-balloon-visible]:before { | |||||
transform: translate(0, -50%); } | |||||
[aria-label][data-balloon-pos="right"]:after { | |||||
left: 100%; | |||||
margin-left: 10px; | |||||
top: 50%; | |||||
transform: translate(calc(var(--balloon-move) * -1), -50%); } | |||||
[aria-label][data-balloon-pos="right"]:before { | |||||
width: 0; | |||||
height: 0; | |||||
border: 5px solid transparent; | |||||
border-right-color: var(--balloon-color); | |||||
left: 100%; | |||||
top: 50%; | |||||
transform: translate(calc(var(--balloon-move) * -1), -50%); } | |||||
[aria-label][data-balloon-pos="right"]:hover:after, [aria-label][data-balloon-pos="right"][data-balloon-visible]:after { | |||||
transform: translate(0, -50%); } | |||||
[aria-label][data-balloon-pos="right"]:hover:before, [aria-label][data-balloon-pos="right"][data-balloon-visible]:before { | |||||
transform: translate(0, -50%); } | |||||
[aria-label][data-balloon-length="small"]:after { | |||||
white-space: normal; | |||||
width: 80px; } | |||||
[aria-label][data-balloon-length="medium"]:after { | |||||
white-space: normal; | |||||
width: 150px; } | |||||
[aria-label][data-balloon-length="large"]:after { | |||||
white-space: normal; | |||||
width: 260px; } | |||||
[aria-label][data-balloon-length="xlarge"]:after { | |||||
white-space: normal; | |||||
width: 380px; } | |||||
@media screen and (max-width: 768px) { | |||||
[aria-label][data-balloon-length="xlarge"]:after { | |||||
white-space: normal; | |||||
width: 90vw; } } | |||||
[aria-label][data-balloon-length="fit"]:after { | |||||
white-space: normal; | |||||
width: 100%; } | |||||
.awesomplete [hidden] { | .awesomplete [hidden] { | ||||
display: none; | display: none; | ||||
@@ -5,13 +5,15 @@ | |||||
"license": "GPL-3.0", | "license": "GPL-3.0", | ||||
"author": "Guillaume Vincent <guillaume@oslab.fr>", | "author": "Guillaume Vincent <guillaume@oslab.fr>", | ||||
"scripts": { | "scripts": { | ||||
"start": "NODE_ENV=production node server.js", | |||||
"start": "http-server dist -p 8080", | |||||
"build": "rm -rf dist && NODE_ENV=production webpack --mode=production --config webpack.prod.js", | "build": "rm -rf dist && NODE_ENV=production webpack --mode=production --config webpack.prod.js", | ||||
"build:i18n": "cd scripts && node buildI18n.js", | "build:i18n": "cd scripts && node buildI18n.js", | ||||
"dev": "webpack-dev-server --config webpack.dev.js", | "dev": "webpack-dev-server --config webpack.dev.js", | ||||
"test": "npm run test:unit", | |||||
"nightwatch": "^1.1.13", | |||||
"cypress:run": "cypress run", | |||||
"test": "npm run test:unit && npm run test:e2e", | |||||
"test:unit": "jest", | "test:unit": "jest", | ||||
"test:e2e": "node test/e2e/runner.js" | |||||
"test:e2e": "npm run build && start-server-and-test start http://localhost:8080 cypress:run" | |||||
}, | }, | ||||
"babel": { | "babel": { | ||||
"presets": [ | "presets": [ | ||||
@@ -21,17 +23,22 @@ | |||||
"@babel/plugin-proposal-object-rest-spread" | "@babel/plugin-proposal-object-rest-spread" | ||||
] | ] | ||||
}, | }, | ||||
"jest": { | |||||
"testPathIgnorePatterns": [ | |||||
"cypress" | |||||
] | |||||
}, | |||||
"dependencies": { | "dependencies": { | ||||
"@oslab/atob": "0.1.0", | "@oslab/atob": "0.1.0", | ||||
"@oslab/btoa": "0.1.0", | "@oslab/btoa": "0.1.0", | ||||
"awesomplete": "^1.1.4", | "awesomplete": "^1.1.4", | ||||
"axios": "0.18.0", | |||||
"balloon-css": "0.5.0", | |||||
"axios": "^0.19.0", | |||||
"balloon-css": "^1.0.2", | |||||
"bootstrap": "^4.3.1", | "bootstrap": "^4.3.1", | ||||
"copy-text-to-clipboard": "^2.0.0", | "copy-text-to-clipboard": "^2.0.0", | ||||
"font-awesome": "4.7.0", | "font-awesome": "4.7.0", | ||||
"jwt-decode": "2.2.0", | "jwt-decode": "2.2.0", | ||||
"lesspass": "6.0.0", | |||||
"lesspass": "^8.1.1", | |||||
"lodash.debounce": "4.0.8", | "lodash.debounce": "4.0.8", | ||||
"lodash.uniqby": "4.7.0", | "lodash.uniqby": "4.7.0", | ||||
"vue": "^2.6.10", | "vue": "^2.6.10", | ||||
@@ -41,51 +48,5 @@ | |||||
"vuex": "^3.1.0", | "vuex": "^3.1.0", | ||||
"vuex-persistedstate": "^2.5.4", | "vuex-persistedstate": "^2.5.4", | ||||
"vuex-router-sync": "5.0.0" | "vuex-router-sync": "5.0.0" | ||||
}, | |||||
"devDependencies": { | |||||
"@babel/core": "^7.1.6", | |||||
"@babel/plugin-proposal-object-rest-spread": "^7.0.0", | |||||
"@babel/plugin-transform-object-assign": "^7.0.0", | |||||
"@babel/plugin-transform-react-jsx": "^7.1.6", | |||||
"@babel/preset-env": "^7.1.6", | |||||
"@babel/register": "^7.4.0", | |||||
"axios-mock-adapter": "^1.16.0", | |||||
"babel-loader": "^8.0.5", | |||||
"chromedriver": "^2.46.0", | |||||
"clean-webpack-plugin": "^2.0.1", | |||||
"copy-webpack-plugin": "^5.0.1", | |||||
"cross-env": "^5.2.0", | |||||
"css-loader": "^2.1.1", | |||||
"express": "^4.16.4", | |||||
"file-loader": "^3.0.1", | |||||
"html-webpack-plugin": "^3.2.0", | |||||
"mini-css-extract-plugin": "^0.5.0", | |||||
"nightwatch": "^1.0.19", | |||||
"nock": "^10.0.6", | |||||
"node-sass": "^4.11.0", | |||||
"postcss-loader": "^3.0.0", | |||||
"prettier": "^1.16.4", | |||||
"raw-loader": "^2.0.0", | |||||
"sass-loader": "^7.1.0", | |||||
"style-loader": "^0.23.1", | |||||
"timekeeper": "^2.2.0", | |||||
"url-loader": "^1.1.2", | |||||
"vue-loader": "^15.7.0", | |||||
"vue-polyglot-utils": "0.1.1", | |||||
"vue-template-compiler": "^2.6.10", | |||||
"walk": "^2.3.14", | |||||
"webpack": "^4.29.6", | |||||
"webpack-cli": "^3.3.0", | |||||
"webpack-dev-server": "^3.2.1", | |||||
"webpack-merge": "^4.2.1" | |||||
}, | |||||
"ava": { | |||||
"require": [ | |||||
"@babel/register" | |||||
], | |||||
"babel": "inherit", | |||||
"files": [ | |||||
"test/unit/**/*.js" | |||||
] | |||||
} | } | ||||
} | } |
@@ -1,22 +0,0 @@ | |||||
var express = require('express'); | |||||
var app = express(); | |||||
app.use('/', express.static(__dirname + '/dist')); | |||||
var _resolve; | |||||
var readyPromise = new Promise(resolve => { | |||||
_resolve = resolve | |||||
}); | |||||
var server = app.listen(8080, function() { | |||||
console.log('frontend listening on port 8080'); | |||||
_resolve(); | |||||
}); | |||||
module.exports = { | |||||
ready: readyPromise, | |||||
close: () => { | |||||
server.close() | |||||
} | |||||
}; |
@@ -73,7 +73,7 @@ | |||||
}, | }, | ||||
data(){ | data(){ | ||||
return { | return { | ||||
fingerprint: '', | |||||
fingerprint: null, | |||||
icon1: '', | icon1: '', | ||||
icon2: '', | icon2: '', | ||||
icon3: '', | icon3: '', | ||||
@@ -113,19 +113,16 @@ | |||||
}, | }, | ||||
setFingerprint(password){ | setFingerprint(password){ | ||||
LessPass.createFingerprint(password).then(fingerprint => { | LessPass.createFingerprint(password).then(fingerprint => { | ||||
this.fingerprint = fingerprint; | |||||
this.icon1 = fingerprint[0].icon; | |||||
this.color1 = fingerprint[0].color; | |||||
const hash1 = fingerprint.substring(0, 6); | |||||
this.icon1 = this.getIcon(hash1); | |||||
this.color1 = this.getColor(hash1); | |||||
this.icon2 = fingerprint[1].icon; | |||||
this.color2 = fingerprint[1].color; | |||||
const hash2 = fingerprint.substring(6, 12); | |||||
this.icon2 = this.getIcon(hash2); | |||||
this.color2 = this.getColor(hash2); | |||||
this.icon3 = fingerprint[2].icon; | |||||
this.color3 = fingerprint[2].color; | |||||
const hash3 = fingerprint.substring(12, 18); | |||||
this.icon3 = this.getIcon(hash3); | |||||
this.color3 = this.getColor(hash3); | |||||
this.fingerprint = fingerprint; | |||||
}); | }); | ||||
}, | }, | ||||
showRealFingerprint: debounce(function(password) { | showRealFingerprint: debounce(function(password) { | ||||
@@ -9,7 +9,6 @@ | |||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" | content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" | ||||
/> | /> | ||||
<link rel="shortcut icon" href="dist/favicon.ico" /> | <link rel="shortcut icon" href="dist/favicon.ico" /> | ||||
<link rel="stylesheet" href="dist/lesspass.min.css" /> | |||||
<style> | <style> | ||||
div.center { | div.center { | ||||
max-width: 420px; | max-width: 420px; | ||||
@@ -29,6 +28,5 @@ | |||||
<div class="center lesspass--full-width"> | <div class="center lesspass--full-width"> | ||||
<div id="lesspass"></div> | <div id="lesspass"></div> | ||||
</div> | </div> | ||||
<script src="dist/lesspass.min.js"></script> | |||||
</body> | </body> | ||||
</html> | </html> |
@@ -223,12 +223,17 @@ export default { | |||||
}); | }); | ||||
}, | }, | ||||
focusBestInputField() { | focusBestInputField() { | ||||
const site = this.$refs.site.$refs.siteField; | |||||
const login = this.$refs.login; | |||||
const masterPassword = this.$refs.masterPassword; | |||||
if (site && !site.value) return void site.focus(); | |||||
if (login && !login.value) return void login.focus(); | |||||
masterPassword.$refs.passwordField.focus(); | |||||
try { | |||||
const site = this.$refs.site.$refs.siteField; | |||||
const login = this.$refs.login; | |||||
const masterPassword = this.$refs.masterPassword; | |||||
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") | |||||
} | |||||
}, | }, | ||||
copyPassword() { | copyPassword() { | ||||
const copied = copy(this.passwordGenerated); | const copied = copy(this.passwordGenerated); | ||||
@@ -1,12 +0,0 @@ | |||||
var chromedriver = require("chromedriver"); | |||||
module.exports = { | |||||
before: function(done) { | |||||
chromedriver.start(); | |||||
done(); | |||||
}, | |||||
after: function() { | |||||
chromedriver.stop(); | |||||
} | |||||
}; |
@@ -1,32 +0,0 @@ | |||||
module.exports = { | |||||
src_folders: ["test/e2e/specs"], | |||||
output_folder: "test/e2e/reports", | |||||
globals_path: "test/e2e/globals.js", | |||||
selenium: { | |||||
start_process: false | |||||
}, | |||||
test_settings: { | |||||
default: { | |||||
launch_url: "http://localhost:8080", | |||||
selenium_port: 9515, | |||||
selenium_host: "localhost", | |||||
default_path_prefix: "", | |||||
globals: { | |||||
waitForConditionTimeout: 5000 | |||||
}, | |||||
desiredCapabilities: { | |||||
browserName: "chrome", | |||||
chromeOptions: { | |||||
args: [ | |||||
"--headless", | |||||
"--no-sandbox", | |||||
"--disable-gpu", | |||||
"--allow-running-insecure-content", | |||||
"--ignore-certificate-errors", | |||||
"--window-size=1920x1080" | |||||
] | |||||
} | |||||
} | |||||
} | |||||
} | |||||
}; |
@@ -1,24 +0,0 @@ | |||||
process.env.NODE_ENV = "testing"; | |||||
var child_process = require("child_process"); | |||||
var server = require("../../server.js"); | |||||
server.ready.then(() => { | |||||
var opts = process.argv.slice(2); | |||||
if (opts.indexOf("--config") === -1) { | |||||
opts = opts.concat(["--config", "test/e2e/nightwatch.conf.js"]); | |||||
} | |||||
var runner = child_process.spawn("./node_modules/.bin/nightwatch", opts, { | |||||
stdio: "inherit" | |||||
}); | |||||
runner.on("exit", function(code) { | |||||
server.close(); | |||||
process.exit(code); | |||||
}); | |||||
runner.on("error", function(err) { | |||||
server.close(); | |||||
throw err; | |||||
}); | |||||
}); |
@@ -1,32 +0,0 @@ | |||||
var assert = require("assert"); | |||||
module.exports = { | |||||
"Can save password profile": function(browser) { | |||||
browser | |||||
.url(browser.launch_url) | |||||
.waitForElementVisible(".fa-sign-in") | |||||
.click(".fa-sign-in") | |||||
.setValue("#email", "test@lesspass.com") | |||||
.setValue("#passwordField", "test@lesspass.com") | |||||
.waitForElementVisible("#encryptMasterPassword__btn") | |||||
.click("#encryptMasterPassword__btn") | |||||
.waitForElementVisible("#fingerprint .fa-university") | |||||
.waitForElementVisible("#fingerprint .fa-btc") | |||||
.waitForElementVisible("#fingerprint .fa-subway") | |||||
.waitForElementVisible("#signInButton") | |||||
.click("#signInButton") | |||||
.waitForElementVisible("#siteField") | |||||
.setValue("#siteField", "lesspass.com") | |||||
.pause(100) | |||||
.setValue("#login", "test@lesspass.com") | |||||
.pause(100) | |||||
.setValue("#passwordField", "test@lesspass.com") | |||||
.pause(100) | |||||
.waitForElementVisible("#generatePassword__btn") | |||||
.click("#generatePassword__btn") | |||||
.waitForElementVisible("#generated-password") | |||||
.waitForElementVisible(".fa-save"); | |||||
browser.end(); | |||||
} | |||||
}; |
@@ -1,27 +0,0 @@ | |||||
var assert = require("assert"); | |||||
module.exports = { | |||||
"Login logout tests": function(browser) { | |||||
browser | |||||
.url(browser.launch_url) | |||||
.waitForElementVisible(".fa-sign-in") | |||||
.click(".fa-sign-in") | |||||
.waitForElementVisible("#baseURL") | |||||
.assert.value("#baseURL", "https://lesspass.com") | |||||
.setValue("#email", "test@lesspass.com") | |||||
.setValue("#passwordField", "test@lesspass.com") | |||||
.waitForElementVisible("#fingerprint .fa-cutlery") | |||||
.waitForElementVisible("#fingerprint .fa-subway") | |||||
.waitForElementVisible("#fingerprint .fa-plane") | |||||
.waitForElementVisible("#encryptMasterPassword__btn") | |||||
.click("#encryptMasterPassword__btn") | |||||
.waitForElementVisible("#fingerprint .fa-university") | |||||
.waitForElementVisible("#fingerprint .fa-btc") | |||||
.waitForElementVisible("#fingerprint .fa-subway") | |||||
.waitForElementVisible("#signInButton") | |||||
.click("#signInButton") | |||||
.waitForElementVisible("#siteField"); | |||||
browser.end(); | |||||
} | |||||
}; |
@@ -1,82 +0,0 @@ | |||||
var assert = require("assert"); | |||||
function clickAndAssertOption(browser, button, password1, password2) { | |||||
browser | |||||
.waitForElementVisible(button) | |||||
.click(button) | |||||
.click("#generatePassword__btn") | |||||
.waitForElementVisible("#generated-password") | |||||
.assert.value("#generated-password", password1) | |||||
.click(button) | |||||
.click("#generatePassword__btn") | |||||
.waitForElementVisible("#generated-password") | |||||
.assert.value("#generated-password", password2); | |||||
} | |||||
module.exports = { | |||||
"Password generation tests": function(browser) { | |||||
browser | |||||
.url(browser.launch_url) | |||||
.waitForElementVisible("#siteField") | |||||
.setValue("#siteField", "lesspass.com") | |||||
.setValue("#login", "test@lesspass.com") | |||||
.setValue("#passwordField", "test@lesspass.com") | |||||
.waitForElementVisible("#fingerprint .fa-cutlery") | |||||
.waitForElementVisible("#fingerprint .fa-subway") | |||||
.waitForElementVisible("#fingerprint .fa-plane") | |||||
.waitForElementVisible("#generatePassword__btn") | |||||
.click("#generatePassword__btn") | |||||
.waitForElementVisible("#generated-password") | |||||
.assert.value("#generated-password", "hjV@\\5ULp3bIs,6B") | |||||
.click(".showOptions__btn") | |||||
.waitForElementVisible("#decreaseLength__btn") | |||||
.click("#decreaseLength__btn") | |||||
.assert.value("#passwordLength", "15") | |||||
.click("#generatePassword__btn") | |||||
.waitForElementVisible("#generated-password") | |||||
.assert.value("#generated-password", "h6j@r\\ULp3!CIs6") | |||||
.click("#increaseLength__btn") | |||||
.assert.value("#passwordLength", "16") | |||||
.click("#generatePassword__btn") | |||||
.waitForElementVisible("#generated-password") | |||||
.assert.value("#generated-password", "hjV@\\5ULp3bIs,6B") | |||||
.waitForElementVisible("#decreaseCounter__btn") | |||||
.click("#increaseCounter__btn") | |||||
.assert.value("#passwordCounter", "2") | |||||
.click("#generatePassword__btn") | |||||
.waitForElementVisible("#generated-password") | |||||
.assert.value("#generated-password", "#wOxv!q;URh:k82(") | |||||
.click("#decreaseCounter__btn") | |||||
.assert.value("#passwordCounter", "1") | |||||
.click("#generatePassword__btn") | |||||
.waitForElementVisible("#generated-password") | |||||
.assert.value("#generated-password", "hjV@\\5ULp3bIs,6B"); | |||||
clickAndAssertOption( | |||||
browser, | |||||
"#lowercase__btn", | |||||
"^>_9>+}OV?[3[_U,", | |||||
"hjV@\\5ULp3bIs,6B" | |||||
); | |||||
clickAndAssertOption( | |||||
browser, | |||||
"#uppercase__btn", | |||||
"^>_9>+}ov?[3[_u,", | |||||
"hjV@\\5ULp3bIs,6B" | |||||
); | |||||
clickAndAssertOption( | |||||
browser, | |||||
"#numbers__btn", | |||||
'jCmMpNy=T."+u^ZQ', | |||||
"hjV@\\5ULp3bIs,6B" | |||||
); | |||||
clickAndAssertOption( | |||||
browser, | |||||
"#symbols__btn", | |||||
"XAwlOl5mtjGSY6PA", | |||||
"hjV@\\5ULp3bIs,6B" | |||||
); | |||||
browser.end(); | |||||
} | |||||
}; |
@@ -1,16 +0,0 @@ | |||||
var assert = require("assert"); | |||||
module.exports = { | |||||
"Password generation key press non regression test #266": function(browser) { | |||||
browser | |||||
.url(browser.launch_url) | |||||
.waitForElementVisible("#siteField") | |||||
.setValue("#siteField", "lesspass.com") | |||||
.setValue("#login", "test@lesspass.com") | |||||
.setValue("#passwordField", ["test@lesspass.com", browser.Keys.ENTER]) | |||||
.waitForElementVisible("#generated-password") | |||||
.assert.value("#generated-password", "hjV@\\5ULp3bIs,6B"); | |||||
browser.end(); | |||||
} | |||||
}; |
@@ -1,26 +0,0 @@ | |||||
var assert = require("assert"); | |||||
module.exports = { | |||||
"Password reset": function(browser) { | |||||
browser | |||||
.url(browser.launch_url) | |||||
.waitForElementVisible(".fa-sign-in") | |||||
.click(".fa-sign-in") | |||||
.waitForElementVisible("#login__forgot-password-btn") | |||||
.click("#login__forgot-password-btn") | |||||
.waitForElementVisible("#password-reset__reset-password-btn") | |||||
.click("#password-reset__reset-password-btn"); | |||||
browser.getLog(function(logs) { | |||||
logs = logs.filter(function(log) { | |||||
return ["DEBUG", "INFO"].indexOf(log.level) === -1; | |||||
}); | |||||
assert( | |||||
logs.length === 0, | |||||
"Console log error(s):\n" + JSON.stringify(logs, null, 2) | |||||
); | |||||
}); | |||||
browser.end(); | |||||
} | |||||
}; |
@@ -1,25 +0,0 @@ | |||||
var assert = require("assert"); | |||||
module.exports = { | |||||
"User set saved profile": function(browser) { | |||||
browser | |||||
.url(browser.launch_url) | |||||
.waitForElementVisible(".fa-sign-in") | |||||
.click(".fa-sign-in") | |||||
.setValue("#email", "test@lesspass.com") | |||||
.setValue("#passwordField", "test@lesspass.com") | |||||
.waitForElementVisible("#encryptMasterPassword__btn") | |||||
.click("#encryptMasterPassword__btn") | |||||
.waitForElementVisible("#signInButton") | |||||
.click("#signInButton") | |||||
.waitForElementVisible(".fa-key") | |||||
.click(".fa-key") | |||||
.waitForElementVisible(".passwordProfile__meta") | |||||
.click(".passwordProfile__meta") | |||||
.waitForElementVisible("#siteField") | |||||
.assert.value("#siteField", "example.org") | |||||
.assert.value("#login", "contact@example.org"); | |||||
browser.end(); | |||||
} | |||||
}; |
@@ -1,7 +1,7 @@ | |||||
const path = require("path"); | const path = require("path"); | ||||
const webpack = require("webpack"); | const webpack = require("webpack"); | ||||
const HtmlWebpackPlugin = require("html-webpack-plugin"); | const HtmlWebpackPlugin = require("html-webpack-plugin"); | ||||
const CleanWebpackPlugin = require("clean-webpack-plugin"); | |||||
const { CleanWebpackPlugin } = require("clean-webpack-plugin"); | |||||
const VueLoaderPlugin = require("vue-loader/lib/plugin"); | const VueLoaderPlugin = require("vue-loader/lib/plugin"); | ||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); | ||||
const productionMode = process.env.NODE_ENV === "production"; | const productionMode = process.env.NODE_ENV === "production"; | ||||
@@ -2,7 +2,7 @@ const bigInt = require("big-integer"); | |||||
const chars = require("./chars"); | const chars = require("./chars"); | ||||
const { consumeEntropy } = require("./entropy"); | const { consumeEntropy } = require("./entropy"); | ||||
module.exports = function renderPassword(entropy, options) { | |||||
function renderPassword(entropy, options) { | |||||
const rules = chars.getRules(options); | const rules = chars.getRules(options); | ||||
const setOfCharacters = chars.getSetOfCharacters(rules); | const setOfCharacters = chars.getSetOfCharacters(rules); | ||||
const generatedPassword = consumeEntropy( | const generatedPassword = consumeEntropy( | ||||
@@ -11,10 +11,17 @@ module.exports = function renderPassword(entropy, options) { | |||||
setOfCharacters, | setOfCharacters, | ||||
options.length - rules.length | options.length - rules.length | ||||
); | ); | ||||
const charactersToAdd = chars.getOneCharPerRule(generatedPassword.entropy, rules); | |||||
const charactersToAdd = chars.getOneCharPerRule( | |||||
generatedPassword.entropy, | |||||
rules | |||||
); | |||||
return chars.insertStringPseudoRandomly( | return chars.insertStringPseudoRandomly( | ||||
generatedPassword.value, | generatedPassword.value, | ||||
charactersToAdd.entropy, | charactersToAdd.entropy, | ||||
charactersToAdd.value | charactersToAdd.value | ||||
); | ); | ||||
} | |||||
module.exports = { | |||||
renderPassword | |||||
}; | }; |
@@ -1,5 +1,5 @@ | |||||
const assert = require("assert"); | const assert = require("assert"); | ||||
const renderPassword = require("../src/index"); | |||||
const { renderPassword } = require("../src/index"); | |||||
test("render password use remainder of long division between entropy and set of chars length as an index", () => { | test("render password use remainder of long division between entropy and set of chars length as an index", () => { | ||||
const options = { | const options = { | ||||
@@ -1,23 +0,0 @@ | |||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | |||||
# dependencies | |||||
/node_modules | |||||
/.pnp | |||||
.pnp.js | |||||
# testing | |||||
/coverage | |||||
# production | |||||
/build | |||||
# misc | |||||
.DS_Store | |||||
.env.local | |||||
.env.development.local | |||||
.env.test.local | |||||
.env.production.local | |||||
npm-debug.log* | |||||
yarn-debug.log* | |||||
yarn-error.log* |
@@ -1,44 +0,0 @@ | |||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). | |||||
## Available Scripts | |||||
In the project directory, you can run: | |||||
### `npm start` | |||||
Runs the app in the development mode.<br> | |||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser. | |||||
The page will reload if you make edits.<br> | |||||
You will also see any lint errors in the console. | |||||
### `npm test` | |||||
Launches the test runner in the interactive watch mode.<br> | |||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. | |||||
### `npm run build` | |||||
Builds the app for production to the `build` folder.<br> | |||||
It correctly bundles React in production mode and optimizes the build for the best performance. | |||||
The build is minified and the filenames include the hashes.<br> | |||||
Your app is ready to be deployed! | |||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. | |||||
### `npm run eject` | |||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!** | |||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. | |||||
Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. | |||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. | |||||
## Learn More | |||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). | |||||
To learn React, check out the [React documentation](https://reactjs.org/). |
@@ -1,37 +0,0 @@ | |||||
{ | |||||
"name": "lesspass-web-component", | |||||
"version": "8.1.1", | |||||
"description": "LessPass web component", | |||||
"license": "GPL-3.0", | |||||
"author": "Guillaume Vincent <guillaume@oslab.fr>", | |||||
"dependencies": { | |||||
"@fortawesome/fontawesome-svg-core": "^1.2.15", | |||||
"@fortawesome/free-solid-svg-icons": "^5.7.2", | |||||
"@fortawesome/react-fontawesome": "^0.1.4", | |||||
"react": "^16.8.3", | |||||
"react-dom": "^16.8.3", | |||||
"react-router-dom": "^4.3.1", | |||||
"react-scripts": "^2.1.5", | |||||
"styled-components": "^4.1.3" | |||||
}, | |||||
"scripts": { | |||||
"start": "react-scripts start", | |||||
"build": "react-scripts build", | |||||
"test": "CI=true react-scripts test", | |||||
"eject": "react-scripts eject" | |||||
}, | |||||
"eslintConfig": { | |||||
"extends": "react-app" | |||||
}, | |||||
"browserslist": [ | |||||
">0.2%", | |||||
"not dead", | |||||
"not ie <= 11", | |||||
"not op_mini all" | |||||
], | |||||
"devDependencies": { | |||||
"enzyme": "^3.9.0", | |||||
"enzyme-adapter-react-16": "^1.13.2", | |||||
"react-test-renderer": "^16.8.6" | |||||
} | |||||
} |
@@ -1,20 +0,0 @@ | |||||
<!DOCTYPE html> | |||||
<html lang="en"> | |||||
<head> | |||||
<meta charset="utf-8" /> | |||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> | |||||
<meta | |||||
name="viewport" | |||||
content="width=device-width, initial-scale=1, shrink-to-fit=no" | |||||
/> | |||||
<meta name="theme-color" content="#333333" /> | |||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> | |||||
<title>LessPass</title> | |||||
</head> | |||||
<body> | |||||
<noscript> | |||||
You need to enable JavaScript to run this app. | |||||
</noscript> | |||||
<div id="root"></div> | |||||
</body> | |||||
</html> |
@@ -1,15 +0,0 @@ | |||||
{ | |||||
"short_name": "LessPass", | |||||
"name": "Stateless password manager", | |||||
"icons": [ | |||||
{ | |||||
"src": "favicon.ico", | |||||
"sizes": "64x64 32x32 24x24 16x16", | |||||
"type": "image/x-icon" | |||||
} | |||||
], | |||||
"start_url": ".", | |||||
"display": "standalone", | |||||
"theme_color": "#333333", | |||||
"background_color": "#ffffff" | |||||
} |
@@ -1,148 +0,0 @@ | |||||
import React from "react"; | |||||
import { BrowserRouter as Router, Route, Link } from "react-router-dom"; | |||||
import styled, { createGlobalStyle } from "styled-components"; | |||||
import Logo from "./logo.png"; | |||||
import PasswordGenerationPage from "./password/PasswordGenerationPage"; | |||||
import SettingsPage from "./settings/SettingsPage"; | |||||
import HelpPage from "./help/HelpPage"; | |||||
import Icon from "./ui/Icon"; | |||||
const GlobalStyle = createGlobalStyle` | |||||
html { | |||||
box-sizing: border-box; | |||||
background-color: #024379; | |||||
} | |||||
*, *:before, *:after { | |||||
box-sizing: inherit; | |||||
} | |||||
input[type=number]::-webkit-outer-spin-button, | |||||
input[type=number]::-webkit-inner-spin-button { | |||||
-webkit-appearance: none; | |||||
margin: 0; | |||||
} | |||||
input[type=number] { | |||||
-moz-appearance:textfield; | |||||
} | |||||
`; | |||||
const MainContent = styled.div` | |||||
background-color: #ffffff; | |||||
display: flex; | |||||
flex-direction: column; | |||||
width: 100%; | |||||
max-width: 320px; | |||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, | |||||
Helvetica Neue, Arial, sans-serif; | |||||
color: #333333; | |||||
box-sizing: border-box; | |||||
`; | |||||
const Header = styled.div` | |||||
color: #eeeeee; | |||||
background-color: #333333; | |||||
display: flex; | |||||
justify-content: center; | |||||
padding: 1em 0; | |||||
`; | |||||
const HeaderImg = styled.img` | |||||
width: 180px; | |||||
height: 39px; | |||||
`; | |||||
const Content = styled.div` | |||||
background-color: #fff; | |||||
display: flex; | |||||
flex-direction: column; | |||||
padding: 1em; | |||||
min-height: 400px; | |||||
`; | |||||
const Footer = styled.ul` | |||||
color: #eeeeee; | |||||
background-color: #333333; | |||||
display: flex; | |||||
justify-content: space-around; | |||||
margin: 0; | |||||
padding: 0; | |||||
list-style: none; | |||||
`; | |||||
const FooterItem = styled.li``; | |||||
const FooterLink = styled(Link)` | |||||
padding: 0.5em; | |||||
padding-top: 0.7em; | |||||
width: 75px; | |||||
text-align: center; | |||||
display: flex; | |||||
flex-direction: column; | |||||
justify-content: center; | |||||
align-items: center; | |||||
cursor: pointer; | |||||
text-decoration: none; | |||||
color: ${props => (props.active ? "#eeeeee" : "#aaaaaa")}; | |||||
&:focus { | |||||
background-color: #0275d8; | |||||
color: #eeeeee; | |||||
} | |||||
`; | |||||
const FooterIcon = styled(Icon)` | |||||
font-size: 1em; | |||||
margin-bottom: 0.5em; | |||||
`; | |||||
const FooterText = styled.div` | |||||
font-size: 0.8em; | |||||
`; | |||||
class LessPass extends React.Component { | |||||
render() { | |||||
const { db } = this.props; | |||||
return ( | |||||
<Router> | |||||
<MainContent> | |||||
<GlobalStyle /> | |||||
<Header> | |||||
<HeaderImg src={Logo} alt="LessPass" /> | |||||
</Header> | |||||
<Content> | |||||
<Route | |||||
exact | |||||
path="/" | |||||
render={props => <PasswordGenerationPage db={db} {...props} />} | |||||
/> | |||||
<Route | |||||
path="/settings" | |||||
render={props => <SettingsPage db={db} {...props} />} | |||||
/> | |||||
<Route path="/help" component={HelpPage} /> | |||||
</Content> | |||||
<Footer> | |||||
<FooterItem> | |||||
<FooterLink active="true" tabIndex={11} to="/"> | |||||
<FooterIcon icon="user-secret" /> | |||||
<FooterText>LessPass</FooterText> | |||||
</FooterLink> | |||||
</FooterItem> | |||||
<FooterItem> | |||||
<FooterLink tabIndex={12} to="settings"> | |||||
<FooterIcon icon="cogs" /> | |||||
<FooterText>Settings</FooterText> | |||||
</FooterLink> | |||||
</FooterItem> | |||||
<FooterItem> | |||||
<FooterLink tabIndex={13} to="help"> | |||||
<FooterIcon icon="question" /> | |||||
<FooterText>Help</FooterText> | |||||
</FooterLink> | |||||
</FooterItem> | |||||
</Footer> | |||||
</MainContent> | |||||
</Router> | |||||
); | |||||
} | |||||
} | |||||
export default LessPass; |
@@ -1,9 +0,0 @@ | |||||
import React from "react"; | |||||
import ReactDOM from "react-dom"; | |||||
import LessPass from "./LessPass"; | |||||
it("LessPass renders without crashing", () => { | |||||
const div = document.createElement("div"); | |||||
ReactDOM.render(<LessPass />, div); | |||||
ReactDOM.unmountComponentAtNode(div); | |||||
}); |
@@ -1,9 +0,0 @@ | |||||
import React from "react"; | |||||
class HelpPage extends React.Component { | |||||
render() { | |||||
return <div>HelpPage</div>; | |||||
} | |||||
} | |||||
export default HelpPage; |
@@ -1,9 +0,0 @@ | |||||
import React from "react"; | |||||
import ReactDOM from "react-dom"; | |||||
import HelpPage from "./HelpPage"; | |||||
it("HelpPage renders without crashing", () => { | |||||
const div = document.createElement("div"); | |||||
ReactDOM.render(<HelpPage />, div); | |||||
ReactDOM.unmountComponentAtNode(div); | |||||
}); |
@@ -1,5 +0,0 @@ | |||||
import React from "react"; | |||||
import ReactDOM from "react-dom"; | |||||
import LessPass from "./LessPass"; | |||||
ReactDOM.render(<LessPass />, document.getElementById("root")); |
@@ -1,132 +0,0 @@ | |||||
import React from "react"; | |||||
import styled from "styled-components"; | |||||
import CheckBoxInput from "../ui/CheckBoxInput"; | |||||
import Input from "../ui/Input"; | |||||
import InputNumber from "../ui/InputNumber"; | |||||
const PasswordGeneration = styled.div` | |||||
display: flex; | |||||
justify-content: space-around; | |||||
height: 100%; | |||||
flex-direction: column; | |||||
`; | |||||
const Options = styled.div` | |||||
display: flex; | |||||
flex-direction: row; | |||||
justify-content: space-between; | |||||
align-items: center; | |||||
color: #333; | |||||
`; | |||||
const Button = styled.button` | |||||
width: 100%; | |||||
background-color: #333; | |||||
color: #eee; | |||||
border: none; | |||||
padding: 1em; | |||||
border-radius: 3px; | |||||
&:focus { | |||||
background-color: #0275d8; | |||||
} | |||||
`; | |||||
class PasswordGenerationPage extends React.Component { | |||||
state = { | |||||
site: "", | |||||
login: "", | |||||
masterPassword: "", | |||||
lowercase: true, | |||||
uppercase: true, | |||||
digits: true, | |||||
symbols: true, | |||||
length: 16, | |||||
counter: 1 | |||||
}; | |||||
render() { | |||||
const { | |||||
site, | |||||
login, | |||||
masterPassword, | |||||
lowercase, | |||||
uppercase, | |||||
digits, | |||||
symbols, | |||||
length, | |||||
counter | |||||
} = this.state; | |||||
return ( | |||||
<PasswordGeneration> | |||||
<Input | |||||
autoFocus | |||||
label="Site" | |||||
value={site} | |||||
onChange={site => this.setState({ site })} | |||||
tabIndex={1} | |||||
/> | |||||
<Input | |||||
label="Login" | |||||
value={login} | |||||
onChange={login => this.setState({ login })} | |||||
tabIndex={2} | |||||
/> | |||||
<Input | |||||
label="Master Password" | |||||
value={masterPassword} | |||||
type="password" | |||||
onChange={masterPassword => this.setState({ masterPassword })} | |||||
tabIndex={3} | |||||
/> | |||||
<Options> | |||||
<CheckBoxInput | |||||
id="lowercase" | |||||
label="a-z" | |||||
checked={lowercase} | |||||
onChange={lowercase => this.setState({ lowercase })} | |||||
tabIndex={5} | |||||
/> | |||||
<CheckBoxInput | |||||
id="uppercase" | |||||
label="A-Z" | |||||
checked={uppercase} | |||||
onChange={uppercase => this.setState({ uppercase })} | |||||
tabIndex={6} | |||||
/> | |||||
<CheckBoxInput | |||||
id="digits" | |||||
label="0-9" | |||||
checked={digits} | |||||
onChange={digits => this.setState({ digits })} | |||||
tabIndex={7} | |||||
/> | |||||
<CheckBoxInput | |||||
id="symbols" | |||||
label="!@%" | |||||
checked={symbols} | |||||
onChange={symbols => this.setState({ symbols })} | |||||
tabIndex={8} | |||||
/> | |||||
</Options> | |||||
<Options> | |||||
<InputNumber | |||||
label="Length" | |||||
value={length} | |||||
tabIndex={9} | |||||
onChange={length => this.setState({ length })} | |||||
/> | |||||
<InputNumber | |||||
label="Counter" | |||||
value={counter} | |||||
tabIndex={9} | |||||
onChange={counter => this.setState({ counter })} | |||||
/> | |||||
</Options> | |||||
<Button tabIndex={4}>GENERATE</Button> | |||||
</PasswordGeneration> | |||||
); | |||||
} | |||||
} | |||||
export default PasswordGenerationPage; |
@@ -1,9 +0,0 @@ | |||||
import React from "react"; | |||||
import ReactDOM from "react-dom"; | |||||
import PasswordGenerationPage from "./PasswordGenerationPage"; | |||||
it("PasswordGenerationPage renders without crashing", () => { | |||||
const div = document.createElement("div"); | |||||
ReactDOM.render(<PasswordGenerationPage />, div); | |||||
ReactDOM.unmountComponentAtNode(div); | |||||
}); |
@@ -1,9 +0,0 @@ | |||||
import React from "react"; | |||||
class SettingPage extends React.Component { | |||||
render() { | |||||
return <div>SettingPage</div>; | |||||
} | |||||
} | |||||
export default SettingPage; |
@@ -1,9 +0,0 @@ | |||||
import React from "react"; | |||||
import ReactDOM from "react-dom"; | |||||
import SettingsPage from "./SettingsPage"; | |||||
it("SettingsPage renders without crashing", () => { | |||||
const div = document.createElement("div"); | |||||
ReactDOM.render(<SettingsPage />, div); | |||||
ReactDOM.unmountComponentAtNode(div); | |||||
}); |
@@ -1,10 +0,0 @@ | |||||
import { configure } from "enzyme"; | |||||
import Adapter from "enzyme-adapter-react-16"; | |||||
configure({ adapter: new Adapter() }); | |||||
const spy = jest.spyOn(global.console, "error"); | |||||
afterEach(() => { | |||||
expect(spy).not.toHaveBeenCalled(); | |||||
}); |
@@ -1,31 +0,0 @@ | |||||
import React from "react"; | |||||
import styled from "styled-components"; | |||||
const CheckboxWrapper = styled.div` | |||||
display: flex; | |||||
align-items: center; | |||||
label { | |||||
font-size: 0.8em; | |||||
color: #666; | |||||
} | |||||
`; | |||||
class CheckBoxInput extends React.Component { | |||||
render() { | |||||
const { id, label, checked, onChange, ...props } = this.props; | |||||
return ( | |||||
<CheckboxWrapper> | |||||
<input | |||||
id={id} | |||||
type="checkbox" | |||||
checked={checked} | |||||
onChange={event => onChange(event.target.checked)} | |||||
{...props} | |||||
/> | |||||
<label htmlFor={id}>{label}</label> | |||||
</CheckboxWrapper> | |||||
); | |||||
} | |||||
} | |||||
export default CheckBoxInput; |
@@ -1,9 +0,0 @@ | |||||
import React from "react"; | |||||
import ReactDOM from "react-dom"; | |||||
import CheckBoxInput from "./CheckBoxInput"; | |||||
it("CheckBoxInput renders without crashing", () => { | |||||
const div = document.createElement("div"); | |||||
ReactDOM.render(<CheckBoxInput />, div); | |||||
ReactDOM.unmountComponentAtNode(div); | |||||
}); |
@@ -1,13 +0,0 @@ | |||||
import { library } from "@fortawesome/fontawesome-svg-core"; | |||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; | |||||
import { | |||||
faCogs, | |||||
faQuestion, | |||||
faUserSecret | |||||
} from "@fortawesome/free-solid-svg-icons"; | |||||
library.add(faCogs); | |||||
library.add(faQuestion); | |||||
library.add(faUserSecret); | |||||
export default FontAwesomeIcon; |
@@ -1,21 +0,0 @@ | |||||
import React from "react"; | |||||
import ReactDOM from "react-dom"; | |||||
import Icon from "./Icon"; | |||||
it("Icon renders cogs icon without crashing", () => { | |||||
const div = document.createElement("div"); | |||||
ReactDOM.render(<Icon icon="cogs" />, div); | |||||
ReactDOM.unmountComponentAtNode(div); | |||||
}); | |||||
it("Icon renders user-secret icon without crashing", () => { | |||||
const div = document.createElement("div"); | |||||
ReactDOM.render(<Icon icon="user-secret" />, div); | |||||
ReactDOM.unmountComponentAtNode(div); | |||||
}); | |||||
it("Icon renders question icon without crashing", () => { | |||||
const div = document.createElement("div"); | |||||
ReactDOM.render(<Icon icon="question" />, div); | |||||
ReactDOM.unmountComponentAtNode(div); | |||||
}); |
@@ -1,59 +0,0 @@ | |||||
import React from "react"; | |||||
import styled from "styled-components"; | |||||
const InputWrapper = styled.div` | |||||
position: relative; | |||||
input { | |||||
width: 100%; | |||||
padding: 1em 0.5em; | |||||
outline: none; | |||||
border: 1px solid #dddddd; | |||||
border-radius: 3px; | |||||
} | |||||
input:focus { | |||||
border-color: #333333; | |||||
} | |||||
label { | |||||
background-color: #ffffff; | |||||
color: #333333; | |||||
padding: 0 0.5em; | |||||
position: absolute; | |||||
top: -0.5em; | |||||
font-size: 0.8em; | |||||
left: 10px; | |||||
} | |||||
`; | |||||
class Input extends React.Component { | |||||
state = { | |||||
focused: false | |||||
}; | |||||
onBlur = () => { | |||||
this.setState({ focused: false }); | |||||
}; | |||||
onFocus = () => { | |||||
this.setState({ focused: true }); | |||||
}; | |||||
render() { | |||||
const { focused } = this.state; | |||||
const { label, value, onChange, ...props } = this.props; | |||||
return ( | |||||
<InputWrapper> | |||||
{(focused || value) && <label>{label}</label>} | |||||
<input | |||||
onFocus={this.onFocus} | |||||
onBlur={this.onBlur} | |||||
value={value} | |||||
onChange={event => onChange(event.target.value)} | |||||
placeholder={focused ? "" : label} | |||||
{...props} | |||||
/> | |||||
</InputWrapper> | |||||
); | |||||
} | |||||
} | |||||
export default Input; |
@@ -1,9 +0,0 @@ | |||||
import React from "react"; | |||||
import ReactDOM from "react-dom"; | |||||
import Input from "./Input"; | |||||
it("Input renders without crashing", () => { | |||||
const div = document.createElement("div"); | |||||
ReactDOM.render(<Input />, div); | |||||
ReactDOM.unmountComponentAtNode(div); | |||||
}); |
@@ -1,68 +0,0 @@ | |||||
import React from "react"; | |||||
import styled from "styled-components"; | |||||
const InputNumberBox = styled.div` | |||||
display: flex; | |||||
flex-direction: column; | |||||
label { | |||||
font-size: 0.8em; | |||||
padding-bottom: 0.5em; | |||||
color: #666; | |||||
} | |||||
`; | |||||
const InputNumberWrapper = styled.div` | |||||
display: flex; | |||||
border-radius: 3px; | |||||
border: 1px solid #333; | |||||
span { | |||||
background-color: #333; | |||||
color: #eee; | |||||
padding: 0 1em; | |||||
cursor: pointer; | |||||
} | |||||
input { | |||||
width: 50px; | |||||
border: none; | |||||
text-align: center; | |||||
} | |||||
&:focus-within { | |||||
border: 1px solid #0275d8; | |||||
span { | |||||
background-color: #0275d8; | |||||
} | |||||
} | |||||
`; | |||||
class InputNumber extends React.Component { | |||||
increment = () => { | |||||
const { value, onChange } = this.props; | |||||
const newValue = value + 1; | |||||
onChange(newValue); | |||||
}; | |||||
decrement = () => { | |||||
const { value, onChange } = this.props; | |||||
const newValue = value - 1; | |||||
onChange(newValue); | |||||
}; | |||||
render() { | |||||
const { label, value, onChange, ...props } = this.props; | |||||
return ( | |||||
<InputNumberBox> | |||||
<label>{label}</label> | |||||
<InputNumberWrapper> | |||||
<span id="decrement" onClick={this.decrement}>-</span> | |||||
<input | |||||
type="number" | |||||
value={value} | |||||
onChange={event => onChange(event.target.value)} | |||||
{...props} | |||||
/> | |||||
<span id="increment" onClick={this.increment}>+</span> | |||||
</InputNumberWrapper> | |||||
</InputNumberBox> | |||||
); | |||||
} | |||||
} | |||||
export default InputNumber; |
@@ -1,28 +0,0 @@ | |||||
import React from "react"; | |||||
import ReactDOM from "react-dom"; | |||||
import { shallow } from "enzyme"; | |||||
import InputNumber from "./InputNumber"; | |||||
it("InputNumber renders without crashing", () => { | |||||
const div = document.createElement("div"); | |||||
ReactDOM.render(<InputNumber />, div); | |||||
ReactDOM.unmountComponentAtNode(div); | |||||
}); | |||||
it("InputNumber increment", () => { | |||||
const mockCallback = jest.fn(); | |||||
const inputNumber = shallow( | |||||
<InputNumber value={16} onChange={mockCallback} /> | |||||
); | |||||
inputNumber.find("span#increment").simulate("click"); | |||||
expect(mockCallback.mock.calls[0][0]).toBe(17); | |||||
}); | |||||
it("InputNumber decrement", () => { | |||||
const mockCallback = jest.fn(); | |||||
const inputNumber = shallow( | |||||
<InputNumber value={16} onChange={mockCallback} /> | |||||
); | |||||
inputNumber.find("span#decrement").simulate("click"); | |||||
expect(mockCallback.mock.calls[0][0]).toBe(15); | |||||
}); |
@@ -12,18 +12,12 @@ | |||||
"zip": "cd extension && git archive -o ../build/lesspass.zip HEAD && cd .. && npm run zip:firefox && npm run zip:chrome && rm build/lesspass.zip", | "zip": "cd extension && git archive -o ../build/lesspass.zip HEAD && cd .. && npm run zip:firefox && npm run zip:chrome && rm build/lesspass.zip", | ||||
"build": "npm run clean && gulp", | "build": "npm run clean && gulp", | ||||
"release": "npm run zip && npm run build && npm run release-cws && npm run release-amo", | "release": "npm run zip && npm run build && npm run release-cws && npm run release-amo", | ||||
"release-amo": "cd extension && webext submit", | |||||
"release-amo": "cd extension && web-ext-submit", | |||||
"release-cws": "webstore upload --source build/lesspass.chrome-v$npm_package_version.zip --auto-publish", | "release-cws": "webstore upload --source build/lesspass.chrome-v$npm_package_version.zip --auto-publish", | ||||
"dev": "cd extension && web-ext run", | "dev": "cd extension && web-ext run", | ||||
"test": "echo 0" | "test": "echo 0" | ||||
}, | }, | ||||
"dependencies": { | "dependencies": { | ||||
"lesspass-pure": "5.1.8" | |||||
}, | |||||
"devDependencies": { | |||||
"chrome-webstore-upload-cli": "^1.2.0", | |||||
"dot-json": "^1.1.0", | |||||
"gulp": "^4.0.0", | |||||
"webext": "1.9.1-with-submit.1" | |||||
"lesspass-pure": "^8.1.1" | |||||
} | } | ||||
} | } |
@@ -0,0 +1,41 @@ | |||||
const LessPassFingerprint = require("lesspass-fingerprint"); | |||||
const LessPassEntropy = require("lesspass-entropy"); | |||||
const LessPassRenderPassword = require("lesspass-render-password"); | |||||
function generatePassword(site, login, masterPassword, passwordProfile) { | |||||
return LessPassEntropy.calcEntropy( | |||||
{ | |||||
site, | |||||
login, | |||||
options: { | |||||
counter: passwordProfile.counter | |||||
} | |||||
}, | |||||
masterPassword | |||||
).then(entropy => { | |||||
const options = { | |||||
length: passwordProfile.length, | |||||
lowercase: passwordProfile.lowercase, | |||||
uppercase: passwordProfile.uppercase, | |||||
digits: passwordProfile.numbers, | |||||
symbols: passwordProfile.symbols | |||||
}; | |||||
const generatedPassword = LessPassRenderPassword.renderPassword( | |||||
entropy, | |||||
options | |||||
); | |||||
return Promise.resolve(generatedPassword); | |||||
}); | |||||
} | |||||
function createFingerprint(string) { | |||||
return LessPassFingerprint.createHmac("sha256", string).then(hmac => { | |||||
const fingerprint = LessPassFingerprint.createFingerprint(hmac); | |||||
return Promise.resolve(fingerprint); | |||||
}); | |||||
} | |||||
module.exports = { | |||||
generatePassword, | |||||
createFingerprint | |||||
}; |
@@ -0,0 +1,19 @@ | |||||
{ | |||||
"name": "lesspass", | |||||
"version": "8.1.1", | |||||
"description": "LessPass core module", | |||||
"license": "GPL-3.0", | |||||
"author": "Guillaume Vincent <guillaume@oslab.fr>", | |||||
"files": [ | |||||
"index.js" | |||||
], | |||||
"main": "index.js", | |||||
"scripts": { | |||||
"test": "jest" | |||||
}, | |||||
"dependencies": { | |||||
"lesspass-entropy": "^8.1.1", | |||||
"lesspass-fingerprint": "^8.1.1", | |||||
"lesspass-render-password": "^8.1.1" | |||||
} | |||||
} |
@@ -0,0 +1,40 @@ | |||||
const LessPass = require("./index"); | |||||
test("generatePassword", () => { | |||||
const passwordProfile = { | |||||
lowercase: true, | |||||
uppercase: true, | |||||
numbers: true, | |||||
symbols: true, | |||||
length: 16, | |||||
counter: 1, | |||||
version: 2 | |||||
}; | |||||
LessPass.generatePassword( | |||||
"example.org", | |||||
"contact@example.org", | |||||
"password", | |||||
passwordProfile | |||||
).then(passwordGenerated => { | |||||
expect(passwordGenerated).toBe("WHLpUL)e00[iHR+w"); | |||||
}); | |||||
}); | |||||
test("createFingerprint", () => { | |||||
LessPass.createFingerprint("password").then(fingerprint => { | |||||
expect(fingerprint).toEqual([ | |||||
{ | |||||
color: "#FFB5DA", | |||||
icon: "fa-flask" | |||||
}, | |||||
{ | |||||
color: "#009191", | |||||
icon: "fa-archive" | |||||
}, | |||||
{ | |||||
color: "#B5DAFE", | |||||
icon: "fa-beer" | |||||
} | |||||
]); | |||||
}); | |||||
}); |
@@ -0,0 +1,37 @@ | |||||
<button id="copyButton" onclick="Run()">Generate</button> | |||||
<script> | |||||
function GetButton() { | |||||
return document.getElementById("copyButton"); | |||||
} | |||||
function UpdateButtonText(text) { | |||||
GetButton().innerText = text; | |||||
} | |||||
function DisableButton() { | |||||
GetButton().disabled = true; | |||||
} | |||||
function GeneratePassword() { | |||||
UpdateButtonText("Generating"); | |||||
setTimeout(() => UpdateButtonText("Generating."), 1000); | |||||
setTimeout(() => UpdateButtonText("Generating.."), 2000); | |||||
setTimeout(() => UpdateButtonText("Generating..."), 3000); | |||||
return new Promise(resolve => { | |||||
setTimeout(resolve, 4000, 'm<=u6T8`hM"nxwuY'); | |||||
}); | |||||
} | |||||
function CopyToClipboard(password) { | |||||
return navigator.clipboard.writeText(password).then(() => { | |||||
UpdateButtonText("Copied in clipboard!"); | |||||
DisableButton(); | |||||
}); | |||||
} | |||||
function Run() { | |||||
GeneratePassword().then(password => { | |||||
CopyToClipboard(password).catch(error => { | |||||
UpdateButtonText("Click to copy!"); | |||||
GetButton().onclick = () => CopyToClipboard(password); | |||||
}); | |||||
}); | |||||
} | |||||
</script> | |||||