From 5728fd294efeb3d4f8b021c9d2e89075ffd7de2d Mon Sep 17 00:00:00 2001 From: Ricky Stretch Date: Sun, 15 Mar 2026 22:47:46 -0400 Subject: [PATCH] v0.9.30 more bugfixes --- .env.example | 2 +- backend/package.json | 2 +- backend/src/routes/usergroups.js | 123 +++++++++++++------------------ build.sh | 2 +- frontend/package.json | 2 +- 5 files changed, 54 insertions(+), 77 deletions(-) diff --git a/.env.example b/.env.example index 06640ca..b26a250 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,7 @@ PROJECT_NAME=jama # Image version to run (set by build.sh, or use 'latest') -JAMA_VERSION=0.9.29 +JAMA_VERSION=0.9.30 # App port — the host port Docker maps to the container PORT=3000 diff --git a/backend/package.json b/backend/package.json index d102e21..b6a5c8f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "jama-backend", - "version": "0.9.29", + "version": "0.9.30", "description": "TeamChat backend server", "main": "src/index.js", "scripts": { diff --git a/backend/src/routes/usergroups.js b/backend/src/routes/usergroups.js index 0e965a2..c0c5eef 100644 --- a/backend/src/routes/usergroups.js +++ b/backend/src/routes/usergroups.js @@ -7,8 +7,8 @@ module.exports = function(io) { // ── Helpers ─────────────────────────────────────────────────────────────────── -function postSysMsg(db, groupId, userId, content) { - const r = db.prepare(`INSERT INTO messages (group_id, user_id, content, type) VALUES (?, ?, ?, 'system')`).run(groupId, userId, content); +function postSysMsg(db, groupId, actorId, content) { + const r = db.prepare(`INSERT INTO messages (group_id, user_id, content, type) VALUES (?, ?, ?, 'system')`).run(groupId, actorId, content); const msg = db.prepare(` SELECT m.*, u.name as user_name, u.display_name as user_display_name, u.avatar as user_avatar, u.role as user_role, u.status as user_status, @@ -18,22 +18,23 @@ function postSysMsg(db, groupId, userId, content) { if (msg) { msg.reactions = []; io.to(`group:${groupId}`).emit('message:new', msg); } } -// Add user silently (no system message) — used during initial group creation -function addUserToDmGroupSilent(db, dmGroupId, userId) { +// Add user silently — no system message (used during initial creation) +function addUserSilent(db, dmGroupId, userId) { db.prepare("INSERT OR IGNORE INTO group_members (group_id, user_id, joined_at) VALUES (?, ?, datetime('now'))").run(dmGroupId, userId); io.in(`user:${userId}`).socketsJoin(`group:${dmGroupId}`); const dmGroup = db.prepare('SELECT * FROM groups WHERE id = ?').get(dmGroupId); - io.to(`user:${userId}`).emit('group:new', { group: dmGroup }); + if (dmGroup) io.to(`user:${userId}`).emit('group:new', { group: dmGroup }); } -// Add user with system message — used when adding to existing group -function addUserToDmGroup(db, dmGroupId, userId, actorId) { - addUserToDmGroupSilent(db, dmGroupId, userId); +// Add user with system message (used when editing existing group) +function addUser(db, dmGroupId, userId, actorId) { + addUserSilent(db, dmGroupId, userId); const u = db.prepare('SELECT name, display_name FROM users WHERE id = ?').get(userId); postSysMsg(db, dmGroupId, actorId, `${u?.display_name || u?.name || 'A user'} has joined the conversation.`); } -function removeUserFromDmGroup(db, dmGroupId, userId, actorId) { +// Remove user with system message +function removeUser(db, dmGroupId, userId, actorId) { db.prepare('DELETE FROM group_members WHERE group_id = ? AND user_id = ?').run(dmGroupId, userId); io.in(`user:${userId}`).socketsLeave(`group:${dmGroupId}`); io.to(`user:${userId}`).emit('group:deleted', { groupId: dmGroupId }); @@ -41,24 +42,11 @@ function removeUserFromDmGroup(db, dmGroupId, userId, actorId) { postSysMsg(db, dmGroupId, actorId, `${u?.display_name || u?.name || 'A user'} has been removed from the conversation.`); } -// Get all user_ids for a user group function getUserIdsForGroup(db, userGroupId) { return db.prepare('SELECT user_id FROM user_group_members WHERE user_group_id = ?').all(userGroupId).map(r => r.user_id); } -// ── USER GROUPS ─────────────────────────────────────────────────────────────── - -router.get('/', authMiddleware, adminMiddleware, (req, res) => { - const db = getDb(); - const groups = db.prepare(` - SELECT ug.*, - (SELECT COUNT(*) FROM user_group_members WHERE user_group_id = ug.id) as member_count - FROM user_groups ug ORDER BY ug.name ASC - `).all(); - res.json({ groups }); -}); - -// ── MULTI-GROUP DMs ─────────────────────────────────────────────────────────── +// ── MULTI-GROUP DMs — must come before /:id ─────────────────────────────────── router.get('/multigroup', authMiddleware, adminMiddleware, (req, res) => { const db = getDb(); @@ -67,7 +55,6 @@ router.get('/multigroup', authMiddleware, adminMiddleware, (req, res) => { (SELECT COUNT(*) FROM multi_group_dm_members WHERE multi_group_dm_id = mgd.id) as group_count FROM multi_group_dms mgd ORDER BY mgd.name ASC `).all(); - // Attach member user group IDs for (const dm of dms) { dm.memberGroupIds = db.prepare('SELECT user_group_id FROM multi_group_dm_members WHERE multi_group_dm_id = ?').all(dm.id).map(r => r.user_group_id); } @@ -92,17 +79,14 @@ router.post('/multigroup', authMiddleware, adminMiddleware, (req, res) => { const addedUsers = new Set(); for (const ugId of validGroupIds) { db.prepare('INSERT OR IGNORE INTO multi_group_dm_members (multi_group_dm_id, user_group_id) VALUES (?, ?)').run(mgId, ugId); - const uids = getUserIdsForGroup(db, ugId); - for (const uid of uids) { - if (!addedUsers.has(uid)) { - addedUsers.add(uid); - addUserToDmGroupSilent(db, dmGroupId, uid); - } + for (const uid of getUserIdsForGroup(db, ugId)) { + if (!addedUsers.has(uid)) { addedUsers.add(uid); addUserSilent(db, dmGroupId, uid); } } } const dm = db.prepare('SELECT * FROM multi_group_dms WHERE id = ?').get(mgId); dm.memberGroupIds = validGroupIds; + dm.group_count = validGroupIds.length; res.json({ dm }); }); @@ -117,33 +101,27 @@ router.patch('/multigroup/:id', authMiddleware, adminMiddleware, (req, res) => { return res.status(400).json({ error: 'Name already in use' }); } db.prepare("UPDATE multi_group_dms SET name = ?, updated_at = datetime('now') WHERE id = ?").run(name.trim(), mg.id); - if (mg.dm_group_id) { - db.prepare("UPDATE groups SET name = ?, updated_at = datetime('now') WHERE id = ?").run(name.trim(), mg.dm_group_id); - } + if (mg.dm_group_id) db.prepare("UPDATE groups SET name = ?, updated_at = datetime('now') WHERE id = ?").run(name.trim(), mg.dm_group_id); } if (Array.isArray(userGroupIds) && mg.dm_group_id) { const newGroupIds = new Set(userGroupIds.map(Number).filter(Boolean)); const currentGroupIds = new Set(db.prepare('SELECT user_group_id FROM multi_group_dm_members WHERE multi_group_dm_id = ?').all(mg.id).map(r => r.user_group_id)); - // Add new user groups for (const ugId of newGroupIds) { if (!currentGroupIds.has(ugId)) { db.prepare("INSERT OR IGNORE INTO multi_group_dm_members (multi_group_dm_id, user_group_id) VALUES (?, ?)").run(mg.id, ugId); - const uids = getUserIdsForGroup(db, ugId); - for (const uid of uids) addUserToDmGroup(db, mg.dm_group_id, uid, req.user.id); + for (const uid of getUserIdsForGroup(db, ugId)) addUser(db, mg.dm_group_id, uid, req.user.id); const ug = db.prepare('SELECT name FROM user_groups WHERE id = ?').get(ugId); if (ug) postSysMsg(db, mg.dm_group_id, req.user.id, `Group "${ug.name}" has been added to this conversation.`); } } - // Remove dropped user groups for (const ugId of currentGroupIds) { if (!newGroupIds.has(ugId)) { db.prepare('DELETE FROM multi_group_dm_members WHERE multi_group_dm_id = ? AND user_group_id = ?').run(mg.id, ugId); - const uids = getUserIdsForGroup(db, ugId); - for (const uid of uids) { - const stillInOtherGroup = db.prepare('SELECT 1 FROM multi_group_dm_members mgdm JOIN user_group_members ugm ON ugm.user_group_id = mgdm.user_group_id WHERE mgdm.multi_group_dm_id = ? AND ugm.user_id = ?').get(mg.id, uid); - if (!stillInOtherGroup) removeUserFromDmGroup(db, mg.dm_group_id, uid, req.user.id); + for (const uid of getUserIdsForGroup(db, ugId)) { + const stillIn = db.prepare('SELECT 1 FROM multi_group_dm_members mgdm JOIN user_group_members ugm ON ugm.user_group_id = mgdm.user_group_id WHERE mgdm.multi_group_dm_id = ? AND ugm.user_id = ?').get(mg.id, uid); + if (!stillIn) removeUser(db, mg.dm_group_id, uid, req.user.id); } const ug = db.prepare('SELECT name FROM user_groups WHERE id = ?').get(ugId); if (ug) postSysMsg(db, mg.dm_group_id, req.user.id, `Group "${ug.name}" has been removed from this conversation.`); @@ -153,6 +131,7 @@ router.patch('/multigroup/:id', authMiddleware, adminMiddleware, (req, res) => { const updated = db.prepare('SELECT * FROM multi_group_dms WHERE id = ?').get(req.params.id); updated.memberGroupIds = db.prepare('SELECT user_group_id FROM multi_group_dm_members WHERE multi_group_dm_id = ?').all(mg.id).map(r => r.user_group_id); + updated.group_count = updated.memberGroupIds.length; res.json({ dm: updated }); }); @@ -169,8 +148,17 @@ router.delete('/multigroup/:id', authMiddleware, adminMiddleware, (req, res) => res.json({ success: true }); }); -return router; -}; +// ── USER GROUPS ─────────────────────────────────────────────────────────────── + +router.get('/', authMiddleware, adminMiddleware, (req, res) => { + const db = getDb(); + const groups = db.prepare(` + SELECT ug.*, + (SELECT COUNT(*) FROM user_group_members WHERE user_group_id = ug.id) as member_count + FROM user_groups ug ORDER BY ug.name ASC + `).all(); + res.json({ groups }); +}); router.get('/:id', authMiddleware, adminMiddleware, (req, res) => { const db = getDb(); @@ -192,18 +180,14 @@ router.post('/', authMiddleware, adminMiddleware, (req, res) => { return res.status(400).json({ error: 'A group with that name already exists' }); } const admin = db.prepare('SELECT id FROM users WHERE is_default_admin = 1').get(); - const dmResult = db.prepare(` - INSERT INTO groups (name, type, owner_id, is_readonly, is_direct, is_managed) - VALUES (?, 'private', ?, 0, 0, 1) - `).run(name.trim(), admin?.id || req.user.id); + const dmResult = db.prepare(`INSERT INTO groups (name, type, owner_id, is_readonly, is_direct, is_managed) VALUES (?, 'private', ?, 0, 0, 1)`).run(name.trim(), admin?.id || req.user.id); const dmGroupId = dmResult.lastInsertRowid; const ugResult = db.prepare(`INSERT INTO user_groups (name, dm_group_id) VALUES (?, ?)`).run(name.trim(), dmGroupId); const ugId = ugResult.lastInsertRowid; - const validIds = Array.isArray(memberIds) ? memberIds.map(Number).filter(Boolean) : []; - for (const uid of validIds) { + for (const uid of (Array.isArray(memberIds) ? memberIds.map(Number).filter(Boolean) : [])) { db.prepare("INSERT OR IGNORE INTO user_group_members (user_group_id, user_id) VALUES (?, ?)").run(ugId, uid); - addUserToDmGroupSilent(db, dmGroupId, uid); + addUserSilent(db, dmGroupId, uid); } const group = db.prepare('SELECT * FROM user_groups WHERE id = ?').get(ugId); res.json({ group }); @@ -220,43 +204,34 @@ router.patch('/:id', authMiddleware, adminMiddleware, (req, res) => { return res.status(400).json({ error: 'Name already in use' }); } db.prepare("UPDATE user_groups SET name = ?, updated_at = datetime('now') WHERE id = ?").run(name.trim(), ug.id); - if (ug.dm_group_id) { - db.prepare("UPDATE groups SET name = ?, updated_at = datetime('now') WHERE id = ?").run(name.trim(), ug.dm_group_id); - } + if (ug.dm_group_id) db.prepare("UPDATE groups SET name = ?, updated_at = datetime('now') WHERE id = ?").run(name.trim(), ug.dm_group_id); } if (Array.isArray(memberIds) && ug.dm_group_id) { const newIds = new Set(memberIds.map(Number).filter(Boolean)); - const current = db.prepare('SELECT user_id FROM user_group_members WHERE user_group_id = ?').all(ug.id).map(r => r.user_id); - const currentSet = new Set(current); + const currentSet = new Set(db.prepare('SELECT user_id FROM user_group_members WHERE user_group_id = ?').all(ug.id).map(r => r.user_id)); + for (const uid of newIds) { if (!currentSet.has(uid)) { db.prepare("INSERT OR IGNORE INTO user_group_members (user_group_id, user_id) VALUES (?, ?)").run(ug.id, uid); - addUserToDmGroup(db, ug.dm_group_id, uid, req.user.id); - // Also add to any multi-group DMs that include this user group - const mgDms = db.prepare('SELECT mgd.dm_group_id FROM multi_group_dm_members mgdm JOIN multi_group_dms mgd ON mgd.id = mgdm.multi_group_dm_id WHERE mgdm.user_group_id = ?').all(ug.id); - for (const mg of mgDms) { - if (mg.dm_group_id) addUserToDmGroup(db, mg.dm_group_id, uid, req.user.id); + addUser(db, ug.dm_group_id, uid, req.user.id); + // Also add to multi-group DMs that include this user group + for (const mg of db.prepare('SELECT mgd.dm_group_id FROM multi_group_dm_members mgdm JOIN multi_group_dms mgd ON mgd.id = mgdm.multi_group_dm_id WHERE mgdm.user_group_id = ?').all(ug.id)) { + if (mg.dm_group_id) addUser(db, mg.dm_group_id, uid, req.user.id); } } } for (const uid of currentSet) { if (!newIds.has(uid)) { db.prepare('DELETE FROM user_group_members WHERE user_group_id = ? AND user_id = ?').run(ug.id, uid); - // Only remove from DM group if user isn't in another user group that also has access - const otherUgMemberships = db.prepare(` - SELECT ugm.user_group_id FROM user_group_members ugm - WHERE ugm.user_id = ? AND ugm.user_group_id != ? - AND EXISTS (SELECT 1 FROM group_members gm WHERE gm.group_id = ? AND gm.user_id = ?) - `).all(uid, ug.id, ug.dm_group_id, uid); - if (otherUgMemberships.length === 0) { - removeUserFromDmGroup(db, ug.dm_group_id, uid, req.user.id); - // Remove from multi-group DMs they got access through this group - const mgDms = db.prepare('SELECT mgd.dm_group_id FROM multi_group_dm_members mgdm JOIN multi_group_dms mgd ON mgd.id = mgdm.multi_group_dm_id WHERE mgdm.user_group_id = ?').all(ug.id); - for (const mg of mgDms) { + // Only remove if not in another user group with access to the same DM + const stillHasAccess = db.prepare(`SELECT 1 FROM user_group_members ugm WHERE ugm.user_id = ? AND ugm.user_group_id != ? AND EXISTS (SELECT 1 FROM group_members gm WHERE gm.group_id = ? AND gm.user_id = ?)`).get(uid, ug.id, ug.dm_group_id, uid); + if (!stillHasAccess) { + removeUser(db, ug.dm_group_id, uid, req.user.id); + for (const mg of db.prepare('SELECT mgd.dm_group_id FROM multi_group_dm_members mgdm JOIN multi_group_dms mgd ON mgd.id = mgdm.multi_group_dm_id WHERE mgdm.user_group_id = ?').all(ug.id)) { if (mg.dm_group_id) { - const stillMember = db.prepare('SELECT 1 FROM group_members WHERE group_id = ? AND user_id = ?').get(mg.dm_group_id, uid); - if (stillMember) removeUserFromDmGroup(db, mg.dm_group_id, uid, req.user.id); + const stillInMg = db.prepare('SELECT 1 FROM multi_group_dm_members mgdm JOIN user_group_members ugm ON ugm.user_group_id = mgdm.user_group_id WHERE mgdm.multi_group_dm_id = (SELECT id FROM multi_group_dms WHERE dm_group_id = ?) AND ugm.user_id = ?').get(mg.dm_group_id, uid); + if (!stillInMg) removeUser(db, mg.dm_group_id, uid, req.user.id); } } } @@ -281,3 +256,5 @@ router.delete('/:id', authMiddleware, adminMiddleware, (req, res) => { res.json({ success: true }); }); +return router; +}; diff --git a/build.sh b/build.sh index 3c55cef..07e8c61 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.9.29}" +VERSION="${1:-0.9.30}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="jama" diff --git a/frontend/package.json b/frontend/package.json index ad5d9f8..1c25a8a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "jama-frontend", - "version": "0.9.29", + "version": "0.9.30", "private": true, "scripts": { "dev": "vite",