From fb958a920001dcfb798e723f048bd9644cee42e8 Mon Sep 17 00:00:00 2001 From: Alex Seubert Date: Sat, 11 Apr 2026 22:49:47 -0500 Subject: [PATCH] Add logout button, begin reply implementation, add Biome for formatting/linting --- .cta.json | 30 +-- .vscode/settings.json | 20 +- biome.json | 41 ++++ package-lock.json | 176 +++++++++++++++++ package.json | 105 +++++----- public/manifest.json | 46 ++--- src/components/Footer.tsx | 22 +-- src/components/Header.tsx | 44 +++-- src/components/ThemeToggle.tsx | 128 ++++++------ src/lib/db.ts | 2 +- src/lib/oauth-client.ts | 108 +++++----- src/routeTree.gen.ts | 318 +++++++++++++++-------------- src/router.tsx | 20 +- src/routes/__root.tsx | 122 +++++++----- src/routes/about.tsx | 32 +-- src/routes/callback.tsx | 42 ++-- src/routes/client-metadata.tsx | 18 +- src/routes/feed.tsx | 121 ++++++++---- src/routes/index.tsx | 158 +++++++-------- src/routes/jwks.tsx | 18 +- src/routes/login.tsx | 70 ++++--- src/routes/logout.tsx | 18 ++ src/start.ts | 36 ++-- src/styles.css | 351 +++++++++++++++++---------------- tsconfig.json | 48 ++--- vite.config.ts | 44 ++--- 26 files changed, 1260 insertions(+), 878 deletions(-) create mode 100644 biome.json create mode 100644 src/routes/logout.tsx diff --git a/.cta.json b/.cta.json index 5538de7..84e222c 100644 --- a/.cta.json +++ b/.cta.json @@ -1,16 +1,16 @@ { - "projectName": "dudesky", - "mode": "file-router", - "typescript": true, - "tailwind": true, - "packageManager": "npm", - "git": true, - "install": true, - "addOnOptions": {}, - "includeExamples": true, - "envVarValues": {}, - "routerOnly": false, - "version": 1, - "framework": "react", - "chosenAddOns": [] -} \ No newline at end of file + "projectName": "dudesky", + "mode": "file-router", + "typescript": true, + "tailwind": true, + "packageManager": "npm", + "git": true, + "install": true, + "addOnOptions": {}, + "includeExamples": true, + "envVarValues": {}, + "routerOnly": false, + "version": 1, + "framework": "react", + "chosenAddOns": [] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 00b5278..1b689f9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,13 @@ { - "files.watcherExclude": { - "**/routeTree.gen.ts": true - }, - "search.exclude": { - "**/routeTree.gen.ts": true - }, - "files.readonlyInclude": { - "**/routeTree.gen.ts": true - } + "files.watcherExclude": { + "**/routeTree.gen.ts": true + }, + "search.exclude": { + "**/routeTree.gen.ts": true + }, + "files.readonlyInclude": { + "**/routeTree.gen.ts": true + }, + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true } diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..4dc5e90 --- /dev/null +++ b/biome.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.11/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false, + "includes": ["**", "!src/routeTree.gen.ts"] + }, + "css": { + "parser": { + "tailwindDirectives": true + } + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "semicolons": "asNeeded" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/package-lock.json b/package-lock.json index 78bc3bc..a5af7ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "tailwindcss": "^4.1.18" }, "devDependencies": { + "@biomejs/biome": "2.4.11", "@tailwindcss/typography": "^0.5.16", "@tanstack/devtools-vite": "latest", "@testing-library/dom": "^10.4.1", @@ -683,6 +684,181 @@ "node": ">=6.9.0" } }, + "node_modules/@biomejs/biome": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.11.tgz", + "integrity": "sha512-nWxHX8tf3Opb/qRgZpBbsTOqOodkbrkJ7S+JxJAruxOReaDPPmPuLBAGQ8vigyUgo0QBB+oQltNEAvalLcjggA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.4.11", + "@biomejs/cli-darwin-x64": "2.4.11", + "@biomejs/cli-linux-arm64": "2.4.11", + "@biomejs/cli-linux-arm64-musl": "2.4.11", + "@biomejs/cli-linux-x64": "2.4.11", + "@biomejs/cli-linux-x64-musl": "2.4.11", + "@biomejs/cli-win32-arm64": "2.4.11", + "@biomejs/cli-win32-x64": "2.4.11" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.11.tgz", + "integrity": "sha512-wOt+ed+L2dgZanWyL6i29qlXMc088N11optzpo10peayObBaAshbTcxKUchzEMp9QSY8rh5h6VfAFE3WTS1rqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.11.tgz", + "integrity": "sha512-gZ6zR8XmZlExfi/Pz/PffmdpWOQ8Qhy7oBztgkR8/ylSRyLwfRPSadmiVCV8WQ8PoJ2MWUy2fgID9zmtgUUJmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.11.tgz", + "integrity": "sha512-avdJaEElXrKceK0va9FkJ4P5ci3N01TGkc6ni3P8l3BElqbOz42Wg2IyX3gbh0ZLEd4HVKEIrmuVu/AMuSeFFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.11.tgz", + "integrity": "sha512-+Sbo1OAmlegtdwqFE8iOxFIWLh1B3OEgsuZfBpyyN/kWuqZ8dx9ZEes6zVnDMo+zRHF2wLynRVhoQmV7ohxl2Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.11.tgz", + "integrity": "sha512-TagWV0iomp5LnEnxWFg4nQO+e52Fow349vaX0Q/PIcX6Zhk4GGBgp3qqZ8PVkpC+cuehRctMf3+6+FgQ8jCEFQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.11.tgz", + "integrity": "sha512-bexd2IklK7ZgPhrz6jXzpIL6dEAH9MlJU1xGTrypx+FICxrXUp4CqtwfiuoDKse+UlgAlWtzML3jrMqeEAHEhA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.11.tgz", + "integrity": "sha512-RJhaTnY8byzxDt4bDVb7AFPHkPcjOPK3xBip4ZRTrN3TEfyhjLRm3r3mqknqydgVTB74XG8l4jMLwEACEeihVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.11.tgz", + "integrity": "sha512-A8D3JM/00C2KQgUV3oj8Ba15EHEYwebAGCy5Sf9GAjr5Y3+kJIYOiESoqRDeuRZueuMdCsbLZIUqmPhpYXJE9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@bramus/specificity": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", diff --git a/package.json b/package.json index 2ed1b6f..2b020d1 100644 --- a/package.json +++ b/package.json @@ -1,54 +1,55 @@ { - "name": "dudesky", - "private": true, - "type": "module", - "imports": { - "#/*": "./src/*" - }, - "scripts": { - "dev": "vite dev --port 3000", - "build": "vite build", - "preview": "vite preview", - "test": "vitest run" - }, - "dependencies": { - "@atproto/api": "^0.19.8", - "@atproto/oauth-client-node": "^0.3.17", - "@playwright/test": "^1.59.1", - "@tailwindcss/vite": "^4.1.18", - "@tanstack/react-devtools": "latest", - "@tanstack/react-router": "latest", - "@tanstack/react-router-devtools": "latest", - "@tanstack/react-router-ssr-query": "latest", - "@tanstack/react-start": "latest", - "@tanstack/router-plugin": "^1.132.0", - "better-sqlite3": "^12.8.0", - "lucide-react": "^0.545.0", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "tailwindcss": "^4.1.18" - }, - "devDependencies": { - "@tailwindcss/typography": "^0.5.16", - "@tanstack/devtools-vite": "latest", - "@testing-library/dom": "^10.4.1", - "@testing-library/react": "^16.3.0", - "@types/better-sqlite3": "^7.6.13", - "@types/node": "^22.10.2", - "@types/react": "^19.2.0", - "@types/react-dom": "^19.2.0", - "@vitejs/plugin-react": "^5.1.4", - "jsdom": "^28.1.0", - "typescript": "^5.7.2", - "vite": "^7.3.1", - "vite-tsconfig-paths": "^5.1.4", - "vitest": "^3.0.5" - }, - "pnpm": { - "onlyBuiltDependencies": [ - "better-sqlite3", - "esbuild", - "lightningcss" - ] - } + "name": "dudesky", + "private": true, + "type": "module", + "imports": { + "#/*": "./src/*" + }, + "scripts": { + "dev": "vite dev --port 3000", + "build": "vite build", + "preview": "vite preview", + "test": "vitest run" + }, + "dependencies": { + "@atproto/api": "^0.19.8", + "@atproto/oauth-client-node": "^0.3.17", + "@playwright/test": "^1.59.1", + "@tailwindcss/vite": "^4.1.18", + "@tanstack/react-devtools": "latest", + "@tanstack/react-router": "latest", + "@tanstack/react-router-devtools": "latest", + "@tanstack/react-router-ssr-query": "latest", + "@tanstack/react-start": "latest", + "@tanstack/router-plugin": "^1.132.0", + "better-sqlite3": "^12.8.0", + "lucide-react": "^0.545.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "tailwindcss": "^4.1.18" + }, + "devDependencies": { + "@biomejs/biome": "2.4.11", + "@tailwindcss/typography": "^0.5.16", + "@tanstack/devtools-vite": "latest", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.0", + "@types/better-sqlite3": "^7.6.13", + "@types/node": "^22.10.2", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^5.1.4", + "jsdom": "^28.1.0", + "typescript": "^5.7.2", + "vite": "^7.3.1", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.0.5" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "better-sqlite3", + "esbuild", + "lightningcss" + ] + } } diff --git a/public/manifest.json b/public/manifest.json index 078ef50..4d9581b 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,25 +1,25 @@ { - "short_name": "TanStack App", - "name": "Create TanStack App Sample", - "icons": [ - { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" - } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff" + "short_name": "TanStack App", + "name": "Create TanStack App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" } diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 3a0b6b2..c74b84c 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,14 +1,14 @@ export default function Footer() { - const year = new Date().getFullYear() + const year = new Date().getFullYear() - return ( - - ) + return ( + + ) } diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 497110f..00d4484 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,19 +1,31 @@ import { Link } from '@tanstack/react-router' -export default function Header() { - return ( -
- -
- ) +export default function Header({ did }: { did: string | null }) { + return ( +
+ +
+ ) } diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx index 081ebe2..bb6bee7 100644 --- a/src/components/ThemeToggle.tsx +++ b/src/components/ThemeToggle.tsx @@ -3,79 +3,79 @@ import { useEffect, useState } from 'react' type ThemeMode = 'light' | 'dark' | 'auto' function getInitialMode(): ThemeMode { - if (typeof window === 'undefined') { - return 'auto' - } + if (typeof window === 'undefined') { + return 'auto' + } - const stored = window.localStorage.getItem('theme') - if (stored === 'light' || stored === 'dark' || stored === 'auto') { - return stored - } + const stored = window.localStorage.getItem('theme') + if (stored === 'light' || stored === 'dark' || stored === 'auto') { + return stored + } - return 'auto' + return 'auto' } function applyThemeMode(mode: ThemeMode) { - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches - const resolved = mode === 'auto' ? (prefersDark ? 'dark' : 'light') : mode + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches + const resolved = mode === 'auto' ? (prefersDark ? 'dark' : 'light') : mode - document.documentElement.classList.remove('light', 'dark') - document.documentElement.classList.add(resolved) + document.documentElement.classList.remove('light', 'dark') + document.documentElement.classList.add(resolved) - if (mode === 'auto') { - document.documentElement.removeAttribute('data-theme') - } else { - document.documentElement.setAttribute('data-theme', mode) - } + if (mode === 'auto') { + document.documentElement.removeAttribute('data-theme') + } else { + document.documentElement.setAttribute('data-theme', mode) + } - document.documentElement.style.colorScheme = resolved + document.documentElement.style.colorScheme = resolved } export default function ThemeToggle() { - const [mode, setMode] = useState('auto') - - useEffect(() => { - const initialMode = getInitialMode() - setMode(initialMode) - applyThemeMode(initialMode) - }, []) - - useEffect(() => { - if (mode !== 'auto') { - return - } - - const media = window.matchMedia('(prefers-color-scheme: dark)') - const onChange = () => applyThemeMode('auto') - - media.addEventListener('change', onChange) - return () => { - media.removeEventListener('change', onChange) - } - }, [mode]) - - function toggleMode() { - const nextMode: ThemeMode = - mode === 'light' ? 'dark' : mode === 'dark' ? 'auto' : 'light' - setMode(nextMode) - applyThemeMode(nextMode) - window.localStorage.setItem('theme', nextMode) - } - - const label = - mode === 'auto' - ? 'Theme mode: auto (system). Click to switch to light mode.' - : `Theme mode: ${mode}. Click to switch mode.` - - return ( - - ) + const [mode, setMode] = useState('auto') + + useEffect(() => { + const initialMode = getInitialMode() + setMode(initialMode) + applyThemeMode(initialMode) + }, []) + + useEffect(() => { + if (mode !== 'auto') { + return + } + + const media = window.matchMedia('(prefers-color-scheme: dark)') + const onChange = () => applyThemeMode('auto') + + media.addEventListener('change', onChange) + return () => { + media.removeEventListener('change', onChange) + } + }, [mode]) + + function toggleMode() { + const nextMode: ThemeMode = + mode === 'light' ? 'dark' : mode === 'dark' ? 'auto' : 'light' + setMode(nextMode) + applyThemeMode(nextMode) + window.localStorage.setItem('theme', nextMode) + } + + const label = + mode === 'auto' + ? 'Theme mode: auto (system). Click to switch to light mode.' + : `Theme mode: ${mode}. Click to switch mode.` + + return ( + + ) } diff --git a/src/lib/db.ts b/src/lib/db.ts index 4be284d..fbb6d6f 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -1,5 +1,5 @@ -import Database from 'better-sqlite3' import path from 'node:path' +import Database from 'better-sqlite3' const DB_PATH = process.env.DB_PATH ?? path.join(process.cwd(), 'dudesky.db') diff --git a/src/lib/oauth-client.ts b/src/lib/oauth-client.ts index 6621f6b..4fafee4 100644 --- a/src/lib/oauth-client.ts +++ b/src/lib/oauth-client.ts @@ -1,4 +1,9 @@ -import { JoseKey, NodeOAuthClient, type NodeSavedState, type NodeSavedSession } from '@atproto/oauth-client-node' +import { + JoseKey, + NodeOAuthClient, + type NodeSavedSession, + type NodeSavedState, +} from '@atproto/oauth-client-node' import { db } from '#/lib/db' // OAuth PKCE/DPoP state is only needed for the ~10 minutes between /login and @@ -8,65 +13,72 @@ const STATE_TTL_SECONDS = 600 const rootUrl = process.env.VITE_APP_URL export const client = new NodeOAuthClient({ - clientMetadata: { - client_id: `${rootUrl}/client-metadata`, - client_name: "Dudesky", - client_uri: rootUrl, - redirect_uris: [`${rootUrl}/callback`], - grant_types: ["authorization_code", "refresh_token"], - scope: "atproto transition:generic", - application_type: "web", - token_endpoint_auth_method: "private_key_jwt", - token_endpoint_auth_signing_alg: "ES256", - dpop_bound_access_tokens: true, - jwks_uri: `${rootUrl}/jwks`, - }, + clientMetadata: { + client_id: `${rootUrl}/client-metadata`, + client_name: 'Dudesky', + client_uri: rootUrl, + redirect_uris: [`${rootUrl}/callback`], + grant_types: ['authorization_code', 'refresh_token'], + scope: 'atproto transition:generic', + application_type: 'web', + token_endpoint_auth_method: 'private_key_jwt', + token_endpoint_auth_signing_alg: 'ES256', + dpop_bound_access_tokens: true, + jwks_uri: `${rootUrl}/jwks`, + }, - keyset: await Promise.all([ - JoseKey.fromImportable(process.env.PRIVATE_KEY_0!, 'key1'), - JoseKey.fromImportable(process.env.PRIVATE_KEY_1!, 'key2'), - JoseKey.fromImportable(process.env.PRIVATE_KEY_2!, 'key3'), - ]), + keyset: await Promise.all([ + // biome-ignore lint/style/noNonNullAssertion: required env vars + JoseKey.fromImportable(process.env.PRIVATE_KEY_0!, 'key1'), + // biome-ignore lint/style/noNonNullAssertion: required env vars + JoseKey.fromImportable(process.env.PRIVATE_KEY_1!, 'key2'), + // biome-ignore lint/style/noNonNullAssertion: required env vars + JoseKey.fromImportable(process.env.PRIVATE_KEY_2!, 'key3'), + ]), - stateStore: { - async set(key: string, value: NodeSavedState): Promise { - db.prepare(` + stateStore: { + async set(key: string, value: NodeSavedState): Promise { + db.prepare(` INSERT INTO oauth_state (key, value, created_at) VALUES (?, ?, unixepoch()) ON CONFLICT(key) DO UPDATE SET value = excluded.value, created_at = excluded.created_at `).run(key, JSON.stringify(value)) - // Prune expired state entries - db.prepare(`DELETE FROM oauth_state WHERE created_at < unixepoch() - ?`) - .run(STATE_TTL_SECONDS) - }, - async get(key: string): Promise { - const row = db.prepare(` + // Prune expired state entries + db.prepare( + `DELETE FROM oauth_state WHERE created_at < unixepoch() - ?`, + ).run(STATE_TTL_SECONDS) + }, + async get(key: string): Promise { + const row = db + .prepare(` SELECT value FROM oauth_state WHERE key = ? AND created_at >= unixepoch() - ? - `).get(key, STATE_TTL_SECONDS) as { value: string } | undefined - return row ? JSON.parse(row.value) : undefined - }, - async del(key: string): Promise { - db.prepare(`DELETE FROM oauth_state WHERE key = ?`).run(key) - }, - }, + `) + .get(key, STATE_TTL_SECONDS) as { value: string } | undefined + return row ? JSON.parse(row.value) : undefined + }, + async del(key: string): Promise { + db.prepare(`DELETE FROM oauth_state WHERE key = ?`).run(key) + }, + }, - sessionStore: { - async set(did: string, value: NodeSavedSession): Promise { - db.prepare(` + sessionStore: { + async set(did: string, value: NodeSavedSession): Promise { + db.prepare(` INSERT INTO oauth_session (did, value, updated_at) VALUES (?, ?, unixepoch()) ON CONFLICT(did) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at `).run(did, JSON.stringify(value)) - }, - async get(did: string): Promise { - const row = db.prepare(`SELECT value FROM oauth_session WHERE did = ?`) - .get(did) as { value: string } | undefined - return row ? JSON.parse(row.value) : undefined - }, - async del(did: string): Promise { - db.prepare(`DELETE FROM oauth_session WHERE did = ?`).run(did) - }, - }, + }, + async get(did: string): Promise { + const row = db + .prepare(`SELECT value FROM oauth_session WHERE did = ?`) + .get(did) as { value: string } | undefined + return row ? JSON.parse(row.value) : undefined + }, + async del(did: string): Promise { + db.prepare(`DELETE FROM oauth_session WHERE did = ?`).run(did) + }, + }, }) diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index e530012..9f7d5e1 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -9,191 +9,213 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' -import { Route as LoginRouteImport } from './routes/login' -import { Route as JwksRouteImport } from './routes/jwks' -import { Route as FeedRouteImport } from './routes/feed' -import { Route as ClientMetadataRouteImport } from './routes/client-metadata' -import { Route as CallbackRouteImport } from './routes/callback' import { Route as AboutRouteImport } from './routes/about' +import { Route as CallbackRouteImport } from './routes/callback' +import { Route as ClientMetadataRouteImport } from './routes/client-metadata' +import { Route as FeedRouteImport } from './routes/feed' import { Route as IndexRouteImport } from './routes/index' +import { Route as JwksRouteImport } from './routes/jwks' +import { Route as LoginRouteImport } from './routes/login' +import { Route as LogoutRouteImport } from './routes/logout' +const LogoutRoute = LogoutRouteImport.update({ + id: '/logout', + path: '/logout', + getParentRoute: () => rootRouteImport, +} as any) const LoginRoute = LoginRouteImport.update({ - id: '/login', - path: '/login', - getParentRoute: () => rootRouteImport, + id: '/login', + path: '/login', + getParentRoute: () => rootRouteImport, } as any) const JwksRoute = JwksRouteImport.update({ - id: '/jwks', - path: '/jwks', - getParentRoute: () => rootRouteImport, + id: '/jwks', + path: '/jwks', + getParentRoute: () => rootRouteImport, } as any) const FeedRoute = FeedRouteImport.update({ - id: '/feed', - path: '/feed', - getParentRoute: () => rootRouteImport, + id: '/feed', + path: '/feed', + getParentRoute: () => rootRouteImport, } as any) const ClientMetadataRoute = ClientMetadataRouteImport.update({ - id: '/client-metadata', - path: '/client-metadata', - getParentRoute: () => rootRouteImport, + id: '/client-metadata', + path: '/client-metadata', + getParentRoute: () => rootRouteImport, } as any) const CallbackRoute = CallbackRouteImport.update({ - id: '/callback', - path: '/callback', - getParentRoute: () => rootRouteImport, + id: '/callback', + path: '/callback', + getParentRoute: () => rootRouteImport, } as any) const AboutRoute = AboutRouteImport.update({ - id: '/about', - path: '/about', - getParentRoute: () => rootRouteImport, + id: '/about', + path: '/about', + getParentRoute: () => rootRouteImport, } as any) const IndexRoute = IndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => rootRouteImport, + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, } as any) export interface FileRoutesByFullPath { - '/': typeof IndexRoute - '/about': typeof AboutRoute - '/callback': typeof CallbackRoute - '/client-metadata': typeof ClientMetadataRoute - '/feed': typeof FeedRoute - '/jwks': typeof JwksRoute - '/login': typeof LoginRoute + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/callback': typeof CallbackRoute + '/client-metadata': typeof ClientMetadataRoute + '/feed': typeof FeedRoute + '/jwks': typeof JwksRoute + '/login': typeof LoginRoute + '/logout': typeof LogoutRoute } export interface FileRoutesByTo { - '/': typeof IndexRoute - '/about': typeof AboutRoute - '/callback': typeof CallbackRoute - '/client-metadata': typeof ClientMetadataRoute - '/feed': typeof FeedRoute - '/jwks': typeof JwksRoute - '/login': typeof LoginRoute + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/callback': typeof CallbackRoute + '/client-metadata': typeof ClientMetadataRoute + '/feed': typeof FeedRoute + '/jwks': typeof JwksRoute + '/login': typeof LoginRoute + '/logout': typeof LogoutRoute } export interface FileRoutesById { - __root__: typeof rootRouteImport - '/': typeof IndexRoute - '/about': typeof AboutRoute - '/callback': typeof CallbackRoute - '/client-metadata': typeof ClientMetadataRoute - '/feed': typeof FeedRoute - '/jwks': typeof JwksRoute - '/login': typeof LoginRoute + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/callback': typeof CallbackRoute + '/client-metadata': typeof ClientMetadataRoute + '/feed': typeof FeedRoute + '/jwks': typeof JwksRoute + '/login': typeof LoginRoute + '/logout': typeof LogoutRoute } export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: - | '/' - | '/about' - | '/callback' - | '/client-metadata' - | '/feed' - | '/jwks' - | '/login' - fileRoutesByTo: FileRoutesByTo - to: - | '/' - | '/about' - | '/callback' - | '/client-metadata' - | '/feed' - | '/jwks' - | '/login' - id: - | '__root__' - | '/' - | '/about' - | '/callback' - | '/client-metadata' - | '/feed' - | '/jwks' - | '/login' - fileRoutesById: FileRoutesById + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/about' + | '/callback' + | '/client-metadata' + | '/feed' + | '/jwks' + | '/login' + | '/logout' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/about' + | '/callback' + | '/client-metadata' + | '/feed' + | '/jwks' + | '/login' + | '/logout' + id: + | '__root__' + | '/' + | '/about' + | '/callback' + | '/client-metadata' + | '/feed' + | '/jwks' + | '/login' + | '/logout' + fileRoutesById: FileRoutesById } export interface RootRouteChildren { - IndexRoute: typeof IndexRoute - AboutRoute: typeof AboutRoute - CallbackRoute: typeof CallbackRoute - ClientMetadataRoute: typeof ClientMetadataRoute - FeedRoute: typeof FeedRoute - JwksRoute: typeof JwksRoute - LoginRoute: typeof LoginRoute + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute + CallbackRoute: typeof CallbackRoute + ClientMetadataRoute: typeof ClientMetadataRoute + FeedRoute: typeof FeedRoute + JwksRoute: typeof JwksRoute + LoginRoute: typeof LoginRoute + LogoutRoute: typeof LogoutRoute } declare module '@tanstack/react-router' { - interface FileRoutesByPath { - '/login': { - id: '/login' - path: '/login' - fullPath: '/login' - preLoaderRoute: typeof LoginRouteImport - parentRoute: typeof rootRouteImport - } - '/jwks': { - id: '/jwks' - path: '/jwks' - fullPath: '/jwks' - preLoaderRoute: typeof JwksRouteImport - parentRoute: typeof rootRouteImport - } - '/feed': { - id: '/feed' - path: '/feed' - fullPath: '/feed' - preLoaderRoute: typeof FeedRouteImport - parentRoute: typeof rootRouteImport - } - '/client-metadata': { - id: '/client-metadata' - path: '/client-metadata' - fullPath: '/client-metadata' - preLoaderRoute: typeof ClientMetadataRouteImport - parentRoute: typeof rootRouteImport - } - '/callback': { - id: '/callback' - path: '/callback' - fullPath: '/callback' - preLoaderRoute: typeof CallbackRouteImport - parentRoute: typeof rootRouteImport - } - '/about': { - id: '/about' - path: '/about' - fullPath: '/about' - preLoaderRoute: typeof AboutRouteImport - parentRoute: typeof rootRouteImport - } - '/': { - id: '/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof IndexRouteImport - parentRoute: typeof rootRouteImport - } - } + interface FileRoutesByPath { + '/logout': { + id: '/logout' + path: '/logout' + fullPath: '/logout' + preLoaderRoute: typeof LogoutRouteImport + parentRoute: typeof rootRouteImport + } + '/login': { + id: '/login' + path: '/login' + fullPath: '/login' + preLoaderRoute: typeof LoginRouteImport + parentRoute: typeof rootRouteImport + } + '/jwks': { + id: '/jwks' + path: '/jwks' + fullPath: '/jwks' + preLoaderRoute: typeof JwksRouteImport + parentRoute: typeof rootRouteImport + } + '/feed': { + id: '/feed' + path: '/feed' + fullPath: '/feed' + preLoaderRoute: typeof FeedRouteImport + parentRoute: typeof rootRouteImport + } + '/client-metadata': { + id: '/client-metadata' + path: '/client-metadata' + fullPath: '/client-metadata' + preLoaderRoute: typeof ClientMetadataRouteImport + parentRoute: typeof rootRouteImport + } + '/callback': { + id: '/callback' + path: '/callback' + fullPath: '/callback' + preLoaderRoute: typeof CallbackRouteImport + parentRoute: typeof rootRouteImport + } + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } } const rootRouteChildren: RootRouteChildren = { - IndexRoute: IndexRoute, - AboutRoute: AboutRoute, - CallbackRoute: CallbackRoute, - ClientMetadataRoute: ClientMetadataRoute, - FeedRoute: FeedRoute, - JwksRoute: JwksRoute, - LoginRoute: LoginRoute, + IndexRoute: IndexRoute, + AboutRoute: AboutRoute, + CallbackRoute: CallbackRoute, + ClientMetadataRoute: ClientMetadataRoute, + FeedRoute: FeedRoute, + JwksRoute: JwksRoute, + LoginRoute: LoginRoute, + LogoutRoute: LogoutRoute, } export const routeTree = rootRouteImport - ._addFileChildren(rootRouteChildren) - ._addFileTypes() + ._addFileChildren(rootRouteChildren) + ._addFileTypes() import type { getRouter } from './router.tsx' import type { startInstance } from './start.ts' + declare module '@tanstack/react-start' { - interface Register { - ssr: true - router: Awaited> - config: Awaited> - } + interface Register { + ssr: true + router: Awaited> + config: Awaited> + } } diff --git a/src/router.tsx b/src/router.tsx index e7b1c4d..f14c65c 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -2,18 +2,18 @@ import { createRouter as createTanStackRouter } from '@tanstack/react-router' import { routeTree } from './routeTree.gen' export function getRouter() { - const router = createTanStackRouter({ - routeTree, - scrollRestoration: true, - defaultPreload: 'intent', - defaultPreloadStaleTime: 0, - }) + const router = createTanStackRouter({ + routeTree, + scrollRestoration: true, + defaultPreload: 'intent', + defaultPreloadStaleTime: 0, + }) - return router + return router } declare module '@tanstack/react-router' { - interface Register { - router: ReturnType - } + interface Register { + router: ReturnType + } } diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index a087034..b390773 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -1,6 +1,13 @@ -import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router' -import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' import { TanStackDevtools } from '@tanstack/react-devtools' +import { + createRootRoute, + HeadContent, + Outlet, + Scripts, +} from '@tanstack/react-router' +import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' +import { createServerFn } from '@tanstack/react-start' +import { getCookie } from '@tanstack/react-start/server' import Footer from '../components/Footer' import Header from '../components/Header' @@ -8,55 +15,72 @@ import appCss from '../styles.css?url' const THEME_INIT_SCRIPT = `(function(){try{var stored=window.localStorage.getItem('theme');var mode=(stored==='light'||stored==='dark'||stored==='auto')?stored:'auto';var prefersDark=window.matchMedia('(prefers-color-scheme: dark)').matches;var resolved=mode==='auto'?(prefersDark?'dark':'light'):mode;var root=document.documentElement;root.classList.remove('light','dark');root.classList.add(resolved);if(mode==='auto'){root.removeAttribute('data-theme')}else{root.setAttribute('data-theme',mode)}root.style.colorScheme=resolved;}catch(e){}})();` +const getAuthState = createServerFn({ method: 'GET' }).handler(async () => { + const did = getCookie('did') + return { did: did ?? null } +}) + export const Route = createRootRoute({ - head: () => ({ - meta: [ - { - charSet: 'utf-8', - }, - { - name: 'viewport', - content: 'width=device-width, initial-scale=1', - }, - { - title: 'Dudesky', - }, - ], - links: [ - { - rel: 'stylesheet', - href: appCss, - }, - ], - }), - shellComponent: RootDocument, - notFoundComponent: () =>
404 Not Found, Try Again
, + loader: () => getAuthState(), + head: () => ({ + meta: [ + { + charSet: 'utf-8', + }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + { + title: 'Dudesky', + }, + ], + links: [ + { + rel: 'stylesheet', + href: appCss, + }, + ], + }), + shellComponent: RootDocument, + component: RootLayout, + notFoundComponent: () =>
404 Not Found, Try Again
, }) function RootDocument({ children }: { children: React.ReactNode }) { - return ( - - -