v0.11.26 new rules for default admin user
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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 = [];
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
2
build.sh
2
build.sh
@@ -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"
|
||||||
|
|||||||
BIN
frontend/public/avatar/admin.png
Normal file
BIN
frontend/public/avatar/admin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user