diff --git a/backend/src/models/db.js b/backend/src/models/db.js index 24b1786..d4c8375 100644 --- a/backend/src/models/db.js +++ b/backend/src/models/db.js @@ -283,8 +283,8 @@ async function seedAdmin(schema) { if (!existing) { const hash = bcrypt.hashSync(adminPass, 10); const ur = await queryResult(schema, ` - INSERT INTO users (name, email, password, role, status, is_default_admin, must_change_password) - VALUES ($1, $2, $3, 'admin', 'active', TRUE, TRUE) RETURNING id + INSERT INTO users (name, email, password, role, status, is_default_admin, must_change_password, avatar) + VALUES ($1, $2, $3, 'admin', 'active', TRUE, TRUE, '/avatar/admin.png') RETURNING id `, [adminName, adminEmail, hash]); const adminId = ur.rows[0].id; @@ -312,6 +312,10 @@ async function seedAdmin(schema) { } console.log(`[DB:${schema}] Default admin exists (id=${existing.id})`); + // Always ensure admin has the fixed avatar + await exec(schema, + "UPDATE users SET avatar='/avatar/admin.png', updated_at=NOW() WHERE is_default_admin=TRUE AND (avatar IS NULL OR avatar != '/avatar/admin.png')" + ); if (pwReset) { const hash = bcrypt.hashSync(adminPass, 10); await exec(schema, diff --git a/backend/src/routes/groups.js b/backend/src/routes/groups.js index 915f415..d012506 100644 --- a/backend/src/routes/groups.js +++ b/backend/src/routes/groups.js @@ -188,7 +188,13 @@ router.post('/', authMiddleware, async (req, res) => { for (const u of allUsers) await exec(req.schema, 'INSERT INTO group_members (group_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [groupId, u.id]); } else { await exec(req.schema, 'INSERT INTO group_members (group_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [groupId, req.user.id]); - if (memberIds?.length > 0) for (const uid of memberIds) await exec(req.schema, 'INSERT INTO group_members (group_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [groupId, uid]); + if (memberIds?.length > 0) { + const defaultAdmin = await queryOne(req.schema, 'SELECT id FROM users WHERE is_default_admin=TRUE'); + for (const uid of memberIds) { + if (defaultAdmin && uid === defaultAdmin.id) continue; + await exec(req.schema, 'INSERT INTO group_members (group_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [groupId, uid]); + } + } } await emitGroupNew(req.schema, io, groupId); res.json({ group: await queryOne(req.schema, 'SELECT * FROM groups WHERE id=$1', [groupId]) }); @@ -231,6 +237,8 @@ router.post('/:id/members', authMiddleware, async (req, res) => { if (group.type !== 'private') return res.status(400).json({ error: 'Cannot manually add members to public groups' }); if (group.is_direct) return res.status(400).json({ error: 'Cannot add members to a direct message' }); if (group.owner_id !== req.user.id && req.user.role !== 'admin') return res.status(403).json({ error: 'Only owner can add members' }); + const targetUser = await queryOne(req.schema, 'SELECT is_default_admin FROM users WHERE id=$1', [userId]); + if (targetUser?.is_default_admin) return res.status(400).json({ error: 'Default admin cannot be added to private groups' }); await exec(req.schema, 'INSERT INTO group_members (group_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [group.id, userId]); const addedUser = await queryOne(req.schema, 'SELECT name,display_name FROM users WHERE id=$1', [userId]); const addedName = addedUser?.display_name || addedUser?.name || 'Unknown'; diff --git a/backend/src/routes/usergroups.js b/backend/src/routes/usergroups.js index ba4106e..001a8e0 100644 --- a/backend/src/routes/usergroups.js +++ b/backend/src/routes/usergroups.js @@ -225,7 +225,9 @@ router.post('/', authMiddleware, teamManagerMiddleware, async (req, res) => { [name.trim(), dmGroupId] ); const ugId = ugr.rows[0].id; + const defaultAdmin = await queryOne(req.schema, 'SELECT id FROM users WHERE is_default_admin=TRUE'); for (const uid of memberIds) { + if (defaultAdmin && uid === defaultAdmin.id) continue; await exec(req.schema, 'INSERT INTO user_group_members (user_group_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [ugId, uid]); await addUserSilent(req.schema, dmGroupId, uid); } @@ -249,7 +251,9 @@ router.patch('/:id', authMiddleware, teamManagerMiddleware, async (req, res) => } if (Array.isArray(memberIds) && ug.dm_group_id) { + const defaultAdmin = await queryOne(req.schema, 'SELECT id FROM users WHERE is_default_admin=TRUE'); const newIds = new Set(memberIds.map(Number).filter(Boolean)); + if (defaultAdmin) newIds.delete(defaultAdmin.id); // default admin cannot be in user groups const currentSet = new Set((await query(req.schema, 'SELECT user_id FROM user_group_members WHERE user_group_id=$1', [ug.id])).map(r => r.user_id)); const addedUids = [], removedUids = []; diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js index c5ded0c..337bbc5 100644 --- a/backend/src/routes/users.js +++ b/backend/src/routes/users.js @@ -281,6 +281,7 @@ router.patch('/me/profile', authMiddleware, async (req, res) => { // Upload avatar router.post('/me/avatar', authMiddleware, uploadAvatar.single('avatar'), async (req, res) => { + if (req.user.is_default_admin) return res.status(403).json({ error: 'Default admin avatar cannot be changed' }); if (!req.file) return res.status(400).json({ error: 'No file uploaded' }); try { const sharp = require('sharp'); diff --git a/build.sh b/build.sh index 5b152b9..16f598d 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.11.25}" +VERSION="${1:-0.11.26}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="jama" diff --git a/frontend/public/avatar/admin.png b/frontend/public/avatar/admin.png new file mode 100644 index 0000000..650f2a6 Binary files /dev/null and b/frontend/public/avatar/admin.png differ diff --git a/frontend/src/components/Avatar.jsx b/frontend/src/components/Avatar.jsx index 41ad42a..2de0215 100644 --- a/frontend/src/components/Avatar.jsx +++ b/frontend/src/components/Avatar.jsx @@ -1,6 +1,14 @@ export default function Avatar({ user, size = 'md', className = '' }) { if (!user) return null; + if (user.is_default_admin) { + return ( +
+ Admin +
+ ); + } + const initials = (() => { const name = user.display_name || user.name || ''; const parts = name.trim().split(' ').filter(Boolean); diff --git a/frontend/src/components/ProfileModal.jsx b/frontend/src/components/ProfileModal.jsx index ef53f21..0c33d2f 100644 --- a/frontend/src/components/ProfileModal.jsx +++ b/frontend/src/components/ProfileModal.jsx @@ -76,17 +76,19 @@ export default function ProfileModal({ onClose }) {
- + {!user?.is_default_admin && ( + + )}
{user?.display_name || user?.name}