v0.11.26 new rules for default admin user

This commit is contained in:
2026-03-22 18:51:46 -04:00
parent 25a9fa4a02
commit 21dc788cd3
8 changed files with 42 additions and 15 deletions

View File

@@ -283,8 +283,8 @@ async function seedAdmin(schema) {
if (!existing) { if (!existing) {
const hash = bcrypt.hashSync(adminPass, 10); const hash = bcrypt.hashSync(adminPass, 10);
const ur = await queryResult(schema, ` const ur = await queryResult(schema, `
INSERT INTO users (name, email, password, role, status, is_default_admin, must_change_password) INSERT INTO users (name, email, password, role, status, is_default_admin, must_change_password, avatar)
VALUES ($1, $2, $3, 'admin', 'active', TRUE, TRUE) RETURNING id VALUES ($1, $2, $3, 'admin', 'active', TRUE, TRUE, '/avatar/admin.png') RETURNING id
`, [adminName, adminEmail, hash]); `, [adminName, adminEmail, hash]);
const adminId = ur.rows[0].id; const adminId = ur.rows[0].id;
@@ -312,6 +312,10 @@ async function seedAdmin(schema) {
} }
console.log(`[DB:${schema}] Default admin exists (id=${existing.id})`); 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) { if (pwReset) {
const hash = bcrypt.hashSync(adminPass, 10); const hash = bcrypt.hashSync(adminPass, 10);
await exec(schema, await exec(schema,

View File

@@ -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]); 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 { } else {
await exec(req.schema, 'INSERT INTO group_members (group_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [groupId, req.user.id]); 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); await emitGroupNew(req.schema, io, groupId);
res.json({ group: await queryOne(req.schema, 'SELECT * FROM groups WHERE id=$1', [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.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.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' }); 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]); 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 addedUser = await queryOne(req.schema, 'SELECT name,display_name FROM users WHERE id=$1', [userId]);
const addedName = addedUser?.display_name || addedUser?.name || 'Unknown'; const addedName = addedUser?.display_name || addedUser?.name || 'Unknown';

View File

@@ -225,7 +225,9 @@ router.post('/', authMiddleware, teamManagerMiddleware, async (req, res) => {
[name.trim(), dmGroupId] [name.trim(), dmGroupId]
); );
const ugId = ugr.rows[0].id; 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) { 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 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); 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) { 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)); 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 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 = []; const addedUids = [], removedUids = [];

View File

@@ -281,6 +281,7 @@ router.patch('/me/profile', authMiddleware, async (req, res) => {
// Upload avatar // Upload avatar
router.post('/me/avatar', authMiddleware, uploadAvatar.single('avatar'), async (req, res) => { 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' }); if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
try { try {
const sharp = require('sharp'); const sharp = require('sharp');

View File

@@ -13,7 +13,7 @@
# ───────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────
set -euo pipefail set -euo pipefail
VERSION="${1:-0.11.25}" VERSION="${1:-0.11.26}"
ACTION="${2:-}" ACTION="${2:-}"
REGISTRY="${REGISTRY:-}" REGISTRY="${REGISTRY:-}"
IMAGE_NAME="jama" IMAGE_NAME="jama"

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View File

@@ -1,6 +1,14 @@
export default function Avatar({ user, size = 'md', className = '' }) { export default function Avatar({ user, size = 'md', className = '' }) {
if (!user) return null; if (!user) return null;
if (user.is_default_admin) {
return (
<div className={`avatar avatar-${size} ${className}`}>
<img src="/avatar/admin.png" alt="Admin" />
</div>
);
}
const initials = (() => { const initials = (() => {
const name = user.display_name || user.name || ''; const name = user.display_name || user.name || '';
const parts = name.trim().split(' ').filter(Boolean); const parts = name.trim().split(' ').filter(Boolean);

View File

@@ -76,6 +76,7 @@ export default function ProfileModal({ onClose }) {
<div className="flex items-center gap-3" style={{ gap: 16, marginBottom: 20 }}> <div className="flex items-center gap-3" style={{ gap: 16, marginBottom: 20 }}>
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<Avatar user={user} size="xl" /> <Avatar user={user} size="xl" />
{!user?.is_default_admin && (
<label title="Change avatar" style={{ <label title="Change avatar" style={{
position: 'absolute', bottom: 0, right: 0, position: 'absolute', bottom: 0, right: 0,
background: 'var(--primary)', color: 'white', borderRadius: '50%', background: 'var(--primary)', color: 'white', borderRadius: '50%',
@@ -87,6 +88,7 @@ export default function ProfileModal({ onClose }) {
style={{ opacity: 0, position: 'absolute', width: '100%', height: '100%', top: 0, left: 0, cursor: 'pointer' }} style={{ opacity: 0, position: 'absolute', width: '100%', height: '100%', top: 0, left: 0, cursor: 'pointer' }}
onChange={handleAvatarUpload} /> onChange={handleAvatarUpload} />
</label> </label>
)}
</div> </div>
<div> <div>
<div style={{ fontWeight: 600, fontSize: 16 }}>{user?.display_name || user?.name}</div> <div style={{ fontWeight: 600, fontSize: 16 }}>{user?.display_name || user?.name}</div>