From 874749df46cc291ca61f2f98092ccabfdeffbf16 Mon Sep 17 00:00:00 2001 From: yyamashita Date: Sat, 26 Sep 2026 00:19:53 +0900 Subject: Add passkey login and favorite search terms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single-user auth: any registered passkey logs in as the owner. First registration needs a one-time token that the app writes to data/registration-token when no passkey exists yet; once logged in, more passkeys can be added without a token. Logged in, /favorites manages a list of terms (e.g. band names). Events whose title or artist contains a term (NFKC + case-insensitive) get a ★, and the event list gains an "お気に入りのみ" filter. Co-Authored-By: Claude Opus 5.5 --- app/routes/api.auth.$step.ts | 63 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 app/routes/api.auth.$step.ts (limited to 'app/routes/api.auth.$step.ts') diff --git a/app/routes/api.auth.$step.ts b/app/routes/api.auth.$step.ts new file mode 100644 index 0000000..64940f2 --- /dev/null +++ b/app/routes/api.auth.$step.ts @@ -0,0 +1,63 @@ +/** + * Resource route: POST /api/auth/:step + * + * register-options / register-verify — パスキー登録(ログイン済み or 登録トークン必須) + * login-options / login-verify — パスキーでログイン + * logout — セッション破棄して /events へ + */ +import { redirect } from "react-router"; +import type { Route } from "./+types/api.auth.$step"; +import { + authenticationOptions, + canRegister, + isLoggedIn, + logout, + registrationOptions, + verifyAuthentication, + verifyRegistration, +} from "~/lib/auth.server"; + +export async function action({ request, params }: Route.ActionArgs) { + switch (params.step) { + case "register-options": { + const { token } = await request.json(); + if (!canRegister(request, token ?? null)) return forbidden(); + const { options, setCookie } = await registrationOptions(request); + return Response.json(options, { headers: { "Set-Cookie": setCookie } }); + } + case "register-verify": { + const { token, response, label } = await request.json(); + const loggedIn = isLoggedIn(request); + if (!canRegister(request, token ?? null)) return forbidden(); + return withSession(() => + verifyRegistration(request, response, label ?? null, !loggedIn) + ); + } + case "login-options": { + const { options, setCookie } = await authenticationOptions(request); + return Response.json(options, { headers: { "Set-Cookie": setCookie } }); + } + case "login-verify": { + const { response } = await request.json(); + return withSession(() => verifyAuthentication(request, response)); + } + case "logout": + return redirect("/events", { headers: { "Set-Cookie": logout(request) } }); + default: + throw new Response("Not Found", { status: 404 }); + } +} + +async function withSession(verify: () => Promise): Promise { + try { + const setCookie = await verify(); + return Response.json({ ok: true }, { headers: { "Set-Cookie": setCookie } }); + } catch (e) { + console.warn("[auth]", e); + return Response.json({ ok: false, error: (e as Error).message }, { status: 400 }); + } +} + +function forbidden(): Response { + return Response.json({ ok: false, error: "登録トークンが無効です" }, { status: 403 }); +} -- cgit v1.3