193 lines
4.9 KiB
Vue
193 lines
4.9 KiB
Vue
<template>
|
|
<div class="p-6">
|
|
<h2 class="kestrel-page-heading mb-2">
|
|
Members
|
|
</h2>
|
|
<p
|
|
v-if="!user"
|
|
class="text-sm text-kestrel-muted"
|
|
>
|
|
Sign in to view members.
|
|
</p>
|
|
<p
|
|
v-else-if="!canEditPois"
|
|
class="text-sm text-kestrel-muted"
|
|
>
|
|
You don't have access to the members list.
|
|
</p>
|
|
<template v-else>
|
|
<p
|
|
v-if="isAdmin"
|
|
class="mb-3 text-xs text-kestrel-muted"
|
|
>
|
|
Only admins can change roles and manage local users. OIDC users are managed via your identity provider.
|
|
</p>
|
|
<div
|
|
v-if="isAdmin"
|
|
class="mb-3 flex justify-start"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="rounded border border-kestrel-accent bg-kestrel-accent/10 px-3 py-1.5 text-sm font-medium text-kestrel-accent hover:bg-kestrel-accent-dim"
|
|
@click="openAddUserModal"
|
|
>
|
|
Add user
|
|
</button>
|
|
</div>
|
|
<MembersTable
|
|
:users="users"
|
|
:role-by-user-id="roleByUserId"
|
|
:role-options="roleOptions"
|
|
:is-admin="isAdmin"
|
|
:current-user-id="user?.id ?? null"
|
|
:open-role-dropdown-id="openRoleDropdownId"
|
|
@toggle-role-dropdown="toggleRoleDropdown"
|
|
@close-role-dropdown="openRoleDropdownId = null"
|
|
@select-role="selectRole"
|
|
@save-role="saveRole"
|
|
@edit-user="openEditUser"
|
|
@delete-confirm="openDeleteConfirm"
|
|
/>
|
|
<!-- Add user modal -->
|
|
<AddUserModal
|
|
:show="addUserModalOpen"
|
|
:submit-error="createError"
|
|
@close="closeAddUserModal"
|
|
@submit="onAddUserSubmit"
|
|
/>
|
|
<DeleteUserConfirmModal
|
|
:user="deleteConfirmUser"
|
|
@close="deleteConfirmUser = null"
|
|
@confirm="confirmDeleteUser"
|
|
/>
|
|
<EditUserModal
|
|
:user="editUserModal"
|
|
:submit-error="editError"
|
|
@close="editUserModal = null"
|
|
@submit="onEditUserSubmit"
|
|
/>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
const { user, isAdmin, canEditPois, refresh: refreshUser } = useUser()
|
|
|
|
const { data: usersData, refresh: refreshUsers } = useAsyncData(
|
|
'users',
|
|
() => $fetch('/api/users').catch(() => []),
|
|
{ default: () => [] },
|
|
)
|
|
const users = computed(() => Object.freeze([...(usersData.value ?? [])]))
|
|
|
|
const roleOptions = ['admin', 'leader', 'member']
|
|
const pendingRoleUpdates = ref({})
|
|
const roleByUserId = computed(() => {
|
|
const base = Object.fromEntries(users.value.map(u => [u.id, u.role]))
|
|
return { ...base, ...pendingRoleUpdates.value }
|
|
})
|
|
const openRoleDropdownId = ref(null)
|
|
|
|
const addUserModalOpen = ref(false)
|
|
const createError = ref('')
|
|
const editUserModal = ref(null)
|
|
const editError = ref('')
|
|
const deleteConfirmUser = ref(null)
|
|
|
|
watch(user, () => {
|
|
if (canEditPois.value) refreshUsers()
|
|
}, { immediate: true })
|
|
|
|
function toggleRoleDropdown(userId) {
|
|
openRoleDropdownId.value = openRoleDropdownId.value === userId ? null : userId
|
|
}
|
|
|
|
function selectRole(userId, role) {
|
|
pendingRoleUpdates.value = { ...pendingRoleUpdates.value, [userId]: role }
|
|
openRoleDropdownId.value = null
|
|
}
|
|
|
|
async function saveRole(id) {
|
|
const role = roleByUserId.value[id]
|
|
if (!role) return
|
|
try {
|
|
await $fetch(`/api/users/${id}`, { method: 'PATCH', body: { role } })
|
|
await refreshUsers()
|
|
pendingRoleUpdates.value = Object.fromEntries(
|
|
Object.entries(pendingRoleUpdates.value).filter(([k]) => k !== id),
|
|
)
|
|
}
|
|
catch {
|
|
// could set error state
|
|
}
|
|
}
|
|
|
|
function openAddUserModal() {
|
|
addUserModalOpen.value = true
|
|
createError.value = ''
|
|
}
|
|
|
|
function closeAddUserModal() {
|
|
addUserModalOpen.value = false
|
|
createError.value = ''
|
|
}
|
|
|
|
async function onAddUserSubmit(payload) {
|
|
createError.value = ''
|
|
try {
|
|
await $fetch('/api/users', {
|
|
method: 'POST',
|
|
body: {
|
|
identifier: payload.identifier,
|
|
password: payload.password,
|
|
role: payload.role,
|
|
},
|
|
})
|
|
closeAddUserModal()
|
|
await refreshUsers()
|
|
}
|
|
catch (e) {
|
|
createError.value = e.data?.message || e.message || 'Failed to create user'
|
|
}
|
|
}
|
|
|
|
function openEditUser(u) {
|
|
editUserModal.value = u
|
|
editError.value = ''
|
|
}
|
|
|
|
async function onEditUserSubmit(payload) {
|
|
const u = editUserModal.value
|
|
if (!u) return
|
|
editError.value = ''
|
|
const body = { identifier: payload.identifier.trim() }
|
|
if (payload.password) body.password = payload.password
|
|
try {
|
|
await $fetch(`/api/users/${u.id}`, { method: 'PATCH', body })
|
|
editUserModal.value = null
|
|
await refreshUsers()
|
|
await refreshUser()
|
|
}
|
|
catch (e) {
|
|
editError.value = e.data?.message || e.message || 'Failed to update user'
|
|
}
|
|
}
|
|
|
|
function openDeleteConfirm(u) {
|
|
deleteConfirmUser.value = u
|
|
}
|
|
|
|
async function confirmDeleteUser() {
|
|
const u = deleteConfirmUser.value
|
|
if (!u) return
|
|
try {
|
|
await $fetch(`/api/users/${u.id}`, { method: 'DELETE' })
|
|
deleteConfirmUser.value = null
|
|
await refreshUsers()
|
|
}
|
|
catch (e) {
|
|
alert(e.data?.message || e.message || 'Failed to remove user')
|
|
}
|
|
}
|
|
</script>
|