refactor:
This commit is contained in:
parent
38b5413a37
commit
7f01bec7e6
17 changed files with 121 additions and 132 deletions
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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[] {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
30
www/src/scripts/guestbook-sign.ts
Normal file
30
www/src/scripts/guestbook-sign.ts
Normal 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';
|
||||
}
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue