diff --git a/frontend/js/api.js b/frontend/js/api.js index 8aad3ab..446b9f5 100644 --- a/frontend/js/api.js +++ b/frontend/js/api.js @@ -178,6 +178,12 @@ export const api = { // workspaceId, role, mustChangePassword } adminCreateUser: (data) => request('/admin/users', { method: 'POST', body: JSON.stringify(data) }), + // Per-user workspace membership management (platform Users page modal). + adminGetUserWorkspaces: (id) => request(`/admin/users/${id}/workspaces`), + adminAddUserWorkspace: (id, workspaceId, role) => request(`/admin/users/${id}/workspaces`, { method: 'POST', body: JSON.stringify({ workspaceId, role }) }), + adminSetUserWorkspaceRole: (id, workspaceId, role) => request(`/admin/users/${id}/workspaces/${workspaceId}`, { method: 'PUT', body: JSON.stringify({ role }) }), + adminRemoveUserWorkspace: (id, workspaceId) => request(`/admin/users/${id}/workspaces/${workspaceId}`, { method: 'DELETE' }), + // Admin - Users getUsers: () => request('/auth/users'), deleteUser: (id) => request(`/auth/users/${id}`, { method: 'DELETE' }), diff --git a/frontend/js/components/admin-user-workspaces-modal.js b/frontend/js/components/admin-user-workspaces-modal.js new file mode 100644 index 0000000..2544799 --- /dev/null +++ b/frontend/js/components/admin-user-workspaces-modal.js @@ -0,0 +1,163 @@ +// "Manage workspaces" modal for the platform Users admin page. Lets a platform +// admin see/manage ALL of a user's workspace memberships: list each with an +// inline role dropdown + Remove, and add the user to more workspaces via a +// type-to-filter picker. Backed by /api/admin/users/:id/workspaces. +import { api } from '../api.js'; +import { t } from '../i18n.js'; +import { showToast } from '../components/toast.js'; + +// Display order = least-privilege first (the default for the add row). The SET +// must match the server's accepted WORKSPACE_ROLES (routes/admin.js). +const WORKSPACE_ROLES = ['workspace_viewer', 'workspace_editor', 'workspace_admin']; +const STAFF_ROLES = ['platform_admin', 'superadmin', 'platform_operator']; + +function esc(s) { + return String(s ?? '').replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); +} +function roleOptions(selected) { + return WORKSPACE_ROLES.map(r => ``).join(''); +} +const wsLabel = w => `${w.organization_name || '—'} / ${w.name}`; + +// user: { id, name, email, role }; opts.onClose fires (once) if anything changed. +export function openManageWorkspacesModal(user, opts = {}) { + const { onClose } = opts; + const isStaff = STAFF_ROLES.includes(user.role); + + const overlay = document.createElement('div'); + overlay.className = 'modal-overlay'; + overlay.innerHTML = ` +
${t('manage_ws.staff_note')}
` : ''} +${t('manage_ws.empty')}
`; + return; + } + listEl.innerHTML = memberships.map(m => ` +${esc(e.message || 'Failed to load')}
`; + } + })(); +} diff --git a/frontend/js/i18n/en.js b/frontend/js/i18n/en.js index 694d399..3f36d0e 100644 --- a/frontend/js/i18n/en.js +++ b/frontend/js/i18n/en.js @@ -798,8 +798,23 @@ export default { 'admin.col.actions': 'Actions', 'admin.workspace.unassigned': 'Unassigned', 'admin.workspace.multi': '{n} workspaces', - 'admin.workspace.multi_hint': 'Belongs to multiple workspaces - manage in the workspace members view', 'admin.workspace.platform_all': 'Platform (all)', + 'admin.workspace.manage': 'Manage', + // "Manage workspaces" modal (per-user membership management) + 'manage_ws.title': 'Manage workspaces — {user}', + 'manage_ws.staff_note': 'This user has platform-wide access; the memberships below are in addition to that.', + 'manage_ws.current': 'Current workspaces', + 'manage_ws.empty': 'Not a member of any workspace.', + 'manage_ws.add': 'Add to workspace', + 'manage_ws.filter': 'Filter workspaces…', + 'manage_ws.pick': 'Select a workspace…', + 'manage_ws.pick_required': 'Pick a workspace to add.', + 'manage_ws.add_btn': 'Add', + 'manage_ws.remove': 'Remove', + 'manage_ws.done': 'Done', + 'manage_ws.toast.added': 'Added to workspace', + 'manage_ws.toast.removed': 'Removed from workspace', + 'manage_ws.toast.role': 'Role updated', 'admin.col.devices': 'Devices', 'admin.col.storage': 'Storage', 'admin.col.monthly': 'Monthly', diff --git a/frontend/js/views/admin.js b/frontend/js/views/admin.js index 01283d0..acf40f0 100644 --- a/frontend/js/views/admin.js +++ b/frontend/js/views/admin.js @@ -3,6 +3,7 @@ import { showToast } from '../components/toast.js'; import { esc, isPlatformAdmin } from '../utils.js'; import { t } from '../i18n.js'; import { openAddUserModal } from '../components/workspace-members-add-user-modal.js'; +import { openManageWorkspacesModal } from '../components/admin-user-workspaces-modal.js'; // Reuse the members view's server-error -> friendly-string mapper (handles the // 409 duplicate-email / weak-password / invalid-email cases) so we don't fork a // second mapper. @@ -24,37 +25,26 @@ function isPlatformStaffRole(role) { return role === 'platform_admin' || role === 'superadmin' || role === 'platform_operator'; } -// Build the org-grouped workspace `; - let currentOrg = null; - for (const w of list) { - const org = w.organization_name || '—'; - if (org !== currentOrg) { - if (currentOrg !== null) html += ''; - html += `'; - return html; +// Short summary of a user's workspace membership for the Users-table cell. +// Platform staff have cross-org access (not per-workspace membership) -> "Platform +// (all)". Otherwise: Unassigned (0), the workspace name (1), or "N workspaces". +function workspaceSummary(u) { + if (isPlatformStaffRole(u.role)) return t('admin.workspace.platform_all'); + const count = u.workspace_count || 0; + if (count === 0) return t('admin.workspace.unassigned'); + if (count === 1) return esc(u.workspace_name || ''); + return t('admin.workspace.multi', { n: count }); } -// Workspace cell for one user row. Editable