summaryrefslogtreecommitdiff
path: root/app/routes/auth.login.tsx
diff options
context:
space:
mode:
authoryyamashita <yyamashita@hetzner.yyamashita.com>2026-06-20 09:51:12 +0900
committeryyamashita <yyamashita@hetzner.yyamashita.com>2026-06-20 09:51:12 +0900
commitd7a8e8a42b752aeaa22315e96b7e5fb69811fdbc (patch)
tree4ef9d41b1db3e8bf21ced831952e7cd29599c7c6 /app/routes/auth.login.tsx
parent077fdc0b950deb018a0470894c700cc4ab83557a (diff)
Move passkey login inline to home page, remove /auth/login routeHEADmaster
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'app/routes/auth.login.tsx')
-rw-r--r--app/routes/auth.login.tsx108
1 files changed, 0 insertions, 108 deletions
diff --git a/app/routes/auth.login.tsx b/app/routes/auth.login.tsx
deleted file mode 100644
index c02045b..0000000
--- a/app/routes/auth.login.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-import { useState } from "react";
-import { redirect } from "react-router";
-import type { AuthenticationResponseJSON, AuthenticatorTransportFuture } from "@simplewebauthn/server";
-import type { Route } from "./+types/auth.login";
-import { getSession } from "~/lib/session.server";
-import { listCredentials } from "~/lib/db.server";
-
-export async function loader({ request }: Route.LoaderArgs) {
- const session = await getSession(request.headers.get("Cookie"));
- if (session.get("authenticated")) return redirect("/");
- if (listCredentials().length === 0) return redirect("/auth/register");
- return {};
-}
-
-function bufferToB64url(buf: ArrayBuffer): string {
- const bytes = new Uint8Array(buf);
- let binary = "";
- for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
-}
-
-export default function Login() {
- const [status, setStatus] = useState<"idle" | "loading" | "error">("idle");
- const [errorMsg, setErrorMsg] = useState("");
-
- async function handleLogin() {
- setStatus("loading");
- setErrorMsg("");
- try {
- const optRes = await fetch("/api/passkey?intent=login-options", { method: "POST" });
- if (!optRes.ok) throw new Error("オプション取得に失敗しました");
- const options = await optRes.json();
-
- const b64urlToBuffer = (b64url: string): ArrayBuffer => {
- const base64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
- const pad = "=".repeat((4 - (base64.length % 4)) % 4);
- const binary = atob(base64 + pad);
- const bytes = new Uint8Array(binary.length);
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
- return bytes.buffer;
- };
-
- const credential = await navigator.credentials.get({
- publicKey: {
- challenge: b64urlToBuffer(options.challenge as string),
- rpId: options.rpId as string,
- allowCredentials: (options.allowCredentials as Array<{ id: string; transports?: string[] }> ?? []).map((c) => ({
- id: b64urlToBuffer(c.id),
- type: "public-key" as const,
- transports: c.transports as AuthenticatorTransport[] | undefined,
- })),
- userVerification: "required" as UserVerificationRequirement,
- timeout: 60000,
- },
- }) as PublicKeyCredential | null;
-
- if (!credential) throw new Error("認証に失敗しました");
-
- const assertion = credential.response as AuthenticatorAssertionResponse;
- const authResponse: AuthenticationResponseJSON = {
- id: credential.id,
- rawId: bufferToB64url(credential.rawId),
- response: {
- clientDataJSON: bufferToB64url(assertion.clientDataJSON),
- authenticatorData: bufferToB64url(assertion.authenticatorData),
- signature: bufferToB64url(assertion.signature),
- userHandle: assertion.userHandle ? bufferToB64url(assertion.userHandle) : undefined,
- },
- authenticatorAttachment: credential.authenticatorAttachment ?? undefined,
- clientExtensionResults: credential.getClientExtensionResults(),
- type: "public-key",
- };
-
- const verRes = await fetch("/api/passkey?intent=login-verify", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(authResponse),
- });
- if (!verRes.ok) {
- const err = await verRes.json();
- throw new Error(err.error ?? "認証に失敗しました");
- }
-
- window.location.href = "/";
- } catch (e) {
- setStatus("error");
- setErrorMsg(e instanceof Error ? e.message : "エラーが発生しました");
- }
- }
-
- return (
- <div className="wrap">
- <header className="site-header">
- <h1>log</h1>
- </header>
- <div className="auth-box">
- <h2>ログイン</h2>
- <p>登録済みのパスキーで認証します。</p>
- <button className="btn" onClick={handleLogin} disabled={status === "loading"}>
- {status === "loading" ? "認証中…" : "パスキーでログイン"}
- </button>
- {status === "error" && (
- <p className="error-msg" style={{ marginTop: ".75rem" }}>{errorMsg}</p>
- )}
- </div>
- </div>
- );
-}