282 lines
8.6 KiB
TypeScript
282 lines
8.6 KiB
TypeScript
"use server";
|
|
|
|
import { randomBytes } from "node:crypto";
|
|
import { Prisma, RoomVisibility } from "@prisma/client";
|
|
import { revalidatePath } from "next/cache";
|
|
import { redirect } from "next/navigation";
|
|
import { prisma } from "./prisma";
|
|
import { requireCurrentUser, userIsAdmin } from "./session";
|
|
import { getAppSettings } from "./settings";
|
|
|
|
function normalizeSlug(value: string) {
|
|
return value
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9_-]+/g, "-")
|
|
.replace(/^-+|-+$/g, "")
|
|
.slice(0, 48);
|
|
}
|
|
|
|
export async function createRoom(formData: FormData) {
|
|
const user = await requireCurrentUser();
|
|
const settings = await getAppSettings();
|
|
const name = String(formData.get("name") || "").trim();
|
|
const visibility = String(formData.get("visibility") || settings.defaultRoomVisibility);
|
|
const baseSlug = normalizeSlug(name);
|
|
|
|
if (!name || !baseSlug) {
|
|
redirect("/rooms?error=invalid-room");
|
|
}
|
|
|
|
let slug = baseSlug;
|
|
let suffix = 2;
|
|
while (await prisma.room.findUnique({ where: { slug }, select: { id: true } })) {
|
|
slug = `${baseSlug}-${suffix}`;
|
|
suffix += 1;
|
|
}
|
|
|
|
const allowedVisibility: RoomVisibility[] = ["PUBLIC", "FRIENDS", "EXPLICIT", "ROLE_RESTRICTED"];
|
|
const roomVisibility = allowedVisibility.includes(visibility as RoomVisibility)
|
|
? (visibility as RoomVisibility)
|
|
: "FRIENDS";
|
|
|
|
const room = await prisma.room.create({
|
|
data: {
|
|
name,
|
|
slug,
|
|
ownerId: user.id,
|
|
visibility: roomVisibility
|
|
},
|
|
select: { slug: true }
|
|
});
|
|
|
|
revalidatePath("/rooms");
|
|
revalidatePath("/dashboard");
|
|
redirect(`/rooms/${encodeURIComponent(room.slug)}`);
|
|
}
|
|
|
|
export async function deleteRoom(formData: FormData) {
|
|
const user = await requireCurrentUser();
|
|
const roomId = String(formData.get("roomId") || "");
|
|
if (!roomId) return;
|
|
|
|
const room = await prisma.room.findUnique({
|
|
where: { id: roomId },
|
|
include: { members: { where: { userId: user.id }, select: { canManage: true } } }
|
|
});
|
|
|
|
if (!room || room.isPersonal || !canManageRoom(user, room)) return;
|
|
|
|
await prisma.$transaction([
|
|
prisma.auditEvent.create({
|
|
data: {
|
|
actorId: user.id,
|
|
roomId: room.id,
|
|
action: "room.delete",
|
|
metadata: { slug: room.slug, name: room.name }
|
|
}
|
|
}),
|
|
prisma.room.delete({ where: { id: room.id } })
|
|
]);
|
|
|
|
revalidatePath("/rooms");
|
|
revalidatePath("/dashboard");
|
|
redirect("/rooms?deleted=1");
|
|
}
|
|
|
|
export async function resetPersonalRoom(formData: FormData) {
|
|
const user = await requireCurrentUser();
|
|
const roomId = String(formData.get("roomId") || "");
|
|
if (!roomId) return;
|
|
|
|
const room = await prisma.room.findUnique({
|
|
where: { id: roomId },
|
|
include: { members: { where: { userId: user.id }, select: { canManage: true } } }
|
|
});
|
|
|
|
if (!room || !room.isPersonal || !canManageRoom(user, room)) return;
|
|
|
|
await prisma.$transaction([
|
|
prisma.mediaSource.deleteMany({ where: { roomId: room.id } }),
|
|
prisma.roomMessage.deleteMany({ where: { roomId: room.id } }),
|
|
prisma.room.update({ where: { id: room.id }, data: { currentState: Prisma.JsonNull } }),
|
|
prisma.auditEvent.create({
|
|
data: {
|
|
actorId: user.id,
|
|
roomId: room.id,
|
|
action: "room.reset",
|
|
metadata: { slug: room.slug }
|
|
}
|
|
})
|
|
]);
|
|
|
|
revalidateRoom(room.slug);
|
|
redirect(`/rooms/${encodeURIComponent(room.slug)}?rail=Settings&saved=1`);
|
|
}
|
|
|
|
export async function updateRoomSettings(formData: FormData) {
|
|
const user = await requireCurrentUser();
|
|
const roomId = String(formData.get("roomId") || "");
|
|
const name = String(formData.get("name") || "").trim().slice(0, 80);
|
|
const visibility = String(formData.get("visibility") || "FRIENDS");
|
|
|
|
const room = await prisma.room.findUnique({
|
|
where: { id: roomId },
|
|
include: { members: { where: { userId: user.id }, select: { canManage: true } } }
|
|
});
|
|
|
|
if (!room || !canManageRoom(user, room) || !name) return;
|
|
|
|
const allowedVisibility: RoomVisibility[] = ["PUBLIC", "FRIENDS", "EXPLICIT", "ROLE_RESTRICTED"];
|
|
const roomVisibility = allowedVisibility.includes(visibility as RoomVisibility)
|
|
? (visibility as RoomVisibility)
|
|
: room.visibility;
|
|
|
|
await prisma.room.update({
|
|
where: { id: room.id },
|
|
data: {
|
|
name,
|
|
visibility: roomVisibility
|
|
}
|
|
});
|
|
|
|
revalidateRoom(room.slug);
|
|
redirect(`/rooms/${encodeURIComponent(room.slug)}?rail=Settings&saved=1`);
|
|
}
|
|
|
|
export async function addRoomMember(formData: FormData) {
|
|
const user = await requireCurrentUser();
|
|
const roomId = String(formData.get("roomId") || "");
|
|
const username = String(formData.get("username") || "").trim().toLowerCase();
|
|
const canManage = formData.get("canManage") === "on";
|
|
|
|
const room = await prisma.room.findUnique({
|
|
where: { id: roomId },
|
|
include: { members: { where: { userId: user.id }, select: { canManage: true } } }
|
|
});
|
|
|
|
if (!room || !canManageRoom(user, room) || !username) return;
|
|
|
|
const invited = await prisma.user.findUnique({ where: { username }, select: { id: true } });
|
|
if (!invited || invited.id === room.ownerId) {
|
|
redirect(`/rooms/${encodeURIComponent(room.slug)}?rail=Invite&error=user`);
|
|
}
|
|
|
|
await prisma.roomMember.upsert({
|
|
where: { roomId_userId: { roomId: room.id, userId: invited.id } },
|
|
update: { canManage },
|
|
create: { roomId: room.id, userId: invited.id, canManage }
|
|
});
|
|
|
|
revalidateRoom(room.slug);
|
|
redirect(`/rooms/${encodeURIComponent(room.slug)}?rail=Invite&saved=1`);
|
|
}
|
|
|
|
export async function createRoomInvite(formData: FormData) {
|
|
const user = await requireCurrentUser();
|
|
const roomId = String(formData.get("roomId") || "");
|
|
const expiresDays = Number(formData.get("expiresDays") || 0);
|
|
if (!roomId) return;
|
|
|
|
const room = await prisma.room.findUnique({
|
|
where: { id: roomId },
|
|
include: { members: { where: { userId: user.id }, select: { canManage: true } } }
|
|
});
|
|
|
|
if (!room || !canManageRoom(user, room)) return;
|
|
|
|
const expiresAt = Number.isFinite(expiresDays) && expiresDays > 0 ? new Date(Date.now() + expiresDays * 24 * 60 * 60 * 1000) : null;
|
|
const invite = await prisma.invite.create({
|
|
data: {
|
|
code: randomBytes(12).toString("base64url"),
|
|
creatorId: user.id,
|
|
roomId: room.id,
|
|
expiresAt
|
|
}
|
|
});
|
|
|
|
await prisma.auditEvent.create({
|
|
data: {
|
|
actorId: user.id,
|
|
roomId: room.id,
|
|
action: "room.invite.create",
|
|
metadata: { inviteId: invite.id, expiresAt: expiresAt?.toISOString() || null }
|
|
}
|
|
});
|
|
|
|
revalidateRoom(room.slug);
|
|
redirect(`/rooms/${encodeURIComponent(room.slug)}?rail=Invite&saved=1`);
|
|
}
|
|
|
|
export async function revokeRoomInvite(formData: FormData) {
|
|
const user = await requireCurrentUser();
|
|
const inviteId = String(formData.get("inviteId") || "");
|
|
if (!inviteId) return;
|
|
|
|
const invite = await prisma.invite.findUnique({
|
|
where: { id: inviteId },
|
|
include: {
|
|
room: {
|
|
include: { members: { where: { userId: user.id }, select: { canManage: true } } }
|
|
}
|
|
}
|
|
});
|
|
|
|
if (!invite?.room || !canManageRoom(user, invite.room)) return;
|
|
|
|
await prisma.$transaction([
|
|
prisma.invite.update({ where: { id: invite.id }, data: { status: "REVOKED" } }),
|
|
prisma.auditEvent.create({
|
|
data: {
|
|
actorId: user.id,
|
|
roomId: invite.room.id,
|
|
action: "room.invite.revoke",
|
|
metadata: { inviteId: invite.id }
|
|
}
|
|
})
|
|
]);
|
|
|
|
revalidateRoom(invite.room.slug);
|
|
redirect(`/rooms/${encodeURIComponent(invite.room.slug)}?rail=Invite&saved=1`);
|
|
}
|
|
|
|
export async function removeRoomMember(formData: FormData) {
|
|
const user = await requireCurrentUser();
|
|
const roomId = String(formData.get("roomId") || "");
|
|
const userId = String(formData.get("userId") || "");
|
|
if (!roomId || !userId) return;
|
|
|
|
const room = await prisma.room.findUnique({
|
|
where: { id: roomId },
|
|
include: { members: { where: { userId: user.id }, select: { canManage: true } } }
|
|
});
|
|
|
|
if (!room || !canManageRoom(user, room) || userId === room.ownerId) return;
|
|
|
|
await prisma.roomMember.deleteMany({ where: { roomId: room.id, userId } });
|
|
await prisma.auditEvent.create({
|
|
data: {
|
|
actorId: user.id,
|
|
roomId: room.id,
|
|
action: "room.member.remove",
|
|
metadata: { userId }
|
|
}
|
|
});
|
|
|
|
revalidateRoom(room.slug);
|
|
redirect(`/rooms/${encodeURIComponent(room.slug)}?rail=Invite&saved=1`);
|
|
}
|
|
|
|
function canManageRoom(
|
|
user: Awaited<ReturnType<typeof requireCurrentUser>>,
|
|
room: { ownerId: string | null; members: Array<{ canManage: boolean }> }
|
|
) {
|
|
return room.ownerId === user.id || userIsAdmin(user) || room.members.some((member) => member.canManage);
|
|
}
|
|
|
|
function revalidateRoom(slug: string) {
|
|
revalidatePath(`/rooms/${encodeURIComponent(slug)}`);
|
|
revalidatePath("/rooms");
|
|
revalidatePath("/dashboard");
|
|
}
|