Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Init password gen for links #9691

Merged
merged 18 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Enhancement: Password generator for public links

We've added a new button on the password input field for public links,
clicking on that button will fill the input with a generated password.

If a password policy is set, those rules will also be applied.

https://github.com/owncloud/web/pull/9691
https://github.com/owncloud/web/issues/9666
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/design-system/src/assets/icons/sparkling-fill.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/design-system/src/assets/icons/sparkling-line.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions packages/design-system/src/components/OcModal/OcModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
:label="inputLabel"
:type="inputType"
:password-policy="inputPasswordPolicy"
:generate-password-method="inputGeneratePasswordMethod"
:description-message="inputDescription"
:disabled="inputDisabled"
:fix-message-line="true"
Expand Down Expand Up @@ -371,6 +372,14 @@ export default defineComponent({
required: false,
default: () => ({})
},
/**
* Method to generate random password for the input
*/
inputGeneratePasswordMethod: {
type: Function,
AlexAndBear marked this conversation as resolved.
Show resolved Hide resolved
required: false,
default: null
},
/**
* Overwrite default focused element
* Can be `#id, .class`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ describe('OcTextInput', () => {
inputField: '.oc-text-input',
infoIcon: '.oc-text-input-message .oc-icon',
showPasswordToggleBtn: '.oc-text-input-show-password-toggle',
copyPasswordBtn: '.oc-text-input-copy-password-button'
copyPasswordBtn: '.oc-text-input-copy-password-button',
generatePasswordBtn: '.oc-text-input-generate-password-button'
}

describe('id prop', () => {
Expand Down Expand Up @@ -121,6 +122,30 @@ describe('OcTextInput', () => {
expect(wrapper.find(selectors.inputField).attributes().type).toBe('password')
})
})
describe('generate password button', () => {
it('should not exist if type is not "password" or prop "generatePasswordMethod" is not provided', () => {
const wrapper = getMountedWrapper()
expect(wrapper.find(selectors.generatePasswordBtn).exists()).toBeFalsy()

const wrapper2 = getMountedWrapper({ props: { type: 'password' } })
expect(wrapper2.find(selectors.generatePasswordBtn).exists()).toBeFalsy()
})
it('should exist if type is "password" and prop "generatePasswordMethod" is provided', () => {
const wrapper = getMountedWrapper({
props: { generatePasswordMethod: jest.fn(), type: 'password' }
})
expect(wrapper.find(selectors.generatePasswordBtn).exists()).toBeTruthy()
})
it('should fill input with generated password if clicked', async () => {
const wrapper = getMountedWrapper({
props: { generatePasswordMethod: jest.fn(() => 'PAssword12#!'), type: 'password' }
})
await wrapper.find(selectors.generatePasswordBtn).trigger('click')
expect((wrapper.find(selectors.inputField).element as HTMLInputElement).value).toEqual(
'PAssword12#!'
)
})
})
describe('password policy', () => {
it('should emit "passwordChallengeFailed" if password does not match criteria', async () => {
const wrapper = getMountedWrapper(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,14 @@ export default defineComponent({
passwordPolicy: {
type: Object as PropType<PasswordPolicy>,
default: () => ({})
},
/**
* Method to generate random password
*/
generatePasswordMethod: {
type: Function,
required: false,
default: null
}
},
emits: [
Expand Down Expand Up @@ -264,6 +272,7 @@ export default defineComponent({
}
if (this.type === 'password') {
additionalAttrs['password-policy'] = this.passwordPolicy
additionalAttrs['generate-password-method'] = this.generatePasswordMethod
}
// Exclude listeners for events which are handled via methods in this component
// eslint-disable-next-line no-unused-vars
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,26 @@

exports[`OcTextInput password input field password policy displays error state if password does not match criteria 1`] = `
<div class="">
<label class="oc-label" for="oc-textinput-17"></label>
<label class="oc-label" for="oc-textinput-21"></label>
<div class="oc-position-relative">
<!--v-if-->
<div class="oc-text-input-password-wrapper">
<input aria-invalid="false" class="oc-text-input oc-input oc-rounded" id="oc-textinput-17" type="password">
<button class="oc-button oc-rounded oc-button-s oc-button-justify-content-center oc-button-gap-m oc-button-passive oc-button-passive-raw oc-text-input-copy-password-button oc-px-s oc-background-default" type="button">
<input aria-invalid="false" class="oc-text-input oc-input oc-rounded" id="oc-textinput-21" type="password">
<button class="oc-button oc-rounded oc-button-s oc-button-justify-content-center oc-button-gap-m oc-button-passive oc-button-passive-raw oc-text-input-show-password-toggle oc-px-s oc-background-default" type="button">
<!--v-if-->
<!-- @slot Content of the button -->
<span class="oc-icon oc-icon-s oc-icon-passive">
<!---->
</span>
</button>
<button class="oc-button oc-rounded oc-button-s oc-button-justify-content-center oc-button-gap-m oc-button-passive oc-button-passive-raw oc-text-input-show-password-toggle oc-px-s oc-background-default" type="button">
<button class="oc-button oc-rounded oc-button-s oc-button-justify-content-center oc-button-gap-m oc-button-passive oc-button-passive-raw oc-text-input-copy-password-button oc-px-s oc-background-default" type="button">
<!--v-if-->
<!-- @slot Content of the button -->
<span class="oc-icon oc-icon-s oc-icon-passive">
<!---->
</span>
</button>
<!--v-if-->
</div>
<portal to="app.design-system.password-policy">
<div class="oc-text-small oc-flex oc-flex-column">
Expand All @@ -42,25 +43,26 @@ exports[`OcTextInput password input field password policy displays error state i

exports[`OcTextInput password input field password policy displays success state if password matches criteria 1`] = `
<div class="">
<label class="oc-label" for="oc-textinput-18"></label>
<label class="oc-label" for="oc-textinput-22"></label>
<div class="oc-position-relative">
<!--v-if-->
<div class="oc-text-input-password-wrapper">
<input aria-invalid="false" class="oc-text-input oc-input oc-rounded" id="oc-textinput-18" type="password">
<button class="oc-button oc-rounded oc-button-s oc-button-justify-content-center oc-button-gap-m oc-button-passive oc-button-passive-raw oc-text-input-copy-password-button oc-px-s oc-background-default" type="button">
<input aria-invalid="false" class="oc-text-input oc-input oc-rounded" id="oc-textinput-22" type="password">
<button class="oc-button oc-rounded oc-button-s oc-button-justify-content-center oc-button-gap-m oc-button-passive oc-button-passive-raw oc-text-input-show-password-toggle oc-px-s oc-background-default" type="button">
<!--v-if-->
<!-- @slot Content of the button -->
<span class="oc-icon oc-icon-s oc-icon-passive">
<!---->
</span>
</button>
<button class="oc-button oc-rounded oc-button-s oc-button-justify-content-center oc-button-gap-m oc-button-passive oc-button-passive-raw oc-text-input-show-password-toggle oc-px-s oc-background-default" type="button">
<button class="oc-button oc-rounded oc-button-s oc-button-justify-content-center oc-button-gap-m oc-button-passive oc-button-passive-raw oc-text-input-copy-password-button oc-px-s oc-background-default" type="button">
<!--v-if-->
<!-- @slot Content of the button -->
<span class="oc-icon oc-icon-s oc-icon-passive">
<!---->
</span>
</button>
<!--v-if-->
</div>
<portal to="app.design-system.password-policy">
<div class="oc-text-small oc-flex oc-flex-column">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,40 @@
<template>
<div ref="inputPasswordWrapper" class="oc-text-input-password-wrapper">
<input v-bind="$attrs" :type="showPassword ? 'text' : 'password'" @input="onInput" />
<div class="oc-text-input-password-wrapper">
<input
v-bind="$attrs"
:type="showPassword ? 'text' : 'password'"
v-model="password"
@input="onPasswordEntered"
/>
<oc-button
v-if="password"
class="oc-text-input-show-password-toggle oc-px-s oc-background-default"
appearance="raw"
size="small"
@click="showPassword = !showPassword"
v-oc-tooltip="$gettext('Show password')"
>
<oc-icon size="small" :name="showPassword ? 'eye-off' : 'eye'" />
</oc-button>
<oc-button
v-if="password"
class="oc-text-input-copy-password-button oc-px-s oc-background-default"
appearance="raw"
size="small"
@click="copyPasswordToClipboard"
v-oc-tooltip="$gettext('Copy password')"
>
<oc-icon size="small" :name="copyPasswordIcon" />
</oc-button>
<oc-button
v-if="password"
class="oc-text-input-show-password-toggle oc-px-s oc-background-default"
v-if="generatePasswordMethod"
class="oc-text-input-generate-password-button oc-px-s oc-background-default"
appearance="raw"
size="small"
@click="showPassword = !showPassword"
@click="showGeneratedPassword"
v-oc-tooltip="$gettext('Generate password')"
>
<oc-icon size="small" :name="showPassword ? 'eye-off' : 'eye'" />
<oc-icon size="small" name="refresh" fill-type="line" />
</oc-button>
</div>
<portal v-if="showPasswordPolicyInformation" to="app.design-system.password-policy">
Expand Down Expand Up @@ -58,31 +75,38 @@ export default defineComponent({
release: '1.0.0',
inheritAttrs: true,
props: {
value: {
type: String,
required: false,
default: ''
},
passwordPolicy: {
type: Object as PropType<PasswordPolicy>,
default: () => ({})
},
generatePasswordMethod: {
type: Function,
required: false,
default: null
}
},
emits: ['passwordChallengeCompleted', 'passwordChallengeFailed'],
setup(props, { emit }) {
const { $gettext } = useGettext()
const password = ref(props.value)
const showPassword = ref(false)
const passwordEntered = ref(false)
const password = ref('')
const copyPasswordIconInitial = 'file-copy'
const copyPasswordIcon = ref(copyPasswordIconInitial)

const showPasswordPolicyInformation = computed(() => {
return !!(Object.keys(props.passwordPolicy?.rules || {}).length && unref(passwordEntered))
})

const testedPasswordPolicy = computed(() => {
return props.passwordPolicy.missing(unref(password))
})

const onInput = (event) => {
passwordEntered.value = true
password.value = event.target.value
}

const getPasswordPolicyRuleMessage = (rule) => {
const paramObj = {}

Expand All @@ -99,6 +123,16 @@ export default defineComponent({
setTimeout(() => (copyPasswordIcon.value = copyPasswordIconInitial), 500)
}

const showGeneratedPassword = () => {
const generatedPassword = props.generatePasswordMethod()
password.value = generatedPassword
showPassword.value = true
}

const onPasswordEntered = () => {
passwordEntered.value = true
}

watch(password, (value) => {
if (!Object.keys(props.passwordPolicy).length) {
return
Expand All @@ -113,14 +147,15 @@ export default defineComponent({

return {
$gettext,
onInput,
password,
showPassword,
showPasswordPolicyInformation,
testedPasswordPolicy,
getPasswordPolicyRuleMessage,
copyPasswordToClipboard,
copyPasswordIcon
showGeneratedPassword,
copyPasswordIcon,
onPasswordEntered
}
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,7 @@ export default defineComponent({
confirmDisabled: true,
inputLabel: this.$gettext('Password'),
inputPasswordPolicy: this.passwordPolicyService.getPolicy(),
inputGeneratePasswordMethod: () => this.passwordPolicyService.generatePassword(),
inputPlaceholder: this.link.password ? '●●●●●●●●' : null,
inputType: 'password',
onCancel: this.hideModal,
Expand Down
1 change: 1 addition & 0 deletions packages/web-app-files/src/quickActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export function showQuickLinkPasswordModal({ $gettext, store, passwordPolicyServ
hasInput: true,
inputDescription: $gettext('Passwords for links are required.'),
inputPasswordPolicy: passwordPolicyService.getPolicy(),
inputGeneratePasswordMethod: () => passwordPolicyService.generatePassword(),
inputLabel: $gettext('Password'),
inputType: 'password',
onCancel: () => store.dispatch('hideModal'),
Expand Down
5 changes: 3 additions & 2 deletions packages/web-pkg/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@
"design-system": "workspace:@ownclouders/design-system@*",
"filesize": "^9.0.11",
"fuse.js": "^6.5.3",
"js-generate-password": "^0.1.9",
"lodash-es": "^4.17.21",
"luxon": "^2.4.0",
"mark.js": "^8.11.1",
"password-sheriff": "^1.1.1",
"pinia": "^2.1.3",
"qs": "^6.10.3",
"semver": "^7.3.8",
Expand All @@ -34,7 +36,6 @@
"vue3-gettext": "2.5.0-alpha.1",
"vuex": "4.1.0",
"web-client": "workspace:@ownclouders/web-client@*",
"web-pkg": "workspace:@ownclouders/web-pkg@*",
"password-sheriff": "^1.1.1"
"web-pkg": "workspace:@ownclouders/web-pkg@*"
}
}
Loading