From cba06f7e23417a9aef6c15ae26715385feb225ba Mon Sep 17 00:00:00 2001 From: yyamashita Date: Fri, 19 Jun 2026 19:45:41 +0900 Subject: Add white theme, post delete, and passkey authentication - Rewrite CSS to light/white theme - Add delete button (auth-gated) with confirm dialog - Add passkey register/login/logout routes via @simplewebauthn - Gate compose form and delete behind session authentication - Add credentials table to SQLite schema - Add SESSION_SECRET and WebAuthn env vars to docker-compose Co-Authored-By: Claude Sonnet 4.6 --- app/routes/home.tsx | 121 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 86 insertions(+), 35 deletions(-) (limited to 'app/routes/home.tsx') diff --git a/app/routes/home.tsx b/app/routes/home.tsx index 58004c9..7732326 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -1,31 +1,51 @@ import { useEffect, useRef } from "react"; -import { Form, redirect, useNavigation } from "react-router"; +import { Form, Link, redirect, useNavigation } from "react-router"; import type { Route } from "./+types/home"; -import { createPost, listPosts } from "~/lib/db.server"; +import { createPost, deletePost, listPosts, listCredentials } from "~/lib/db.server"; import { renderMarkdown } from "~/lib/md.server"; +import { getSession, commitSession } from "~/lib/session.server"; -export async function loader() { +export async function loader({ request }: Route.LoaderArgs) { + const session = await getSession(request.headers.get("Cookie")); + const isAuthenticated = session.get("authenticated") === true; + const hasCredentials = listCredentials().length > 0; const posts = listPosts(); - return { posts: posts.map((p) => ({ ...p, html: renderMarkdown(p.content) })) }; + return { + posts: posts.map((p) => ({ ...p, html: renderMarkdown(p.content) })), + isAuthenticated, + hasCredentials, + }; } export async function action({ request }: Route.ActionArgs) { + const session = await getSession(request.headers.get("Cookie")); + if (session.get("authenticated") !== true) { + throw new Response("Unauthorized", { status: 401 }); + } + const form = await request.formData(); - const content = String(form.get("content") ?? "").trim(); + const intent = String(form.get("intent") ?? "create"); - if (!content) { - return { error: "内容を入力してください", content }; - } - if (content.length > 10000) { - return { error: "投稿が長すぎます(最大10,000文字)", content }; + if (intent === "delete") { + const id = String(form.get("id") ?? ""); + if (id) deletePost(id); + return redirect("/", { + headers: { "Set-Cookie": await commitSession(session) }, + }); } + const content = String(form.get("content") ?? "").trim(); + if (!content) return { error: "内容を入力してください", content }; + if (content.length > 10000) return { error: "投稿が長すぎます(最大10,000文字)", content }; + createPost({ id: crypto.randomUUID(), content }); - return redirect("/"); + return redirect("/", { + headers: { "Set-Cookie": await commitSession(session) }, + }); } export default function Home({ loaderData, actionData }: Route.ComponentProps) { - const { posts } = loaderData; + const { posts, isAuthenticated, hasCredentials } = loaderData; const navigation = useNavigation(); const isSubmitting = navigation.state === "submitting"; const formRef = useRef(null); @@ -43,28 +63,42 @@ export default function Home({ loaderData, actionData }: Route.ComponentProps) {

log

+
-
-
-