diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js index 337bbc5..d9c2d26 100644 --- a/backend/src/routes/users.js +++ b/backend/src/routes/users.js @@ -4,7 +4,7 @@ const multer = require('multer'); const path = require('path'); const router = express.Router(); const { query, queryOne, queryResult, exec, addUserToPublicGroups, getOrCreateSupportGroup } = require('../models/db'); -const { authMiddleware, adminMiddleware, teamManagerMiddleware } = require('../middleware/auth'); +const { authMiddleware, teamManagerMiddleware } = require('../middleware/auth'); const avatarStorage = multer.diskStorage({ destination: '/app/uploads/avatars', @@ -30,7 +30,7 @@ async function resolveUniqueName(schema, baseName, excludeId = null) { function isValidEmail(e) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e); } // List users -router.get('/', authMiddleware, adminMiddleware, async (req, res) => { +router.get('/', authMiddleware, teamManagerMiddleware, async (req, res) => { try { const users = await query(req.schema, "SELECT id,name,email,role,status,is_default_admin,must_change_password,avatar,about_me,display_name,allow_dm,created_at,last_online FROM users WHERE status != 'deleted' ORDER BY created_at ASC" @@ -81,7 +81,7 @@ router.get('/check-display-name', authMiddleware, async (req, res) => { }); // Create user -router.post('/', authMiddleware, adminMiddleware, async (req, res) => { +router.post('/', authMiddleware, teamManagerMiddleware, async (req, res) => { const { name, email, password, role } = req.body; if (!name || !email) return res.status(400).json({ error: 'Name and email required' }); if (!isValidEmail(email)) return res.status(400).json({ error: 'Invalid email address' }); @@ -107,7 +107,7 @@ router.post('/', authMiddleware, adminMiddleware, async (req, res) => { }); // Bulk create -router.post('/bulk', authMiddleware, adminMiddleware, async (req, res) => { +router.post('/bulk', authMiddleware, teamManagerMiddleware, async (req, res) => { const { users } = req.body; const results = { created: [], skipped: [] }; const seenEmails = new Set(); @@ -144,7 +144,7 @@ router.post('/bulk', authMiddleware, adminMiddleware, async (req, res) => { }); // Patch name -router.patch('/:id/name', authMiddleware, adminMiddleware, async (req, res) => { +router.patch('/:id/name', authMiddleware, teamManagerMiddleware, async (req, res) => { const { name } = req.body; if (!name?.trim()) return res.status(400).json({ error: 'Name required' }); try { @@ -157,7 +157,7 @@ router.patch('/:id/name', authMiddleware, adminMiddleware, async (req, res) => { }); // Patch role -router.patch('/:id/role', authMiddleware, adminMiddleware, async (req, res) => { +router.patch('/:id/role', authMiddleware, teamManagerMiddleware, async (req, res) => { const { role } = req.body; if (!['member','admin'].includes(role)) return res.status(400).json({ error: 'Invalid role' }); try { @@ -174,7 +174,7 @@ router.patch('/:id/role', authMiddleware, adminMiddleware, async (req, res) => { }); // Reset password -router.patch('/:id/reset-password', authMiddleware, adminMiddleware, async (req, res) => { +router.patch('/:id/reset-password', authMiddleware, teamManagerMiddleware, async (req, res) => { const { password } = req.body; if (!password || password.length < 6) return res.status(400).json({ error: 'Password too short' }); try { @@ -185,7 +185,7 @@ router.patch('/:id/reset-password', authMiddleware, adminMiddleware, async (req, }); // Suspend / activate / delete -router.patch('/:id/suspend', authMiddleware, adminMiddleware, async (req, res) => { +router.patch('/:id/suspend', authMiddleware, teamManagerMiddleware, async (req, res) => { try { const t = await queryOne(req.schema, 'SELECT * FROM users WHERE id=$1', [req.params.id]); if (!t) return res.status(404).json({ error: 'User not found' }); @@ -196,13 +196,13 @@ router.patch('/:id/suspend', authMiddleware, adminMiddleware, async (req, res) res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); -router.patch('/:id/activate', authMiddleware, adminMiddleware, async (req, res) => { +router.patch('/:id/activate', authMiddleware, teamManagerMiddleware, async (req, res) => { try { await exec(req.schema, "UPDATE users SET status='active', updated_at=NOW() WHERE id=$1", [req.params.id]); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); -router.delete('/:id', authMiddleware, adminMiddleware, async (req, res) => { +router.delete('/:id', authMiddleware, teamManagerMiddleware, async (req, res) => { try { const t = await queryOne(req.schema, 'SELECT * FROM users WHERE id=$1', [req.params.id]); if (!t) return res.status(404).json({ error: 'User not found' }); diff --git a/build.sh b/build.sh index df769a0..57b365e 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.11.26}" +VERSION="${1:-0.11.27}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="rosterchirp" diff --git a/frontend/src/components/ChatWindow.jsx b/frontend/src/components/ChatWindow.jsx index d8a853c..eb62333 100644 --- a/frontend/src/components/ChatWindow.jsx +++ b/frontend/src/components/ChatWindow.jsx @@ -296,19 +296,28 @@ export default function ChatWindow({ group, onBack, onGroupUpdated, onDirectMess )} - {messages.map((msg, i) => ( - - ))} + {messages.map((msg, i) => { + // Skip deleted entries when looking for the effective previous message. + // Deleted messages render null, so they must not affect date separators + // or avatar-grouping for the messages that follow them. + let effectivePrev = null; + for (let j = i - 1; j >= 0; j--) { + if (!messages[j].is_deleted) { effectivePrev = messages[j]; break; } + } + return ( + + ); + })} {typing.length > 0 && (