diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 575fe53..8410239 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -8,6 +8,8 @@ import { requireInitialSetup } from "@/lib/setup"; import Link from "next/link"; import { redirect } from "next/navigation"; +export const dynamic = "force-dynamic"; + export default async function AdminPage() { await requireInitialSetup(); const user = await requireCurrentUser(); diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index f453e85..768affb 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -7,6 +7,8 @@ import { requireCurrentUser, userIsAdmin } from "@/lib/session"; import { requireInitialSetup } from "@/lib/setup"; import Link from "next/link"; +export const dynamic = "force-dynamic"; + export default async function DashboardPage() { await requireInitialSetup(); const user = await requireCurrentUser(); diff --git a/src/app/friends/page.tsx b/src/app/friends/page.tsx index 4633e8d..7c01bd6 100644 --- a/src/app/friends/page.tsx +++ b/src/app/friends/page.tsx @@ -7,6 +7,8 @@ import { requireCurrentUser, userIsAdmin } from "@/lib/session"; import { requireInitialSetup } from "@/lib/setup"; import Link from "next/link"; +export const dynamic = "force-dynamic"; + export default async function FriendsPage() { await requireInitialSetup(); const user = await requireCurrentUser(); diff --git a/src/app/globals.css b/src/app/globals.css index f081c1e..b0c9c7b 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -416,6 +416,17 @@ select { font-weight: 700; } +.form-error { + border: 1px solid color-mix(in srgb, var(--danger) 45%, var(--border)); + border-radius: 8px; + background: color-mix(in srgb, var(--danger) 10%, var(--panel)); + color: var(--danger); + margin: 0 0 14px; + padding: 10px 12px; + font-size: 13px; + font-weight: 700; +} + @media (max-width: 1100px) { .room-layout { grid-template-columns: 1fr; diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 4f4b357..48357d8 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -3,6 +3,8 @@ import { loginUser } from "@/lib/user-actions"; import { hasAdminUser } from "@/lib/setup"; import { redirect } from "next/navigation"; +export const dynamic = "force-dynamic"; + export default async function LoginPage() { if (!(await hasAdminUser())) { redirect("/setup"); diff --git a/src/app/page.tsx b/src/app/page.tsx index 8c82da7..a6c6e9a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,8 @@ import { redirect } from "next/navigation"; import { hasAdminUser } from "@/lib/setup"; +export const dynamic = "force-dynamic"; + export default async function HomePage() { if (!(await hasAdminUser())) { redirect("/setup"); diff --git a/src/app/register/page.tsx b/src/app/register/page.tsx index 37776e4..9cab39a 100644 --- a/src/app/register/page.tsx +++ b/src/app/register/page.tsx @@ -3,6 +3,8 @@ import { registerUser } from "@/lib/user-actions"; import { hasAdminUser } from "@/lib/setup"; import { redirect } from "next/navigation"; +export const dynamic = "force-dynamic"; + export default async function RegisterPage() { if (!(await hasAdminUser())) { redirect("/setup"); diff --git a/src/app/rooms/[slug]/page.tsx b/src/app/rooms/[slug]/page.tsx index 2c8c906..8482c33 100644 --- a/src/app/rooms/[slug]/page.tsx +++ b/src/app/rooms/[slug]/page.tsx @@ -7,6 +7,8 @@ import { requireCurrentUser, userIsAdmin } from "@/lib/session"; import { requireInitialSetup } from "@/lib/setup"; import { redirect } from "next/navigation"; +export const dynamic = "force-dynamic"; + export default async function RoomPage({ params }: { params: Promise<{ slug: string }> }) { await requireInitialSetup(); const user = await requireCurrentUser(); diff --git a/src/app/setup/page.tsx b/src/app/setup/page.tsx index 3c2432e..aeddde4 100644 --- a/src/app/setup/page.tsx +++ b/src/app/setup/page.tsx @@ -7,14 +7,21 @@ import { hasAdminUser } from "@/lib/setup"; export const dynamic = "force-dynamic"; +function normalizeUsername(value: FormDataEntryValue | null) { + return String(value || "") + .trim() + .toLowerCase() + .replace(/[^a-z0-9_-]/g, ""); +} + async function createFirstAdmin(formData: FormData) { "use server"; - const username = String(formData.get("username") || "").trim().toLowerCase(); + const username = normalizeUsername(formData.get("username")); 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."); + redirect("/setup?error=invalid"); } const existingAdmin = await prisma.userRole.findFirst({ @@ -26,6 +33,15 @@ async function createFirstAdmin(formData: FormData) { redirect("/login"); } + const existingUser = await prisma.user.findUnique({ + where: { username }, + select: { id: true } + }); + + if (existingUser) { + redirect("/setup?error=username"); + } + const passwordHash = await hash(password, 12); const user = await prisma.$transaction(async (tx) => { @@ -83,10 +99,11 @@ async function createFirstAdmin(formData: FormData) { redirect("/dashboard"); } -export default async function SetupPage() { +export default async function SetupPage({ searchParams }: { searchParams: Promise<{ error?: string }> }) { if (await hasAdminUser()) { redirect("/login"); } + const { error } = await searchParams; return (
@@ -95,6 +112,7 @@ export default async function SetupPage() {

WatchLink first setup

Create the first admin account. This screen locks after setup.

+ {error ?

{setupErrorMessage(error)}

: null}
); } + +function setupErrorMessage(error: string) { + if (error === "username") return "This username already exists. Choose another username."; + return "Use a username with letters, numbers, dashes or underscores and a password with at least 10 characters."; +} diff --git a/src/lib/setup.ts b/src/lib/setup.ts index 01e7693..8e9f4f7 100644 --- a/src/lib/setup.ts +++ b/src/lib/setup.ts @@ -1,7 +1,9 @@ import { redirect } from "next/navigation"; +import { unstable_noStore as noStore } from "next/cache"; import { prisma } from "./prisma"; export async function hasAdminUser() { + noStore(); try { const admin = await prisma.userRole.findFirst({ where: { role: { name: "admin" } },