From 85a2ffcb5073ca0ed09638dbd79cbb6866388d10 Mon Sep 17 00:00:00 2001 From: Sin-Kang Date: Wed, 3 Jun 2026 20:07:17 +0900 Subject: [PATCH] feat: manage a user's roles and groups from the Users page Each user row gets "Manage roles" (id-card) and "Manage groups" (users) actions opening the reusable PickList assignment dialog (available | assigned, save-as-diff). Roles assign/revoke via rolesApi.assignToUser/revokeFromUser; group membership via groupsApi.addMember/removeMember. Current assignments are read from the new GET /users/{id}/roles and /users/{id}/groups (kit 0.4.2). ko/en i18n. Built green (vue-tsc + vite). --- src/api/users.ts | 8 ++++ src/i18n/locales/en.ts | 6 +++ src/i18n/locales/ko.ts | 6 +++ src/views/UsersView.vue | 95 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 114 insertions(+), 1 deletion(-) diff --git a/src/api/users.ts b/src/api/users.ts index 85eed03..0954f43 100644 --- a/src/api/users.ts +++ b/src/api/users.ts @@ -54,4 +54,12 @@ export const usersApi = { remove(id: string) { return client.delete(`${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) + }, } diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 65a26f0..d513cc0 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -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', @@ -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.', @@ -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', diff --git a/src/i18n/locales/ko.ts b/src/i18n/locales/ko.ts index bb270b6..b0ca8e4 100644 --- a/src/i18n/locales/ko.ts +++ b/src/i18n/locales/ko.ts @@ -176,6 +176,8 @@ export default { ariaLockToggle: { lock: '잠금', unlock: '잠금 해제' }, ariaResetPassword: '비밀번호 초기화', ariaChangeStatus: '상태 변경', + ariaManageRoles: '역할 관리', + ariaManageGroups: '그룹 관리', createDialog: { title: '사용자 생성', loginIdPlaceholder: '로그인 ID', @@ -185,6 +187,8 @@ export default { }, passwordDialog: { title: '비밀번호 초기화', prompt: '{loginId} 의 새 비밀번호' }, statusDialog: { title: '상태 변경', prompt: '{loginId} 의 상태' }, + rolesDialog: { title: '역할 — {loginId}' }, + groupsDialog: { title: '그룹 — {loginId}' }, deleteConfirm: { header: '사용자 삭제', message: '사용자 "{loginId}" 를 삭제할까요? 되돌릴 수 없습니다.', @@ -194,6 +198,8 @@ export default { deleted: '사용자 삭제됨', statusUpdated: '상태 업데이트됨', passwordReset: '비밀번호 초기화됨', + rolesUpdated: '역할 업데이트됨', + groupsUpdated: '그룹 업데이트됨', lockFailed: '잠금 / 잠금 해제 실패', loadFailed: '사용자 불러오기 실패', passwordFailed: '비밀번호 초기화 실패', diff --git a/src/views/UsersView.vue b/src/views/UsersView.vue index 781c1c3..2a3e577 100644 --- a/src/views/UsersView.vue +++ b/src/views/UsersView.vue @@ -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() @@ -39,6 +43,19 @@ const newPassword = ref('') const statusTarget = ref(null) const newStatus = ref('ACTIVE') +// Role / group assignment (PickList dialogs) — manage a user's access from the user side. +const rolesOpen = ref(false) +const rolesUser = ref(null) +const allRoles = ref([]) +const assignedRoleIds = ref([]) +const rolesSaving = ref(false) + +const groupsOpen = ref(false) +const groupsUser = ref(null) +const allGroups = ref([]) +const assignedGroupIds = ref([]) +const groupsSaving = ref(false) + const statusOptions = ['ACTIVE', 'LOCKED', 'DISABLED', 'PENDING_VERIFICATION'] const statusSeverity = (s: UserAccount['status']) => @@ -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 }), @@ -187,7 +260,7 @@ onMounted(reload) - + + + + +