Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
51 changes: 13 additions & 38 deletions apps/settings/src/components/AdminTwoFactor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
<NcCheckboxRadioSwitch
v-else
id="two-factor-enforced"
v-model="enforced"
v-model="twoFactorState.enforced"
type="switch">
{{ t('settings', 'Enforce two-factor authentication') }}
</NcCheckboxRadioSwitch>
<template v-if="enforced">
<template v-if="twoFactorState.enforced">
<h3>{{ t('settings', 'Limit to groups') }}</h3>
{{ t('settings', 'Enforcement of two-factor authentication can be set for certain groups only.') }}
<p class="top-margin">
Expand All @@ -29,7 +29,7 @@
<span>{{ t('settings', 'Enforced groups') }}</span>
</label>
<NcSelect
v-model="enforcedGroups"
v-model="twoFactorState.enforcedGroups"
input-id="enforcedGroups"
:options="groups"
:disabled="loading"
Expand All @@ -46,7 +46,7 @@
<span>{{ t('settings', 'Excluded groups') }}</span>
</label>
<NcSelect
v-model="excludedGroups"
v-model="twoFactorState.excludedGroups"
input-id="excludedGroups"
:options="groups"
:disabled="loading"
Expand Down Expand Up @@ -104,48 +104,23 @@ export default {
groups: [],
loadingGroups: false,
twoFactorAdminDoc: loadState('settings', 'two-factor-admin-doc'),
twoFactorState: loadState('settings', 'mandatory2FAState'),
}
},

computed: {
enforced: {
get() {
return this.$store.state.enforced
},

set(val) {
this.dirty = true
this.$store.commit('setEnforced', val)
},
},

enforcedGroups: {
get() {
return this.$store.state.enforcedGroups
},

set(val) {
this.dirty = true
this.$store.commit('setEnforcedGroups', val)
},
},

excludedGroups: {
get() {
return this.$store.state.excludedGroups
},

set(val) {
watch: {
twoFactorState: {
deep: true,
handler() {
this.dirty = true
this.$store.commit('setExcludedGroups', val)
},
},
},

mounted() {
// Groups are loaded dynamically, but the assigned ones *should*
// be valid groups, so let's add them as initial state
this.groups = sortedUniq(uniq(this.enforcedGroups.concat(this.excludedGroups)))
this.groups = sortedUniq(uniq(this.twoFactorState.enforcedGroups.concat(this.twoFactorState.excludedGroups)))

// Populate the groups with a first set so the dropdown is not empty
// when opening the page the first time
Expand All @@ -167,9 +142,9 @@ export default {
this.loading = true

const data = {
enforced: this.enforced,
enforcedGroups: this.enforcedGroups,
excludedGroups: this.excludedGroups,
enforced: this.twoFactorState.enforced,
enforcedGroups: this.twoFactorState.enforcedGroups,
excludedGroups: this.twoFactorState.excludedGroups,
}
axios.put(generateUrl('/settings/api/admin/twofactorauth'), data, { confirmPassword: PwdConfirmationMode.Strict })
.then((resp) => resp.data)
Expand Down
25 changes: 9 additions & 16 deletions apps/settings/src/components/AppNavigationGroupList.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { createTestingPinia } from '@pinia/testing'
import { mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import NcAppNavigationCaption from '@nextcloud/vue/components/NcAppNavigationCaption'

// The component builds a real Vuex store via useStore(); mock it so this stays
// a focused component test that controls its own data.
vi.mock('../store/index.js', () => ({
useStore: () => ({
getters: {
getServerData: { isAdmin: false, isDelegatedAdmin: false },
getSortedGroups: [],
getSubAdminGroups: [],
getSearchQuery: '',
},
commit: vi.fn(),
dispatch: vi.fn(),
}),
}))

vi.mock('vue-router/composables', async (importActual) => ({
...(await importActual<object>()),
useRoute: () => ({ params: {} }),
Expand All @@ -42,7 +28,14 @@ import AppNavigationGroupList from './AppNavigationGroupList.vue'

describe('AppNavigationGroupList', () => {
it('does not expose the group list as a heading (BITV 9.1.3.1a)', () => {
const wrapper = mount(AppNavigationGroupList)
// A testing pinia stubs the store actions, so this stays a focused
// component test that controls its own data.
const wrapper = mount(AppNavigationGroupList, {
pinia: createTestingPinia({
createSpy: vi.fn,
initialState: { 'settings-users': { serverData: { isAdmin: false, isDelegatedAdmin: false } } },
}),
})

// The sidebar group list is navigation, not document structure. It must
// not emit a heading, which would sit before the page <h1> in the DOM
Expand Down
18 changes: 9 additions & 9 deletions apps/settings/src/components/AppNavigationGroupList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,13 @@ import GroupListItem from './GroupListItem.vue'
import { useFormatGroups } from '../composables/useGroupsNavigation.ts'
import logger from '../logger.ts'
import { searchGroups } from '../service/groups.ts'
import { useStore } from '../store/index.js'
import { useUsersStore } from '../store/users.ts'

const emit = defineEmits<{
(e: 'select'): void
}>()

const store = useStore()
const usersStore = useUsersStore()
const route = useRoute()
const router = useRouter()

Expand All @@ -97,14 +97,14 @@ const selectedGroup = computed(() => route.params?.selectedGroup)
/** Current active group - URL decoded */
const selectedGroupDecoded = computed(() => selectedGroup.value ? decodeURIComponent(selectedGroup.value) : null)
/** Server settings for current user */
const settings = computed(() => store.getters.getServerData)
const settings = computed(() => usersStore.serverData)
/** True if the current user is a (delegated) admin */
const isAdminOrDelegatedAdmin = computed(() => settings.value.isAdmin || settings.value.isDelegatedAdmin)
/** All available groups */
const groups = computed(() => {
return isAdminOrDelegatedAdmin.value
? store.getters.getSortedGroups
: store.getters.getSubAdminGroups
? usersStore.sortedGroups
: usersStore.subAdminGroups
})
/** User groups */
const { userGroups } = useFormatGroups(groups)
Expand All @@ -123,7 +123,7 @@ const loadingGroups = ref(false)
/** Search offset */
const offset = ref(0)
/** Search query — shared via Vuex store */
const groupsSearchQuery = computed(() => store.getters.getSearchQuery)
const groupsSearchQuery = computed(() => usersStore.searchQuery)
const filteredGroups = computed(() => {
if (isAdminOrDelegatedAdmin.value) {
return userGroups.value
Expand All @@ -148,7 +148,7 @@ watch(isLastGroupVisible, async () => {
})

watch(groupsSearchQuery, async () => {
store.commit('resetGroups')
usersStore.resetGroups()
offset.value = 0
await loadGroups()
})
Expand Down Expand Up @@ -179,7 +179,7 @@ async function loadGroups() {
offset.value += 25
}
for (const group of groups) {
store.commit('addGroup', group)
usersStore.addGroup(group)
}
} catch (error) {
logger.error(t('settings', 'Failed to load groups'), { error })
Expand All @@ -203,7 +203,7 @@ async function createGroup() {
loadingAddGroup.value = true

try {
await store.dispatch('addGroup', groupId)
await usersStore.createGroup(groupId)
await router.push({
name: 'group',
params: {
Expand Down
13 changes: 10 additions & 3 deletions apps/settings/src/components/GroupListItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import AccountGroup from 'vue-material-design-icons/AccountGroupOutline.vue'
import Pencil from 'vue-material-design-icons/PencilOutline.vue'
import Delete from 'vue-material-design-icons/TrashCanOutline.vue'
import { useUsersStore } from '../store/users.ts'

export default {
name: 'GroupListItem',
Expand Down Expand Up @@ -144,6 +145,12 @@ export default {

emits: ['select'],

setup() {
return {
usersStore: useUsersStore(),
}
},

data() {
return {
loadingRenameGroup: false,
Expand All @@ -154,7 +161,7 @@ export default {

computed: {
settings() {
return this.$store.getters.getServerData
return this.usersStore.serverData
},
},

Expand All @@ -179,7 +186,7 @@ export default {
try {
this.openGroupMenu = false
this.loadingRenameGroup = true
await this.$store.dispatch('renameGroup', {
await this.usersStore.renameGroup({
groupid: gid.trim(),
displayName: displayName.trim(),
})
Expand All @@ -193,7 +200,7 @@ export default {

async removeGroup() {
try {
await this.$store.dispatch('removeGroup', this.id)
await this.usersStore.removeGroup(this.id)
this.showRemoveGroupModal = false
} catch {
showError(t('settings', 'Failed to delete group "{group}"', { group: this.name }))
Expand Down
Loading
Loading