diff --git a/apps/journal/app/routes.ts b/apps/journal/app/routes.ts index 93980c1..eaf2f3f 100644 --- a/apps/journal/app/routes.ts +++ b/apps/journal/app/routes.ts @@ -6,5 +6,7 @@ export default [ route("auth/login", "routes/auth.login.tsx"), route("auth/verify", "routes/auth.verify.tsx"), route("auth/logout", "routes/auth.logout.tsx"), + route("api/auth/register", "routes/api.auth.register.ts"), + route("api/auth/login", "routes/api.auth.login.ts"), route("users/:username", "routes/users.$username.tsx"), ] satisfies RouteConfig; diff --git a/apps/journal/app/routes/api.auth.login.ts b/apps/journal/app/routes/api.auth.login.ts new file mode 100644 index 0000000..926d803 --- /dev/null +++ b/apps/journal/app/routes/api.auth.login.ts @@ -0,0 +1,32 @@ +import { data } from "react-router"; +import type { Route } from "./+types/api.auth.login"; +import { startAuthentication, finishAuthentication, createMagicToken, createSession } from "~/lib/auth.server"; + +export async function action({ request }: Route.ActionArgs) { + const body = await request.json(); + const { step, response, challenge, email } = body; + + try { + if (step === "start-passkey") { + const options = await startAuthentication(); + return data({ step: "challenge", options }); + } + + if (step === "finish-passkey") { + const userId = await finishAuthentication(response, challenge); + const cookie = await createSession(userId, request); + return data({ step: "done" }, { headers: { "Set-Cookie": cookie } }); + } + + if (step === "magic-link") { + const token = await createMagicToken(email); + const link = `${process.env.ORIGIN ?? "http://localhost:3000"}/auth/verify?token=${token}`; + console.log(`[Magic Link] ${email}: ${link}`); + return data({ step: "magic-link-sent" }); + } + + return data({ error: "Invalid step" }, { status: 400 }); + } catch (e) { + return data({ error: (e as Error).message }, { status: 400 }); + } +} diff --git a/apps/journal/app/routes/api.auth.register.ts b/apps/journal/app/routes/api.auth.register.ts new file mode 100644 index 0000000..ef1cb6f --- /dev/null +++ b/apps/journal/app/routes/api.auth.register.ts @@ -0,0 +1,25 @@ +import { data } from "react-router"; +import type { Route } from "./+types/api.auth.register"; +import { startRegistration, finishRegistration, createSession } from "~/lib/auth.server"; + +export async function action({ request }: Route.ActionArgs) { + const body = await request.json(); + const { step, email, username, response, challenge, userId } = body; + + try { + if (step === "start") { + const result = await startRegistration(email, username); + return data({ step: "challenge", options: result.options, userId: result.userId }); + } + + if (step === "finish") { + const newUserId = await finishRegistration(userId, email, username, response, challenge); + const cookie = await createSession(newUserId, request); + return data({ step: "done" }, { headers: { "Set-Cookie": cookie } }); + } + + return data({ error: "Invalid step" }, { status: 400 }); + } catch (e) { + return data({ error: (e as Error).message }, { status: 400 }); + } +} diff --git a/apps/journal/app/routes/auth.login.tsx b/apps/journal/app/routes/auth.login.tsx index 6be8e85..335febe 100644 --- a/apps/journal/app/routes/auth.login.tsx +++ b/apps/journal/app/routes/auth.login.tsx @@ -1,37 +1,4 @@ import { useState } from "react"; -import { data, redirect } from "react-router"; -import type { Route } from "./+types/auth.login"; -import { startAuthentication, finishAuthentication, createMagicToken, createSession } from "~/lib/auth.server"; - -export async function action({ request }: Route.ActionArgs) { - const formData = await request.json(); - const { step, response, challenge, email } = formData; - - try { - if (step === "start-passkey") { - const options = await startAuthentication(); - return data({ step: "challenge", options }); - } - - if (step === "finish-passkey") { - const userId = await finishAuthentication(response, challenge); - const cookie = await createSession(userId, request); - return redirect("/", { headers: { "Set-Cookie": cookie } }); - } - - if (step === "magic-link") { - const token = await createMagicToken(email); - // In production, send email. For dev, log the link. - const link = `${process.env.ORIGIN ?? "http://localhost:3000"}/auth/verify?token=${token}`; - console.log(`[Magic Link] ${email}: ${link}`); - return data({ step: "magic-link-sent" }); - } - - return data({ error: "Invalid step" }, { status: 400 }); - } catch (e) { - return data({ error: (e as Error).message }, { status: 400 }); - } -} export default function LoginPage() { const [mode, setMode] = useState<"passkey" | "magic-link">("passkey"); @@ -45,7 +12,7 @@ export default function LoginPage() { setLoading(true); try { - const startResp = await fetch("/auth/login", { + const startResp = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ step: "start-passkey" }), @@ -61,7 +28,7 @@ export default function LoginPage() { const { startAuthentication: startWebAuthn } = await import("@simplewebauthn/browser"); const webAuthnResp = await startWebAuthn(startData.options); - const finishResp = await fetch("/auth/login", { + const finishResp = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -71,13 +38,12 @@ export default function LoginPage() { }), }); - if (finishResp.redirected) { - window.location.href = finishResp.url; - return; - } - const finishData = await finishResp.json(); - if (finishData.error) setError(finishData.error); + if (finishData.error) { + setError(finishData.error); + } else if (finishData.step === "done") { + window.location.href = "/"; + } } catch (err) { setError((err as Error).message); } finally { @@ -91,7 +57,7 @@ export default function LoginPage() { setLoading(true); try { - const resp = await fetch("/auth/login", { + const resp = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ step: "magic-link", email }), diff --git a/apps/journal/app/routes/auth.register.tsx b/apps/journal/app/routes/auth.register.tsx index 6a2fda3..b3bbcf6 100644 --- a/apps/journal/app/routes/auth.register.tsx +++ b/apps/journal/app/routes/auth.register.tsx @@ -1,29 +1,4 @@ import { useState } from "react"; -import { data, redirect } from "react-router"; -import type { Route } from "./+types/auth.register"; -import { startRegistration, finishRegistration, createSession } from "~/lib/auth.server"; - -export async function action({ request }: Route.ActionArgs) { - const formData = await request.json(); - const { step, email, username, response, challenge, userId } = formData; - - try { - if (step === "start") { - const { options, userId } = await startRegistration(email, username); - return data({ step: "challenge", options, userId }); - } - - if (step === "finish") { - const newUserId = await finishRegistration(userId, email, username, response, challenge); - const cookie = await createSession(newUserId, request); - return redirect("/", { headers: { "Set-Cookie": cookie } }); - } - - return data({ error: "Invalid step" }, { status: 400 }); - } catch (e) { - return data({ error: (e as Error).message }, { status: 400 }); - } -} export default function RegisterPage() { const [email, setEmail] = useState(""); @@ -38,7 +13,7 @@ export default function RegisterPage() { try { // Step 1: Get registration options from server - const startResp = await fetch("/auth/register", { + const startResp = await fetch("/api/auth/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ step: "start", email, username }), @@ -56,7 +31,7 @@ export default function RegisterPage() { const webAuthnResp = await startWebAuthn(startData.options); // Step 3: Send response to server - const finishResp = await fetch("/auth/register", { + const finishResp = await fetch("/api/auth/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -69,14 +44,11 @@ export default function RegisterPage() { }), }); - if (finishResp.redirected) { - window.location.href = finishResp.url; - return; - } - const finishData = await finishResp.json(); if (finishData.error) { setError(finishData.error); + } else if (finishData.step === "done") { + window.location.href = "/"; } } catch (err) { setError((err as Error).message);