refactor:

This commit is contained in:
Lewis Wynne 2026-01-31 22:41:24 +00:00
parent 38b5413a37
commit 7f01bec7e6
17 changed files with 121 additions and 132 deletions

View file

@ -1,3 +1,29 @@
import { getSession } from 'auth-astro/server';
type Session = { user?: { id?: string; name?: string | null } };
export function isAdmin(userId: string | undefined): boolean {
return userId === import.meta.env.ADMIN_GITHUB_ID;
}
export async function requireAdminSession(request: Request): Promise<
| { session: Session; error: null }
| { session: null; error: Response | null }
> {
let session: Session | null;
try {
session = await getSession(request);
} catch {
return { session: null, error: new Response('Auth not configured', { status: 500 }) };
}
if (!session) {
return { session: null, error: null };
}
if (!isAdmin(session.user?.id)) {
return { session: null, error: new Response('Forbidden', { status: 403 }) };
}
return { session, error: null };
}

View file

@ -13,3 +13,27 @@ export function extractDomain(url: string): string {
return url;
}
}
export function formatListItem(
date: Date,
url: string,
title: string,
options?: { pinned?: boolean; suffix?: string }
): string {
const pinnedBadge = options?.pinned ? ' [pinned]' : '';
const suffix = options?.suffix ? ` ${options.suffix}` : '';
return `<span class="muted">${formatDate(date)}</span> <a href="${url}">${title}</a>${pinnedBadge}${suffix}`;
}
interface Sortable {
date: Date;
pinned?: boolean;
}
export function sortByPinnedThenDate<T extends Sortable>(items: T[]): T[] {
return items.slice().sort((a, b) => {
if (a.pinned && !b.pinned) return -1;
if (!a.pinned && b.pinned) return 1;
return b.date.getTime() - a.date.getTime();
});
}

View file

@ -7,7 +7,7 @@ export function getSlug(postId: string): string {
return parts[parts.length - 1];
}
export function sortPosts(posts: Post[]): Post[] {
function sortPosts(posts: Post[]): Post[] {
return posts.slice().sort((a, b) => {
if (a.data.pinned && !b.data.pinned) return -1;
if (!a.data.pinned && b.data.pinned) return 1;

View file

@ -2,6 +2,7 @@ import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import yaml from 'js-yaml';
import { sortByPinnedThenDate } from './format';
function getGitDate(filePath: string): Date {
try {
@ -40,18 +41,14 @@ export function getTxtFiles(): TxtFile[] {
const config = loadTxtConfig();
const pinnedSet = new Set(config.pinned || []);
return fs.readdirSync(txtDir)
const files = fs.readdirSync(txtDir)
.filter(file => file.endsWith('.txt'))
.map(name => ({
name,
date: getGitDate(path.join(txtDir, name)),
pinned: pinnedSet.has(name),
}))
.sort((a, b) => {
if (a.pinned && !b.pinned) return -1;
if (!a.pinned && b.pinned) return 1;
return b.date.getTime() - a.date.getTime();
});
}));
return sortByPinnedThenDate(files);
}
export function getTxtFileNames(): string[] {

View file

@ -1,26 +1,14 @@
---
export const prerender = false;
import { getSession } from 'auth-astro/server';
import { getPendingEntries, type GuestbookEntry } from '../lib/db';
import { isAdmin } from '../lib/auth';
import { requireAdminSession } from '../lib/auth';
import Layout from '../layouts/Layout.astro';
import { formatDate } from '../lib/format';
let session;
try {
session = await getSession(Astro.request);
} catch {
return new Response('Auth not configured', { status: 500 });
}
if (!session) {
return Astro.redirect('/api/auth/signin');
}
if (!isAdmin(session.user?.id)) {
return new Response('Forbidden', { status: 403 });
}
const { session, error } = await requireAdminSession(Astro.request);
if (error) return error;
if (!session) return Astro.redirect('/api/auth/signin');
let entries: GuestbookEntry[] = [];
try {

View file

@ -1,7 +1,7 @@
---
import { getCollection } from 'astro:content';
import Layout from '../../layouts/Layout.astro';
import { formatDate, extractDomain } from '../../lib/format';
import { formatListItem, extractDomain } from '../../lib/format';
const bookmarksCollection = await getCollection('bookmarks');
const bookmarks = bookmarksCollection
@ -11,6 +11,6 @@ const bookmarks = bookmarksCollection
<details open>
<summary>bookmarks</summary>
<pre set:html={bookmarks.map(b => `<span class="muted">${formatDate(b.data.date)}</span> <a href="${b.data.url}">${b.data.title}</a> <span class="muted">(${extractDomain(b.data.url)})</span>`).join('\n')} />
<pre set:html={bookmarks.map(b => formatListItem(b.data.date, b.data.url, b.data.title, { suffix: `<span class="muted">(${extractDomain(b.data.url)})</span>` })).join('\n')} />
</details>
</Layout>

View file

@ -1,27 +1,15 @@
---
export const prerender = false;
import { getSession } from 'auth-astro/server';
import { getCollection, render } from 'astro:content';
import { isAdmin } from '../../lib/auth';
import { requireAdminSession } from '../../lib/auth';
import Layout from '../../layouts/Layout.astro';
import { formatDate } from '../../lib/format';
import { getSlug } from '../../lib/posts';
import { getSlug } from '../../lib/md';
let session;
try {
session = await getSession(Astro.request);
} catch {
return new Response('Auth not configured', { status: 500 });
}
if (!session) {
return Astro.redirect('/api/auth/signin');
}
if (!isAdmin(session.user?.id)) {
return new Response('Forbidden', { status: 403 });
}
const { session, error } = await requireAdminSession(Astro.request);
if (error) return error;
if (!session) return Astro.redirect('/api/auth/signin');
const slug = Astro.params.slug;
const posts = await getCollection('md', ({ data }) => data.draft === true);

View file

@ -1,27 +1,15 @@
---
export const prerender = false;
import { getSession } from 'auth-astro/server';
import { getCollection } from 'astro:content';
import { isAdmin } from '../../lib/auth';
import { requireAdminSession } from '../../lib/auth';
import Layout from '../../layouts/Layout.astro';
import { formatDate } from '../../lib/format';
import { organizePostsByCategory, getSlug } from '../../lib/posts';
import { formatListItem } from '../../lib/format';
import { organizePostsByCategory, getSlug } from '../../lib/md';
let session;
try {
session = await getSession(Astro.request);
} catch {
return new Response('Auth not configured', { status: 500 });
}
if (!session) {
return Astro.redirect('/api/auth/signin');
}
if (!isAdmin(session.user?.id)) {
return new Response('Forbidden', { status: 403 });
}
const { session, error } = await requireAdminSession(Astro.request);
if (error) return error;
if (!session) return Astro.redirect('/api/auth/signin');
const posts = await getCollection('md', ({ data }) => data.draft === true);
const { grouped, categories: sortedCategories } = organizePostsByCategory(posts);
@ -36,7 +24,7 @@ const { grouped, categories: sortedCategories } = organizePostsByCategory(posts)
sortedCategories.map(category => (
<details open>
<summary>{category}</summary>
<pre set:html={grouped[category].map(post => `<span class="muted">${formatDate(post.data.date)}</span> <a href="/draft/${getSlug(post.id)}">${post.data.title}</a>${post.data.pinned ? ' [pinned]' : ''}`).join('\n')} />
<pre set:html={grouped[category].map(post => formatListItem(post.data.date, `/draft/${getSlug(post.id)}`, post.data.title, { pinned: post.data.pinned })).join('\n')} />
</details>
))
)}

View file

@ -1,7 +1,7 @@
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
import type { APIContext } from 'astro';
import { getSlug } from '../lib/posts';
import { getSlug } from '../lib/md';
import { getTxtFiles } from '../lib/txt';
export async function GET(context: APIContext) {

View file

@ -29,33 +29,7 @@ try {
</details>
<script>
document.getElementById('sign-guestbook')?.addEventListener('click', async (e) => {
e.preventDefault();
const status = document.getElementById('guestbook-status')!;
const name = prompt('name:');
if (!name) return;
const message = prompt('message:');
if (!message) return;
const url = prompt('url (optional):');
try {
const res = await fetch('/api/guestbook', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, message, url: url || null }),
});
if (res.ok) {
status.textContent = ' thanks! pending approval.';
} else {
status.textContent = ' error';
}
} catch {
status.textContent = ' failed';
}
});
import { initGuestbookSign } from '../../scripts/guestbook-sign';
initGuestbookSign();
</script>
</Layout>

View file

@ -2,8 +2,8 @@
import { getCollection } from 'astro:content';
import Layout from '../layouts/Layout.astro';
import { getApprovedEntries, type GuestbookEntry } from '../lib/db';
import { formatDate, extractDomain } from '../lib/format';
import { organizePostsByCategory, getSlug } from '../lib/posts';
import { formatDate, extractDomain, formatListItem } from '../lib/format';
import { organizePostsByCategory, getSlug } from '../lib/md';
import { getTxtFiles } from '../lib/txt';
const posts = await getCollection('md', ({ data }) => data.draft !== true);
@ -30,7 +30,7 @@ try {
<details open>
<summary>{category}</summary>
<pre set:html={[
...categoryPosts.slice(0, 10).map(post => `<span class="muted">${formatDate(post.data.date)}</span> <a href="/md/${getSlug(post.id)}">${post.data.title}</a>${post.data.pinned ? ' [pinned]' : ''}`),
...categoryPosts.slice(0, 10).map(post => formatListItem(post.data.date, `/md/${getSlug(post.id)}`, post.data.title, { pinned: post.data.pinned })),
...(categoryPosts.length > 10 ? [`<a href="/md/">+${categoryPosts.length - 10} more</a>`] : [])
].join('\n')} />
</details>
@ -40,7 +40,7 @@ try {
<details open>
<summary>txt</summary>
<pre set:html={[
...txtFiles.slice(0, 10).map(f => `<span class="muted">${formatDate(f.date)}</span> <a href="/txt/${f.name}">${f.name}</a>${f.pinned ? ' [pinned]' : ''}`),
...txtFiles.slice(0, 10).map(f => formatListItem(f.date, `/txt/${f.name}`, f.name, { pinned: f.pinned })),
...(txtFiles.length > 10 ? [`<a href="/txt/">+${txtFiles.length - 10} more</a>`] : [])
].join('\n')} />
</details>
@ -48,7 +48,7 @@ try {
<details open>
<summary>bookmarks</summary>
<pre set:html={[
...bookmarks.slice(0, 10).map(b => `<span class="muted">${formatDate(b.data.date)}</span> <a href="${b.data.url}">${b.data.title}</a> <span class="muted">(${extractDomain(b.data.url)})</span>`),
...bookmarks.slice(0, 10).map(b => formatListItem(b.data.date, b.data.url, b.data.title, { suffix: `<span class="muted">(${extractDomain(b.data.url)})</span>` })),
...(bookmarks.length > 10 ? [`<a href="/bookmarks/">+${bookmarks.length - 10} more</a>`] : [])
].join('\n')} />
</details>
@ -70,33 +70,7 @@ try {
</details>
<script>
document.getElementById('sign-guestbook')?.addEventListener('click', async (e) => {
e.preventDefault();
const status = document.getElementById('guestbook-status')!;
const name = prompt('name:');
if (!name) return;
const message = prompt('message:');
if (!message) return;
const url = prompt('url (optional):');
try {
const res = await fetch('/api/guestbook', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, message, url: url || null }),
});
if (res.ok) {
status.textContent = ' thanks! pending approval.';
} else {
status.textContent = ' error';
}
} catch {
status.textContent = ' failed';
}
});
import { initGuestbookSign } from '../scripts/guestbook-sign';
initGuestbookSign();
</script>
</Layout>

View file

@ -2,7 +2,7 @@
import { getCollection, render } from 'astro:content';
import Layout from '../../layouts/Layout.astro';
import { formatDate } from '../../lib/format';
import { getSlug } from '../../lib/posts';
import { getSlug } from '../../lib/md';
export async function getStaticPaths() {
const posts = await getCollection('md', ({ data }) => data.draft !== true);

View file

@ -1,8 +1,8 @@
---
import { getCollection } from 'astro:content';
import Layout from '../../layouts/Layout.astro';
import { formatDate } from '../../lib/format';
import { organizePostsByCategory, getSlug } from '../../lib/posts';
import { formatListItem } from '../../lib/format';
import { organizePostsByCategory, getSlug } from '../../lib/md';
const posts = await getCollection('md', ({ data }) => data.draft !== true);
const { grouped, categories: sortedCategories } = organizePostsByCategory(posts);
@ -12,7 +12,7 @@ const { grouped, categories: sortedCategories } = organizePostsByCategory(posts)
{sortedCategories.map(category => (
<details open>
<summary>{category}</summary>
<pre set:html={grouped[category].map(post => `<span class="muted">${formatDate(post.data.date)}</span> <a href="/md/${getSlug(post.id)}">${post.data.title}</a>${post.data.pinned ? ' [pinned]' : ''}`).join('\n')} />
<pre set:html={grouped[category].map(post => formatListItem(post.data.date, `/md/${getSlug(post.id)}`, post.data.title, { pinned: post.data.pinned })).join('\n')} />
</details>
))}
</Layout>

View file

@ -1,6 +1,6 @@
import { getCollection } from 'astro:content';
import type { APIContext } from 'astro';
import { getSlug } from '../lib/posts';
import { getSlug } from '../lib/md';
import { getTxtFileNames } from '../lib/txt';
export const prerender = false;

View file

@ -1,6 +1,6 @@
import { getCollection } from 'astro:content';
import type { APIContext } from 'astro';
import { getSlug } from '../lib/posts';
import { getSlug } from '../lib/md';
import { getTxtFileNames } from '../lib/txt';
const SUBDOMAINS = [

View file

@ -1,6 +1,6 @@
---
import Layout from '../../layouts/Layout.astro';
import { formatDate } from '../../lib/format';
import { formatListItem } from '../../lib/format';
import { getTxtFiles } from '../../lib/txt';
const txtFiles = getTxtFiles();
@ -9,6 +9,6 @@ const txtFiles = getTxtFiles();
<details open>
<summary>txt</summary>
<pre set:html={txtFiles.map(f => `<span class="muted">${formatDate(f.date)}</span> <a href="/txt/${f.name}">${f.name}</a>${f.pinned ? ' [pinned]' : ''}`).join('\n')} />
<pre set:html={txtFiles.map(f => formatListItem(f.date, `/txt/${f.name}`, f.name, { pinned: f.pinned })).join('\n')} />
</details>
</Layout>

View file

@ -0,0 +1,30 @@
export function initGuestbookSign() {
document.getElementById('sign-guestbook')?.addEventListener('click', async (e) => {
e.preventDefault();
const status = document.getElementById('guestbook-status')!;
const name = prompt('name:');
if (!name) return;
const message = prompt('message:');
if (!message) return;
const url = prompt('url (optional):');
try {
const res = await fetch('/api/guestbook', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, message, url: url || null }),
});
if (res.ok) {
status.textContent = ' thanks! pending approval.';
} else {
status.textContent = ' error';
}
} catch {
status.textContent = ' failed';
}
});
}