dialog: eigene Diskussion pro Beitrag (MVP, flach, eingeladene-only)
- DB: public.comments (thread, parent_id, user_id, author_name/-avatar, body, deleted) - API: GET /api/comments (öffentlich lesen), POST/DELETE (eingeloggt), POST /api/auth/login (Token fürs Widget) - Vanilla-Widget static/dialog.js: Karten mit Name+Bild, flacher Dialog mit optionalem Bezug (↳ Antwort auf), Inline-Login, Löschen (eigene/Admin) - eingebettet in single.html (thread = Beitrags-Pfad), Styling im Theme-Look - Autorname/-bild kommen aus dem Profil (data/authors.json) Realtime (Supabase) folgt als nächster Schritt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import publish from './routes/publish.js';
|
||||
import upload from './routes/upload.js';
|
||||
import profile from './routes/profile.js';
|
||||
import users from './routes/users.js';
|
||||
import { listComments, createComment, deleteComment, login } from './routes/comments.js';
|
||||
import { requireAuth } from './auth.js';
|
||||
|
||||
const SITE_DIR = process.env.SITE_DIR || '/site';
|
||||
@@ -18,9 +19,14 @@ const app = new Hono();
|
||||
|
||||
// --- API ---
|
||||
app.get('/api/health', (c) => c.json({ ok: true, hugo: '0.161.1+extended' }));
|
||||
// Alles unter /api/* (ausser /health oben) braucht ein gültiges Supabase-Token.
|
||||
// Öffentlich (ohne Login): Dialog lesen + Login fürs Dialog-Widget.
|
||||
app.get('/api/comments', listComments);
|
||||
app.post('/api/auth/login', login);
|
||||
// Alles weitere unter /api/* braucht ein gültiges Supabase-Token.
|
||||
app.use('/api/*', requireAuth);
|
||||
app.get('/api/me', (c) => c.json({ email: c.get('email'), isAdmin: c.get('isAdmin') }));
|
||||
app.post('/api/comments', createComment);
|
||||
app.delete('/api/comments/:id', deleteComment);
|
||||
app.route('/api/content', content);
|
||||
app.route('/api/preview', preview);
|
||||
app.route('/api/publish', publish);
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { supabase } from '../supabase.js';
|
||||
|
||||
// Dialog: flache Wortmeldungen pro Thread (= Beitrags-Pfad), optionaler Bezug.
|
||||
const SITE_DIR = process.env.SITE_DIR || '/site';
|
||||
|
||||
// Anzeigename + Avatar aus dem Profil (data/authors.json), Fallback = Mail-Teil.
|
||||
async function profileFor(email) {
|
||||
try {
|
||||
const all = JSON.parse(await readFile(path.join(SITE_DIR, 'data', 'authors.json'), 'utf8'));
|
||||
return all[email] || null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
const COLS = 'id,thread,parent_id,author_name,author_avatar,body,created_at,deleted';
|
||||
|
||||
// ÖFFENTLICH: Wortmeldungen eines Threads lesen.
|
||||
export async function listComments(c) {
|
||||
const thread = c.req.query('thread');
|
||||
if (!thread) return c.json({ error: 'thread fehlt' }, 400);
|
||||
const { data, error } = await supabase
|
||||
.from('comments').select(COLS).eq('thread', thread).order('created_at', { ascending: true });
|
||||
if (error) return c.json({ error: error.message }, 500);
|
||||
const out = (data || []).map((r) => (r.deleted ? { ...r, body: '[gelöscht]', author_avatar: null } : r));
|
||||
return c.json(out);
|
||||
}
|
||||
|
||||
// EINGELOGGT: Wortmeldung schreiben.
|
||||
export async function createComment(c) {
|
||||
const user = c.get('user');
|
||||
const email = c.get('email');
|
||||
const { thread, body, parent_id } = await c.req.json();
|
||||
if (!thread || !body || !body.trim()) return c.json({ error: 'thread und Text nötig' }, 400);
|
||||
|
||||
const prof = await profileFor(email);
|
||||
const row = {
|
||||
thread,
|
||||
parent_id: parent_id || null,
|
||||
user_id: user.id,
|
||||
author_name: prof?.name || email.split('@')[0],
|
||||
author_avatar: prof?.avatar || null,
|
||||
body: body.trim(),
|
||||
};
|
||||
const { data, error } = await supabase.from('comments').insert(row).select(COLS).single();
|
||||
if (error) return c.json({ error: error.message }, 400);
|
||||
return c.json(data, 201);
|
||||
}
|
||||
|
||||
// EINGELOGGT: eigene Wortmeldung (oder als Admin jede) löschen.
|
||||
export async function deleteComment(c) {
|
||||
const user = c.get('user');
|
||||
const isAdmin = c.get('isAdmin');
|
||||
const id = c.req.param('id');
|
||||
const { data: row, error: e1 } = await supabase.from('comments').select('user_id').eq('id', id).single();
|
||||
if (e1 || !row) return c.json({ error: 'Nicht gefunden' }, 404);
|
||||
if (!isAdmin && row.user_id !== user.id) return c.json({ error: 'Kein Recht' }, 403);
|
||||
const { error } = await supabase.from('comments').update({ deleted: true }).eq('id', id);
|
||||
if (error) return c.json({ error: error.message }, 400);
|
||||
return c.json({ ok: true });
|
||||
}
|
||||
|
||||
// ÖFFENTLICH: Login fürs Dialog-Widget — gibt das User-Token zurück.
|
||||
export async function login(c) {
|
||||
const { email, password } = await c.req.json();
|
||||
if (!email || !password) return c.json({ error: 'E-Mail und Passwort nötig' }, 400);
|
||||
const { data, error } = await supabase.auth.signInWithPassword({ email, password });
|
||||
if (error) return c.json({ error: error.message }, 401);
|
||||
const prof = await profileFor((data.user.email || '').toLowerCase());
|
||||
return c.json({
|
||||
access_token: data.session.access_token,
|
||||
email: data.user.email,
|
||||
name: prof?.name || (data.user.email || '').split('@')[0],
|
||||
});
|
||||
}
|
||||
@@ -31,3 +31,21 @@ create index if not exists posts_section_idx on public.posts (section);
|
||||
-- RLS aktivieren; die api nutzt den Service-Key (umgeht RLS). Wenn das
|
||||
-- Frontend später direkt liest, hier gezielte Policies ergänzen.
|
||||
alter table public.posts enable row level security;
|
||||
|
||||
-- ── Dialog / Diskussionen ───────────────────────────────────────────────
|
||||
-- Thread = Pfad des Beitrags (z.B. /library/software/stack/). Flache Wortmeldungen
|
||||
-- mit optionalem Bezug (parent_id). Idempotent — auf bestehende DB anwendbar.
|
||||
create table if not exists public.comments (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
thread text not null,
|
||||
parent_id uuid references public.comments(id) on delete cascade,
|
||||
user_id uuid,
|
||||
author_name text,
|
||||
author_avatar text,
|
||||
body text not null,
|
||||
created_at timestamptz not null default now(),
|
||||
deleted boolean not null default false
|
||||
);
|
||||
create index if not exists comments_thread_idx on public.comments (thread, created_at);
|
||||
alter table public.comments enable row level security;
|
||||
grant all on public.comments to anon, authenticated, service_role;
|
||||
|
||||
Reference in New Issue
Block a user