trails/apps/journal/app/routes/auth.register.tsx
Ullrich Schäfer 6950fd7de5
Fix username validation: strip invalid chars instead of pattern error
Browser's native pattern validation fired before preventDefault,
showing "The string did not match the expected pattern" error.
Replace HTML pattern with onChange that strips invalid characters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 21:51:34 +01:00

121 lines
3.9 KiB
TypeScript

import { useState } from "react";
export default function RegisterPage() {
const [email, setEmail] = useState("");
const [username, setUsername] = useState("");
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const handleRegister = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setLoading(true);
try {
// Step 1: Get registration options from server
const startResp = await fetch("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ step: "start", email, username }),
});
const startData = await startResp.json();
if (startData.error) {
setError(startData.error);
setLoading(false);
return;
}
// Step 2: Create passkey via browser
const { startRegistration: startWebAuthn } = await import("@simplewebauthn/browser");
const webAuthnResp = await startWebAuthn(startData.options);
// Step 3: Send response to server
const finishResp = await fetch("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
step: "finish",
email,
username,
response: webAuthnResp,
challenge: startData.options.challenge,
userId: startData.userId,
}),
});
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);
} finally {
setLoading(false);
}
};
return (
<div className="mx-auto max-w-md px-4 py-16">
<h1 className="text-2xl font-bold text-gray-900">Create Account</h1>
<p className="mt-2 text-sm text-gray-600">
Register with a passkey no password needed.
</p>
<form onSubmit={handleRegister} className="mt-8 space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
Email
</label>
<input
id="email"
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
<div>
<label htmlFor="username" className="block text-sm font-medium text-gray-700">
Username
</label>
<input
id="username"
type="text"
required
value={username}
onChange={(e) => setUsername(e.target.value.toLowerCase().replace(/[^a-z0-9_-]/g, ""))}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
placeholder="alice"
/>
<p className="mt-1 text-xs text-gray-500">
Your handle will be @{username || "..."}@{typeof window !== "undefined" ? window.location.hostname : "trails.cool"}
</p>
</div>
{error && (
<p className="text-sm text-red-600">{error}</p>
)}
<button
type="submit"
disabled={loading}
className="w-full rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50"
>
{loading ? "Creating passkey..." : "Register with Passkey"}
</button>
</form>
<p className="mt-6 text-center text-sm text-gray-500">
Already have an account?{" "}
<a href="/auth/login" className="text-blue-600 hover:underline">
Sign in
</a>
</p>
</div>
);
}