Initial WatchLink scaffold
This commit is contained in:
123
src/app/setup/page.tsx
Normal file
123
src/app/setup/page.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { hash } from "bcryptjs";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { SYSTEM_PERMISSIONS } from "@/lib/access";
|
||||
import { setSession } from "@/lib/session";
|
||||
|
||||
async function hasAdmin() {
|
||||
try {
|
||||
const admin = await prisma.userRole.findFirst({
|
||||
where: { role: { name: "admin" } },
|
||||
select: { userId: true }
|
||||
});
|
||||
return Boolean(admin);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createFirstAdmin(formData: FormData) {
|
||||
"use server";
|
||||
|
||||
const username = String(formData.get("username") || "").trim().toLowerCase();
|
||||
const password = String(formData.get("password") || "");
|
||||
|
||||
if (!username || password.length < 10) {
|
||||
throw new Error("Username is required and password must be at least 10 characters.");
|
||||
}
|
||||
|
||||
const existingAdmin = await prisma.userRole.findFirst({
|
||||
where: { role: { name: "admin" } },
|
||||
select: { userId: true }
|
||||
});
|
||||
|
||||
if (existingAdmin) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
const passwordHash = await hash(password, 12);
|
||||
|
||||
const user = await prisma.$transaction(async (tx) => {
|
||||
const permissions = await Promise.all(
|
||||
SYSTEM_PERMISSIONS.map((key) =>
|
||||
tx.permission.upsert({
|
||||
where: { key },
|
||||
update: {},
|
||||
create: { key, description: key }
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const adminRole = await tx.role.upsert({
|
||||
where: { name: "admin" },
|
||||
update: {},
|
||||
create: {
|
||||
name: "admin",
|
||||
description: "Full system administrator"
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
permissions.map((permission) =>
|
||||
tx.rolePermission.upsert({
|
||||
where: { roleId_permissionId: { roleId: adminRole.id, permissionId: permission.id } },
|
||||
update: {},
|
||||
create: { roleId: adminRole.id, permissionId: permission.id }
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const createdUser = await tx.user.create({
|
||||
data: {
|
||||
username,
|
||||
displayName: username,
|
||||
passwordHash
|
||||
}
|
||||
});
|
||||
|
||||
await tx.userRole.create({ data: { userId: createdUser.id, roleId: adminRole.id } });
|
||||
await tx.room.create({
|
||||
data: {
|
||||
slug: `@${username}`,
|
||||
name: `${username}'s room`,
|
||||
ownerId: createdUser.id,
|
||||
visibility: "FRIENDS"
|
||||
}
|
||||
});
|
||||
|
||||
return createdUser;
|
||||
});
|
||||
|
||||
await setSession(user.id);
|
||||
redirect("/dashboard");
|
||||
}
|
||||
|
||||
export default async function SetupPage() {
|
||||
if (await hasAdmin()) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="auth-page">
|
||||
<section className="auth-card">
|
||||
<div className="title-block" style={{ marginBottom: 18 }}>
|
||||
<h1>WatchLink first setup</h1>
|
||||
<p>Create the first admin account. This screen locks after setup.</p>
|
||||
</div>
|
||||
<form className="form" action={createFirstAdmin}>
|
||||
<label>
|
||||
Username
|
||||
<input className="input" name="username" autoComplete="username" required />
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
<input className="input" name="password" type="password" autoComplete="new-password" minLength={10} required />
|
||||
</label>
|
||||
<button className="button primary" type="submit">
|
||||
Create admin
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user