Skip to content

Commit

Permalink
Init password gen for links (#9691)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jan authored Sep 19, 2023
1 parent 29a732d commit 0da7dba
Show file tree
Hide file tree
Showing 13 changed files with 183 additions and 27 deletions.
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
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 as PropType<(...args: unknown[]) => string>,
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 as PropType<(...args: unknown[]) => string>,
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 as PropType<(...args: unknown[]) => string>,
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

0 comments on commit 0da7dba

Please sign in to comment.