v0.9.88 major change sqlite to postgres
This commit is contained in:
@@ -1,318 +1,264 @@
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const router = express.Router();
|
||||
const { getDb, addUserToPublicGroups, getOrCreateSupportGroup } = require('../models/db');
|
||||
const bcrypt = require('bcryptjs');
|
||||
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 avatarStorage = multer.diskStorage({
|
||||
destination: '/app/uploads/avatars',
|
||||
filename: (req, file, cb) => {
|
||||
const ext = path.extname(file.originalname);
|
||||
cb(null, `avatar_${req.user.id}_${Date.now()}${ext}`);
|
||||
}
|
||||
filename: (req, file, cb) => cb(null, `avatar_${req.user.id}_${Date.now()}${path.extname(file.originalname)}`),
|
||||
});
|
||||
const uploadAvatar = multer({
|
||||
storage: avatarStorage,
|
||||
limits: { fileSize: 2 * 1024 * 1024 },
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (file.mimetype.startsWith('image/')) cb(null, true);
|
||||
else cb(new Error('Images only'));
|
||||
}
|
||||
fileFilter: (req, file, cb) => file.mimetype.startsWith('image/') ? cb(null, true) : cb(new Error('Images only')),
|
||||
});
|
||||
|
||||
// Resolve unique name: "John Doe" exists → return "John Doe (1)", then "(2)" etc.
|
||||
function resolveUniqueName(db, baseName, excludeId = null) {
|
||||
const existing = db.prepare(
|
||||
"SELECT name FROM users WHERE status != 'deleted' AND id != ? AND (name = ? OR name LIKE ?)"
|
||||
).all(excludeId ?? -1, baseName, `${baseName} (%)`);
|
||||
async function resolveUniqueName(schema, baseName, excludeId = null) {
|
||||
const existing = await query(schema,
|
||||
"SELECT name FROM users WHERE status != 'deleted' AND id != $1 AND (name = $2 OR name LIKE $3)",
|
||||
[excludeId ?? -1, baseName, `${baseName} (%)`]
|
||||
);
|
||||
if (existing.length === 0) return baseName;
|
||||
let max = 0;
|
||||
for (const u of existing) {
|
||||
const m = u.name.match(/\((\d+)\)$/);
|
||||
if (m) max = Math.max(max, parseInt(m[1]));
|
||||
else max = Math.max(max, 0);
|
||||
}
|
||||
for (const u of existing) { const m = u.name.match(/\((\d+)\)$/); if (m) max = Math.max(max, parseInt(m[1])); else max = Math.max(max, 0); }
|
||||
return `${baseName} (${max + 1})`;
|
||||
}
|
||||
|
||||
function isValidEmail(email) {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
}
|
||||
function isValidEmail(e) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e); }
|
||||
|
||||
function getDefaultPassword(db) {
|
||||
return process.env.USER_PASS || 'user@1234';
|
||||
}
|
||||
|
||||
// List users (admin)
|
||||
router.get('/', authMiddleware, teamManagerMiddleware, (req, res) => {
|
||||
const db = getDb();
|
||||
const users = db.prepare(`
|
||||
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
|
||||
`).all();
|
||||
res.json({ users });
|
||||
// List users
|
||||
router.get('/', authMiddleware, adminMiddleware, 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"
|
||||
);
|
||||
res.json({ users });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// Search users (public-ish for mentions/add-member)
|
||||
router.get('/search', authMiddleware, (req, res) => {
|
||||
// Search users
|
||||
router.get('/search', authMiddleware, async (req, res) => {
|
||||
const { q, groupId } = req.query;
|
||||
const db = getDb();
|
||||
let users;
|
||||
if (groupId) {
|
||||
const group = db.prepare('SELECT type, is_direct FROM groups WHERE id = ?').get(parseInt(groupId));
|
||||
if (group && (group.type === 'private' || group.is_direct)) {
|
||||
// Private group or direct message — only show members of this group
|
||||
users = db.prepare(`
|
||||
SELECT u.id, u.name, u.display_name, u.avatar, u.role, u.status, u.hide_admin_tag, u.allow_dm
|
||||
FROM users u
|
||||
JOIN group_members gm ON gm.user_id = u.id AND gm.group_id = ?
|
||||
WHERE u.status = 'active' AND u.id != ?
|
||||
AND (u.name LIKE ? OR u.display_name LIKE ?)
|
||||
LIMIT 10
|
||||
`).all(parseInt(groupId), req.user.id, `%${q}%`, `%${q}%`);
|
||||
try {
|
||||
let users;
|
||||
if (groupId) {
|
||||
const group = await queryOne(req.schema, 'SELECT type, is_direct FROM groups WHERE id = $1', [parseInt(groupId)]);
|
||||
if (group && (group.type === 'private' || group.is_direct)) {
|
||||
users = await query(req.schema,
|
||||
"SELECT u.id,u.name,u.display_name,u.avatar,u.role,u.status,u.hide_admin_tag,u.allow_dm FROM users u JOIN group_members gm ON gm.user_id=u.id AND gm.group_id=$1 WHERE u.status='active' AND u.id!=$2 AND (u.name ILIKE $3 OR u.display_name ILIKE $3) LIMIT 10",
|
||||
[parseInt(groupId), req.user.id, `%${q}%`]
|
||||
);
|
||||
} else {
|
||||
users = await query(req.schema,
|
||||
"SELECT id,name,display_name,avatar,role,status,hide_admin_tag,allow_dm FROM users WHERE status='active' AND id!=$1 AND (name ILIKE $2 OR display_name ILIKE $2) LIMIT 10",
|
||||
[req.user.id, `%${q}%`]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Public group — all active users
|
||||
users = db.prepare(`
|
||||
SELECT id, name, display_name, avatar, role, status, hide_admin_tag, allow_dm FROM users
|
||||
WHERE status = 'active' AND id != ? AND (name LIKE ? OR display_name LIKE ?)
|
||||
LIMIT 10
|
||||
`).all(req.user.id, `%${q}%`, `%${q}%`);
|
||||
users = await query(req.schema,
|
||||
"SELECT id,name,display_name,avatar,role,status,hide_admin_tag,allow_dm FROM users WHERE status='active' AND (name ILIKE $1 OR display_name ILIKE $1) LIMIT 10",
|
||||
[`%${q}%`]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
users = db.prepare(`
|
||||
SELECT id, name, display_name, avatar, role, status, hide_admin_tag, allow_dm FROM users
|
||||
WHERE status = 'active' AND (name LIKE ? OR display_name LIKE ?)
|
||||
LIMIT 10
|
||||
`).all(`%${q}%`, `%${q}%`);
|
||||
}
|
||||
res.json({ users });
|
||||
res.json({ users });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// Check if a display name is already taken (excludes self)
|
||||
router.get('/check-display-name', authMiddleware, (req, res) => {
|
||||
// Check display name
|
||||
router.get('/check-display-name', authMiddleware, async (req, res) => {
|
||||
const { name } = req.query;
|
||||
if (!name) return res.json({ taken: false });
|
||||
const db = getDb();
|
||||
const conflict = db.prepare(
|
||||
"SELECT id FROM users WHERE LOWER(display_name) = LOWER(?) AND id != ? AND status != 'deleted'"
|
||||
).get(name, req.user.id);
|
||||
res.json({ taken: !!conflict });
|
||||
try {
|
||||
const conflict = await queryOne(req.schema,
|
||||
"SELECT id FROM users WHERE LOWER(display_name)=LOWER($1) AND id!=$2 AND status!='deleted'",
|
||||
[name, req.user.id]
|
||||
);
|
||||
res.json({ taken: !!conflict });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// Create user (admin) — req 3: skip duplicate email, req 4: suffix duplicate names
|
||||
router.post('/', authMiddleware, adminMiddleware, (req, res) => {
|
||||
// Create user
|
||||
router.post('/', authMiddleware, adminMiddleware, 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' });
|
||||
|
||||
const db = getDb();
|
||||
const exists = db.prepare('SELECT id FROM users WHERE email = ?').get(email);
|
||||
if (exists) return res.status(400).json({ error: 'Email already in use' });
|
||||
|
||||
const resolvedName = resolveUniqueName(db, name.trim());
|
||||
const pw = (password || '').trim() || getDefaultPassword(db);
|
||||
const hash = bcrypt.hashSync(pw, 10);
|
||||
const result = db.prepare(`
|
||||
INSERT INTO users (name, email, password, role, status, must_change_password)
|
||||
VALUES (?, ?, ?, ?, 'active', 1)
|
||||
`).run(resolvedName, email, hash, role === 'admin' ? 'admin' : 'member');
|
||||
|
||||
addUserToPublicGroups(result.lastInsertRowid);
|
||||
// Admin users are automatically added to the Support group
|
||||
if (role === 'admin') {
|
||||
const supportGroupId = getOrCreateSupportGroup();
|
||||
if (supportGroupId) {
|
||||
db.prepare('INSERT OR IGNORE INTO group_members (group_id, user_id) VALUES (?, ?)').run(supportGroupId, result.lastInsertRowid);
|
||||
try {
|
||||
const exists = await queryOne(req.schema, 'SELECT id FROM users WHERE email = $1', [email]);
|
||||
if (exists) return res.status(400).json({ error: 'Email already in use' });
|
||||
const resolvedName = await resolveUniqueName(req.schema, name.trim());
|
||||
const pw = (password || '').trim() || process.env.USER_PASS || 'user@1234';
|
||||
const hash = bcrypt.hashSync(pw, 10);
|
||||
const r = await queryResult(req.schema,
|
||||
"INSERT INTO users (name,email,password,role,status,must_change_password) VALUES ($1,$2,$3,$4,'active',TRUE) RETURNING id",
|
||||
[resolvedName, email, hash, role === 'admin' ? 'admin' : 'member']
|
||||
);
|
||||
const userId = r.rows[0].id;
|
||||
await addUserToPublicGroups(req.schema, userId);
|
||||
if (role === 'admin') {
|
||||
const sgId = await getOrCreateSupportGroup(req.schema);
|
||||
if (sgId) await exec(req.schema, 'INSERT INTO group_members (group_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [sgId, userId]);
|
||||
}
|
||||
}
|
||||
const user = db.prepare('SELECT id, name, email, role, status, must_change_password, created_at FROM users WHERE id = ?').get(result.lastInsertRowid);
|
||||
res.json({ user });
|
||||
const user = await queryOne(req.schema, 'SELECT id,name,email,role,status,must_change_password,created_at FROM users WHERE id=$1', [userId]);
|
||||
res.json({ user });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// Bulk create users
|
||||
router.post('/bulk', authMiddleware, adminMiddleware, (req, res) => {
|
||||
// Bulk create
|
||||
router.post('/bulk', authMiddleware, adminMiddleware, async (req, res) => {
|
||||
const { users } = req.body;
|
||||
const db = getDb();
|
||||
const results = { created: [], skipped: [] };
|
||||
const seenEmails = new Set();
|
||||
const defaultPw = getDefaultPassword(db);
|
||||
|
||||
const insertUser = db.prepare(`
|
||||
INSERT INTO users (name, email, password, role, status, must_change_password)
|
||||
VALUES (?, ?, ?, ?, 'active', 1)
|
||||
`);
|
||||
|
||||
for (const u of users) {
|
||||
const email = (u.email || '').trim().toLowerCase();
|
||||
const name = (u.name || '').trim();
|
||||
if (!name || !email) { results.skipped.push({ email: email || '(blank)', reason: 'Missing name or email' }); continue; }
|
||||
if (!isValidEmail(email)) { results.skipped.push({ email, reason: 'Invalid email address' }); continue; }
|
||||
if (seenEmails.has(email)) { results.skipped.push({ email, reason: 'Duplicate email in CSV' }); continue; }
|
||||
seenEmails.add(email);
|
||||
const exists = db.prepare('SELECT id FROM users WHERE email = ?').get(email);
|
||||
if (exists) { results.skipped.push({ email, reason: 'Email already exists' }); continue; }
|
||||
try {
|
||||
const resolvedName = resolveUniqueName(db, name);
|
||||
const pw = (u.password || '').trim() || defaultPw;
|
||||
const hash = bcrypt.hashSync(pw, 10);
|
||||
const newRole = u.role === 'admin' ? 'admin' : 'member';
|
||||
const r = insertUser.run(resolvedName, email, hash, newRole);
|
||||
addUserToPublicGroups(r.lastInsertRowid);
|
||||
if (newRole === 'admin') {
|
||||
const supportGroupId = getOrCreateSupportGroup();
|
||||
if (supportGroupId) {
|
||||
db.prepare('INSERT OR IGNORE INTO group_members (group_id, user_id) VALUES (?, ?)').run(supportGroupId, r.lastInsertRowid);
|
||||
const defaultPw = process.env.USER_PASS || 'user@1234';
|
||||
try {
|
||||
for (const u of users) {
|
||||
const email = (u.email || '').trim().toLowerCase();
|
||||
const name = (u.name || '').trim();
|
||||
if (!name || !email) { results.skipped.push({ email: email || '(blank)', reason: 'Missing name or email' }); continue; }
|
||||
if (!isValidEmail(email)) { results.skipped.push({ email, reason: 'Invalid email address' }); continue; }
|
||||
if (seenEmails.has(email)) { results.skipped.push({ email, reason: 'Duplicate email in CSV' }); continue; }
|
||||
seenEmails.add(email);
|
||||
const exists = await queryOne(req.schema, 'SELECT id FROM users WHERE email=$1', [email]);
|
||||
if (exists) { results.skipped.push({ email, reason: 'Email already exists' }); continue; }
|
||||
try {
|
||||
const resolvedName = await resolveUniqueName(req.schema, name);
|
||||
const pw = (u.password || '').trim() || defaultPw;
|
||||
const hash = bcrypt.hashSync(pw, 10);
|
||||
const newRole = u.role === 'admin' ? 'admin' : 'member';
|
||||
const r = await queryResult(req.schema,
|
||||
"INSERT INTO users (name,email,password,role,status,must_change_password) VALUES ($1,$2,$3,$4,'active',TRUE) RETURNING id",
|
||||
[resolvedName, email, hash, newRole]
|
||||
);
|
||||
await addUserToPublicGroups(req.schema, r.rows[0].id);
|
||||
if (newRole === 'admin') {
|
||||
const sgId = await getOrCreateSupportGroup(req.schema);
|
||||
if (sgId) await exec(req.schema, 'INSERT INTO group_members (group_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [sgId, r.rows[0].id]);
|
||||
}
|
||||
}
|
||||
results.created.push(email);
|
||||
} catch (e) {
|
||||
results.skipped.push({ email, reason: e.message });
|
||||
results.created.push(email);
|
||||
} catch (e) { results.skipped.push({ email, reason: e.message }); }
|
||||
}
|
||||
}
|
||||
|
||||
res.json(results);
|
||||
res.json(results);
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// Update user name (admin only — req 5)
|
||||
router.patch('/:id/name', authMiddleware, adminMiddleware, (req, res) => {
|
||||
// Patch name
|
||||
router.patch('/:id/name', authMiddleware, adminMiddleware, async (req, res) => {
|
||||
const { name } = req.body;
|
||||
if (!name || !name.trim()) return res.status(400).json({ error: 'Name required' });
|
||||
const db = getDb();
|
||||
const target = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id);
|
||||
if (!target) return res.status(404).json({ error: 'User not found' });
|
||||
// Pass the target's own id so their current name is excluded from the duplicate check
|
||||
const resolvedName = resolveUniqueName(db, name.trim(), req.params.id);
|
||||
db.prepare("UPDATE users SET name = ?, updated_at = datetime('now') WHERE id = ?").run(resolvedName, target.id);
|
||||
res.json({ success: true, name: resolvedName });
|
||||
if (!name?.trim()) return res.status(400).json({ error: 'Name required' });
|
||||
try {
|
||||
const target = await queryOne(req.schema, 'SELECT * FROM users WHERE id=$1', [req.params.id]);
|
||||
if (!target) return res.status(404).json({ error: 'User not found' });
|
||||
const resolvedName = await resolveUniqueName(req.schema, name.trim(), req.params.id);
|
||||
await exec(req.schema, 'UPDATE users SET name=$1, updated_at=NOW() WHERE id=$2', [resolvedName, target.id]);
|
||||
res.json({ success: true, name: resolvedName });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// Update user role (admin)
|
||||
router.patch('/:id/role', authMiddleware, adminMiddleware, (req, res) => {
|
||||
// Patch role
|
||||
router.patch('/:id/role', authMiddleware, adminMiddleware, async (req, res) => {
|
||||
const { role } = req.body;
|
||||
const db = getDb();
|
||||
const target = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id);
|
||||
if (!target) return res.status(404).json({ error: 'User not found' });
|
||||
if (target.is_default_admin) return res.status(403).json({ error: 'Cannot modify default admin role' });
|
||||
if (!['member', 'admin'].includes(role)) return res.status(400).json({ error: 'Invalid role' });
|
||||
db.prepare("UPDATE users SET role = ?, updated_at = datetime('now') WHERE id = ?").run(role, target.id);
|
||||
// If promoted to admin, ensure they're in the Support group
|
||||
if (role === 'admin') {
|
||||
const supportGroupId = getOrCreateSupportGroup();
|
||||
if (supportGroupId) {
|
||||
db.prepare('INSERT OR IGNORE INTO group_members (group_id, user_id) VALUES (?, ?)').run(supportGroupId, target.id);
|
||||
if (!['member','admin'].includes(role)) return res.status(400).json({ error: 'Invalid role' });
|
||||
try {
|
||||
const target = await queryOne(req.schema, 'SELECT * FROM users WHERE id=$1', [req.params.id]);
|
||||
if (!target) return res.status(404).json({ error: 'User not found' });
|
||||
if (target.is_default_admin) return res.status(403).json({ error: 'Cannot modify default admin role' });
|
||||
await exec(req.schema, 'UPDATE users SET role=$1, updated_at=NOW() WHERE id=$2', [role, target.id]);
|
||||
if (role === 'admin') {
|
||||
const sgId = await getOrCreateSupportGroup(req.schema);
|
||||
if (sgId) await exec(req.schema, 'INSERT INTO group_members (group_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [sgId, target.id]);
|
||||
}
|
||||
}
|
||||
res.json({ success: true });
|
||||
res.json({ success: true });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// Reset user password (admin)
|
||||
router.patch('/:id/reset-password', authMiddleware, adminMiddleware, (req, res) => {
|
||||
// Reset password
|
||||
router.patch('/:id/reset-password', authMiddleware, adminMiddleware, async (req, res) => {
|
||||
const { password } = req.body;
|
||||
if (!password || password.length < 6) return res.status(400).json({ error: 'Password too short' });
|
||||
const db = getDb();
|
||||
const hash = bcrypt.hashSync(password, 10);
|
||||
db.prepare("UPDATE users SET password = ?, must_change_password = 1, updated_at = datetime('now') WHERE id = ?").run(hash, req.params.id);
|
||||
res.json({ success: true });
|
||||
try {
|
||||
const hash = bcrypt.hashSync(password, 10);
|
||||
await exec(req.schema, 'UPDATE users SET password=$1, must_change_password=TRUE, updated_at=NOW() WHERE id=$2', [hash, req.params.id]);
|
||||
res.json({ success: true });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// Suspend user (admin)
|
||||
router.patch('/:id/suspend', authMiddleware, adminMiddleware, (req, res) => {
|
||||
const db = getDb();
|
||||
const target = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id);
|
||||
if (!target) return res.status(404).json({ error: 'User not found' });
|
||||
if (target.is_default_admin) return res.status(403).json({ error: 'Cannot suspend default admin' });
|
||||
db.prepare("UPDATE users SET status = 'suspended', updated_at = datetime('now') WHERE id = ?").run(target.id);
|
||||
res.json({ success: true });
|
||||
// Suspend / activate / delete
|
||||
router.patch('/:id/suspend', authMiddleware, adminMiddleware, 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' });
|
||||
if (t.is_default_admin) return res.status(403).json({ error: 'Cannot suspend default admin' });
|
||||
await exec(req.schema, "UPDATE users SET status='suspended', updated_at=NOW() WHERE id=$1", [t.id]);
|
||||
res.json({ success: true });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
router.patch('/:id/activate', authMiddleware, adminMiddleware, 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) => {
|
||||
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' });
|
||||
if (t.is_default_admin) return res.status(403).json({ error: 'Cannot delete default admin' });
|
||||
await exec(req.schema, "UPDATE users SET status='deleted', updated_at=NOW() WHERE id=$1", [t.id]);
|
||||
res.json({ success: true });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// Activate user (admin)
|
||||
router.patch('/:id/activate', authMiddleware, adminMiddleware, (req, res) => {
|
||||
const db = getDb();
|
||||
db.prepare("UPDATE users SET status = 'active', updated_at = datetime('now') WHERE id = ?").run(req.params.id);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// Delete user (admin)
|
||||
router.delete('/:id', authMiddleware, adminMiddleware, (req, res) => {
|
||||
const db = getDb();
|
||||
const target = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id);
|
||||
if (!target) return res.status(404).json({ error: 'User not found' });
|
||||
if (target.is_default_admin) return res.status(403).json({ error: 'Cannot delete default admin' });
|
||||
db.prepare("UPDATE users SET status = 'deleted', updated_at = datetime('now') WHERE id = ?").run(target.id);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// Update own profile — display name must be unique (req 6)
|
||||
router.patch('/me/profile', authMiddleware, (req, res) => {
|
||||
// Update own profile
|
||||
router.patch('/me/profile', authMiddleware, async (req, res) => {
|
||||
const { displayName, aboutMe, hideAdminTag, allowDm } = req.body;
|
||||
const db = getDb();
|
||||
if (displayName) {
|
||||
const conflict = db.prepare(
|
||||
"SELECT id FROM users WHERE LOWER(display_name) = LOWER(?) AND id != ? AND status != 'deleted'"
|
||||
).get(displayName, req.user.id);
|
||||
if (conflict) return res.status(400).json({ error: 'Display name already in use' });
|
||||
}
|
||||
db.prepare("UPDATE users SET display_name = ?, about_me = ?, hide_admin_tag = ?, allow_dm = ?, updated_at = datetime('now') WHERE id = ?")
|
||||
.run(displayName || null, aboutMe || null, hideAdminTag ? 1 : 0, allowDm === false ? 0 : 1, req.user.id);
|
||||
const user = db.prepare('SELECT id, name, email, role, status, avatar, about_me, display_name, hide_admin_tag, allow_dm FROM users WHERE id = ?').get(req.user.id);
|
||||
res.json({ user });
|
||||
try {
|
||||
if (displayName) {
|
||||
const conflict = await queryOne(req.schema,
|
||||
"SELECT id FROM users WHERE LOWER(display_name)=LOWER($1) AND id!=$2 AND status!='deleted'",
|
||||
[displayName, req.user.id]
|
||||
);
|
||||
if (conflict) return res.status(400).json({ error: 'Display name already in use' });
|
||||
}
|
||||
await exec(req.schema,
|
||||
'UPDATE users SET display_name=$1, about_me=$2, hide_admin_tag=$3, allow_dm=$4, updated_at=NOW() WHERE id=$5',
|
||||
[displayName || null, aboutMe || null, !!hideAdminTag, allowDm !== false, req.user.id]
|
||||
);
|
||||
const user = await queryOne(req.schema,
|
||||
'SELECT id,name,email,role,status,avatar,about_me,display_name,hide_admin_tag,allow_dm FROM users WHERE id=$1',
|
||||
[req.user.id]
|
||||
);
|
||||
res.json({ user });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// Upload avatar — resize if needed, skip compression for files under 500 KB
|
||||
// Upload avatar
|
||||
router.post('/me/avatar', authMiddleware, uploadAvatar.single('avatar'), async (req, res) => {
|
||||
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
|
||||
try {
|
||||
const sharp = require('sharp');
|
||||
const sharp = require('sharp');
|
||||
const filePath = req.file.path;
|
||||
const fileSizeBytes = req.file.size;
|
||||
const FIVE_HUNDRED_KB = 500 * 1024;
|
||||
const MAX_DIM = 256; // max width/height in pixels
|
||||
|
||||
const image = sharp(filePath);
|
||||
const meta = await image.metadata();
|
||||
const needsResize = (meta.width > MAX_DIM || meta.height > MAX_DIM);
|
||||
|
||||
if (fileSizeBytes < FIVE_HUNDRED_KB && !needsResize) {
|
||||
// Small enough and already correctly sized — serve as-is
|
||||
} else {
|
||||
// Resize (and compress only if over 500 KB)
|
||||
const outPath = filePath.replace(/(\.[^.]+)$/, '_p$1');
|
||||
let pipeline = sharp(filePath).resize(MAX_DIM, MAX_DIM, { fit: 'cover', withoutEnlargement: true });
|
||||
if (fileSizeBytes >= FIVE_HUNDRED_KB) {
|
||||
// Compress: use webp for best size/quality ratio
|
||||
pipeline = pipeline.webp({ quality: 82 });
|
||||
await pipeline.toFile(outPath + '.webp');
|
||||
const fs = require('fs');
|
||||
fs.unlinkSync(filePath);
|
||||
fs.renameSync(outPath + '.webp', filePath.replace(/\.[^.]+$/, '.webp'));
|
||||
const newPath = filePath.replace(/\.[^.]+$/, '.webp');
|
||||
const newFilename = path.basename(newPath);
|
||||
const db = getDb();
|
||||
const avatarUrl = `/uploads/avatars/${newFilename}`;
|
||||
db.prepare("UPDATE users SET avatar = ?, updated_at = datetime('now') WHERE id = ?").run(avatarUrl, req.user.id);
|
||||
return res.json({ avatarUrl });
|
||||
} else {
|
||||
// Under 500 KB but needs resize — resize only, keep original format
|
||||
await pipeline.toFile(outPath);
|
||||
const fs = require('fs');
|
||||
fs.unlinkSync(filePath);
|
||||
fs.renameSync(outPath, filePath);
|
||||
}
|
||||
const MAX_DIM = 256;
|
||||
const image = sharp(filePath);
|
||||
const meta = await image.metadata();
|
||||
const needsResize = meta.width > MAX_DIM || meta.height > MAX_DIM;
|
||||
if (req.file.size >= 500 * 1024 || needsResize) {
|
||||
const outPath = filePath.replace(/\.[^.]+$/, '.webp');
|
||||
await sharp(filePath).resize(MAX_DIM,MAX_DIM,{fit:'cover',withoutEnlargement:true}).webp({quality:82}).toFile(outPath);
|
||||
const fs = require('fs');
|
||||
fs.unlinkSync(filePath);
|
||||
const avatarUrl = `/uploads/avatars/${path.basename(outPath)}`;
|
||||
await exec(req.schema, 'UPDATE users SET avatar=$1, updated_at=NOW() WHERE id=$2', [avatarUrl, req.user.id]);
|
||||
return res.json({ avatarUrl });
|
||||
}
|
||||
|
||||
const avatarUrl = `/uploads/avatars/${req.file.filename}`;
|
||||
const db = getDb();
|
||||
db.prepare("UPDATE users SET avatar = ?, updated_at = datetime('now') WHERE id = ?").run(avatarUrl, req.user.id);
|
||||
await exec(req.schema, 'UPDATE users SET avatar=$1, updated_at=NOW() WHERE id=$2', [avatarUrl, req.user.id]);
|
||||
res.json({ avatarUrl });
|
||||
} catch (err) {
|
||||
console.error('Avatar processing error:', err);
|
||||
// Fall back to serving unprocessed file
|
||||
console.error('Avatar error:', err);
|
||||
const avatarUrl = `/uploads/avatars/${req.file.filename}`;
|
||||
const db = getDb();
|
||||
db.prepare("UPDATE users SET avatar = ?, updated_at = datetime('now') WHERE id = ?").run(avatarUrl, req.user.id);
|
||||
await exec(req.schema, 'UPDATE users SET avatar=$1, updated_at=NOW() WHERE id=$2', [avatarUrl, req.user.id]).catch(()=>{});
|
||||
res.json({ avatarUrl });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user