diff --git a/src/lib/api.ts b/src/lib/api.ts index f9cf4bd..18ec991 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,6 +1,6 @@ import type { AstroCookies } from "astro"; import { isOwner } from "./auth"; -import { BLOG_URL, PDS_URL, DID } from "./constants"; +import { BLOG_URL, PDS_URL, DID, SESSION_DID_COOKIE } from "./constants"; export function checkOrigin(request: Request): Response | null { const origin = request.headers.get("origin"); @@ -11,7 +11,7 @@ export function checkOrigin(request: Request): Response | null { } export function checkAuth(cookies: AstroCookies): Response | null { - const sessionDid = cookies.get("session_did")?.value; + const sessionDid = cookies.get(SESSION_DID_COOKIE)?.value; if (!sessionDid || !isOwner(sessionDid)) { return new Response(JSON.stringify({ error: "Not authorized" }), { status: 403 }); } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index df087df..81761e7 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -12,6 +12,16 @@ export const MAX_CONTENT_LENGTH = 100_000; export const MAX_ABOUT_LENGTH = 5_000; export const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5 MB +// Session +export const SESSION_DID_COOKIE = "session_did"; +export const SESSION_HANDLE_COOKIE = "session_handle"; +export const SESSION_MAX_AGE = 86400 * 7; // 7 days + +// RSS +export const RSS_MAX_ITEMS = 20; +export const RSS_EXCERPT_LENGTH = 300; +export const DEFAULT_EXCERPT_LENGTH = 160; + // Cache TTLs (milliseconds) export const PROFILE_TTL = 3600_000; // 1 hour export const ENTRIES_TTL = 300_000; // 5 minutes diff --git a/src/lib/pds.ts b/src/lib/pds.ts index fb3b232..a6e4f15 100644 --- a/src/lib/pds.ts +++ b/src/lib/pds.ts @@ -25,7 +25,7 @@ export interface BlogEntry { blobs?: AtBlob[]; } -export interface AuthorProfile { +interface AuthorProfile { displayName: string; description: string; avatarUrl: string | null; @@ -249,7 +249,8 @@ export async function getBlogEntry(rkey: string): Promise { entryCache.set(rkey, { data: entry, expiresAt: Date.now() + ENTRY_TTL }); return entry; - } catch { + } catch (err) { + console.error("Failed to fetch blog entry:", err); return staleData ?? null; // Network error — serve stale if available } } @@ -295,7 +296,8 @@ export async function getRawBlogEntry( theme: val.theme as string | undefined, blobs: parseBlobsFromValue(val), }; - } catch { + } catch (err) { + console.error("Failed to fetch raw blog entry:", err); return null; } } @@ -330,7 +332,8 @@ export async function getAbout(): Promise { aboutCache = { data: content, expiresAt: Date.now() + ABOUT_TTL }; return content; - } catch { + } catch (err) { + console.error("Failed to fetch about:", err); return staleAbout?.data ?? DEFAULT_ABOUT; } } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index cdc3a86..57229df 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -8,7 +8,9 @@ export function formatDate(iso: string, opts?: { weekday?: boolean }): string { return new Date(iso).toLocaleDateString("en-US", options); } -export function excerpt(content: string, maxLen = 160): string { +import { DEFAULT_EXCERPT_LENGTH } from "./constants"; + +export function excerpt(content: string, maxLen = DEFAULT_EXCERPT_LENGTH): string { const plain = content .replace(/^#{1,6}\s+/gm, "") .replace(/\*{1,2}([^*]+)\*{1,2}/g, "$1") diff --git a/src/pages/[rkey].astro b/src/pages/[rkey].astro index c0a8e51..64075ab 100644 --- a/src/pages/[rkey].astro +++ b/src/pages/[rkey].astro @@ -1,7 +1,7 @@ --- import Base from "../layouts/Base.astro"; import { getBlogEntry, getRawBlogEntry, isValidRkey } from "../lib/pds"; -import { HANDLE, PDS_URL, DID, BLOG_COLLECTION } from "../lib/constants"; +import { HANDLE, PDS_URL, DID, BLOG_COLLECTION, SESSION_DID_COOKIE } from "../lib/constants"; import { formatDate } from "../lib/utils"; import { isOwner } from "../lib/auth"; import { marked } from "marked"; @@ -21,7 +21,7 @@ if (!rkey || !isValidRkey(rkey)) { } // Auth check before fetching — owner can see drafts -const sessionDid = Astro.cookies.get("session_did")?.value; +const sessionDid = Astro.cookies.get(SESSION_DID_COOKIE)?.value; const isAuthor = sessionDid ? isOwner(sessionDid) : false; const entry = isAuthor diff --git a/src/pages/api/logout.ts b/src/pages/api/logout.ts index db09be5..ef3fbf1 100644 --- a/src/pages/api/logout.ts +++ b/src/pages/api/logout.ts @@ -1,7 +1,8 @@ import type { APIRoute } from "astro"; +import { SESSION_DID_COOKIE, SESSION_HANDLE_COOKIE } from "../../lib/constants"; export const GET: APIRoute = async ({ cookies, redirect }) => { - cookies.delete("session_did", { path: "/" }); - cookies.delete("session_handle", { path: "/" }); + cookies.delete(SESSION_DID_COOKIE, { path: "/" }); + cookies.delete(SESSION_HANDLE_COOKIE, { path: "/" }); return redirect("/", 303); }; diff --git a/src/pages/index.astro b/src/pages/index.astro index 29a1366..8785143 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,7 +1,7 @@ --- import Base from "../layouts/Base.astro"; import { getBlogEntries, getProfile, getAbout } from "../lib/pds"; -import { HANDLE } from "../lib/constants"; +import { HANDLE, SESSION_DID_COOKIE } from "../lib/constants"; import { isOwner } from "../lib/auth"; import { formatDate, excerpt } from "../lib/utils"; import { marked } from "marked"; @@ -15,7 +15,7 @@ const aboutHtml = DOMPurify.sanitize(await marked.parse(aboutRaw), { ADD_ATTR: ["target"], }); -const sessionDid = Astro.cookies.get("session_did")?.value; +const sessionDid = Astro.cookies.get(SESSION_DID_COOKIE)?.value; const isAuthor = sessionDid ? isOwner(sessionDid) : false; --- diff --git a/src/pages/rss.xml.ts b/src/pages/rss.xml.ts index c66fbcd..595e835 100644 --- a/src/pages/rss.xml.ts +++ b/src/pages/rss.xml.ts @@ -1,17 +1,23 @@ import type { APIRoute } from "astro"; import { getBlogEntries, getProfile } from "../lib/pds"; -import { BLOG_URL, HANDLE } from "../lib/constants"; +import { BLOG_URL, HANDLE, RSS_MAX_ITEMS, RSS_EXCERPT_LENGTH } from "../lib/constants"; import { escapeXml, excerpt } from "../lib/utils"; export const GET: APIRoute = async () => { - const [entries, profile] = await Promise.all([ - getBlogEntries(), - getProfile(), - ]); + let entries, profile; + try { + [entries, profile] = await Promise.all([ + getBlogEntries(), + getProfile(), + ]); + } catch (err) { + console.error("RSS feed fetch failed:", err); + return new Response("RSS feed temporarily unavailable", { status: 503 }); + } - const items = entries.slice(0, 20).map((entry) => { + const items = entries.slice(0, RSS_MAX_ITEMS).map((entry) => { const pubDate = new Date(entry.createdAt).toUTCString(); - const desc = excerpt(entry.content, 300); + const desc = excerpt(entry.content, RSS_EXCERPT_LENGTH); return ` ${escapeXml(entry.title)} diff --git a/src/pages/write.astro b/src/pages/write.astro index 2649587..a97ab83 100644 --- a/src/pages/write.astro +++ b/src/pages/write.astro @@ -3,6 +3,7 @@ import Base from "../layouts/Base.astro"; import { verifyProxyTicket, isOwner, getLoginUrl } from "../lib/auth"; import { getDraftEntries, getRawBlogEntry, type BlogEntry } from "../lib/pds"; import { formatDate } from "../lib/utils"; +import { SESSION_DID_COOKIE, SESSION_HANDLE_COOKIE, SESSION_MAX_AGE } from "../lib/constants"; let authed = false; let handle = ""; @@ -12,27 +13,27 @@ const ticket = Astro.url.searchParams.get("_atauth_ticket"); if (ticket) { const user = await verifyProxyTicket(ticket); if (user && isOwner(user.did)) { - Astro.cookies.set("session_did", user.did, { + Astro.cookies.set(SESSION_DID_COOKIE, user.did, { httpOnly: true, secure: true, sameSite: "lax", path: "/", - maxAge: 86400 * 7, + maxAge: SESSION_MAX_AGE, }); - Astro.cookies.set("session_handle", user.handle, { + Astro.cookies.set(SESSION_HANDLE_COOKIE, user.handle, { httpOnly: true, secure: true, sameSite: "lax", path: "/", - maxAge: 86400 * 7, + maxAge: SESSION_MAX_AGE, }); return Astro.redirect("/write"); } } // Check existing session cookie -const sessionDid = Astro.cookies.get("session_did")?.value; -const sessionHandle = Astro.cookies.get("session_handle")?.value; +const sessionDid = Astro.cookies.get(SESSION_DID_COOKIE)?.value; +const sessionHandle = Astro.cookies.get(SESSION_HANDLE_COOKIE)?.value; if (sessionDid && sessionHandle && isOwner(sessionDid)) { authed = true; handle = sessionHandle;