@@ -31,7 +31,7 @@ Options: | |||
--no-symbols remove symbols from password | |||
-c, --clipboard copy generated password to clipboard rather than displaying it. | |||
Need pbcopy (OSX), xsel or xclip (Linux) or clip (Windows). | |||
-v, --version lesspass version number | |||
-v, --version lesspass version | |||
Examples: | |||
@@ -0,0 +1,16 @@ | |||
from django.db import migrations | |||
class Migration(migrations.Migration): | |||
dependencies = [ | |||
("api", "0010_alter_password_site_and_login"), | |||
] | |||
operations = [ | |||
migrations.RenameField( | |||
model_name="password", | |||
old_name="numbers", | |||
new_name="digits", | |||
), | |||
] |
@@ -78,7 +78,7 @@ class Password(DateMixin): | |||
lowercase = models.BooleanField(default=True) | |||
uppercase = models.BooleanField(default=True) | |||
symbols = models.BooleanField(default=True) | |||
numbers = models.BooleanField(default=True) | |||
digits = models.BooleanField(default=True) | |||
length = models.IntegerField(default=16) | |||
counter = models.IntegerField(default=1) | |||
@@ -13,7 +13,7 @@ class PasswordSerializer(serializers.ModelSerializer): | |||
"lowercase", | |||
"uppercase", | |||
"symbols", | |||
"numbers", | |||
"digits", | |||
"counter", | |||
"length", | |||
"version", | |||
@@ -26,6 +26,16 @@ class PasswordSerializer(serializers.ModelSerializer): | |||
user = self.context["request"].user | |||
return models.Password.objects.create(user=user, **validated_data) | |||
def to_internal_value(self, data): | |||
if "number" in data and "digits" not in data: | |||
data["digits"] = data["number"] | |||
if "numbers" in data and "digits" not in data: | |||
data["digits"] = data["numbers"] | |||
if "symbol" in data and "symbols" not in data: | |||
data["symbols"] = data["symbol"] | |||
data = super().to_internal_value(data) | |||
return data | |||
class EncryptedPasswordProfileSerializer(serializers.ModelSerializer): | |||
class Meta: | |||
@@ -45,53 +45,98 @@ class LoginPasswordsTestCase(APITestCase): | |||
self.assertEqual(404, request.status_code) | |||
self.assertEqual(1, models.Password.objects.all().count()) | |||
def test_create_password(self): | |||
def test_create_password_old_api(self): | |||
password = { | |||
"site": "lesspass.com", | |||
"login": "test@oslab.fr", | |||
"lowercase": True, | |||
"login": "test@lesspass.com", | |||
"lowercase": False, | |||
"uppercase": True, | |||
"number": True, | |||
"symbol": True, | |||
"counter": 1, | |||
"numbers": False, | |||
"symbols": False, | |||
"counter": 2, | |||
"length": 12, | |||
} | |||
self.assertEqual(0, models.Password.objects.count()) | |||
self.client.post("/api/passwords/", password) | |||
self.assertEqual(1, models.Password.objects.count()) | |||
profile = models.Password.objects.first() | |||
self.assertEqual(profile.site, "lesspass.com") | |||
self.assertEqual(profile.login, "test@lesspass.com") | |||
self.assertFalse(profile.lowercase) | |||
self.assertTrue(profile.uppercase) | |||
self.assertFalse(profile.digits) | |||
self.assertFalse(profile.symbols) | |||
self.assertEqual(profile.counter, 2) | |||
self.assertEqual(profile.length, 12) | |||
self.assertEqual(profile.version, 2) | |||
def test_create_password_v2(self): | |||
def test_create_password_with_missing_s_old_api(self): | |||
password = { | |||
"site": "lesspass.com", | |||
"login": "test@oslab.fr", | |||
"login": "test@lesspass.com", | |||
"lowercase": True, | |||
"uppercase": True, | |||
"number": True, | |||
"symbol": True, | |||
"number": False, | |||
"symbol": False, | |||
"counter": 1, | |||
"length": 12, | |||
"length": 16, | |||
"version": 2, | |||
} | |||
self.client.post("/api/passwords/", password) | |||
self.assertEqual(2, models.Password.objects.first().version) | |||
profile = models.Password.objects.first() | |||
self.assertFalse(profile.digits) | |||
self.assertFalse(profile.symbols) | |||
def test_create_password_v2(self): | |||
password = { | |||
"site": "lesspass.com", | |||
"login": "testv2@lesspass.com", | |||
"lowercase": True, | |||
"uppercase": False, | |||
"digits": False, | |||
"symbols": False, | |||
"counter": 3, | |||
"length": 16, | |||
"version": 2, | |||
} | |||
self.client.post("/api/passwords/", password) | |||
profile = models.Password.objects.first() | |||
self.assertEqual(profile.site, "lesspass.com") | |||
self.assertEqual(profile.login, "testv2@lesspass.com") | |||
self.assertTrue(profile.lowercase) | |||
self.assertFalse(profile.uppercase) | |||
self.assertFalse(profile.digits) | |||
self.assertFalse(profile.symbols) | |||
self.assertEqual(profile.counter, 3) | |||
self.assertEqual(profile.length, 16) | |||
self.assertEqual(profile.version, 2) | |||
def test_update_password(self): | |||
password = factories.PasswordFactory(user=self.user) | |||
self.assertNotEqual("facebook.com", password.site) | |||
new_password = { | |||
"site": "facebook.com", | |||
"login": "test@oslab.fr", | |||
"login": "test@lesspass.com", | |||
"lowercase": True, | |||
"uppercase": True, | |||
"number": True, | |||
"symbol": True, | |||
"counter": 1, | |||
"length": 12, | |||
"digits": False, | |||
"symbols": False, | |||
"counter": 2, | |||
"length": 20, | |||
"version": 2, | |||
} | |||
request = self.client.put("/api/passwords/%s/" % password.id, new_password) | |||
self.assertEqual(200, request.status_code, request.content.decode("utf-8")) | |||
password_updated = models.Password.objects.get(id=password.id) | |||
self.assertEqual("facebook.com", password_updated.site) | |||
self.assertEqual(password_updated.site, "facebook.com") | |||
self.assertEqual(password_updated.login, "test@lesspass.com") | |||
self.assertTrue(password_updated.lowercase) | |||
self.assertTrue(password_updated.uppercase) | |||
self.assertFalse(password_updated.digits) | |||
self.assertFalse(password_updated.symbols) | |||
self.assertEqual(password_updated.counter, 2) | |||
self.assertEqual(password_updated.length, 20) | |||
self.assertEqual(password_updated.version, 2) | |||
def test_cant_update_other_password(self): | |||
not_my_password = factories.PasswordFactory(user=factories.UserFactory()) | |||
@@ -356,7 +356,7 @@ components: | |||
- site | |||
- uppercase | |||
- lowercase | |||
- numbers | |||
- digits | |||
- symbols | |||
- length | |||
- counter | |||
@@ -378,8 +378,8 @@ components: | |||
description: Generated password has lowercase characters | |||
type: boolean | |||
default: true | |||
numbers: | |||
description: Generated password has numbers | |||
digits: | |||
description: Generated password has digits | |||
type: boolean | |||
default: true | |||
symbols: | |||
@@ -51,7 +51,6 @@ describe("generatePassword should not care about the extra number field used for | |||
lowercase: true, | |||
uppercase: true, | |||
digits: true, | |||
number: true, | |||
symbols: true, | |||
}; | |||
return generatePassword("password", passwordProfile).then( | |||
@@ -3,13 +3,23 @@ const initialState = {}; | |||
export default function reduce(state = initialState, action) { | |||
switch (action.type) { | |||
case "SET_PASSWORD_PROFILES": | |||
return action.profiles.reduce((acc, profile) => { | |||
acc[profile.id] = { | |||
...profile, | |||
["digits"]: profile.numbers, | |||
}; | |||
return acc; | |||
}, {}); | |||
return action.profiles | |||
.map((profile) => { | |||
if ("numbers" in profile) { | |||
const { numbers, ...profileWithoutNumbers } = profile; | |||
return { | |||
...profileWithoutNumbers, | |||
digits: numbers, | |||
}; | |||
} | |||
return profile; | |||
}) | |||
.reduce((acc, p) => { | |||
acc[p.id] = { | |||
...p, | |||
}; | |||
return acc; | |||
}, {}); | |||
case "ADD_PASSWORD_PROFILE": | |||
return { | |||
...state, | |||
@@ -31,16 +31,16 @@ describe("profiles reducer", () => { | |||
} | |||
) | |||
).toEqual({ | |||
p1: { id: "p1", numbers: true, digits: true }, | |||
p2: { id: "p2", numbers: false, digits: false }, | |||
p1: { id: "p1", digits: true }, | |||
p2: { id: "p2", digits: false }, | |||
}); | |||
}); | |||
it("REMOVE_PASSWORD_PROFILE", () => { | |||
expect( | |||
reducer( | |||
{ | |||
p1: { id: "p1", numbers: true, digits: true }, | |||
p2: { id: "p2", numbers: false, digits: false }, | |||
p1: { id: "p1", digits: true }, | |||
p2: { id: "p2", digits: false }, | |||
}, | |||
{ | |||
type: "REMOVE_PASSWORD_PROFILE", | |||
@@ -48,7 +48,7 @@ describe("profiles reducer", () => { | |||
} | |||
) | |||
).toEqual({ | |||
p2: { id: "p2", numbers: false, digits: false }, | |||
p2: { id: "p2", digits: false }, | |||
}); | |||
}); | |||
it("ADD_PASSWORD_PROFILE", () => { | |||
@@ -54,7 +54,7 @@ | |||
type="checkbox" | |||
tabindex="1" | |||
class="form-check-input" | |||
v-model="options.numbers" | |||
v-model="options.digits" | |||
/> | |||
<label class="form-check-label" for="numbers__btn"> | |||
0-9 | |||
@@ -1,6 +1,6 @@ | |||
{ | |||
"AlreadyOnLessPass": "Already on LessPass? Sign In", | |||
"AtLeastOneOptionShouldBeSelected": "You must select at least one option among lowercase, uppercase, numbers or symbols.", | |||
"AtLeastOneOptionShouldBeSelected": "You must select at least one option among lowercase, uppercase, digits or symbols.", | |||
"Change my password": "Change my password", | |||
"ChangePasswordError": "We cannot change your password with the information provided.", | |||
"ChangePasswordSuccessful": "Your password was changed successfully.", | |||
@@ -22,10 +22,7 @@ export function removeSiteSubdomain(url) { | |||
const tld = mostUsedTlds[i]; | |||
const tldWithDot = `.${tld}`; | |||
if (hostname.endsWith(tldWithDot)) { | |||
const domain = hostname | |||
.replace(tldWithDot, "") | |||
.split(".") | |||
.pop(); | |||
const domain = hostname.replace(tldWithDot, "").split(".").pop(); | |||
if (domain) { | |||
return domain + tldWithDot; | |||
} | |||
@@ -44,7 +41,7 @@ export function getSuggestions(url) { | |||
const urlElements = cleanedUrl | |||
.toLowerCase() | |||
.split(".") | |||
.filter(element => element.length >= 2); | |||
.filter((element) => element.length >= 2); | |||
if (urlElements.length < 2) return []; | |||
const baseName = urlElements[urlElements.length - 2]; | |||
const tld = urlElements[urlElements.length - 1]; | |||
@@ -62,13 +59,13 @@ export function getSuggestions(url) { | |||
} | |||
export function getSite() { | |||
return new Promise(resolve => { | |||
return new Promise((resolve) => { | |||
if ( | |||
typeof chrome !== "undefined" && | |||
typeof chrome.tabs !== "undefined" && | |||
typeof chrome.tabs.query !== "undefined" | |||
) { | |||
chrome.tabs.query({ active: true, currentWindow: true }, tabs => { | |||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { | |||
resolve(tabs[0].url); | |||
}); | |||
} else { | |||
@@ -79,19 +76,24 @@ export function getSite() { | |||
function passwordProfileFromRawQuery(query) { | |||
const password = {}; | |||
["uppercase", "lowercase", "numbers", "symbols"].forEach(booleanishQuery => { | |||
if ("numbers" in query) { | |||
password["digits"] = | |||
query["numbers"].toLowerCase() === "true" || | |||
query["numbers"].toLowerCase() === "1"; | |||
} | |||
["uppercase", "lowercase", "digits", "symbols"].forEach((booleanishQuery) => { | |||
if (booleanishQuery in query) { | |||
password[booleanishQuery] = | |||
query[booleanishQuery].toLowerCase() === "true" || | |||
query[booleanishQuery].toLowerCase() === "1"; | |||
} | |||
}); | |||
["site", "login"].forEach(stringQuery => { | |||
["site", "login"].forEach((stringQuery) => { | |||
if (stringQuery in query) { | |||
password[stringQuery] = query[stringQuery]; | |||
} | |||
}); | |||
["length", "counter", "version"].forEach(intQuery => { | |||
["length", "counter", "version"].forEach((intQuery) => { | |||
if (intQuery in query) { | |||
password[intQuery] = parseInt(query[intQuery], 10); | |||
} | |||
@@ -3,7 +3,7 @@ export default { | |||
site: "", | |||
uppercase: true, | |||
lowercase: true, | |||
numbers: true, | |||
digits: true, | |||
symbols: true, | |||
length: 16, | |||
counter: 1, | |||
@@ -63,7 +63,6 @@ export default { | |||
let lines = [["name", "url", "username", "password"]]; | |||
for (let i = 0; i < this.passwords.length; i++) { | |||
const passwordProfile = this.passwords[i]; | |||
passwordProfile["digits"] = passwordProfile["numbers"]; | |||
const generatedPassword = await LessPass.generatePassword( | |||
passwordProfile, | |||
this.masterPassword | |||
@@ -128,19 +128,19 @@ export default { | |||
RemoveAutoComplete, | |||
InputSite, | |||
MasterPassword, | |||
Options | |||
Options, | |||
}, | |||
computed: { | |||
...mapState(["password", "passwords"]), | |||
...mapGetters([ | |||
"passwordURL", | |||
"shouldAutoFillSite", | |||
"shouldRemoveSubdomain" | |||
]) | |||
"shouldRemoveSubdomain", | |||
]), | |||
}, | |||
beforeMount() { | |||
if (this.shouldAutoFillSite) { | |||
getSite().then(site => { | |||
getSite().then((site) => { | |||
const cleanedSite = this.shouldRemoveSubdomain | |||
? removeSiteSubdomain(site) | |||
: cleanUrl(site); | |||
@@ -148,7 +148,7 @@ export default { | |||
}); | |||
} | |||
this.$store.dispatch("getPasswordFromUrlQuery", { | |||
query: this.$route.query | |||
query: this.$route.query, | |||
}); | |||
}, | |||
mounted() { | |||
@@ -160,20 +160,20 @@ export default { | |||
return { | |||
masterPassword: "", | |||
passwordGenerated: "", | |||
cleanTimeout: null | |||
cleanTimeout: null, | |||
}; | |||
}, | |||
watch: { | |||
password: { | |||
handler: function() { | |||
handler: function () { | |||
this.cleanErrors(); | |||
}, | |||
deep: true | |||
deep: true, | |||
}, | |||
masterPassword: function(newMasterPassword) { | |||
masterPassword: function (newMasterPassword) { | |||
this.masterPassword = newMasterPassword; | |||
this.cleanErrors(); | |||
} | |||
}, | |||
}, | |||
methods: { | |||
togglePasswordType(element) { | |||
@@ -211,13 +211,13 @@ export default { | |||
} | |||
const lowercase = this.password.lowercase; | |||
const uppercase = this.password.uppercase; | |||
const numbers = this.password.numbers; | |||
const digits = this.password.digits; | |||
const symbols = this.password.symbols; | |||
if (!lowercase && !uppercase && !numbers && !symbols) { | |||
if (!lowercase && !uppercase && !digits && !symbols) { | |||
message.error( | |||
this.$t( | |||
"AtLeastOneOptionShouldBeSelected", | |||
"You must select at least one option among lowercase, uppercase, numbers or symbols." | |||
"You must select at least one option among lowercase, uppercase, digits or symbols." | |||
) | |||
); | |||
return; | |||
@@ -233,24 +233,23 @@ export default { | |||
} | |||
this.cleanErrors(); | |||
const passwordProfile = { | |||
site, | |||
login, | |||
lowercase, | |||
uppercase, | |||
numbers, | |||
digits, | |||
symbols, | |||
length: this.password.length, | |||
counter: this.password.counter, | |||
version: this.password.version | |||
version: this.password.version, | |||
}; | |||
return LessPass.generatePassword( | |||
site, | |||
login, | |||
masterPassword, | |||
passwordProfile | |||
).then(passwordGenerated => { | |||
this.passwordGenerated = passwordGenerated; | |||
this.copyPassword(); | |||
this.cleanFormIn30Seconds(); | |||
}); | |||
return LessPass.generatePassword(passwordProfile, masterPassword).then( | |||
(passwordGenerated) => { | |||
this.passwordGenerated = passwordGenerated; | |||
this.copyPassword(); | |||
this.cleanFormIn30Seconds(); | |||
} | |||
); | |||
}, | |||
focusBestInputField() { | |||
try { | |||
@@ -311,7 +310,7 @@ export default { | |||
.then(() => { | |||
this.focusBestInputField(); | |||
}); | |||
} | |||
} | |||
}, | |||
}, | |||
}; | |||
</script> |
@@ -144,7 +144,7 @@ test("getPasswordFromUrlQuery", () => { | |||
site: "example.org", | |||
uppercase: true, | |||
lowercase: true, | |||
numbers: true, | |||
digits: true, | |||
symbols: false, | |||
length: 16, | |||
counter: 1, | |||
@@ -156,14 +156,14 @@ test("getPasswordFromUrlQuery", () => { | |||
test("getPasswordFromUrlQuery with base 64 encoded password profile", () => { | |||
const query = { | |||
passwordProfileEncoded: | |||
"eyJsb2dpbiI6InRlc3RAZXhhbXBsZS5vcmciLCJzaXRlIjoiZXhhbXBsZS5vcmciLCJ1cHBlcmNhc2UiOnRydWUsImxvd2VyY2FzZSI6dHJ1ZSwibnVtYmVycyI6dHJ1ZSwic3ltYm9scyI6ZmFsc2UsImxlbmd0aCI6MTYsImNvdW50ZXIiOjEsInZlcnNpb24iOjJ9", | |||
"eyJsb2dpbiI6InRlc3RAZXhhbXBsZS5vcmciLCJzaXRlIjoiZXhhbXBsZS5vcmciLCJ1cHBlcmNhc2UiOnRydWUsImxvd2VyY2FzZSI6dHJ1ZSwiZGlnaXRzIjp0cnVlLCJzeW1ib2xzIjpmYWxzZSwibGVuZ3RoIjoxNiwiY291bnRlciI6MSwidmVyc2lvbiI6Mn0", | |||
}; | |||
const expectedPassword = { | |||
login: "test@example.org", | |||
site: "example.org", | |||
uppercase: true, | |||
lowercase: true, | |||
numbers: true, | |||
digits: true, | |||
symbols: false, | |||
length: 16, | |||
counter: 1, | |||
@@ -182,7 +182,7 @@ test("getPasswordFromUrlQuery booleanish", () => { | |||
const expectedPassword = { | |||
uppercase: true, | |||
lowercase: true, | |||
numbers: true, | |||
digits: true, | |||
symbols: false, | |||
}; | |||
expect(urlParser.getPasswordFromUrlQuery(query)).toEqual(expectedPassword); | |||
@@ -7,7 +7,7 @@ test("passwordURL", () => { | |||
site: "example.org", | |||
uppercase: true, | |||
lowercase: true, | |||
numbers: true, | |||
digits: true, | |||
symbols: false, | |||
length: 16, | |||
counter: 1, | |||
@@ -19,7 +19,7 @@ test("passwordURL", () => { | |||
}; | |||
expect(getters.passwordURL(state)).toBe( | |||
"https://www.lesspass.com/#/?passwordProfileEncoded=eyJsb2dpbiI6InRlc3RAZXhhbXBsZS5vcmciLCJzaXRlIjoiZXhhbXBsZS5vcmciLCJ1cHBlcmNhc2UiOnRydWUsImxvd2VyY2FzZSI6dHJ1ZSwibnVtYmVycyI6dHJ1ZSwic3ltYm9scyI6ZmFsc2UsImxlbmd0aCI6MTYsImNvdW50ZXIiOjEsInZlcnNpb24iOjJ9" | |||
"https://www.lesspass.com/#/?passwordProfileEncoded=eyJsb2dpbiI6InRlc3RAZXhhbXBsZS5vcmciLCJzaXRlIjoiZXhhbXBsZS5vcmciLCJ1cHBlcmNhc2UiOnRydWUsImxvd2VyY2FzZSI6dHJ1ZSwiZGlnaXRzIjp0cnVlLCJzeW1ib2xzIjpmYWxzZSwibGVuZ3RoIjoxNiwiY291bnRlciI6MSwidmVyc2lvbiI6Mn0%3D" | |||
); | |||
}); | |||
@@ -65,7 +65,7 @@ test("SET_DEFAULT_OPTIONS", () => { | |||
login: "", | |||
uppercase: true, | |||
lowercase: true, | |||
numbers: true, | |||
digits: true, | |||
symbols: true, | |||
length: 16, | |||
counter: 1, | |||
@@ -122,7 +122,7 @@ describe("SET_PASSWORDS", () => { | |||
lowercase: true, | |||
uppercase: true, | |||
symbols: true, | |||
numbers: true, | |||
digits: true, | |||
counter: 1, | |||
length: 16, | |||
version: 2 | |||
@@ -134,7 +134,7 @@ describe("SET_PASSWORDS", () => { | |||
lowercase: true, | |||
uppercase: false, | |||
symbols: false, | |||
numbers: true, | |||
digits: true, | |||
counter: 1, | |||
length: 8, | |||
version: 2 | |||
@@ -146,7 +146,7 @@ describe("SET_PASSWORDS", () => { | |||
lowercase: true, | |||
uppercase: true, | |||
symbols: true, | |||
numbers: true, | |||
digits: true, | |||
counter: 1, | |||
length: 12, | |||
version: 1 | |||
@@ -160,7 +160,7 @@ describe("SET_PASSWORDS", () => { | |||
site: "www.example.org", | |||
uppercase: true, | |||
lowercase: true, | |||
numbers: true, | |||
digits: true, | |||
symbols: true, | |||
length: 16, | |||
counter: 1, | |||
@@ -172,7 +172,7 @@ describe("SET_PASSWORDS", () => { | |||
site: "", | |||
uppercase: true, | |||
lowercase: true, | |||
numbers: true, | |||
digits: true, | |||
symbols: true, | |||
length: 16, | |||
counter: 1, | |||
@@ -271,7 +271,7 @@ | |||
<div class="row air"> | |||
<div class="col-12 col-sm-4 py-5 feature"> | |||
<p class="lead"> | |||
Manage complex passwords with LessPass options (numbers only, | |||
Manage complex passwords with LessPass options (digits only, | |||
adjust length, etc...) | |||
</p> | |||
<img | |||
@@ -39,7 +39,7 @@ test("createFingerprint", () => { | |||
}); | |||
}); | |||
test("generatePassword simpler API", () => { | |||
test("generatePassword api v2", () => { | |||
const passwordProfile = { | |||
site: "example.org", | |||
login: "contact@example.org", | |||