Files
WatchLink/src/lib/user-actions.ts

114 lines
3.1 KiB
TypeScript
Raw Normal View History

2026-05-15 03:11:41 +02:00
"use server";
import { compare, hash } from "bcryptjs";
import { redirect } from "next/navigation";
2026-05-15 20:13:29 +02:00
import { Prisma, type User } from "@prisma/client";
2026-05-15 03:11:41 +02:00
import { prisma } from "./prisma";
2026-05-15 20:13:29 +02:00
import { clearSession, setSession } from "./session";
import { getAppSettings } from "./settings";
2026-05-15 03:11:41 +02:00
function normalizeUsername(value: FormDataEntryValue | null) {
return String(value || "")
.trim()
.toLowerCase()
.replace(/[^a-z0-9_-]/g, "");
}
export async function registerUser(formData: FormData) {
const settings = await getAppSettings();
if (settings.registrationMode === "DISABLED") {
redirect("/register?error=closed");
}
2026-05-15 03:11:41 +02:00
const username = normalizeUsername(formData.get("username"));
const password = String(formData.get("password") || "");
const inviteCode = String(formData.get("inviteCode") || "").trim();
2026-05-15 03:11:41 +02:00
if (!username || password.length < 10) {
2026-05-15 20:13:29 +02:00
redirect("/register?error=invalid");
2026-05-15 03:11:41 +02:00
}
const invite =
settings.registrationMode === "INVITE_ONLY"
? await prisma.invite.findUnique({ where: { code: inviteCode } })
: null;
if (
settings.registrationMode === "INVITE_ONLY" &&
(!invite || invite.status !== "ACTIVE" || (invite.expiresAt && invite.expiresAt.getTime() < Date.now()))
) {
redirect("/register?error=invite");
}
2026-05-15 03:11:41 +02:00
const passwordHash = await hash(password, 12);
2026-05-15 20:13:29 +02:00
let user: User;
try {
user = await prisma.$transaction(async (tx) => {
const created = await tx.user.create({
data: {
username,
displayName: username,
passwordHash,
ownedRooms: {
create: {
slug: `@${username}`,
name: `${username}'s room`,
isPersonal: true,
visibility: "FRIENDS"
}
2026-05-15 20:13:29 +02:00
}
2026-05-15 03:11:41 +02:00
}
});
if (invite) {
await tx.invite.update({
where: { id: invite.id },
data: { status: "USED", usedById: created.id, usedAt: new Date() }
});
if (invite.roomId) {
await tx.roomMember.upsert({
where: { roomId_userId: { roomId: invite.roomId, userId: created.id } },
update: {},
create: { roomId: invite.roomId, userId: created.id }
});
}
2026-05-15 03:11:41 +02:00
}
return created;
2026-05-15 20:13:29 +02:00
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
redirect("/register?error=username");
2026-05-15 03:11:41 +02:00
}
2026-05-15 20:13:29 +02:00
throw error;
}
2026-05-15 03:11:41 +02:00
await setSession(user.id);
redirect("/dashboard");
}
export async function loginUser(formData: FormData) {
const username = normalizeUsername(formData.get("username"));
const password = String(formData.get("password") || "");
const user = await prisma.user.findUnique({ where: { username } });
if (!user) {
2026-05-15 20:13:29 +02:00
redirect("/login?error=credentials");
2026-05-15 03:11:41 +02:00
}
if (user.disabledAt) {
redirect("/login?error=disabled");
}
2026-05-15 03:11:41 +02:00
const ok = await compare(password, user.passwordHash);
if (!ok) {
2026-05-15 20:13:29 +02:00
redirect("/login?error=credentials");
2026-05-15 03:11:41 +02:00
}
await setSession(user.id);
redirect("/dashboard");
}
2026-05-15 20:13:29 +02:00
export async function logoutUser() {
await clearSession();
redirect("/login");
}