summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorRichard Steinmetz <richard@steinmetz.cloud>2022-06-03 18:23:19 +0200
committerRichard Steinmetz <richard@steinmetz.cloud>2022-06-28 10:16:41 +0200
commit51ed90ee20269eb46288b187e6250e23cb25b94f (patch)
tree0d8888341976f037918cc64fb0ac8b0576aa097b /src
parent2e155bb9c0252dd26764fc6dc9f30d7088959707 (diff)
Implement share password settings
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
Diffstat (limited to 'src')
-rw-r--r--src/components/CircleDetails.vue3
-rw-r--r--src/components/CircleDetails/CirclePasswordSettings.vue274
-rw-r--r--src/services/circles.d.ts5
-rw-r--r--src/services/circles.ts15
-rw-r--r--src/store/circles.js25
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 }