@@ -598,15 +770,20 @@ export default function AdminPage() {
key={post.slug}
className={`border rounded-lg p-3 flex items-center gap-3 justify-between ${pinned.includes(post.slug) ? 'bg-yellow-100 border-yellow-400' : ''}`}
>
-
-
{post.title}
-
{post.date}
-
{post.summary}
+
+ {pinned.includes(post.slug) && (
+
📌
+ )}
+
+
{post.title}
+
{post.date}
+
{post.summary}
+
diff --git a/src/app/api/admin/password/route.ts b/src/app/api/admin/password/route.ts
new file mode 100644
index 0000000..dae235b
--- /dev/null
+++ b/src/app/api/admin/password/route.ts
@@ -0,0 +1,67 @@
+import { NextResponse } from 'next/server';
+import fs from 'fs';
+import path from 'path';
+import bcrypt from 'bcrypt';
+
+const adminPath = path.join(process.cwd(), 'posts', 'admin.json');
+const tempPath = path.join(process.cwd(), 'posts', 'admin.json.tmp');
+const BCRYPT_SALT_ROUNDS = 12; // Stronger than minimum recommended
+
+// Generate a bcrypt hash for 'admin' at runtime if needed
+async function getDefaultHash() {
+ return await bcrypt.hash('admin', BCRYPT_SALT_ROUNDS);
+}
+
+async function getAdminData() {
+ try {
+ if (fs.existsSync(adminPath)) {
+ const data = JSON.parse(fs.readFileSync(adminPath, 'utf8'));
+ if (typeof data.hash === 'string') return data;
+ }
+ } catch (e) {
+ // Log error and continue to fallback
+ }
+ // Fallback: default admin/admin
+ return { hash: await getDefaultHash() };
+}
+
+function setAdminDataAtomic(hash: string) {
+ // Write to a temp file first, then rename
+ fs.writeFileSync(tempPath, JSON.stringify({ hash }, null, 2), 'utf8');
+ fs.renameSync(tempPath, adminPath);
+}
+
+export async function GET() {
+ // Check if a password is set (if admin.json exists)
+ const exists = fs.existsSync(adminPath);
+ return NextResponse.json({ passwordSet: exists });
+}
+
+export async function POST(request: Request) {
+ const body = await request.json();
+ const { password, mode } = body;
+ if (!password || typeof password !== 'string') {
+ return NextResponse.json({ error: 'Kein Passwort angegeben.' }, { status: 400 });
+ }
+ if (Buffer.byteLength(password, 'utf8') > 72) {
+ return NextResponse.json({ error: 'Passwort zu lang (max. 72 Zeichen).' }, { status: 400 });
+ }
+ const { hash } = await getAdminData();
+ if (mode === 'login') {
+ const match = await bcrypt.compare(password, hash);
+ if (match) {
+ return NextResponse.json({ success: true });
+ } else {
+ return NextResponse.json({ error: 'Falsches Passwort.' }, { status: 401 });
+ }
+ } else {
+ // Set/change password atomically
+ const newHash = await bcrypt.hash(password, BCRYPT_SALT_ROUNDS);
+ try {
+ setAdminDataAtomic(newHash);
+ return NextResponse.json({ success: true });
+ } catch (e) {
+ return NextResponse.json({ error: 'Fehler beim Speichern des Passworts.' }, { status: 500 });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/admin/posts/route.ts b/src/app/api/admin/posts/route.ts
index fe12743..f9a001b 100644
--- a/src/app/api/admin/posts/route.ts
+++ b/src/app/api/admin/posts/route.ts
@@ -36,4 +36,23 @@ export async function POST(request: Request) {
{ status: 500 }
);
}
+}
+
+export async function PATCH(request: Request) {
+ try {
+ const body = await request.json();
+ const { pinned } = body; // expects an array of slugs
+ if (!Array.isArray(pinned)) {
+ return NextResponse.json({ error: 'Invalid pinned data' }, { status: 400 });
+ }
+ const pinnedPath = path.join(postsDirectory, 'pinned.json');
+ fs.writeFileSync(pinnedPath, JSON.stringify(pinned, null, 2), 'utf8');
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ console.error('Error updating pinned.json:', error);
+ return NextResponse.json(
+ { error: 'Error updating pinned.json' },
+ { status: 500 }
+ );
+ }
}
\ No newline at end of file
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 35b3a16..b6e0386 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -3,6 +3,9 @@ import { Inter } from 'next/font/google';
import './globals.css';
import Link from 'next/link';
import Head from 'next/head';
+import AboutButton from './AboutButton';
+import BadgeButton from './BadgeButton';
+import HeaderButtons from './HeaderButtons';
const inter = Inter({ subsets: ['latin'] });
@@ -11,6 +14,21 @@ export const metadata: Metadata = {
description: 'Ein Blog von Sebastian Zinkl, gebaut mit Next.js und Markdown',
};
+const PersonIcon = (
+
+);
+
+const InfoIcon = (
+
+);
+
export default function RootLayout({
children,
}: {
@@ -42,9 +60,9 @@ export default function RootLayout({