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')) && (