v0.9.88 major change sqlite to postgres
This commit is contained in:
@@ -1,450 +1,318 @@
|
||||
const express = require('express');
|
||||
const fs = require('fs');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../models/db');
|
||||
const fs = require('fs');
|
||||
const router = express.Router();
|
||||
const { query, queryOne, queryResult, exec } = require('../models/db');
|
||||
const { authMiddleware, adminMiddleware } = require('../middleware/auth');
|
||||
|
||||
// Helper: emit group:new to all members of a group
|
||||
function emitGroupNew(io, groupId) {
|
||||
const db = getDb();
|
||||
const group = db.prepare('SELECT * FROM groups WHERE id = ?').get(groupId);
|
||||
function deleteImageFile(imageUrl) {
|
||||
if (!imageUrl) return;
|
||||
try { const fp = '/app' + imageUrl; if (fs.existsSync(fp)) fs.unlinkSync(fp); }
|
||||
catch (e) { console.warn('[Groups] Could not delete image:', e.message); }
|
||||
}
|
||||
|
||||
module.exports = (io) => {
|
||||
|
||||
async function emitGroupNew(schema, io, groupId) {
|
||||
const group = await queryOne(schema, 'SELECT * FROM groups WHERE id=$1', [groupId]);
|
||||
if (!group) return;
|
||||
if (group.type === 'public') {
|
||||
io.emit('group:new', { group });
|
||||
} else {
|
||||
const members = db.prepare('SELECT user_id FROM group_members WHERE group_id = ?').all(groupId);
|
||||
for (const m of members) {
|
||||
io.to(`user:${m.user_id}`).emit('group:new', { group });
|
||||
}
|
||||
const members = await query(schema, 'SELECT user_id FROM group_members WHERE group_id=$1', [groupId]);
|
||||
for (const m of members) io.to(`user:${m.user_id}`).emit('group:new', { group });
|
||||
}
|
||||
}
|
||||
|
||||
// Delete an uploaded image file from disk
|
||||
function deleteImageFile(imageUrl) {
|
||||
if (!imageUrl) return;
|
||||
try {
|
||||
const filePath = '/app' + imageUrl;
|
||||
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
||||
} catch (e) {
|
||||
console.warn('[Groups] Could not delete image file:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: emit group:deleted to all members
|
||||
function emitGroupDeleted(io, groupId, members) {
|
||||
for (const uid of members) {
|
||||
io.to(`user:${uid}`).emit('group:deleted', { groupId });
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: emit group:updated to all members
|
||||
function emitGroupUpdated(io, groupId) {
|
||||
const db = getDb();
|
||||
const group = db.prepare('SELECT * FROM groups WHERE id = ?').get(groupId);
|
||||
async function emitGroupUpdated(schema, io, groupId) {
|
||||
const group = await queryOne(schema, 'SELECT * FROM groups WHERE id=$1', [groupId]);
|
||||
if (!group) return;
|
||||
const members = db.prepare('SELECT user_id FROM group_members WHERE group_id = ?').all(groupId);
|
||||
const uids = group.type === 'public'
|
||||
? db.prepare("SELECT id as user_id FROM users WHERE status = 'active'").all()
|
||||
: members;
|
||||
for (const m of uids) {
|
||||
io.to(`user:${m.user_id}`).emit('group:updated', { group });
|
||||
let uids;
|
||||
if (group.type === 'public') {
|
||||
uids = await query(schema, "SELECT id AS user_id FROM users WHERE status='active'");
|
||||
} else {
|
||||
uids = await query(schema, 'SELECT user_id FROM group_members WHERE group_id=$1', [groupId]);
|
||||
}
|
||||
for (const m of uids) io.to(`user:${m.user_id}`).emit('group:updated', { group });
|
||||
}
|
||||
|
||||
// Inject io into routes
|
||||
|
||||
module.exports = (io) => {
|
||||
|
||||
// Get all groups for current user
|
||||
router.get('/', authMiddleware, (req, res) => {
|
||||
const db = getDb();
|
||||
const userId = req.user.id;
|
||||
|
||||
const publicGroups = db.prepare(`
|
||||
SELECT g.*,
|
||||
(SELECT COUNT(*) FROM messages m WHERE m.group_id = g.id AND m.is_deleted = 0) as message_count,
|
||||
(SELECT m.content FROM messages m WHERE m.group_id = g.id AND m.is_deleted = 0 ORDER BY m.created_at DESC LIMIT 1) as last_message,
|
||||
(SELECT m.created_at FROM messages m WHERE m.group_id = g.id AND m.is_deleted = 0 ORDER BY m.created_at DESC LIMIT 1) as last_message_at,
|
||||
(SELECT m.user_id FROM messages m WHERE m.group_id = g.id AND m.is_deleted = 0 ORDER BY m.created_at DESC LIMIT 1) as last_message_user_id
|
||||
FROM groups g
|
||||
WHERE g.type = 'public'
|
||||
ORDER BY g.is_default DESC, g.name ASC
|
||||
`).all();
|
||||
|
||||
// For direct messages, replace name with opposite user's display name
|
||||
const privateGroupsRaw = db.prepare(`
|
||||
SELECT g.*,
|
||||
u.name as owner_name,
|
||||
(SELECT COUNT(*) FROM messages m WHERE m.group_id = g.id AND m.is_deleted = 0) as message_count,
|
||||
(SELECT m.content FROM messages m WHERE m.group_id = g.id AND m.is_deleted = 0 ORDER BY m.created_at DESC LIMIT 1) as last_message,
|
||||
(SELECT m.created_at FROM messages m WHERE m.group_id = g.id AND m.is_deleted = 0 ORDER BY m.created_at DESC LIMIT 1) as last_message_at,
|
||||
(SELECT m.user_id FROM messages m WHERE m.group_id = g.id AND m.is_deleted = 0 ORDER BY m.created_at DESC LIMIT 1) as last_message_user_id
|
||||
FROM groups g
|
||||
JOIN group_members gm ON g.id = gm.group_id AND gm.user_id = ?
|
||||
LEFT JOIN users u ON g.owner_id = u.id
|
||||
WHERE g.type = 'private'
|
||||
ORDER BY last_message_at DESC NULLS LAST
|
||||
`).all(userId);
|
||||
|
||||
// For direct groups, set the name to the other user's display name
|
||||
// Uses direct_peer1_id / direct_peer2_id so the name survives after a user leaves
|
||||
const privateGroups = privateGroupsRaw.map(g => {
|
||||
if (g.is_direct) {
|
||||
// Backfill peer IDs for groups created before this migration
|
||||
if (!g.direct_peer1_id || !g.direct_peer2_id) {
|
||||
const peers = db.prepare('SELECT user_id FROM group_members WHERE group_id = ? LIMIT 2').all(g.id);
|
||||
if (peers.length === 2) {
|
||||
db.prepare('UPDATE groups SET direct_peer1_id = ?, direct_peer2_id = ? WHERE id = ?')
|
||||
.run(peers[0].user_id, peers[1].user_id, g.id);
|
||||
g.direct_peer1_id = peers[0].user_id;
|
||||
g.direct_peer2_id = peers[1].user_id;
|
||||
}
|
||||
}
|
||||
const otherUserId = g.direct_peer1_id === userId ? g.direct_peer2_id : g.direct_peer1_id;
|
||||
if (otherUserId) {
|
||||
const other = db.prepare('SELECT display_name, name, avatar FROM users WHERE id = ?').get(otherUserId);
|
||||
if (other) {
|
||||
g.peer_id = otherUserId;
|
||||
g.peer_real_name = other.name;
|
||||
g.peer_display_name = other.display_name || null; // null if no custom display name set
|
||||
g.peer_avatar = other.avatar || null;
|
||||
g.name = other.display_name || other.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Apply user's custom group name if set
|
||||
const custom = db.prepare('SELECT name FROM user_group_names WHERE user_id = ? AND group_id = ?').get(userId, g.id);
|
||||
if (custom) {
|
||||
g.owner_name_original = g.name; // original name shown in brackets in GroupInfoModal
|
||||
g.name = custom.name;
|
||||
}
|
||||
return g;
|
||||
});
|
||||
|
||||
res.json({ publicGroups, privateGroups });
|
||||
});
|
||||
|
||||
// Create group
|
||||
router.post('/', authMiddleware, (req, res) => {
|
||||
const { name, type, memberIds, isReadonly, isDirect } = req.body;
|
||||
const db = getDb();
|
||||
|
||||
if (type === 'public' && req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Only admins can create public groups' });
|
||||
}
|
||||
|
||||
// Direct message: find or create
|
||||
if (isDirect && memberIds && memberIds.length === 1) {
|
||||
const otherUserId = memberIds[0];
|
||||
// GET all groups for current user
|
||||
router.get('/', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const publicGroups = await query(req.schema, `
|
||||
SELECT g.*,
|
||||
(SELECT COUNT(*) FROM messages m WHERE m.group_id=g.id AND m.is_deleted=FALSE) AS message_count,
|
||||
(SELECT m.content FROM messages m WHERE m.group_id=g.id AND m.is_deleted=FALSE ORDER BY m.created_at DESC LIMIT 1) AS last_message,
|
||||
(SELECT m.created_at FROM messages m WHERE m.group_id=g.id AND m.is_deleted=FALSE ORDER BY m.created_at DESC LIMIT 1) AS last_message_at,
|
||||
(SELECT m.user_id FROM messages m WHERE m.group_id=g.id AND m.is_deleted=FALSE ORDER BY m.created_at DESC LIMIT 1) AS last_message_user_id
|
||||
FROM groups g WHERE g.type='public' ORDER BY g.is_default DESC, g.name ASC
|
||||
`);
|
||||
|
||||
// Check if a direct group already exists between these two users
|
||||
const existing = db.prepare(`
|
||||
SELECT g.id FROM groups g
|
||||
JOIN group_members gm1 ON gm1.group_id = g.id AND gm1.user_id = ?
|
||||
JOIN group_members gm2 ON gm2.group_id = g.id AND gm2.user_id = ?
|
||||
WHERE g.is_direct = 1
|
||||
LIMIT 1
|
||||
`).get(userId, otherUserId);
|
||||
const privateGroupsRaw = await query(req.schema, `
|
||||
SELECT g.*, u.name AS owner_name,
|
||||
(SELECT COUNT(*) FROM messages m WHERE m.group_id=g.id AND m.is_deleted=FALSE) AS message_count,
|
||||
(SELECT m.content FROM messages m WHERE m.group_id=g.id AND m.is_deleted=FALSE ORDER BY m.created_at DESC LIMIT 1) AS last_message,
|
||||
(SELECT m.created_at FROM messages m WHERE m.group_id=g.id AND m.is_deleted=FALSE ORDER BY m.created_at DESC LIMIT 1) AS last_message_at,
|
||||
(SELECT m.user_id FROM messages m WHERE m.group_id=g.id AND m.is_deleted=FALSE ORDER BY m.created_at DESC LIMIT 1) AS last_message_user_id
|
||||
FROM groups g JOIN group_members gm ON g.id=gm.group_id AND gm.user_id=$1
|
||||
LEFT JOIN users u ON g.owner_id=u.id WHERE g.type='private'
|
||||
ORDER BY last_message_at DESC NULLS LAST
|
||||
`, [userId]);
|
||||
|
||||
if (existing) {
|
||||
const group = db.prepare('SELECT * FROM groups WHERE id = ?').get(existing.id);
|
||||
// Ensure current user is still a member (may have left)
|
||||
db.prepare('INSERT OR IGNORE INTO group_members (group_id, user_id) VALUES (?, ?)').run(existing.id, userId);
|
||||
// Re-set readonly to false so both can post again
|
||||
db.prepare("UPDATE groups SET is_readonly = 0, owner_id = NULL, updated_at = datetime('now') WHERE id = ?").run(existing.id);
|
||||
return res.json({ group: db.prepare('SELECT * FROM groups WHERE id = ?').get(existing.id) });
|
||||
const privateGroups = await Promise.all(privateGroupsRaw.map(async g => {
|
||||
if (g.is_direct) {
|
||||
if (!g.direct_peer1_id || !g.direct_peer2_id) {
|
||||
const peers = await query(req.schema, 'SELECT user_id FROM group_members WHERE group_id=$1 LIMIT 2', [g.id]);
|
||||
if (peers.length === 2) {
|
||||
await exec(req.schema, 'UPDATE groups SET direct_peer1_id=$1, direct_peer2_id=$2 WHERE id=$3', [peers[0].user_id, peers[1].user_id, g.id]);
|
||||
g.direct_peer1_id = peers[0].user_id; g.direct_peer2_id = peers[1].user_id;
|
||||
}
|
||||
}
|
||||
const otherUserId = g.direct_peer1_id === userId ? g.direct_peer2_id : g.direct_peer1_id;
|
||||
if (otherUserId) {
|
||||
const other = await queryOne(req.schema, 'SELECT display_name, name, avatar FROM users WHERE id=$1', [otherUserId]);
|
||||
if (other) {
|
||||
g.peer_id = otherUserId; g.peer_real_name = other.name;
|
||||
g.peer_display_name = other.display_name || null; g.peer_avatar = other.avatar || null;
|
||||
g.name = other.display_name || other.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
const custom = await queryOne(req.schema, 'SELECT name FROM user_group_names WHERE user_id=$1 AND group_id=$2', [userId, g.id]);
|
||||
if (custom) { g.owner_name_original = g.name; g.name = custom.name; }
|
||||
return g;
|
||||
}));
|
||||
|
||||
res.json({ publicGroups, privateGroups });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// POST create group
|
||||
router.post('/', authMiddleware, async (req, res) => {
|
||||
const { name, type, memberIds, isReadonly, isDirect } = req.body;
|
||||
try {
|
||||
if (type === 'public' && req.user.role !== 'admin')
|
||||
return res.status(403).json({ error: 'Only admins can create public groups' });
|
||||
|
||||
// Direct message
|
||||
if (isDirect && memberIds?.length === 1) {
|
||||
const otherUserId = memberIds[0], userId = req.user.id;
|
||||
const existing = await queryOne(req.schema, `
|
||||
SELECT g.id FROM groups g
|
||||
JOIN group_members gm1 ON gm1.group_id=g.id AND gm1.user_id=$1
|
||||
JOIN group_members gm2 ON gm2.group_id=g.id AND gm2.user_id=$2
|
||||
WHERE g.is_direct=TRUE LIMIT 1
|
||||
`, [userId, otherUserId]);
|
||||
if (existing) {
|
||||
await exec(req.schema, "UPDATE groups SET is_readonly=FALSE, owner_id=NULL, updated_at=NOW() WHERE id=$1", [existing.id]);
|
||||
await exec(req.schema, 'INSERT INTO group_members (group_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [existing.id, userId]);
|
||||
return res.json({ group: await queryOne(req.schema, 'SELECT * FROM groups WHERE id=$1', [existing.id]) });
|
||||
}
|
||||
const otherUser = await queryOne(req.schema, 'SELECT name, display_name FROM users WHERE id=$1', [otherUserId]);
|
||||
const dmName = (otherUser?.display_name || otherUser?.name) + ' ↔ ' + (req.user.display_name || req.user.name);
|
||||
const r = await queryResult(req.schema,
|
||||
"INSERT INTO groups (name,type,owner_id,is_readonly,is_direct,direct_peer1_id,direct_peer2_id) VALUES ($1,'private',NULL,FALSE,TRUE,$2,$3) RETURNING id",
|
||||
[dmName, userId, otherUserId]
|
||||
);
|
||||
const groupId = r.rows[0].id;
|
||||
await exec(req.schema, 'INSERT INTO group_members (group_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [groupId, userId]);
|
||||
await exec(req.schema, 'INSERT INTO group_members (group_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [groupId, otherUserId]);
|
||||
await emitGroupNew(req.schema, io, groupId);
|
||||
return res.json({ group: await queryOne(req.schema, 'SELECT * FROM groups WHERE id=$1', [groupId]) });
|
||||
}
|
||||
|
||||
// Get other user's display name for the group name (stored internally, overridden per-user on fetch)
|
||||
const otherUser = db.prepare('SELECT name, display_name FROM users WHERE id = ?').get(otherUserId);
|
||||
const dmName = (otherUser?.display_name || otherUser?.name) + ' ↔ ' + (req.user.display_name || req.user.name);
|
||||
|
||||
const result = db.prepare(`
|
||||
INSERT INTO groups (name, type, owner_id, is_readonly, is_direct, direct_peer1_id, direct_peer2_id)
|
||||
VALUES (?, 'private', NULL, 0, 1, ?, ?)
|
||||
`).run(dmName, userId, otherUserId);
|
||||
|
||||
const groupId = result.lastInsertRowid;
|
||||
db.prepare('INSERT OR IGNORE INTO group_members (group_id, user_id) VALUES (?, ?)').run(groupId, userId);
|
||||
db.prepare('INSERT OR IGNORE INTO group_members (group_id, user_id) VALUES (?, ?)').run(groupId, otherUserId);
|
||||
|
||||
const group = db.prepare('SELECT * FROM groups WHERE id = ?').get(groupId);
|
||||
|
||||
// Notify both users via socket
|
||||
emitGroupNew(io, groupId);
|
||||
|
||||
return res.json({ group });
|
||||
}
|
||||
|
||||
// For private groups: check if exact same set of members already exists in a group
|
||||
if ((type === 'private' || !type) && !isDirect && memberIds && memberIds.length > 0) {
|
||||
const allMemberIds = [...new Set([req.user.id, ...memberIds])].sort((a, b) => a - b);
|
||||
const count = allMemberIds.length;
|
||||
|
||||
// Find all private non-direct groups where the creator is a member
|
||||
const candidates = db.prepare(`
|
||||
SELECT g.id FROM groups g
|
||||
JOIN group_members gm ON gm.group_id = g.id AND gm.user_id = ?
|
||||
WHERE g.type = 'private' AND g.is_direct = 0
|
||||
`).all(req.user.id);
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const members = db.prepare(
|
||||
'SELECT user_id FROM group_members WHERE group_id = ? ORDER BY user_id'
|
||||
).all(candidate.id).map(r => r.user_id);
|
||||
if (members.length === count &&
|
||||
members.every((id, i) => id === allMemberIds[i])) {
|
||||
// Exact duplicate found — return the existing group
|
||||
const group = db.prepare('SELECT * FROM groups WHERE id = ?').get(candidate.id);
|
||||
return res.json({ group, duplicate: true });
|
||||
// Check for duplicate private group
|
||||
if ((type === 'private' || !type) && !isDirect && memberIds?.length > 0) {
|
||||
const allMemberIds = [...new Set([req.user.id, ...memberIds])].sort((a,b) => a-b);
|
||||
const candidates = await query(req.schema,
|
||||
'SELECT g.id FROM groups g JOIN group_members gm ON gm.group_id=g.id AND gm.user_id=$1 WHERE g.type=\'private\' AND g.is_direct=FALSE',
|
||||
[req.user.id]
|
||||
);
|
||||
for (const c of candidates) {
|
||||
const members = (await query(req.schema, 'SELECT user_id FROM group_members WHERE group_id=$1 ORDER BY user_id', [c.id])).map(r => r.user_id);
|
||||
if (members.length === allMemberIds.length && members.every((id,i) => id === allMemberIds[i]))
|
||||
return res.json({ group: await queryOne(req.schema, 'SELECT * FROM groups WHERE id=$1', [c.id]), duplicate: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = db.prepare(`
|
||||
INSERT INTO groups (name, type, owner_id, is_readonly, is_direct)
|
||||
VALUES (?, ?, ?, ?, 0)
|
||||
`).run(name, type || 'private', req.user.id, isReadonly ? 1 : 0);
|
||||
|
||||
const groupId = result.lastInsertRowid;
|
||||
|
||||
if (type === 'public') {
|
||||
const allUsers = db.prepare("SELECT id FROM users WHERE status = 'active'").all();
|
||||
const insert = db.prepare('INSERT OR IGNORE INTO group_members (group_id, user_id) VALUES (?, ?)');
|
||||
for (const u of allUsers) insert.run(groupId, u.id);
|
||||
} else {
|
||||
db.prepare('INSERT OR IGNORE INTO group_members (group_id, user_id) VALUES (?, ?)').run(groupId, req.user.id);
|
||||
if (memberIds && memberIds.length > 0) {
|
||||
const insert = db.prepare('INSERT OR IGNORE INTO group_members (group_id, user_id) VALUES (?, ?)');
|
||||
for (const uid of memberIds) insert.run(groupId, uid);
|
||||
const r = await queryResult(req.schema,
|
||||
'INSERT INTO groups (name,type,owner_id,is_readonly,is_direct) VALUES ($1,$2,$3,$4,FALSE) RETURNING id',
|
||||
[name, type||'private', req.user.id, !!isReadonly]
|
||||
);
|
||||
const groupId = r.rows[0].id;
|
||||
if (type === 'public') {
|
||||
const allUsers = await query(req.schema, "SELECT id FROM users WHERE status='active'");
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
const group = db.prepare('SELECT * FROM groups WHERE id = ?').get(groupId);
|
||||
|
||||
// Notify all members via socket
|
||||
emitGroupNew(io, groupId);
|
||||
|
||||
res.json({ group });
|
||||
await emitGroupNew(req.schema, io, groupId);
|
||||
res.json({ group: await queryOne(req.schema, 'SELECT * FROM groups WHERE id=$1', [groupId]) });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// Rename group
|
||||
router.patch('/:id/rename', authMiddleware, (req, res) => {
|
||||
// PATCH rename
|
||||
router.patch('/:id/rename', authMiddleware, async (req, res) => {
|
||||
const { name } = req.body;
|
||||
const db = getDb();
|
||||
const group = db.prepare('SELECT * FROM groups WHERE id = ?').get(req.params.id);
|
||||
if (!group) return res.status(404).json({ error: 'Group not found' });
|
||||
if (group.is_default) return res.status(403).json({ error: 'Cannot rename default group' });
|
||||
if (group.is_direct) return res.status(403).json({ error: 'Cannot rename a direct message' });
|
||||
if (group.type === 'public' && req.user.role !== 'admin') return res.status(403).json({ error: 'Only admins can rename public groups' });
|
||||
if (group.type === 'private' && group.owner_id !== req.user.id && req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Only owner can rename private group' });
|
||||
}
|
||||
db.prepare("UPDATE groups SET name = ?, updated_at = datetime('now') WHERE id = ?").run(name, group.id);
|
||||
emitGroupUpdated(io, group.id);
|
||||
res.json({ success: true });
|
||||
try {
|
||||
const group = await queryOne(req.schema, 'SELECT * FROM groups WHERE id=$1', [req.params.id]);
|
||||
if (!group) return res.status(404).json({ error: 'Group not found' });
|
||||
if (group.is_default) return res.status(403).json({ error: 'Cannot rename default group' });
|
||||
if (group.is_direct) return res.status(403).json({ error: 'Cannot rename a direct message' });
|
||||
if (group.type === 'public' && req.user.role !== 'admin') return res.status(403).json({ error: 'Only admins can rename public groups' });
|
||||
if (group.type === 'private' && group.owner_id !== req.user.id && req.user.role !== 'admin') return res.status(403).json({ error: 'Only owner can rename' });
|
||||
await exec(req.schema, 'UPDATE groups SET name=$1, updated_at=NOW() WHERE id=$2', [name, group.id]);
|
||||
await emitGroupUpdated(req.schema, io, group.id);
|
||||
res.json({ success: true });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// Get group members
|
||||
router.get('/:id/members', authMiddleware, (req, res) => {
|
||||
const db = getDb();
|
||||
const members = db.prepare(`
|
||||
SELECT u.id, u.name, u.display_name, u.avatar, u.role, u.status
|
||||
FROM group_members gm
|
||||
JOIN users u ON gm.user_id = u.id
|
||||
WHERE gm.group_id = ?
|
||||
ORDER BY u.name ASC
|
||||
`).all(req.params.id);
|
||||
res.json({ members });
|
||||
// GET members
|
||||
router.get('/:id/members', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const members = await query(req.schema,
|
||||
'SELECT u.id,u.name,u.display_name,u.avatar,u.role,u.status FROM group_members gm JOIN users u ON gm.user_id=u.id WHERE gm.group_id=$1 ORDER BY u.name ASC',
|
||||
[req.params.id]
|
||||
);
|
||||
res.json({ members });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// Add member to private group
|
||||
router.post('/:id/members', authMiddleware, (req, res) => {
|
||||
// POST add member
|
||||
router.post('/:id/members', authMiddleware, async (req, res) => {
|
||||
const { userId } = req.body;
|
||||
const db = getDb();
|
||||
const group = db.prepare('SELECT * FROM groups WHERE id = ?').get(req.params.id);
|
||||
if (!group) return res.status(404).json({ error: 'Group not found' });
|
||||
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' });
|
||||
}
|
||||
db.prepare('INSERT OR IGNORE INTO group_members (group_id, user_id) VALUES (?, ?)').run(group.id, userId);
|
||||
|
||||
// Post a system message so all members see who was added
|
||||
const addedUser = db.prepare('SELECT name, display_name FROM users WHERE id = ?').get(userId);
|
||||
const addedName = addedUser?.display_name || addedUser?.name || 'Unknown';
|
||||
const sysResult = db.prepare(`
|
||||
INSERT INTO messages (group_id, user_id, content, type)
|
||||
VALUES (?, ?, ?, 'system')
|
||||
`).run(group.id, userId, `${addedName} has joined the conversation.`);
|
||||
const sysMsg = db.prepare(`
|
||||
SELECT m.*, u.name as user_name, u.display_name as user_display_name,
|
||||
u.avatar as user_avatar, u.role as user_role, u.status as user_status,
|
||||
u.hide_admin_tag as user_hide_admin_tag, u.about_me as user_about_me
|
||||
FROM messages m JOIN users u ON m.user_id = u.id WHERE m.id = ?
|
||||
`).get(sysResult.lastInsertRowid);
|
||||
sysMsg.reactions = [];
|
||||
io.to(`group:${group.id}`).emit('message:new', sysMsg);
|
||||
|
||||
// Join all of the added user's active sockets to the group room server-side,
|
||||
// so they receive messages immediately without needing a client round-trip
|
||||
io.in(`user:${userId}`).socketsJoin(`group:${group.id}`);
|
||||
// Notify the added user in real-time so their sidebar updates without a refresh
|
||||
io.to(`user:${userId}`).emit('group:new', { group });
|
||||
res.json({ success: true });
|
||||
try {
|
||||
const group = await queryOne(req.schema, 'SELECT * FROM groups WHERE id=$1', [req.params.id]);
|
||||
if (!group) return res.status(404).json({ error: 'Group not found' });
|
||||
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' });
|
||||
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';
|
||||
const mr = await queryResult(req.schema,
|
||||
"INSERT INTO messages (group_id,user_id,content,type) VALUES ($1,$2,$3,'system') RETURNING id",
|
||||
[group.id, userId, `${addedName} has joined the conversation.`]
|
||||
);
|
||||
const sysMsg = await queryOne(req.schema,
|
||||
'SELECT m.*,u.name AS user_name,u.display_name AS user_display_name,u.avatar AS user_avatar,u.role AS user_role,u.status AS user_status,u.hide_admin_tag AS user_hide_admin_tag,u.about_me AS user_about_me FROM messages m JOIN users u ON m.user_id=u.id WHERE m.id=$1',
|
||||
[mr.rows[0].id]
|
||||
);
|
||||
sysMsg.reactions = [];
|
||||
io.to(`group:${group.id}`).emit('message:new', sysMsg);
|
||||
io.in(`user:${userId}`).socketsJoin(`group:${group.id}`);
|
||||
io.to(`user:${userId}`).emit('group:new', { group });
|
||||
res.json({ success: true });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// Remove a member from a private group
|
||||
router.delete('/:id/members/:userId', authMiddleware, (req, res) => {
|
||||
const db = getDb();
|
||||
const group = db.prepare('SELECT * FROM groups WHERE id = ?').get(req.params.id);
|
||||
if (!group) return res.status(404).json({ error: 'Group not found' });
|
||||
if (group.type !== 'private') return res.status(400).json({ error: 'Cannot remove members from public groups' });
|
||||
if (group.owner_id !== req.user.id && req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Only owner or admin can remove members' });
|
||||
}
|
||||
const targetId = parseInt(req.params.userId);
|
||||
if (targetId === group.owner_id) return res.status(400).json({ error: 'Cannot remove the group owner' });
|
||||
|
||||
const removedUser = db.prepare('SELECT name, display_name FROM users WHERE id = ?').get(targetId);
|
||||
const removedName = removedUser?.display_name || removedUser?.name || 'Unknown';
|
||||
|
||||
db.prepare('DELETE FROM group_members WHERE group_id = ? AND user_id = ?').run(group.id, targetId);
|
||||
|
||||
// Post system message so remaining members see the removal notice
|
||||
const sysResult = db.prepare(`
|
||||
INSERT INTO messages (group_id, user_id, content, type)
|
||||
VALUES (?, ?, ?, 'system')
|
||||
`).run(group.id, targetId, `${removedName} has been removed from the conversation.`);
|
||||
const sysMsg = db.prepare(`
|
||||
SELECT m.*, u.name as user_name, u.display_name as user_display_name,
|
||||
u.avatar as user_avatar, u.role as user_role, u.status as user_status,
|
||||
u.hide_admin_tag as user_hide_admin_tag, u.about_me as user_about_me
|
||||
FROM messages m JOIN users u ON m.user_id = u.id WHERE m.id = ?
|
||||
`).get(sysResult.lastInsertRowid);
|
||||
sysMsg.reactions = [];
|
||||
io.to(`group:${group.id}`).emit('message:new', sysMsg);
|
||||
|
||||
// Remove the user from the socket room and update their sidebar
|
||||
io.in(`user:${targetId}`).socketsLeave(`group:${group.id}`);
|
||||
io.to(`user:${targetId}`).emit('group:deleted', { groupId: group.id });
|
||||
|
||||
res.json({ success: true });
|
||||
// DELETE remove member
|
||||
router.delete('/:id/members/:userId', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const group = await queryOne(req.schema, 'SELECT * FROM groups WHERE id=$1', [req.params.id]);
|
||||
if (!group) return res.status(404).json({ error: 'Group not found' });
|
||||
if (group.type !== 'private') return res.status(400).json({ error: 'Cannot remove members from public groups' });
|
||||
if (group.owner_id !== req.user.id && req.user.role !== 'admin') return res.status(403).json({ error: 'Only owner or admin can remove members' });
|
||||
const targetId = parseInt(req.params.userId);
|
||||
if (targetId === group.owner_id) return res.status(400).json({ error: 'Cannot remove the group owner' });
|
||||
const removedUser = await queryOne(req.schema, 'SELECT name,display_name FROM users WHERE id=$1', [targetId]);
|
||||
const removedName = removedUser?.display_name || removedUser?.name || 'Unknown';
|
||||
await exec(req.schema, 'DELETE FROM group_members WHERE group_id=$1 AND user_id=$2', [group.id, targetId]);
|
||||
const mr = await queryResult(req.schema,
|
||||
"INSERT INTO messages (group_id,user_id,content,type) VALUES ($1,$2,$3,'system') RETURNING id",
|
||||
[group.id, targetId, `${removedName} has been removed from the conversation.`]
|
||||
);
|
||||
const sysMsg = await queryOne(req.schema,
|
||||
'SELECT m.*,u.name AS user_name,u.display_name AS user_display_name,u.avatar AS user_avatar,u.role AS user_role,u.status AS user_status,u.hide_admin_tag AS user_hide_admin_tag,u.about_me AS user_about_me FROM messages m JOIN users u ON m.user_id=u.id WHERE m.id=$1',
|
||||
[mr.rows[0].id]
|
||||
);
|
||||
sysMsg.reactions = [];
|
||||
io.to(`group:${group.id}`).emit('message:new', sysMsg);
|
||||
io.in(`user:${targetId}`).socketsLeave(`group:${group.id}`);
|
||||
io.to(`user:${targetId}`).emit('group:deleted', { groupId: group.id });
|
||||
res.json({ success: true });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// Leave private group
|
||||
router.delete('/:id/leave', authMiddleware, (req, res) => {
|
||||
const db = getDb();
|
||||
const group = db.prepare('SELECT * FROM groups WHERE id = ?').get(req.params.id);
|
||||
if (!group) return res.status(404).json({ error: 'Group not found' });
|
||||
if (group.type === 'public') return res.status(400).json({ error: 'Cannot leave public groups' });
|
||||
if (group.is_managed && req.user.role !== 'admin') return res.status(403).json({ error: 'This group is managed by an administrator. Contact an admin to be removed.' });
|
||||
|
||||
const userId = req.user.id;
|
||||
const leaverName = req.user.display_name || req.user.name;
|
||||
|
||||
db.prepare('DELETE FROM group_members WHERE group_id = ? AND user_id = ?').run(group.id, userId);
|
||||
|
||||
// Post a system message so remaining members see the leave notice
|
||||
const sysResult = db.prepare(`
|
||||
INSERT INTO messages (group_id, user_id, content, type)
|
||||
VALUES (?, ?, ?, 'system')
|
||||
`).run(group.id, userId, `${leaverName} has left the conversation.`);
|
||||
|
||||
const sysMsg = db.prepare(`
|
||||
SELECT m.*, u.name as user_name, u.display_name as user_display_name,
|
||||
u.avatar as user_avatar, u.role as user_role, u.status as user_status,
|
||||
u.hide_admin_tag as user_hide_admin_tag, u.about_me as user_about_me
|
||||
FROM messages m JOIN users u ON m.user_id = u.id WHERE m.id = ?
|
||||
`).get(sysResult.lastInsertRowid);
|
||||
sysMsg.reactions = [];
|
||||
|
||||
// Broadcast to remaining members in the group room
|
||||
io.to(`group:${group.id}`).emit('message:new', sysMsg);
|
||||
|
||||
// Always remove leaver from socket room and their sidebar
|
||||
io.in(`user:${userId}`).socketsLeave(`group:${group.id}`);
|
||||
io.to(`user:${userId}`).emit('group:deleted', { groupId: group.id });
|
||||
|
||||
if (group.is_direct) {
|
||||
// Make remaining user owner so they can still manage the conversation
|
||||
const remaining = db.prepare('SELECT user_id FROM group_members WHERE group_id = ? LIMIT 1').get(group.id);
|
||||
if (remaining) {
|
||||
db.prepare("UPDATE groups SET owner_id = ?, updated_at = datetime('now') WHERE id = ?")
|
||||
.run(remaining.user_id, group.id);
|
||||
// DELETE leave
|
||||
router.delete('/:id/leave', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const group = await queryOne(req.schema, 'SELECT * FROM groups WHERE id=$1', [req.params.id]);
|
||||
if (!group) return res.status(404).json({ error: 'Group not found' });
|
||||
if (group.type === 'public') return res.status(400).json({ error: 'Cannot leave public groups' });
|
||||
if (group.is_managed && req.user.role !== 'admin') return res.status(403).json({ error: 'This group is managed by an administrator.' });
|
||||
const userId = req.user.id;
|
||||
const leaverName = req.user.display_name || req.user.name;
|
||||
await exec(req.schema, 'DELETE FROM group_members WHERE group_id=$1 AND user_id=$2', [group.id, userId]);
|
||||
const mr = await queryResult(req.schema,
|
||||
"INSERT INTO messages (group_id,user_id,content,type) VALUES ($1,$2,$3,'system') RETURNING id",
|
||||
[group.id, userId, `${leaverName} has left the conversation.`]
|
||||
);
|
||||
const sysMsg = await queryOne(req.schema,
|
||||
'SELECT m.*,u.name AS user_name,u.display_name AS user_display_name,u.avatar AS user_avatar,u.role AS user_role,u.status AS user_status,u.hide_admin_tag AS user_hide_admin_tag,u.about_me AS user_about_me FROM messages m JOIN users u ON m.user_id=u.id WHERE m.id=$1',
|
||||
[mr.rows[0].id]
|
||||
);
|
||||
sysMsg.reactions = [];
|
||||
io.to(`group:${group.id}`).emit('message:new', sysMsg);
|
||||
io.in(`user:${userId}`).socketsLeave(`group:${group.id}`);
|
||||
io.to(`user:${userId}`).emit('group:deleted', { groupId: group.id });
|
||||
if (group.is_direct) {
|
||||
const remaining = await queryOne(req.schema, 'SELECT user_id FROM group_members WHERE group_id=$1 LIMIT 1', [group.id]);
|
||||
if (remaining) await exec(req.schema, 'UPDATE groups SET owner_id=$1, updated_at=NOW() WHERE id=$2', [remaining.user_id, group.id]);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
res.json({ success: true });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// Admin take ownership
|
||||
router.post('/:id/take-ownership', authMiddleware, adminMiddleware, (req, res) => {
|
||||
const db = getDb();
|
||||
const group = db.prepare('SELECT * FROM groups WHERE id = ?').get(req.params.id);
|
||||
if (group?.is_managed) return res.status(403).json({ error: 'Managed groups are administered via the Group Manager.' });
|
||||
db.prepare("UPDATE groups SET owner_id = ?, updated_at = datetime('now') WHERE id = ?").run(req.user.id, req.params.id);
|
||||
db.prepare('INSERT OR IGNORE INTO group_members (group_id, user_id) VALUES (?, ?)').run(req.params.id, req.user.id);
|
||||
res.json({ success: true });
|
||||
// POST take-ownership
|
||||
router.post('/:id/take-ownership', authMiddleware, adminMiddleware, async (req, res) => {
|
||||
try {
|
||||
const group = await queryOne(req.schema, 'SELECT * FROM groups WHERE id=$1', [req.params.id]);
|
||||
if (group?.is_managed) return res.status(403).json({ error: 'Managed groups are administered via the Group Manager.' });
|
||||
await exec(req.schema, 'UPDATE groups SET owner_id=$1, updated_at=NOW() WHERE id=$2', [req.user.id, req.params.id]);
|
||||
await exec(req.schema, 'INSERT INTO group_members (group_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [req.params.id, req.user.id]);
|
||||
res.json({ success: true });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// Delete group
|
||||
router.delete('/:id', authMiddleware, (req, res) => {
|
||||
const db = getDb();
|
||||
const group = db.prepare('SELECT * FROM groups WHERE id = ?').get(req.params.id);
|
||||
if (!group) return res.status(404).json({ error: 'Group not found' });
|
||||
if (group.is_default) return res.status(403).json({ error: 'Cannot delete default group' });
|
||||
if (group.type === 'public' && req.user.role !== 'admin') return res.status(403).json({ error: 'Only admins can delete public groups' });
|
||||
if (group.type === 'private' && group.owner_id !== req.user.id && req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Only owner or admin can delete private groups' });
|
||||
}
|
||||
|
||||
// Collect members before deleting
|
||||
const members = db.prepare('SELECT user_id FROM group_members WHERE group_id = ?').all(group.id).map(m => m.user_id);
|
||||
// Add all active users for public groups
|
||||
if (group.type === 'public') {
|
||||
const all = db.prepare("SELECT id FROM users WHERE status = 'active'").all();
|
||||
all.forEach(u => { if (!members.includes(u.id)) members.push(u.id); });
|
||||
}
|
||||
|
||||
// Collect all image files for this group before deleting
|
||||
const imageMessages = db.prepare("SELECT image_url FROM messages WHERE group_id = ? AND image_url IS NOT NULL").all(group.id);
|
||||
|
||||
db.prepare('DELETE FROM groups WHERE id = ?').run(group.id);
|
||||
|
||||
// Delete image files from disk after DB delete
|
||||
for (const msg of imageMessages) deleteImageFile(msg.image_url);
|
||||
|
||||
// Notify all affected users
|
||||
emitGroupDeleted(io, group.id, members);
|
||||
|
||||
res.json({ success: true });
|
||||
// DELETE group
|
||||
router.delete('/:id', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const group = await queryOne(req.schema, 'SELECT * FROM groups WHERE id=$1', [req.params.id]);
|
||||
if (!group) return res.status(404).json({ error: 'Group not found' });
|
||||
if (group.is_default) return res.status(403).json({ error: 'Cannot delete default group' });
|
||||
if (group.type === 'public' && req.user.role !== 'admin') return res.status(403).json({ error: 'Only admins can delete public groups' });
|
||||
if (group.type === 'private' && group.owner_id !== req.user.id && req.user.role !== 'admin') return res.status(403).json({ error: 'Only owner or admin can delete' });
|
||||
const members = (await query(req.schema, 'SELECT user_id FROM group_members WHERE group_id=$1', [group.id])).map(m => m.user_id);
|
||||
if (group.type === 'public') {
|
||||
const all = await query(req.schema, "SELECT id FROM users WHERE status='active'");
|
||||
for (const u of all) if (!members.includes(u.id)) members.push(u.id);
|
||||
}
|
||||
const imageMessages = await query(req.schema, 'SELECT image_url FROM messages WHERE group_id=$1 AND image_url IS NOT NULL', [group.id]);
|
||||
await exec(req.schema, 'DELETE FROM groups WHERE id=$1', [group.id]);
|
||||
for (const msg of imageMessages) deleteImageFile(msg.image_url);
|
||||
for (const uid of members) io.to(`user:${uid}`).emit('group:deleted', { groupId: group.id });
|
||||
res.json({ success: true });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
|
||||
// Set or update user's custom name for a group
|
||||
router.patch('/:id/custom-name', authMiddleware, (req, res) => {
|
||||
const db = getDb();
|
||||
const groupId = parseInt(req.params.id);
|
||||
const userId = req.user.id;
|
||||
// PATCH custom-name
|
||||
router.patch('/:id/custom-name', authMiddleware, async (req, res) => {
|
||||
const { name } = req.body;
|
||||
|
||||
if (!name || !name.trim()) {
|
||||
// Empty name = remove custom name (revert to owner name)
|
||||
db.prepare('DELETE FROM user_group_names WHERE user_id = ? AND group_id = ?').run(userId, groupId);
|
||||
return res.json({ success: true, name: null });
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO user_group_names (user_id, group_id, name)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(user_id, group_id) DO UPDATE SET name = excluded.name
|
||||
`).run(userId, groupId, name.trim());
|
||||
|
||||
res.json({ success: true, name: name.trim() });
|
||||
const groupId = parseInt(req.params.id), userId = req.user.id;
|
||||
try {
|
||||
if (!name?.trim()) {
|
||||
await exec(req.schema, 'DELETE FROM user_group_names WHERE user_id=$1 AND group_id=$2', [userId, groupId]);
|
||||
return res.json({ success: true, name: null });
|
||||
}
|
||||
await exec(req.schema,
|
||||
'INSERT INTO user_group_names (user_id,group_id,name) VALUES ($1,$2,$3) ON CONFLICT (user_id,group_id) DO UPDATE SET name=EXCLUDED.name',
|
||||
[userId, groupId, name.trim()]
|
||||
);
|
||||
res.json({ success: true, name: name.trim() });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
return router;
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user