diff options
author | Richard Steinmetz <richard@steinmetz.cloud> | 2022-06-03 18:23:19 +0200 |
---|---|---|
committer | Richard Steinmetz <richard@steinmetz.cloud> | 2022-06-28 10:16:41 +0200 |
commit | 51ed90ee20269eb46288b187e6250e23cb25b94f (patch) | |
tree | 0d8888341976f037918cc64fb0ac8b0576aa097b /src | |
parent | 2e155bb9c0252dd26764fc6dc9f30d7088959707 (diff) |
Implement share password settings
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
Diffstat (limited to 'src')
-rw-r--r-- | src/components/CircleDetails.vue | 3 | ||||
-rw-r--r-- | src/components/CircleDetails/CirclePasswordSettings.vue | 274 | ||||
-rw-r--r-- | src/services/circles.d.ts | 5 | ||||
-rw-r--r-- | src/services/circles.ts | 15 | ||||
-rw-r--r-- | src/store/circles.js | 25 |
5 files changed, 320 insertions, 2 deletions
diff --git a/src/components/CircleDetails.vue b/src/components/CircleDetails.vue index 85cc5110..bd06ed61 100644 --- a/src/components/CircleDetails.vue +++ b/src/components/CircleDetails.vue @@ -93,6 +93,7 @@ <section v-if="circle.isOwner && !circle.isPersonal" class="circle-details-section"> <CircleConfigs class="circle-details-section__configs" :circle="circle" /> + <CirclePasswordSettings class="circle-details-section__configs" :circle="circle" /> </section> <section v-else> @@ -137,6 +138,7 @@ import CircleActionsMixin from '../mixins/CircleActionsMixin' import DetailsHeader from './DetailsHeader' import CircleConfigs from './CircleDetails/CircleConfigs' import ContentHeading from './CircleDetails/ContentHeading' +import CirclePasswordSettings from './CircleDetails/CirclePasswordSettings' export default { name: 'CircleDetails', @@ -145,6 +147,7 @@ export default { AppContentDetails, Avatar, CircleConfigs, + CirclePasswordSettings, ContentHeading, DetailsHeader, Login, diff --git a/src/components/CircleDetails/CirclePasswordSettings.vue b/src/components/CircleDetails/CirclePasswordSettings.vue new file mode 100644 index 00000000..c1795393 --- /dev/null +++ b/src/components/CircleDetails/CirclePasswordSettings.vue @@ -0,0 +1,274 @@ +<!-- + - @copyright Copyright (c) 2022 Richard Steinmetz <richard@steinmetz.cloud> + - + - @author Richard Steinmetz <richard@steinmetz.cloud> + - + - @license AGPL-3.0-or-later + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <ul> + <li class="circle-config"> + <ContentHeading class="circle-config__title"> + {{ t('contacts', 'Password protection') }} + </ContentHeading> + + <ul class="circle-config__list"> + <CheckboxRadioSwitch :checked="enforcePasswordProtection" + :loading="loading.includes(ENFORCE_PASSWORD_PROTECTION)" + :disabled="loading.length > 0" + wrapper-element="li" + @update:checked="changePasswordProtection"> + {{ t('contacts', 'Enforce Password protection on Files shared to this circle') }} + </CheckboxRadioSwitch> + + <CheckboxRadioSwitch v-if="enforcePasswordProtection" + :checked="useUniquePassword || showUniquePasswordInput" + :loading="loading.includes(USE_UNIQUE_PASSWORD)" + :disabled="loading.length > 0" + wrapper-element="li" + @update:checked="changeUseUniquePassword"> + {{ t('contacts', 'Use a unique password for all shares to this circles') }} + </CheckboxRadioSwitch> + + <li class="unique-password"> + <template v-if="showUniquePasswordInput"> + <input + v-model="uniquePassword" + :disabled="loading.length > 0" + :placeholder="t('contacts', 'Unique password ...')" + type="text" + @keyup.enter="saveUniquePassword" /> + <Button + type="tertiary-no-background" + :disabled="loading.length > 0 || uniquePassword.length === 0" + @click="saveUniquePassword"> + {{ t('contacts', 'Save') }} + </Button> + </template> + <Button + v-else-if="useUniquePassword" + class="change-unique-password" + @click="onClickChangePassword"> + {{ t('contacts', 'Change unique password') }} + </Button> + + <div v-if="uniquePasswordError" class="unique-password-error"> + {{ t('contacts', 'Failed to save password. Please try again later.') }} + </div> + </li> + </ul> + </li> + </ul> +</template> + +<script> +import ContentHeading from './ContentHeading' +import CheckboxRadioSwitch from '@nextcloud/vue/dist/Components/CheckboxRadioSwitch' +import Button from '@nextcloud/vue/dist/Components/Button' + +// Circle setting keys +const ENFORCE_PASSWORD_PROTECTION = 'enforce_password' +const USE_UNIQUE_PASSWORD = 'password_single_enabled' +const UNIQUE_PASSWORD = 'password_single' + +export default { + name: 'CirclePasswordSettings', + components: { + ContentHeading, + CheckboxRadioSwitch, + Button, + }, + props: { + circle: { + type: Object, + required: true, + }, + }, + data() { + return { + ENFORCE_PASSWORD_PROTECTION, + USE_UNIQUE_PASSWORD, + UNIQUE_PASSWORD, + + loading: [], + + uniquePassword: '', + uniquePasswordError: false, + showUniquePasswordInput: false, + } + }, + computed: { + /** + * @return {string} + */ + circleId() { + return this.circle._data.id + }, + + /** + * @return {boolean} + */ + enforcePasswordProtection() { + const value = this.circle._data.settings[ENFORCE_PASSWORD_PROTECTION] + return value === '1' || value === 'true' + }, + + /** + * @return {boolean} + */ + useUniquePassword() { + const value = this.circle._data.settings[USE_UNIQUE_PASSWORD] + return value === '1' || value === 'true' + }, + }, + methods: { + /** + * Change handler for enforcePasswordProtection checkbox. + */ + async changePasswordProtection() { + this.loading.push(ENFORCE_PASSWORD_PROTECTION) + try { + const newValue = !this.enforcePasswordProtection + + // Also disable unique password setting + if (!newValue && this.useUniquePassword) { + await this.saveUseUniquePassword(false) + } + + // Also hide password input + if (!newValue && this.showUniquePasswordInput) { + this.showUniquePasswordInput = false + } + + await this.$store.dispatch('editCircleSetting', { + circleId: this.circleId, + setting: { + setting: ENFORCE_PASSWORD_PROTECTION, + value: newValue.toString(), + }, + }) + } finally { + this.loading = this.loading.filter(item => item !== ENFORCE_PASSWORD_PROTECTION) + } + }, + + /** + * Change handler for useUniquePassword checkbox. + */ + async changeUseUniquePassword() { + // Only update backend if the user disables the setting. + // It will be enabled once a unique password has been set. + if (!this.useUniquePassword) { + this.showUniquePasswordInput = !this.showUniquePasswordInput + return + } + + await this.saveUseUniquePassword(!this.useUniquePassword) + }, + + /** + * Update backend with given value for useUniquePassword. + * + * @param {boolean} value New value + */ + async saveUseUniquePassword(value) { + this.loading.push(USE_UNIQUE_PASSWORD) + try { + await this.$store.dispatch('editCircleSetting', { + circleId: this.circleId, + setting: { + setting: USE_UNIQUE_PASSWORD, + value: value.toString(), + }, + }) + + // Reset unique password input state if disabled + if (!value) { + this.uniquePassword = '' + this.showUniquePasswordInput = false + } + } finally { + this.loading = this.loading.filter(item => item !== USE_UNIQUE_PASSWORD) + } + }, + + /** + * Persist uniquePassword to backend. + */ + async saveUniquePassword() { + if (this.uniquePassword.length === 0) { + return + } + + this.loading.push(UNIQUE_PASSWORD) + this.uniquePasswordError = false + try { + if (!this.useUniquePassword) { + await this.saveUseUniquePassword(true) + } + + await this.$store.dispatch('editCircleSetting', { + circleId: this.circleId, + setting: { + setting: UNIQUE_PASSWORD, + value: this.uniquePassword, + }, + }) + + // Show change button after saving the password + this.showUniquePasswordInput = false + this.uniquePassword = '' + } catch { + this.uniquePasswordError = true + } finally { + this.loading = this.loading.filter(item => item !== UNIQUE_PASSWORD) + } + }, + + /** + * Click handler for the button to show the uniquePassword input. + */ + onClickChangePassword() { + this.showUniquePasswordInput = true + }, + }, +} +</script> + +<style lang="scss" scoped> +.unique-password { + display: flex; + align-items: center; + flex-wrap: wrap; + width: 100%; + + input { + flex: 1 auto; + max-width: 200px; + } + + .change-unique-password { + margin-top: 5px; + } + + // Force wrap error into a new line + .unique-password-error { + flex: 1 100%; + } +} +</style> diff --git a/src/services/circles.d.ts b/src/services/circles.d.ts index e130d160..614e2b8d 100644 --- a/src/services/circles.d.ts +++ b/src/services/circles.d.ts @@ -31,6 +31,10 @@ export declare enum CircleEdit { Settings = "settings", Config = "config" } +interface CircleSetting { + setting: string; + value: string; +} /** * Get the circles list without the members * @@ -128,4 +132,5 @@ export declare const changeMemberLevel: (circleId: string, memberId: string, lev * @returns {Array} */ export declare const acceptMember: (circleId: string, memberId: string) => Promise<any>; +export declare const editCircleSetting: (circleId: string, setting: CircleSetting) => Promise<any>; export {}; diff --git a/src/services/circles.ts b/src/services/circles.ts index 5d93bc7a..3290de64 100644 --- a/src/services/circles.ts +++ b/src/services/circles.ts @@ -36,6 +36,11 @@ export enum CircleEdit { Config = 'config', } +interface CircleSetting { + setting: string, + value: string +} + /** * Get the circles list without the members * @@ -48,7 +53,7 @@ export const getCircles = async function() { /** * Get a specific circle - * @param {string} circleId + * @param {string} circleId * @returns {Object} */ export const getCircle = async function(circleId: string) { @@ -194,3 +199,11 @@ export const acceptMember = async function(circleId: string, memberId: string) { const response = await axios.put(generateOcsUrl('apps/circles/circles/{circleId}/members/{memberId}', { circleId, memberId })) return response.data.ocs.data } + +export const editCircleSetting = async function(circleId: string, setting: CircleSetting) { + const response = await axios.put( + generateOcsUrl('apps/circles/circles/{circleId}/setting', { circleId }), + setting, + ) + return response.data.ocs.data +} diff --git a/src/store/circles.js b/src/store/circles.js index 5b78bc1f..d570eede 100644 --- a/src/store/circles.js +++ b/src/store/circles.js @@ -23,7 +23,18 @@ import { showError } from '@nextcloud/dialogs' import Vue from 'vue' -import { acceptMember, createCircle, deleteCircle, deleteMember, getCircleMembers, getCircle, getCircles, leaveCircle, addMembers } from '../services/circles.ts' +import { + acceptMember, + createCircle, + deleteCircle, + deleteMember, + getCircleMembers, + getCircle, + getCircles, + leaveCircle, + addMembers, + editCircleSetting, +} from '../services/circles.ts' import Member from '../models/member.ts' import Circle from '../models/circle.ts' import logger from '../services/logger' @@ -95,6 +106,10 @@ const mutations = { // Circles dependencies are managed directly from the model member.delete() }, + + setCircleSettings(state, { circleId, settings }) { + Vue.set(state.circles[circleId]._data, 'settings', settings) + }, } const getters = { @@ -273,6 +288,14 @@ const actions = { await context.commit('addMemberToCircle', { circleId, member }) }, + async editCircleSetting(context, { circleId, setting }) { + const { settings } = await editCircleSetting(circleId, setting) + await context.commit('setCircleSettings', { + circleId, + settings, + }) + }, + } export default { state, mutations, getters, actions } |