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/lib/db.server.ts | 52 +++++++++++++++++++++++++++++++++++++++++++++++ app/lib/passkey.server.ts | 11 ++++++++++ app/lib/session.server.ts | 12 +++++++++++ 3 files changed, 75 insertions(+) create mode 100644 app/lib/passkey.server.ts create mode 100644 app/lib/session.server.ts (limited to 'app/lib') diff --git a/app/lib/db.server.ts b/app/lib/db.server.ts index b076ec7..cd8d198 100644 --- a/app/lib/db.server.ts +++ b/app/lib/db.server.ts @@ -22,9 +22,18 @@ function initSchema(db: Database.Database) { created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_posts_created_at ON posts(created_at DESC); + + CREATE TABLE IF NOT EXISTS credentials ( + id TEXT PRIMARY KEY, + public_key TEXT NOT NULL, + counter INTEGER NOT NULL DEFAULT 0, + transports TEXT + ); `); } +/* ── Posts ── */ + export interface Post { id: string; content: string; @@ -48,3 +57,46 @@ export function createPost(input: CreatePostInput): Post { getDb().prepare("INSERT INTO posts (id, content) VALUES (?, ?)").run(input.id, input.content); return getPostById(input.id)!; } + +export function deletePost(id: string): void { + getDb().prepare("DELETE FROM posts WHERE id = ?").run(id); +} + +/* ── Credentials (passkey) ── */ + +export interface Credential { + id: string; + public_key: string; + counter: number; + transports: string | null; +} + +export function listCredentials(): Credential[] { + return getDb().prepare("SELECT * FROM credentials").all() as Credential[]; +} + +export function getCredentialById(id: string): Credential | null { + return getDb().prepare("SELECT * FROM credentials WHERE id = ?").get(id) as Credential | null; +} + +export function saveCredential(input: { + id: string; + publicKey: Uint8Array; + counter: number; + transports?: string[]; +}): void { + getDb() + .prepare( + "INSERT INTO credentials (id, public_key, counter, transports) VALUES (?, ?, ?, ?)" + ) + .run( + input.id, + Buffer.from(input.publicKey).toString("base64url"), + input.counter, + input.transports ? JSON.stringify(input.transports) : null + ); +} + +export function updateCredentialCounter(id: string, counter: number): void { + getDb().prepare("UPDATE credentials SET counter = ? WHERE id = ?").run(counter, id); +} diff --git a/app/lib/passkey.server.ts b/app/lib/passkey.server.ts new file mode 100644 index 0000000..e88c4f8 --- /dev/null +++ b/app/lib/passkey.server.ts @@ -0,0 +1,11 @@ +export const rpID = + process.env.WEBAUTHN_RP_ID ?? + (process.env.NODE_ENV === "production" ? "blog.yyamashita.com" : "localhost"); + +export const origin = + process.env.WEBAUTHN_ORIGIN ?? + (process.env.NODE_ENV === "production" + ? "https://blog.yyamashita.com" + : "http://localhost:5173"); + +export const rpName = "microblog"; diff --git a/app/lib/session.server.ts b/app/lib/session.server.ts new file mode 100644 index 0000000..13a7cd1 --- /dev/null +++ b/app/lib/session.server.ts @@ -0,0 +1,12 @@ +import { createCookieSessionStorage } from "react-router"; + +export const { getSession, commitSession, destroySession } = createCookieSessionStorage({ + cookie: { + name: "__session", + secrets: [process.env.SESSION_SECRET ?? "dev-secret-change-in-production"], + secure: process.env.NODE_ENV === "production", + httpOnly: true, + sameSite: "lax", + maxAge: 60 * 60 * 24 * 30, + }, +}); -- cgit v1.2.3