@@ -10,7 +10,7 @@ describe("Connected Mode", function() { | |||
cy.get("#fingerprint .fa-plane").should("be.visible"); | |||
cy.get("#signInButton").click(); | |||
cy.wait(500); | |||
cy.get("#siteField").type("lesspass.com"); | |||
cy.get("#site").type("lesspass.com"); | |||
cy.get("#login").type("test@lesspass.com"); | |||
cy.get("#passwordField").type("test@lesspass.com"); | |||
cy.get("#generatePassword__btn").click(); | |||
@@ -32,7 +32,7 @@ describe("Connected Mode", function() { | |||
cy.get("#passwordField").type("test@lesspass.com"); | |||
cy.wait(1000); | |||
cy.get("#signInButton").click(); | |||
cy.get("#siteField").should("be.visible"); | |||
cy.get("#site").should("be.visible"); | |||
cy.get(".fa-key").should("be.visible"); | |||
cy.get(".fa-user") | |||
.first() | |||
@@ -60,7 +60,7 @@ describe("Connected Mode", function() { | |||
cy.get(".passwordProfile__meta") | |||
.first() | |||
.click(); | |||
cy.get("#siteField").should("have.value", "example.org"); | |||
cy.get("#site").should("have.value", "example.org"); | |||
cy.get("#login").should("have.value", "contact@example.org"); | |||
cy.get(".fa-user") | |||
.first() | |||
@@ -6,6 +6,6 @@ describe("LessPass", function() { | |||
it("should focus site field", function() { | |||
cy.visit("/"); | |||
cy.wait(500); | |||
cy.focused().should("have.id", "siteField"); | |||
cy.focused().should("have.id", "site"); | |||
}); | |||
}); |
@@ -18,7 +18,7 @@ describe("Password Generation", function() { | |||
cy.visit("/"); | |||
cy.wait(500); | |||
cy.get("#siteField") | |||
cy.get("#site") | |||
.type("lesspass.com") | |||
.tab(); | |||
cy.get("#login").type("test@lesspass.com"); | |||
@@ -88,7 +88,7 @@ describe("Password Generation", function() { | |||
it("should consider counter as string not hex value nrt_328", function() { | |||
cy.visit("/"); | |||
cy.wait(500); | |||
cy.get("#siteField").type("site"); | |||
cy.get("#site").type("site"); | |||
cy.get("#login").type("login"); | |||
cy.get("#passwordField").type("test"); | |||
cy.get("#passwordCounter") | |||
@@ -100,7 +100,7 @@ describe("Password Generation", function() { | |||
it("should generate password when hit enter nrt_266", function() { | |||
cy.visit("/"); | |||
cy.wait(500); | |||
cy.get("#siteField") | |||
cy.get("#site") | |||
.type("lesspass.com") | |||
.tab(); | |||
cy.get("#login").type("test@lesspass.com"); | |||
@@ -109,26 +109,10 @@ describe("Password Generation", function() { | |||
.type("{enter}"); | |||
cy.get("#generated-password").should("have.value", "hjV@\\5ULp3bIs,6B"); | |||
}); | |||
it("should keep site field in sync nrt_441", function() { | |||
cy.visit("/"); | |||
cy.wait(500); | |||
cy.get("#login").type("user"); | |||
cy.get("#passwordField").type("password"); | |||
cy.get("#siteField") | |||
.type("subdomain.domain.com") | |||
.type("{home}") | |||
.type("{rightarrow}") | |||
.type("{backspace}") | |||
.type("{downarrow}") | |||
.type("{downarrow}") | |||
.type("{enter}"); | |||
cy.get("#generatePassword__btn").click(); | |||
cy.get("#generated-password").should("have.value", "ZT^IK2e!t@k$9`*)"); | |||
}); | |||
it("should clear password generated when master password change", function() { | |||
cy.visit("/"); | |||
cy.wait(500); | |||
cy.get("#siteField") | |||
cy.get("#site") | |||
.type("example.org") | |||
.tab(); | |||
cy.get("#login").type("user"); | |||
@@ -144,7 +128,7 @@ describe("Password Generation", function() { | |||
it("should generate password with 2 tabs and enter", function() { | |||
cy.visit("/"); | |||
cy.wait(500); | |||
cy.get("#siteField") | |||
cy.get("#site") | |||
.type("lesspass.com") | |||
.tab() | |||
.type("test@lesspass.com") | |||
@@ -33,10 +33,9 @@ | |||
"dependencies": { | |||
"@oslab/atob": "^0.1.0", | |||
"@oslab/btoa": "^0.1.0", | |||
"awesomplete": "^1.1.5", | |||
"axios": "^0.21.1", | |||
"balloon-css": "^1.0.3", | |||
"bootstrap": "^4.6.0", | |||
"bootstrap": "^5.0.0-beta3", | |||
"copy-text-to-clipboard": "^3.0.1", | |||
"core-js": "^3.9.1", | |||
"font-awesome": "^4.7.0", | |||
@@ -12,6 +12,7 @@ | |||
#lesspass { | |||
color: #464646; | |||
max-width: 420px; | |||
position: relative; | |||
} | |||
.lesspass__inner-box { | |||
@@ -24,43 +25,11 @@ | |||
} | |||
} | |||
#lesspass, | |||
#lesspass * { | |||
border-radius: 0 !important; | |||
} | |||
button, | |||
.pointer { | |||
cursor: pointer; | |||
} | |||
.inner-addon i { | |||
position: absolute; | |||
padding: 10px; | |||
pointer-events: none; | |||
z-index: 10; | |||
} | |||
.inner-addon { | |||
position: relative; | |||
} | |||
.left-addon i { | |||
left: 0; | |||
} | |||
.right-addon i { | |||
right: 0; | |||
} | |||
.left-addon input { | |||
padding-left: 30px; | |||
} | |||
.right-addon input { | |||
padding-right: 30px; | |||
} | |||
#loading__view { | |||
position: relative; | |||
height: 358px; | |||
@@ -177,7 +146,6 @@ export default { | |||
}), | |||
created() { | |||
this.$store.dispatch("cleanMessage"); | |||
this.$store.dispatch("resetPassword"); | |||
const refresh = localStorage.getItem("refresh_token"); | |||
if (refresh) { | |||
this.isLoading = true; | |||
@@ -1,147 +0,0 @@ | |||
import { mount } from "@vue/test-utils"; | |||
import InputSite from "./InputSite.vue"; | |||
jest.mock("../services/url-parser"); | |||
import { getSuggestions } from "../services/url-parser"; | |||
const createWrapper = data => | |||
mount({ | |||
data: () => { | |||
return { | |||
site: "", | |||
passwords: [], | |||
...data | |||
}; | |||
}, | |||
template: | |||
'<input-site v-model="site" v-bind:passwords="passwords" v-bind:label="\'Site\'"></input-site>', | |||
components: { "input-site": InputSite } | |||
}); | |||
const optionsFor = wrapper => wrapper.findAll("div.awesomplete li"); | |||
const inputField = wrapper => wrapper.find("#siteField"); | |||
describe("InputSite", () => { | |||
beforeEach(() => { | |||
getSuggestions.mockImplementation(() => []); | |||
}); | |||
it("fills the input with the value property", () => { | |||
const wrapper = createWrapper({ site: "lesspass.com" }); | |||
const input = inputField(wrapper); | |||
expect(input.element.value).toBe("lesspass.com"); | |||
}); | |||
// todo fix autocomplete tests | |||
describe.skip("autocomplete", function() { | |||
describe("search", () => { | |||
it("filters according to site name", () => { | |||
const wrapper = createWrapper({ | |||
passwords: [ | |||
{ site: "lesspass", login: "xavier" }, | |||
{ site: "wrongsite", login: "xavier" } | |||
] | |||
}); | |||
inputField(wrapper).setValue("le"); | |||
let options = optionsFor(wrapper); | |||
expect(options.length).toBe(1); | |||
expect(options.at(0).text()).toBe("lesspass xavier"); | |||
}); | |||
it(`shows options that are contained in the user's value`, () => { | |||
const wrapper = createWrapper({ | |||
passwords: [{ site: "lesspass", login: "xavier" }] | |||
}); | |||
inputField(wrapper).setValue("www.lesspass.com"); | |||
let options = optionsFor(wrapper); | |||
expect(options.length).toBe(1); | |||
expect(options.at(0).text()).toBe("lesspass xavier"); | |||
}); | |||
it("filters using suggestion", () => { | |||
getSuggestions.mockImplementation(() => []); | |||
const wrapper = createWrapper(); | |||
getSuggestions.mockImplementation(() => ["lesspass"]); | |||
inputField(wrapper).setValue("lesspass.com"); | |||
let options = optionsFor(wrapper); | |||
expect(options.length).toBe(1); | |||
expect(options.at(0).text()).toBe("lesspass"); | |||
}); | |||
it("shows site and login in the list", () => { | |||
const wrapper = createWrapper({ | |||
passwords: [{ site: "lesspass", login: "xavier" }] | |||
}); | |||
inputField(wrapper).setValue("le"); | |||
let options = optionsFor(wrapper); | |||
expect(options.length).toBe(1); | |||
expect(options.at(0).text()).toBe("lesspass xavier"); | |||
}); | |||
it(`doesn't use login`, () => { | |||
const wrapper = createWrapper({ | |||
passwords: [{ site: "lesspass", login: "xavier" }] | |||
}); | |||
inputField(wrapper).setValue("xa"); | |||
let options = optionsFor(wrapper); | |||
expect(options.length).toBe(0); | |||
}); | |||
it(`prints options sorted by site then login`, () => { | |||
const wrapper = createWrapper({ | |||
passwords: [ | |||
{ site: "lesspass", login: "guillaume" }, | |||
{ site: "passless", login: "xavier" }, | |||
{ site: "passless", login: "guillaume" }, | |||
{ site: "lesspass", login: "xavier" } | |||
] | |||
}); | |||
inputField(wrapper).setValue("le"); | |||
let options = optionsFor(wrapper); | |||
expect(options.length).toBe(4); | |||
expect(options.at(0).text()).toBe("lesspass guillaume"); | |||
expect(options.at(1).text()).toBe("lesspass xavier"); | |||
expect(options.at(2).text()).toBe("passless guillaume"); | |||
expect(options.at(3).text()).toBe("passless xavier"); | |||
}); | |||
}); | |||
describe("completion", () => { | |||
describe("when selecting password", () => { | |||
let wrapper; | |||
beforeEach(() => { | |||
wrapper = createWrapper({ | |||
passwords: [{ site: "lesspass", login: "xavier" }] | |||
}); | |||
inputField(wrapper).setValue("le"); | |||
const options = optionsFor(wrapper); | |||
options.at(0).trigger("click"); | |||
}); | |||
it("completes field", () => { | |||
expect(inputField(wrapper).element.value).toBe("lesspass"); | |||
}); | |||
it('emits a "passwordProfileSelected" with the value', () => { | |||
const emitted = wrapper.findComponent(InputSite).emitted(); | |||
const profileSelected = emitted["passwordProfileSelected"]; | |||
expect(profileSelected.length).toBe(1); | |||
expect(profileSelected[0]).toEqual([ | |||
{ site: "lesspass", login: "xavier" } | |||
]); | |||
}); | |||
}); | |||
}); | |||
describe("when selecting suggestion", () => { | |||
let wrapper; | |||
beforeEach(() => { | |||
getSuggestions.mockImplementation(() => ["lesspass"]); | |||
wrapper = createWrapper(); | |||
inputField(wrapper).setValue("le"); | |||
const options = optionsFor(wrapper); | |||
options.at(0).trigger("click"); | |||
}); | |||
it("completes field", () => { | |||
expect(inputField(wrapper).element.value).toBe("lesspass"); | |||
}); | |||
it('emits a "suggestionSelected" with site value', () => { | |||
const emitted = wrapper.findComponent(InputSite).emitted(); | |||
const profileSelected = emitted["suggestionSelected"]; | |||
expect(profileSelected.length).toBe(1); | |||
expect(profileSelected[0]).toEqual(["lesspass"]); | |||
}); | |||
}); | |||
}); | |||
}); |
@@ -1,94 +0,0 @@ | |||
<style> | |||
.awesomplete mark { | |||
background-color: transparent !important; | |||
padding: 0; | |||
margin: 0; | |||
color: inherit; | |||
} | |||
</style> | |||
<template> | |||
<div class="inputSite"> | |||
<label for="siteField" class="sr-only">{{ label }}</label> | |||
<div class="inner-addon left-addon"> | |||
<i class="fa fa-globe"></i> | |||
<input | |||
id="siteField" | |||
type="text" | |||
name="siteField" | |||
ref="siteField" | |||
class="form-control awesomplete" | |||
tabindex="0" | |||
autocorrect="off" | |||
autocapitalize="none" | |||
v-bind:placeholder="label" | |||
v-model="site" | |||
/> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import Awesomplete from "awesomplete"; | |||
import { getSuggestions } from "../services/url-parser"; | |||
export default { | |||
name: "inputSite", | |||
props: { | |||
value: String, | |||
label: String, | |||
passwords: { | |||
type: Array, | |||
default: () => [] | |||
} | |||
}, | |||
mounted() { | |||
this.awesomplete = new Awesomplete(this.$refs.siteField); | |||
this.awesomplete.item = (element, input) => { | |||
let item = Awesomplete.ITEM(element.value.site, input); | |||
item.innerHTML += ` ${element.value.login}`; | |||
return item; | |||
}; | |||
this.awesomplete.filter = (site, input) => { | |||
return ( | |||
Awesomplete.FILTER_CONTAINS(site, input) || | |||
Awesomplete.FILTER_CONTAINS(input, site) | |||
); | |||
}; | |||
this.awesomplete.data = data => { | |||
return { label: data.site, value: data }; | |||
}; | |||
this.awesomplete.replace = password => { | |||
this.$refs.siteField.value = password.label; | |||
if (password.value.suggestion) { | |||
this.$emit("suggestionSelected", password.value.site); | |||
} else { | |||
this.$emit("passwordProfileSelected", password.value); | |||
} | |||
}; | |||
this.awesomplete.sort = (a, b) => { | |||
return ( | |||
a.value.site.localeCompare(b.value.site) || | |||
a.value.login.localeCompare(b.value.login) | |||
); | |||
}; | |||
}, | |||
computed: { | |||
site: { | |||
get: function() { | |||
return this.value; | |||
}, | |||
set: function(newValue) { | |||
this.$emit("input", newValue); | |||
} | |||
} | |||
}, | |||
watch: { | |||
site: function(newValue) { | |||
const suggestions = getSuggestions(newValue).map(suggestion => { | |||
return { site: suggestion, suggestion: true, login: "" }; | |||
}); | |||
this.awesomplete.list = this.passwords.concat(suggestions); | |||
} | |||
}, | |||
methods: {} | |||
}; | |||
</script> |
@@ -16,9 +16,9 @@ | |||
</style> | |||
<template> | |||
<div class="masterPassword"> | |||
<div class="input-group inner-addon left-addon"> | |||
<label for="passwordField" class="sr-only">{{ label }}</label> | |||
<i class="fa fa-lock"></i> | |||
<label for="passwordField" class="sr-only">{{ label }}</label> | |||
<div class="input-group"> | |||
<span class="input-group-text"><i class="fa fa-lock"></i></span> | |||
<input | |||
id="passwordField" | |||
name="passwordField" | |||
@@ -33,31 +33,32 @@ | |||
v-on:input="updateValue($event.target.value)" | |||
v-on:keyup.enter="$emit('keyupEnter')" | |||
/> | |||
<span | |||
class="input-group-btn" | |||
<button | |||
id="fingerprint" | |||
class="btn" | |||
type="button" | |||
tabindex="-1" | |||
v-if="fingerprint && value" | |||
v-on:click="togglePasswordType" | |||
> | |||
<button id="fingerprint" class="btn" type="button" tabindex="-1"> | |||
<small> | |||
<i | |||
class="fa fa-fw" | |||
v-bind:class="[icon1]" | |||
v-bind:style="{ color: color1 }" | |||
></i> | |||
<i | |||
class="fa fa-fw" | |||
v-bind:class="[icon2]" | |||
v-bind:style="{ color: color2 }" | |||
></i> | |||
<i | |||
class="fa fa-fw" | |||
v-bind:class="[icon3]" | |||
v-bind:style="{ color: color3 }" | |||
></i> | |||
</small> | |||
</button> | |||
</span> | |||
<small> | |||
<i | |||
class="fa fa-fw" | |||
v-bind:class="[icon1]" | |||
v-bind:style="{ color: color1 }" | |||
></i> | |||
<i | |||
class="fa fa-fw" | |||
v-bind:class="[icon2]" | |||
v-bind:style="{ color: color2 }" | |||
></i> | |||
<i | |||
class="fa fa-fw" | |||
v-bind:class="[icon3]" | |||
v-bind:style="{ color: color3 }" | |||
></i> | |||
</small> | |||
</button> | |||
</div> | |||
</div> | |||
</template> | |||
@@ -25,25 +25,9 @@ | |||
>LessPass</span | |||
> | |||
</div> | |||
<div class="col-8 text-right"> | |||
<span v-if="saved && isAuthenticated"> | |||
<small><i class="fa fa-lg fa-check pl-3"></i> saved</small> | |||
</span> | |||
<span | |||
class="white-link" | |||
v-on:click="saveOrUpdatePassword()" | |||
v-if=" | |||
!saved && | |||
isAuthenticated && | |||
$store.state.password.site !== '' && | |||
$store.state.route.path === '/' | |||
" | |||
:title="$t('Save')" | |||
> | |||
<i class="fa fa-lg fa-save pointer"></i> | |||
</span> | |||
<div class="col-8 text-end"> | |||
<router-link | |||
class="white-link pl-3" | |||
class="white-link ps-3" | |||
:to="{ name: 'passwords' }" | |||
v-if="isAuthenticated" | |||
:title="$t('Saved passwords')" | |||
@@ -51,14 +35,14 @@ | |||
<i class="fa fa-lg fa-key"></i> | |||
</router-link> | |||
<router-link | |||
class="white-link pl-3" | |||
class="white-link ps-3" | |||
:to="{ name: 'settings' }" | |||
:title="$t('Settings')" | |||
> | |||
<i class="fa fa-lg fa-cog"></i> | |||
</router-link> | |||
<router-link | |||
class="white-link pl-3" | |||
class="white-link ps-3" | |||
:to="{ name: 'myaccount' }" | |||
v-if="isAuthenticated" | |||
:title="$t('My Account')" | |||
@@ -66,7 +50,7 @@ | |||
<i class="fa fa-lg fa-user pointer"></i> | |||
</router-link> | |||
<router-link | |||
class="white-link pl-3" | |||
class="white-link ps-3" | |||
:to="{ name: 'login' }" | |||
v-if="isGuest" | |||
:title="$t('Sign In')" | |||
@@ -82,22 +66,10 @@ | |||
import { mapGetters } from "vuex"; | |||
export default { | |||
data() { | |||
return { | |||
saved: false | |||
}; | |||
}, | |||
methods: { | |||
fullReload() { | |||
this.$store.dispatch("resetPassword"); | |||
this.$router.push({ name: "home" }).catch(e => {}); | |||
}, | |||
saveOrUpdatePassword() { | |||
this.$store.dispatch("saveOrUpdatePassword"); | |||
this.saved = true; | |||
setTimeout(() => { | |||
this.saved = false; | |||
}, 3000); | |||
} | |||
}, | |||
computed: { | |||
@@ -14,7 +14,7 @@ | |||
#message { | |||
position: absolute; | |||
top: 49px; | |||
top: 41px; | |||
left: 0; | |||
right: 0; | |||
z-index: 20; | |||
@@ -23,8 +23,8 @@ | |||
.close-notification { | |||
float: right; | |||
position: absolute; | |||
top: 0; | |||
right: 1em; | |||
top: 10px; | |||
right: 16px; | |||
cursor: pointer; | |||
} | |||
</style> | |||
@@ -11,92 +11,84 @@ | |||
</style> | |||
<template> | |||
<div id="options"> | |||
<div class="form-group row"> | |||
<div class="col-12"> | |||
<div class="row"> | |||
<div class="col"> | |||
<label for="types">{{ $t("Options") }}</label> | |||
<div class="row mb-2"> | |||
<label for="types">{{ $t("Options") }}</label> | |||
<div id="types" class="row"> | |||
<div class="col-3"> | |||
<div class="form-check"> | |||
<input | |||
id="lowercase__btn" | |||
type="checkbox" | |||
tabindex="1" | |||
class="form-check-input" | |||
v-model="options.lowercase" | |||
/> | |||
<label class="form-check-label" for="lowercase__btn"> | |||
a-z | |||
</label> | |||
</div> | |||
</div> | |||
<div id="types" class="row"> | |||
<div class="col-3"> | |||
<div class="form-check"> | |||
<input | |||
id="lowercase__btn" | |||
type="checkbox" | |||
tabindex="1" | |||
class="form-check-input" | |||
v-model="options.lowercase" | |||
/> | |||
<label class="form-check-label" for="lowercase__btn"> | |||
a-z | |||
</label> | |||
</div> | |||
</div> | |||
<div class="col-3"> | |||
<div class="form-check"> | |||
<input | |||
id="uppercase__btn" | |||
type="checkbox" | |||
tabindex="1" | |||
class="form-check-input" | |||
v-model="options.uppercase" | |||
/> | |||
<label class="form-check-label" for="uppercase__btn"> | |||
A-Z | |||
</label> | |||
</div> | |||
<div class="col-3"> | |||
<div class="form-check"> | |||
<input | |||
id="uppercase__btn" | |||
type="checkbox" | |||
tabindex="1" | |||
class="form-check-input" | |||
v-model="options.uppercase" | |||
/> | |||
<label class="form-check-label" for="uppercase__btn"> | |||
A-Z | |||
</label> | |||
</div> | |||
<div class="col-3"> | |||
<div class="form-check"> | |||
<input | |||
id="numbers__btn" | |||
type="checkbox" | |||
tabindex="1" | |||
class="form-check-input" | |||
v-model="options.numbers" | |||
/> | |||
<label class="form-check-label" for="numbers__btn"> | |||
0-9 | |||
</label> | |||
</div> | |||
</div> | |||
<div class="col-3"> | |||
<div class="form-check"> | |||
<input | |||
id="numbers__btn" | |||
type="checkbox" | |||
tabindex="1" | |||
class="form-check-input" | |||
v-model="options.numbers" | |||
/> | |||
<label class="form-check-label" for="numbers__btn"> | |||
0-9 | |||
</label> | |||
</div> | |||
<div class="col-3"> | |||
<div class="form-check"> | |||
<input | |||
id="symbols__btn" | |||
type="checkbox" | |||
tabindex="1" | |||
class="form-check-input" | |||
v-model="options.symbols" | |||
/> | |||
<label class="form-check-label" for="symbols__btn"> | |||
%!@ | |||
</label> | |||
</div> | |||
</div> | |||
<div class="col-3"> | |||
<div class="form-check"> | |||
<input | |||
id="symbols__btn" | |||
type="checkbox" | |||
tabindex="1" | |||
class="form-check-input" | |||
v-model="options.symbols" | |||
/> | |||
<label class="form-check-label" for="symbols__btn"> | |||
%!@ | |||
</label> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="form-group row mb-0"> | |||
<div class="row"> | |||
<div class="col-5 col-sm-4"> | |||
<label for="passwordLength">{{ $t("Length") }}</label> | |||
<div class="input-group input-group-sm"> | |||
<span class="input-group-btn"> | |||
<button | |||
id="decreaseLength__btn" | |||
class="btn btn-primary btn-sm px-2" | |||
tabindex="1" | |||
type="button" | |||
v-on:click=" | |||
options.length = decrement(options.length, { min: 5, max: 35 }) | |||
" | |||
> | |||
<small> | |||
<i class="fa fa-minus"></i> | |||
</small> | |||
</button> | |||
</span> | |||
<button | |||
id="decreaseLength__btn" | |||
class="btn btn-primary px-2" | |||
tabindex="1" | |||
type="button" | |||
v-on:click=" | |||
options.length = decrement(options.length, { min: 5, max: 35 }) | |||
" | |||
> | |||
<small> | |||
<i class="fa fa-minus"></i> | |||
</small> | |||
</button> | |||
<input | |||
id="passwordLength" | |||
class="form-control form-control-sm" | |||
@@ -106,21 +98,19 @@ | |||
max="35" | |||
v-model.number="options.length" | |||
/> | |||
<span class="input-group-btn"> | |||
<button | |||
id="increaseLength__btn" | |||
class="btn btn-primary btn-sm px-2" | |||
tabindex="1" | |||
type="button" | |||
v-on:click=" | |||
options.length = increment(options.length, { min: 5, max: 35 }) | |||
" | |||
> | |||
<small> | |||
<i class="fa fa-plus"></i> | |||
</small> | |||
</button> | |||
</span> | |||
<button | |||
id="increaseLength__btn" | |||
class="btn btn-primary px-2" | |||
tabindex="1" | |||
type="button" | |||
v-on:click=" | |||
options.length = increment(options.length, { min: 5, max: 35 }) | |||
" | |||
> | |||
<small> | |||
<i class="fa fa-plus"></i> | |||
</small> | |||
</button> | |||
</div> | |||
</div> | |||
<div class="col-5 col-sm-4"> | |||
@@ -137,21 +127,19 @@ | |||
>{{ $t("Counter") }}</label | |||
> | |||
<div class="input-group input-group-sm"> | |||
<span class="input-group-btn"> | |||
<button | |||
id="decreaseCounter__btn" | |||
class="btn btn-primary btn-sm px-2" | |||
tabindex="1" | |||
type="button" | |||
v-on:click=" | |||
options.counter = decrement(options.counter, { min: 1 }) | |||
" | |||
> | |||
<small> | |||
<i class="fa fa-minus"></i> | |||
</small> | |||
</button> | |||
</span> | |||
<button | |||
id="decreaseCounter__btn" | |||
class="btn btn-primary px-2" | |||
tabindex="1" | |||
type="button" | |||
v-on:click=" | |||
options.counter = decrement(options.counter, { min: 1 }) | |||
" | |||
> | |||
<small> | |||
<i class="fa fa-minus"></i> | |||
</small> | |||
</button> | |||
<input | |||
id="passwordCounter" | |||
class="form-control form-control-sm" | |||
@@ -160,21 +148,19 @@ | |||
min="1" | |||
v-model.number="options.counter" | |||
/> | |||
<span class="input-group-btn"> | |||
<button | |||
id="increaseCounter__btn" | |||
class="btn btn-primary btn-sm px-2" | |||
tabindex="1" | |||
type="button" | |||
v-on:click=" | |||
options.counter = increment(options.counter, { min: 1 }) | |||
" | |||
> | |||
<small> | |||
<i class="fa fa-plus"></i> | |||
</small> | |||
</button> | |||
</span> | |||
<button | |||
id="increaseCounter__btn" | |||
class="btn btn-primary px-2" | |||
tabindex="1" | |||
type="button" | |||
v-on:click=" | |||
options.counter = increment(options.counter, { min: 1 }) | |||
" | |||
> | |||
<small> | |||
<i class="fa fa-plus"></i> | |||
</small> | |||
</button> | |||
</div> | |||
</div> | |||
</div> | |||
@@ -8,7 +8,6 @@ import router from "./router"; | |||
import "bootstrap/dist/css/bootstrap.css"; | |||
import "font-awesome/css/font-awesome.css"; | |||
import "balloon-css/balloon.css"; | |||
import "awesomplete/awesomplete.css"; | |||
import { languagesAvailable, locales } from "./i18n"; | |||
@@ -49,22 +49,16 @@ export const getPasswords = ({ commit }) => { | |||
}); | |||
}; | |||
export const saveOrUpdatePassword = ({ commit, state }) => { | |||
const site = state.password.site; | |||
const login = state.password.login; | |||
const existingPassword = state.passwords.find(password => { | |||
return password.site === site && password.login === login; | |||
}); | |||
if (existingPassword) { | |||
const newPassword = Object.assign({}, existingPassword, state.password); | |||
Password.update(newPassword, state).then(() => { | |||
getPasswords({ commit, state }); | |||
}); | |||
} else { | |||
Password.create(state.password, state).then(() => { | |||
getPasswords({ commit, state }); | |||
}); | |||
} | |||
export const createPassword = ({ commit, state }) => { | |||
return Password.create(state.password, state).then(() => | |||
getPasswords({ commit, state }) | |||
); | |||
}; | |||
export const updatePassword = ({ commit, state }) => { | |||
return Password.update(state.password, state).then(() => | |||
getPasswords({ commit, state }) | |||
); | |||
}; | |||
export const deletePassword = ({ commit, state }, payload) => { | |||
@@ -1,8 +1,8 @@ | |||
<template> | |||
<form v-on:submit.prevent="signIn"> | |||
<div class="form-group"> | |||
<div class="inner-addon left-addon"> | |||
<i class="fa fa-globe"></i> | |||
<div class="mb-3"> | |||
<div class="input-group"> | |||
<span class="input-group-text"><i class="fa fa-globe"></i></span> | |||
<input | |||
id="baseURL" | |||
type="text" | |||
@@ -13,9 +13,9 @@ | |||
/> | |||
</div> | |||
</div> | |||
<div class="form-group"> | |||
<div class="inner-addon left-addon"> | |||
<i class="fa fa-user"></i> | |||
<div class="mb-3"> | |||
<div class="input-group"> | |||
<span class="input-group-text"><i class="fa fa-user"></i></span> | |||
<input | |||
id="email" | |||
class="form-control" | |||
@@ -28,13 +28,13 @@ | |||
/> | |||
</div> | |||
</div> | |||
<div class="form-group mb-1"> | |||
<div class="mb-1"> | |||
<master-password | |||
v-model="password" | |||
v-bind:label="$t('Master Password')" | |||
></master-password> | |||
</div> | |||
<div class="form-check form-switch mb-3"> | |||
<div class="form-check mb-3"> | |||
<input | |||
id="encryptMasterPassword" | |||
class="form-check-input" | |||
@@ -47,26 +47,26 @@ | |||
</small> | |||
</label> | |||
</div> | |||
<div class="form-group"> | |||
<button id="signInButton" class="btn btn-primary btn-block"> | |||
<div class="mb-3 d-grid"> | |||
<button id="signInButton" class="btn btn-primary"> | |||
{{ $t("Sign In") }} | |||
</button> | |||
</div> | |||
<div class="form-group"> | |||
<div class="mb-3"> | |||
<button | |||
id="login__forgot-password-btn" | |||
type="button" | |||
class="btn btn-link btn-sm p-0" | |||
class="btn btn-link p-0" | |||
v-on:click="$router.push({ name: 'passwordReset' })" | |||
> | |||
<small>{{ $t("ForgotPassword", "Forgot your password?") }}</small> | |||
</button> | |||
</div> | |||
<div class="form-group mb-0"> | |||
<div class="mb-0 d-grid"> | |||
<button | |||
id="login__no-account-btn" | |||
type="button" | |||
class="btn btn-light btn-block" | |||
class="btn btn-outline-dark" | |||
v-on:click="$router.push({ name: 'register' })" | |||
> | |||
<small>{{ | |||
@@ -1,55 +1,51 @@ | |||
<template> | |||
<div> | |||
<h5>{{ $t("Change my password") }}</h5> | |||
<div class="mb-3"> | |||
<h5>{{ $t("Change my password") }}</h5> | |||
</div> | |||
<form v-on:submit.prevent="changePassword"> | |||
<div class="form-group row"> | |||
<div class="col-12"> | |||
<div class="inner-addon left-addon"> | |||
<i class="fa fa-user"></i> | |||
<input | |||
id="email" | |||
class="form-control" | |||
name="email" | |||
type="email" | |||
placeholder="Email" | |||
v-model="email" | |||
/> | |||
</div> | |||
<div class="mb-3"> | |||
<div class="input-group"> | |||
<span class="input-group-text"><i class="fa fa-user"></i></span> | |||
<input | |||
id="email" | |||
class="form-control" | |||
name="email" | |||
type="email" | |||
placeholder="Email" | |||
v-model="email" | |||
/> | |||
</div> | |||
</div> | |||
<div class="form-group row"> | |||
<div class="col-12"> | |||
<master-password | |||
v-model="current_password" | |||
v-bind:label="$t('Current Master Password')" | |||
></master-password> | |||
</div> | |||
<div class="mb-3"> | |||
<master-password | |||
v-model="current_password" | |||
v-bind:label="$t('Current Master Password')" | |||
></master-password> | |||
</div> | |||
<div class="form-group row"> | |||
<div class="col-12"> | |||
<master-password | |||
v-model="new_password" | |||
v-bind:label="$t('New Master Password')" | |||
></master-password> | |||
</div> | |||
<div class="mb-3"> | |||
<master-password | |||
v-model="new_password" | |||
v-bind:label="$t('New Master Password')" | |||
></master-password> | |||
</div> | |||
<div class="form-group row"> | |||
<div class="col-12"> | |||
<button id="changeMyPasswordButton" class="btn btn-primary btn-block"> | |||
{{ $t("Change my password") }} | |||
</button> | |||
</div> | |||
<div class="mb-3"> | |||
<button id="changeMyPasswordButton" class="btn btn-primary"> | |||
{{ $t("Change my password") }} | |||
</button> | |||
</div> | |||
</form> | |||
<hr /> | |||
<button | |||
id="signOutButton" | |||
class="btn btn-success btn-block" | |||
type="button" | |||
v-on:click="logout" | |||
> | |||
{{ $t("Sign out") }} | |||
</button> | |||
<div> | |||
<button | |||
id="signOutButton" | |||
class="btn btn-success" | |||
type="button" | |||
v-on:click="logout" | |||
> | |||
{{ $t("Sign out") }} | |||
</button> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
@@ -2,14 +2,6 @@ | |||
#generated-password { | |||
font-family: Consolas, Menlo, Monaco, Courier New, monospace, sans-serif; | |||
} | |||
div.awesomplete { | |||
display: block; | |||
} | |||
div.awesomplete > ul { | |||
z-index: 11; | |||
} | |||
</style> | |||
<template> | |||
<form | |||
@@ -17,21 +9,29 @@ div.awesomplete > ul { | |||
v-on:submit.prevent="generatePassword" | |||
novalidate | |||
> | |||
<div class="form-group"> | |||
<input-site | |||
ref="site" | |||
v-model="password.site" | |||
v-bind:passwords="passwords" | |||
v-bind:label="$t('Site')" | |||
v-on:suggestionSelected="setSite" | |||
v-on:passwordProfileSelected="setPasswordProfile" | |||
></input-site> | |||
<div class="mb-3"> | |||
<label for="site" class="sr-only">{{ $t("Site") }}</label> | |||
<div class="input-group"> | |||
<span class="input-group-text"><i class="fa fa-globe"></i></span> | |||
<input | |||
id="site" | |||
type="text" | |||
name="site" | |||
ref="site" | |||
class="form-control" | |||
tabindex="0" | |||
autocorrect="off" | |||
autocapitalize="none" | |||
v-bind:placeholder="$t('Site')" | |||
v-model="password.site" | |||
/> | |||
</div> | |||
</div> | |||
<remove-auto-complete></remove-auto-complete> | |||
<div class="form-group"> | |||
<div class="mb-3"> | |||
<label for="login" class="sr-only">{{ $t("Username") }}</label> | |||
<div class="inner-addon left-addon"> | |||
<i class="fa fa-user"></i> | |||
<div class="input-group"> | |||
<span class="input-group-text"><i class="fa fa-user"></i></span> | |||
<input | |||
id="login" | |||
type="text" | |||
@@ -47,67 +47,63 @@ div.awesomplete > ul { | |||
/> | |||
</div> | |||
</div> | |||
<div class="form-group"> | |||
<div class="mb-3"> | |||
<master-password | |||
ref="masterPassword" | |||
v-model="masterPassword" | |||
v-bind:label="$t('Master Password')" | |||
></master-password> | |||
</div> | |||
<options v-bind:options="password"></options> | |||
<div class="form-group mt-4 mb-0"> | |||
<div class="mb-4"> | |||
<options v-bind:options="password"></options> | |||
</div> | |||
<div class="mb-0 d-grid"> | |||
<button | |||
id="generatePassword__btn" | |||
type="submit" | |||
tabindex="0" | |||
class="btn btn-primary btn-block" | |||
class="btn btn-primary" | |||
v-if="!passwordGenerated" | |||
> | |||
{{ $t("Generate") }} | |||
</button> | |||
<div class="input-group" v-show="passwordGenerated"> | |||
<span class="input-group-btn"> | |||
<button | |||
id="copyPasswordButton" | |||
class="btn btn-primary" | |||
tabindex="0" | |||
type="button" | |||
v-on:click="copyPassword()" | |||
> | |||
<i class="fa fa-clipboard"></i> | |||
</button> | |||
</span> | |||
<input | |||
id="generated-password" | |||
type="password" | |||
class="form-control" | |||
tabindex="-1" | |||
ref="passwordGenerated" | |||
v-bind:value="passwordGenerated" | |||
/> | |||
<span class="input-group-btn"> | |||
<button | |||
id="revealGeneratedPassword" | |||
type="button" | |||
class="btn btn-secondary" | |||
tabindex="0" | |||
v-on:click="togglePasswordType($refs.passwordGenerated)" | |||
> | |||
<i class="fa fa-eye"></i> | |||
</button> | |||
</span> | |||
<span class="input-group-btn"> | |||
<button | |||
id="sharePasswordProfileButton" | |||
type="button" | |||
class="btn btn-secondary" | |||
tabindex="0" | |||
v-on:click="sharePasswordProfile()" | |||
> | |||
<i class="fa fa-share-alt pointer"></i> | |||
</button> | |||
</span> | |||
</div> | |||
</div> | |||
<div class="input-group" v-show="passwordGenerated"> | |||
<button | |||
id="copyPasswordButton" | |||
class="btn btn-primary" | |||
tabindex="0" | |||
type="button" | |||
v-on:click="copyPassword()" | |||
> | |||
<i class="fa fa-clipboard"></i> | |||
</button> | |||
<input | |||
id="generated-password" | |||
type="password" | |||
class="form-control" | |||
tabindex="-1" | |||
ref="passwordGenerated" | |||
v-bind:value="passwordGenerated" | |||
/> | |||
<button | |||
id="revealGeneratedPassword" | |||
type="button" | |||
class="btn btn-secondary" | |||
tabindex="0" | |||
v-on:click="togglePasswordType($refs.passwordGenerated)" | |||
> | |||
<i class="fa fa-eye"></i> | |||
</button> | |||
<button | |||
id="sharePasswordProfileButton" | |||
type="button" | |||
class="btn btn-secondary" | |||
tabindex="0" | |||
v-on:click="sharePasswordProfile()" | |||
> | |||
<i class="fa fa-share-alt"></i> | |||
</button> | |||
</div> | |||
</form> | |||
</template> | |||
@@ -117,7 +113,6 @@ import { mapGetters, mapState } from "vuex"; | |||
import copy from "copy-text-to-clipboard"; | |||
import RemoveAutoComplete from "../components/RemoveAutoComplete.vue"; | |||
import MasterPassword from "../components/MasterPassword.vue"; | |||
import InputSite from "../components/InputSite.vue"; | |||
import Options from "../components/Options.vue"; | |||
import { showTooltip, hideTooltip } from "../services/tooltip"; | |||
import message from "../services/message"; | |||
@@ -127,7 +122,6 @@ export default { | |||
name: "password-generator-view", | |||
components: { | |||
RemoveAutoComplete, | |||
InputSite, | |||
MasterPassword, | |||
Options | |||
}, | |||
@@ -245,7 +239,7 @@ export default { | |||
}, | |||
focusBestInputField() { | |||
try { | |||
const site = this.$refs.site.$refs.siteField; | |||
const site = this.$refs.site; | |||
const login = this.$refs.login; | |||
const masterPassword = this.$refs.masterPassword; | |||
if (site && !site.value) return void site.focus(); | |||
@@ -285,13 +279,6 @@ export default { | |||
}, | |||
setSite(site) { | |||
this.password.site = site; | |||
}, | |||
setPasswordProfile(passwordProfile) { | |||
this.$store | |||
.dispatch("savePassword", { password: passwordProfile }) | |||
.then(() => { | |||
this.focusBestInputField(); | |||
}); | |||
} | |||
} | |||
}; | |||
@@ -1,36 +1,32 @@ | |||
<template> | |||
<form v-on:submit.prevent="resetPassword"> | |||
<div class="form-group row"> | |||
<div class="col-12"> | |||
<label for="email">{{ $t("Email") }}</label> | |||
<div class="inner-addon left-addon"> | |||
<i class="fa fa-user"></i> | |||
<input | |||
id="email" | |||
class="form-control" | |||
name="email" | |||
type="email" | |||
v-bind:placeholder="$t('Email')" | |||
v-model="email" | |||
/> | |||
</div> | |||
<small id="emailHelp" class="form-text text-muted">{{ | |||
$t( | |||
"ResetPasswordHelpText", | |||
"Enter your user account's verified email address and we will send you a password reset link." | |||
) | |||
}}</small> | |||
<div class="mb-3"> | |||
<label for="email">{{ $t("Email") }}</label> | |||
<div class="input-group"> | |||
<span class="input-group-text"><i class="fa fa-user"></i></span> | |||
<input | |||
id="email" | |||
class="form-control" | |||
name="email" | |||
type="email" | |||
v-bind:placeholder="$t('Email')" | |||
v-model="email" | |||
/> | |||
</div> | |||
<small id="emailHelp" class="form-text text-muted">{{ | |||
$t( | |||
"ResetPasswordHelpText", | |||
"Enter your user account's verified email address and we will send you a password reset link." | |||
) | |||
}}</small> | |||
</div> | |||
<div class="form-group row"> | |||
<div class="col-12"> | |||
<button | |||
id="password-reset__reset-password-btn" | |||
class="btn btn-primary btn-block" | |||
> | |||
{{ $t("Reset my password") }} | |||
</button> | |||
</div> | |||
<div class="mb-3 d-grid"> | |||
<button | |||
id="password-reset__reset-password-btn" | |||
class="btn btn-primary" | |||
> | |||
{{ $t("Reset my password") }} | |||
</button> | |||
</div> | |||
</form> | |||
</template> | |||
@@ -1,34 +1,28 @@ | |||
<template> | |||
<form v-on:submit.prevent="resetPasswordConfirm"> | |||
<div class="form-group row"> | |||
<div class="col-12"> | |||
<div class="inner-addon left-addon"> | |||
<i class="fa fa-user"></i> | |||
<input | |||
id="email" | |||
class="form-control" | |||
name="email" | |||
type="email" | |||
placeholder="Email" | |||
v-model="email" | |||
/> | |||
</div> | |||
<div class="mb-3"> | |||
<div class="input-group"> | |||
<span class="input-group-text"><i class="fa fa-user"></i></span> | |||
<input | |||
id="email" | |||
class="form-control" | |||
name="email" | |||
type="email" | |||
placeholder="Email" | |||
v-model="email" | |||
/> | |||
</div> | |||
</div> | |||
<div class="form-group row"> | |||
<div class="col-12"> | |||
<master-password | |||
v-model="password" | |||
v-bind:label="$t('Master Password')" | |||
></master-password> | |||
</div> | |||
<div class="mb-3"> | |||
<master-password | |||
v-model="password" | |||
v-bind:label="$t('Master Password')" | |||
></master-password> | |||
</div> | |||
<div class="form-group row"> | |||
<div class="col-12"> | |||
<button id="resetMyPasswordButton" class="btn btn-primary btn-block"> | |||
{{ $t("Reset my password") }} | |||
</button> | |||
</div> | |||
<div class="mb-3 d-grid"> | |||
<button id="resetMyPasswordButton" class="btn btn-primary"> | |||
{{ $t("Reset my password") }} | |||
</button> | |||
</div> | |||
</form> | |||
</template> | |||
@@ -16,8 +16,8 @@ | |||
<div id="passwords__search" class="pb-3"> | |||
<div class="row"> | |||
<div class="col"> | |||
<div class="inner-addon left-addon"> | |||
<i class="fa fa-search"></i> | |||
<div class="input-group"> | |||
<span class="input-group-text"><i class="fa fa-search"></i></span> | |||
<input | |||
class="form-control" | |||
type="text" | |||
@@ -1,8 +1,8 @@ | |||
<template> | |||
<form v-on:submit.prevent="signIn"> | |||
<div class="form-group"> | |||
<div class="inner-addon left-addon"> | |||
<i class="fa fa-globe"></i> | |||
<div class="mb-3"> | |||
<div class="input-group"> | |||
<span class="input-group-text"><i class="fa fa-globe"></i></span> | |||
<input | |||
id="baseURL" | |||
type="text" | |||
@@ -13,44 +13,42 @@ | |||
/> | |||
</div> | |||
</div> | |||
<div class="form-group row"> | |||
<div class="col-12"> | |||
<div class="inner-addon left-addon"> | |||
<i class="fa fa-user"></i> | |||
<input | |||
id="email" | |||
class="form-control" | |||
name="username" | |||
type="email" | |||
autocapitalize="none" | |||
v-bind:placeholder="$t('Email')" | |||
required | |||
v-model="email" | |||
/> | |||
</div> | |||
<div class="mb-3"> | |||
<div class="input-group"> | |||
<span class="input-group-text"><i class="fa fa-user"></i></span> | |||
<input | |||
id="email" | |||
class="form-control" | |||
name="username" | |||
type="email" | |||
autocapitalize="none" | |||
v-bind:placeholder="$t('Email')" | |||
required | |||
v-model="email" | |||
/> | |||
</div> | |||
</div> | |||
<div class="form-group"> | |||
<div class="mb-3"> | |||
<master-password | |||
v-model="password" | |||
v-bind:label="$t('Master Password')" | |||
></master-password> | |||
</div> | |||
<div class="form-group"> | |||
<div class="mb-3 d-grid"> | |||
<button | |||
id="registerButton" | |||
class="btn btn-primary btn-block" | |||
class="btn btn-primary" | |||
type="button" | |||
v-on:click="register" | |||
> | |||
{{ $t("Register") }} | |||
</button> | |||
</div> | |||
<div class="form-group mb-0"> | |||
<div class="mb-0 d-grid"> | |||
<button | |||
id="login__no-account-btn" | |||
type="button" | |||
class="btn btn-light btn-block" | |||
class="btn btn-outline-dark" | |||
v-on:click="$router.push({ name: 'login' })" | |||
> | |||
<small>{{ | |||
@@ -1,15 +1,17 @@ | |||
<template> | |||
<div> | |||
<h5>{{ $t("Options by default") }}</h5> | |||
<div class="mb-3"> | |||
<h5>{{ $t("Options by default") }}</h5> | |||
</div> | |||
<form | |||
id="lesspass-options-form" | |||
novalidate | |||
v-on:submit.prevent="saveAndExit" | |||
> | |||
<div class="form-group"> | |||
<div class="mb-3"> | |||
<label for="login">{{ $t("Username") }}</label> | |||
<div class="inner-addon left-addon"> | |||
<i class="fa fa-user"></i> | |||
<div class="input-group"> | |||
<span class="input-group-text"><i class="fa fa-user"></i></span> | |||
<input | |||
id="login" | |||
type="text" | |||
@@ -25,13 +27,15 @@ | |||
</div> | |||
</div> | |||
<options v-bind:options="defaultPassword"></options> | |||
<button | |||
type="submit" | |||
id="btn-submit-settings" | |||
class="btn btn-primary btn-block mt-4" | |||
> | |||
{{ $t("Save") }} | |||
</button> | |||
<div> | |||
<button | |||
type="submit" | |||
id="btn-submit-settings" | |||
class="btn btn-primary mt-4" | |||
> | |||
{{ $t("Save") }} | |||
</button> | |||
</div> | |||
</form> | |||
</div> | |||
</template> | |||
@@ -2602,11 +2602,6 @@ autoprefixer@^9.8.6: | |||
postcss "^7.0.32" | |||
postcss-value-parser "^4.1.0" | |||
awesomplete@^1.1.5: | |||
version "1.1.5" | |||
resolved "https://registry.yarnpkg.com/awesomplete/-/awesomplete-1.1.5.tgz#1b2b5dd106d3955595619c03da472a1dc0faf0af" | |||
integrity sha512-UFw1mPW8NaSECDSTC36HbAOTpF9JK2wBUJcNn4MSvlNtK7SZ9N72gB+ajHtA6D1abYXRcszZnBA4nHBwvFwzHw== | |||
aws-sign2@~0.7.0: | |||
version "0.7.0" | |||
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" | |||
@@ -2938,10 +2933,10 @@ boolbase@^1.0.0, boolbase@~1.0.0: | |||
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" | |||
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= | |||
bootstrap@^4.6.0: | |||
version "4.6.0" | |||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.6.0.tgz#97b9f29ac98f98dfa43bf7468262d84392552fd7" | |||
integrity sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw== | |||
bootstrap@^5.0.0-beta3: | |||
version "5.0.0-beta3" | |||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.0.0-beta3.tgz#c959f61fbd03667a1b158f763856994859d7a465" | |||
integrity sha512-0urccjfIOzhrb9qJysN8XW/DRw6rg3zH7qLeKIp4Zyl8+Ens4JWB0NC0cB5AhnSFPd2tftRggjwCMxablo6Tpg== | |||
brace-expansion@^1.1.7: | |||
version "1.1.11" | |||