From e0e800012c27c0fdc17c8f90b63a7196ce15fabc Mon Sep 17 00:00:00 2001 From: Ricky Stretch Date: Sat, 21 Mar 2026 11:55:50 -0400 Subject: [PATCH] v0.10.7 UI rule changes --- backend/package.json | 2 +- backend/src/routes/groups.js | 10 +++++- backend/src/routes/usergroups.js | 16 +++++++++ build.sh | 2 +- frontend/package.json | 2 +- frontend/src/components/GroupInfoModal.jsx | 12 ++++--- frontend/src/contexts/SocketContext.jsx | 5 +++ frontend/src/pages/GroupManagerPage.jsx | 41 ++++++++++++++++++++-- frontend/src/utils/api.js | 2 ++ 9 files changed, 81 insertions(+), 11 deletions(-) diff --git a/backend/package.json b/backend/package.json index c959c29..bf85851 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "jama-backend", - "version": "0.11.6", + "version": "0.11.7", "description": "TeamChat backend server", "main": "src/index.js", "scripts": { diff --git a/backend/src/routes/groups.js b/backend/src/routes/groups.js index 37bdb47..428b2bc 100644 --- a/backend/src/routes/groups.js +++ b/backend/src/routes/groups.js @@ -255,7 +255,15 @@ router.delete('/:id/members/:userId', authMiddleware, async (req, res) => { if (group.type !== 'private') return res.status(400).json({ error: 'Cannot remove members from public groups' }); if (group.owner_id !== req.user.id && req.user.role !== 'admin') return res.status(403).json({ error: 'Only owner or admin can remove members' }); const targetId = parseInt(req.params.userId); - if (targetId === group.owner_id) return res.status(400).json({ error: 'Cannot remove the group owner' }); + // Admins can remove the owner only if the owner is a deleted user (orphan cleanup) + const targetUser = await queryOne(req.schema, 'SELECT status FROM users WHERE id=$1', [targetId]); + const isDeletedOrphan = targetUser?.status === 'deleted'; + if (targetId === group.owner_id && !isDeletedOrphan && req.user.role !== 'admin') { + return res.status(400).json({ error: 'Cannot remove the group owner' }); + } + if (targetId === group.owner_id && !isDeletedOrphan) { + return res.status(400).json({ error: 'Cannot remove the group owner' }); + } const removedUser = await queryOne(req.schema, 'SELECT name,display_name FROM users WHERE id=$1', [targetId]); const removedName = removedUser?.display_name || removedUser?.name || 'Unknown'; await exec(req.schema, 'DELETE FROM group_members WHERE group_id=$1 AND user_id=$2', [group.id, targetId]); diff --git a/backend/src/routes/usergroups.js b/backend/src/routes/usergroups.js index c04cbca..8e5ac46 100644 --- a/backend/src/routes/usergroups.js +++ b/backend/src/routes/usergroups.js @@ -349,5 +349,21 @@ router.put('/:id/restrictions', authMiddleware, teamManagerMiddleware, async (re } catch (e) { res.status(500).json({ error: e.message }); } }); + +// DELETE /api/usergroups/:id/members/:userId — admin force-remove (for deleted/orphaned users) +router.delete('/:id/members/:userId', authMiddleware, adminMiddleware, async (req, res) => { + try { + const ugId = parseInt(req.params.id); + const userId = parseInt(req.params.userId); + const ug = await queryOne(req.schema, 'SELECT id FROM user_groups WHERE id=$1', [ugId]); + if (!ug) return res.status(404).json({ error: 'User group not found' }); + await exec(req.schema, + 'DELETE FROM user_group_members WHERE user_group_id=$1 AND user_id=$2', + [ugId, userId] + ); + res.json({ success: true }); + } catch (e) { res.status(500).json({ error: e.message }); } +}); + return router; }; diff --git a/build.sh b/build.sh index 9547587..f1cde3d 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.11.6}" +VERSION="${1:-0.11.7}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="jama" diff --git a/frontend/package.json b/frontend/package.json index 11c5d3c..dab016c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "jama-frontend", - "version": "0.11.6", + "version": "0.11.7", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/src/components/GroupInfoModal.jsx b/frontend/src/components/GroupInfoModal.jsx index 2d1722e..44f5be1 100644 --- a/frontend/src/components/GroupInfoModal.jsx +++ b/frontend/src/components/GroupInfoModal.jsx @@ -198,14 +198,16 @@ export default function GroupInfoModal({ group, onClose, onUpdated, onBack }) {
{m.name} - {m.id === group.owner_id && Owner} - {canManage && m.id !== group.owner_id && ( + {m.status === 'deleted' && Deleted} + {m.id === group.owner_id && m.status !== 'deleted' && Owner} + {/* Allow removal if: canManage + not owner, OR admin + deleted orphan */} + {(( canManage && m.id !== group.owner_id) || (isAdmin && m.status === 'deleted')) && ( +
+ ))} + +

+ These users were deleted but remain as group members. Remove them to allow this group to be deleted. +

+ + )}
{!isCreating && } diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index aa9f1c2..a66caf9 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -141,6 +141,7 @@ export const api = { deleteMultiGroupDm: (id) => req('DELETE', `/usergroups/multigroup/${id}`), // U2U Restrictions getGroupRestrictions: (id) => req('GET', `/usergroups/${id}/restrictions`), + removeUserGroupMember: (groupId, userId) => req('DELETE', `/usergroups/${groupId}/members/${userId}`), setGroupRestrictions: (id, blockedGroupIds) => req('PUT', `/usergroups/${id}/restrictions`, { blockedGroupIds }), // Multi-group DMs getMultiGroupDms: () => req('GET', '/usergroups/multigroup'), @@ -149,6 +150,7 @@ export const api = { deleteMultiGroupDm: (id) => req('DELETE', `/usergroups/multigroup/${id}`), // U2U Restrictions getGroupRestrictions: (id) => req('GET', `/usergroups/${id}/restrictions`), + removeUserGroupMember: (groupId, userId) => req('DELETE', `/usergroups/${groupId}/members/${userId}`), setGroupRestrictions: (id, blockedGroupIds) => req('PUT', `/usergroups/${id}/restrictions`, { blockedGroupIds }), uploadLogo: (file) => { const form = new FormData(); form.append('logo', file);