feat: group chat owner protection — prevent removing creator, transfer ownership before leaving

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 11:47:18 +03:00
parent 9d12e157b1
commit 18a6359adb
4 changed files with 99 additions and 17 deletions

View File

@@ -80,7 +80,7 @@ const chatRoutes: FastifyPluginAsync = async (fastify) => {
const userId = request.user.sub
const { rows } = await db.query(
`SELECT r.id, r.type, r.name, r.avatar_url,
`SELECT r.id, r.type, r.name, r.avatar_url, r.created_by,
(SELECT COUNT(*) FROM chat_messages m
WHERE m.room_id = r.id AND m.deleted_at IS NULL
AND m.created_at > COALESCE(
@@ -374,8 +374,8 @@ const chatRoutes: FastifyPluginAsync = async (fastify) => {
const allMembers = [...new Set([userId, ...memberIds])]
const { rows: [room] } = await db.query(
`INSERT INTO chat_rooms (hotel_id, type, name) VALUES ($1, 'group', $2) RETURNING id`,
[hotelId, name.trim()],
`INSERT INTO chat_rooms (hotel_id, type, name, created_by) VALUES ($1, 'group', $2, $3) RETURNING id`,
[hotelId, name.trim(), userId],
)
const memberValues = allMembers.map((_, i) => `($1, $${i + 2})`).join(', ')
await db.query(
@@ -388,7 +388,7 @@ const chatRoutes: FastifyPluginAsync = async (fastify) => {
// ── PATCH group room (rename / add/remove members) ────────────────────────
fastify.patch<RoomParam & { Body: { name?: string; avatarUrl?: string; avatar_url?: string; addMemberIds?: string[]; add_member_ids?: string[]; removeMemberIds?: string[]; remove_member_ids?: string[] } }>(
fastify.patch<RoomParam & { Body: { name?: string; avatarUrl?: string; avatar_url?: string; addMemberIds?: string[]; add_member_ids?: string[]; removeMemberIds?: string[]; remove_member_ids?: string[]; transferOwnerTo?: string; transfer_owner_to?: string } }>(
'/api/hotels/:slug/chat/rooms/:roomId/group',
{ onRequest: [fastify.authenticate] },
async (request, reply) => {
@@ -403,6 +403,25 @@ const chatRoutes: FastifyPluginAsync = async (fastify) => {
: Array.isArray(b.addMemberIds) ? b.addMemberIds as string[] : []
const removeMemberIds: string[] = Array.isArray(b.remove_member_ids) ? b.remove_member_ids as string[]
: Array.isArray(b.removeMemberIds) ? b.removeMemberIds as string[] : []
const transferOwnerTo = (b.transfer_owner_to ?? b.transferOwnerTo) as string | undefined
// Fetch current room to get created_by
const roomRes = await db.query('SELECT created_by FROM chat_rooms WHERE id = $1', [roomId])
const createdBy: string | null = roomRes.rows[0]?.created_by ?? null
// Block removal of the creator
if (createdBy && removeMemberIds.includes(createdBy)) {
return reply.code(403).send({ error: 'Cannot remove the group creator' })
}
// Transfer ownership (only current owner or admin can do this)
if (transferOwnerTo) {
const userId = request.user.sub
if (createdBy !== userId && request.user.role !== 'admin') {
return reply.code(403).send({ error: 'Only the group creator can transfer ownership' })
}
await db.query('UPDATE chat_rooms SET created_by = $1 WHERE id = $2', [transferOwnerTo, roomId])
}
if (name) {
await db.query('UPDATE chat_rooms SET name = $1 WHERE id = $2', [name.trim(), roomId])