import { Head } from "fresh/runtime";
import { define, type State } from "../utils.ts";
import type { FreshContext } from "fresh";
import {
createSession,
createUser,
findUserByEmail,
hashPassword,
sessionCookie,
verifyPassword,
} from "../lib/auth.ts";
import { clearDevSignOutCookie, devAutoLoginEnabled } from "@/lib/dev-auth.ts";
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function fail(ctx: FreshContext<State>, error: string) {
return ctx.render(<LoginPage error={error} />);
}
export const handler = define.handlers({
GET(ctx) {
if (ctx.state.user) return ctx.redirect("/");
return ctx.render(<LoginPage />);
},
async POST(ctx) {
const form = await ctx.req.formData();
const intent = String(form.get("intent") ?? "");
const email = String(form.get("email") ?? "").trim().toLowerCase();
const password = String(form.get("password") ?? "");
if (intent !== "signin" && intent !== "signup") {
return fail(ctx, "Invalid request.");
}
if (!EMAIL_RE.test(email)) {
return fail(ctx, "Please enter a valid email address.");
}
if (password.length < 8) {
return fail(ctx, "Password must be at least 8 characters long.");
}
let userId: number;
if (intent === "signup") {
const name = String(form.get("name") ?? "").trim();
try {
const user = await createUser(
email,
name === "" ? null : name,
hashPassword(password),
);
userId = user.id;
} catch (error) {
if (
error instanceof Error &&
error.message.includes("UNIQUE constraint failed")
) {
return fail(ctx, "An account with this email already exists.");
}
throw error;
}
} else {
const user = await findUserByEmail(email);
if (!user || !verifyPassword(password, user.password_hash)) {
return fail(ctx, "Invalid email or password.");
}
userId = user.id;
}
const token = await createSession(userId);
const headers = new Headers({ location: "/" });
headers.append("set-cookie", sessionCookie(ctx.req, token));
if (devAutoLoginEnabled()) {
headers.append("set-cookie", clearDevSignOutCookie());
}
return new Response(null, { status: 303, headers });
},
});
export function LoginPage({ error }: { error?: string }) {
return (
<div class="tapestry margin-vines min-h-screen flex flex-col items-center justify-center px-4 py-12">
<Head>
<title>Profectus — Sign in</title>
</Head>
<header class="relative z-1 text-center mb-10">
<h1 class="font-gothic text-5xl md:text-6xl tracking-[0.2em] text-accent">
Profectus
</h1>
<div class="fleuron-rule mt-4 w-72 mx-auto">
<span aria-hidden="true">☾ ✦ ☽</span>
</div>
<p class="mt-4 text-xs uppercase tracking-[0.35em] text-base-content/60">
A deck-building dungeon adventure
</p>
</header>
{error && (
<div
role="alert"
class="alert alert-error alert-soft relative z-1 mb-8 w-full max-w-2xl"
>
<span>{error}</span>
</div>
)}
<div class="relative z-1 grid w-full max-w-4xl gap-8 md:grid-cols-2">
<section class="arch-panel vellum card">
<div class="corner-fleurons card-body">
<h2 class="card-title font-heading tracking-[0.25em] uppercase justify-center">
Sign in
</h2>
<div class="fleuron-rule my-2">
<span aria-hidden="true">☾ ✧ ☽</span>
</div>
<form method="post" class="flex flex-col gap-4">
<input type="hidden" name="intent" value="signin" />
<fieldset class="fieldset">
<legend class="fieldset-legend">Email</legend>
<input
type="email"
name="email"
required
placeholder="you@example.com"
autocomplete="email"
class="input w-full"
/>
</fieldset>
<fieldset class="fieldset">
<legend class="fieldset-legend">Password</legend>
<input
type="password"
name="password"
required
placeholder="••••••••"
autocomplete="current-password"
class="input w-full"
/>
</fieldset>
<button type="submit" class="btn btn-primary iron-band mt-2">
Enter the city
</button>
</form>
</div>
</section>
<section class="arch-panel vellum card">
<div class="corner-fleurons card-body">
<h2 class="card-title font-heading tracking-[0.25em] uppercase justify-center">
Create account
</h2>
<div class="fleuron-rule my-2">
<span aria-hidden="true">☾ ✧ ☽</span>
</div>
<form method="post" class="flex flex-col gap-4">
<input type="hidden" name="intent" value="signup" />
<fieldset class="fieldset">
<legend class="fieldset-legend">Hero name</legend>
<input
type="text"
name="name"
placeholder="Optional"
autocomplete="nickname"
class="input w-full"
/>
</fieldset>
<fieldset class="fieldset">
<legend class="fieldset-legend">Email</legend>
<input
type="email"
name="email"
required
placeholder="you@example.com"
autocomplete="email"
class="input w-full"
/>
</fieldset>
<fieldset class="fieldset">
<legend class="fieldset-legend">Password</legend>
<input
type="password"
name="password"
required
minlength={8}
placeholder="At least 8 characters"
autocomplete="new-password"
class="input w-full"
/>
</fieldset>
<button type="submit" class="btn btn-outline iron-band mt-2">
Forge an account
</button>
</form>
</div>
</section>
</div>
</div>
);
}
export default LoginPage;