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
8 changes: 8 additions & 0 deletions src/api/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,12 @@ export const usersApi = {
remove(id: string) {
return client.delete<void>(`${base}/${id}`).then((r) => r.data)
},
// Roles/groups assigned to a user (ids). Assign/revoke live on the role & group
// resources (rolesApi.assignToUser / groupsApi.addMember). Kit 0.4.2+.
roles(id: string) {
return client.get<{ value: string }[]>(`${base}/${id}/roles`).then((r) => r.data)
},
groups(id: string) {
return client.get<{ value: string }[]>(`${base}/${id}/groups`).then((r) => r.data)
},
}
6 changes: 6 additions & 0 deletions src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ export default {
ariaLockToggle: { lock: 'Lock', unlock: 'Unlock' },
ariaResetPassword: 'Reset password',
ariaChangeStatus: 'Change status',
ariaManageRoles: 'Manage roles',
ariaManageGroups: 'Manage groups',
createDialog: {
title: 'Create user',
loginIdPlaceholder: 'Login ID',
Expand All @@ -185,6 +187,8 @@ export default {
},
passwordDialog: { title: 'Reset password', prompt: 'New password for {loginId}' },
statusDialog: { title: 'Change status', prompt: 'Status for {loginId}' },
rolesDialog: { title: 'Roles — {loginId}' },
groupsDialog: { title: 'Groups — {loginId}' },
deleteConfirm: {
header: 'Delete user',
message: 'Delete user "{loginId}"? This cannot be undone.',
Expand All @@ -194,6 +198,8 @@ export default {
deleted: 'User deleted',
statusUpdated: 'Status updated',
passwordReset: 'Password reset',
rolesUpdated: 'Roles updated',
groupsUpdated: 'Groups updated',
lockFailed: 'Lock/unlock failed',
loadFailed: 'Failed to load users',
passwordFailed: 'Password reset failed',
Expand Down
6 changes: 6 additions & 0 deletions src/i18n/locales/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ export default {
ariaLockToggle: { lock: '잠금', unlock: '잠금 해제' },
ariaResetPassword: '비밀번호 초기화',
ariaChangeStatus: '상태 변경',
ariaManageRoles: '역할 관리',
ariaManageGroups: '그룹 관리',
createDialog: {
title: '사용자 생성',
loginIdPlaceholder: '로그인 ID',
Expand All @@ -185,6 +187,8 @@ export default {
},
passwordDialog: { title: '비밀번호 초기화', prompt: '{loginId} 의 새 비밀번호' },
statusDialog: { title: '상태 변경', prompt: '{loginId} 의 상태' },
rolesDialog: { title: '역할 — {loginId}' },
groupsDialog: { title: '그룹 — {loginId}' },
deleteConfirm: {
header: '사용자 삭제',
message: '사용자 "{loginId}" 를 삭제할까요? 되돌릴 수 없습니다.',
Expand All @@ -194,6 +198,8 @@ export default {
deleted: '사용자 삭제됨',
statusUpdated: '상태 업데이트됨',
passwordReset: '비밀번호 초기화됨',
rolesUpdated: '역할 업데이트됨',
groupsUpdated: '그룹 업데이트됨',
lockFailed: '잠금 / 잠금 해제 실패',
loadFailed: '사용자 불러오기 실패',
passwordFailed: '비밀번호 초기화 실패',
Expand Down
95 changes: 94 additions & 1 deletion src/views/UsersView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import Tag from 'primevue/tag'
import { useToast } from 'primevue/usetoast'
import { useConfirm } from 'primevue/useconfirm'
import { usersApi, type UserAccount } from '@/api/users'
import { rolesApi } from '@/api/roles'
import { groupsApi } from '@/api/groups'
import AssignDialog from '@/components/AssignDialog.vue'
import type { AssignOption } from '@/components/assign'
import { useAuthStore } from '@/stores/auth'

const auth = useAuthStore()
Expand Down Expand Up @@ -39,6 +43,19 @@ const newPassword = ref('')
const statusTarget = ref<UserAccount | null>(null)
const newStatus = ref<UserAccount['status']>('ACTIVE')

// Role / group assignment (PickList dialogs) — manage a user's access from the user side.
const rolesOpen = ref(false)
const rolesUser = ref<UserAccount | null>(null)
const allRoles = ref<AssignOption[]>([])
const assignedRoleIds = ref<string[]>([])
const rolesSaving = ref(false)

const groupsOpen = ref(false)
const groupsUser = ref<UserAccount | null>(null)
const allGroups = ref<AssignOption[]>([])
const assignedGroupIds = ref<string[]>([])
const groupsSaving = ref(false)

const statusOptions = ['ACTIVE', 'LOCKED', 'DISABLED', 'PENDING_VERIFICATION']

const statusSeverity = (s: UserAccount['status']) =>
Expand Down Expand Up @@ -122,6 +139,62 @@ async function submitStatus() {
}
}

async function openRoles(user: UserAccount) {
rolesUser.value = user
try {
const [roles, assigned] = await Promise.all([rolesApi.list(tenantId.value), usersApi.roles(user.id.value)])
allRoles.value = roles.map((r) => ({ id: r.id.value, label: r.code, sub: r.name }))
assignedRoleIds.value = assigned.map((a) => a.value)
rolesOpen.value = true
} catch (e) {
toast.add({ severity: 'error', summary: t('users.toasts.loadFailed'), detail: extractMsg(e), life: 4000 })
}
}

async function saveRoles(added: string[], removed: string[]) {
if (!rolesUser.value) return
rolesSaving.value = true
const userId = rolesUser.value.id.value
try {
for (const roleId of added) await rolesApi.assignToUser(roleId, userId, tenantId.value)
for (const roleId of removed) await rolesApi.revokeFromUser(roleId, userId)
toast.add({ severity: 'success', summary: t('users.toasts.rolesUpdated'), life: 2500 })
rolesOpen.value = false
} catch (e) {
toast.add({ severity: 'error', summary: t('toasts.updateFailed'), detail: extractMsg(e), life: 4000 })
} finally {
rolesSaving.value = false
}
}

async function openGroups(user: UserAccount) {
groupsUser.value = user
try {
const [groups, assigned] = await Promise.all([groupsApi.list(tenantId.value), usersApi.groups(user.id.value)])
allGroups.value = groups.map((g) => ({ id: g.id.value, label: g.code, sub: g.name }))
assignedGroupIds.value = assigned.map((a) => a.value)
groupsOpen.value = true
} catch (e) {
toast.add({ severity: 'error', summary: t('users.toasts.loadFailed'), detail: extractMsg(e), life: 4000 })
}
}

async function saveGroups(added: string[], removed: string[]) {
if (!groupsUser.value) return
groupsSaving.value = true
const userId = groupsUser.value.id.value
try {
for (const groupId of added) await groupsApi.addMember(groupId, userId)
for (const groupId of removed) await groupsApi.removeMember(groupId, userId)
toast.add({ severity: 'success', summary: t('users.toasts.groupsUpdated'), life: 2500 })
groupsOpen.value = false
} catch (e) {
toast.add({ severity: 'error', summary: t('toasts.updateFailed'), detail: extractMsg(e), life: 4000 })
} finally {
groupsSaving.value = false
}
}

function confirmDelete(row: UserAccount) {
confirm.require({
message: t('users.deleteConfirm.message', { loginId: row.loginId }),
Expand Down Expand Up @@ -187,7 +260,7 @@ onMounted(reload)
</template>
</Column>
<Column field="providerType" :header="t('users.columns.provider')" />
<Column header="" style="width: 14rem; text-align: right">
<Column header="" style="width: 18rem; text-align: right">
<template #body="{ data }">
<div class="flex items-center justify-end gap-1">
<Button
Expand All @@ -199,6 +272,8 @@ onMounted(reload)
@click="toggleLock(data)"
/>
<Button icon="pi pi-key" text rounded :aria-label="t('users.ariaResetPassword')" @click="openPassword(data)" />
<Button icon="pi pi-id-card" text rounded :aria-label="t('users.ariaManageRoles')" @click="openRoles(data)" />
<Button icon="pi pi-users" text rounded :aria-label="t('users.ariaManageGroups')" @click="openGroups(data)" />
<Button icon="pi pi-pencil" text rounded :aria-label="t('users.ariaChangeStatus')" @click="openStatus(data)" />
<Button
icon="pi pi-trash"
Expand Down Expand Up @@ -253,5 +328,23 @@ onMounted(reload)
<Button :label="t('common.save')" icon="pi pi-check" @click="submitStatus" />
</template>
</Dialog>

<AssignDialog
v-model:visible="rolesOpen"
:title="t('users.rolesDialog.title', { loginId: rolesUser?.loginId ?? '' })"
:all="allRoles"
:assigned-ids="assignedRoleIds"
:saving="rolesSaving"
@save="saveRoles"
/>

<AssignDialog
v-model:visible="groupsOpen"
:title="t('users.groupsDialog.title', { loginId: groupsUser?.loginId ?? '' })"
:all="allGroups"
:assigned-ids="assignedGroupIds"
:saving="groupsSaving"
@save="saveGroups"
/>
</div>
</template>
Loading