Kommentar-Sichtbarkeit: Öffentlich/Angemeldete/Nur ich + Badges + Server-Filter

This commit is contained in:
Dotty Dotter 2026-04-10 22:40:27 +02:00
parent ad97a76824
commit 5d2a0338ee
2 changed files with 55 additions and 11 deletions

View File

@ -153,9 +153,12 @@ async def add_comment(user_id: str, user_name: str, drucksache: str,
async def get_comments(drucksache: str, user_id: Optional[str] = None) -> list[dict]:
"""Get comments for a drucksache. Filters by visibility:
- 'all' comments are always visible
- group-scoped comments only visible if user is in that group (TODO)
"""Get comments for a drucksache, server-seitig nach Sichtbarkeit gefiltert.
- 'all': immer sichtbar
- 'authenticated': nur wenn user_id gesetzt (eingeloggt)
- 'private': nur für den Autor
- 'group:XYZ': für Gruppenmitglieder (TODO: Keycloak-Gruppen-Check)
"""
async with aiosqlite.connect(settings.db_path) as db:
db.row_factory = aiosqlite.Row
@ -163,7 +166,22 @@ async def get_comments(drucksache: str, user_id: Optional[str] = None) -> list[d
"SELECT * FROM comments WHERE drucksache=? ORDER BY created_at",
(drucksache,),
)
return [dict(r) for r in await rows.fetchall()]
all_comments = [dict(r) for r in await rows.fetchall()]
# Server-seitig filtern (Defense in Depth — Client filtert auch)
result = []
for c in all_comments:
vis = c.get("visibility", "all")
if vis == "all":
result.append(c)
elif vis == "authenticated" and user_id:
result.append(c)
elif vis == "private" and user_id and c["user_id"] == user_id:
result.append(c)
elif vis.startswith("group:") and user_id:
# TODO: Keycloak-Gruppen-Membership prüfen
result.append(c)
return result
async def delete_comment(comment_id: int, user_id: str) -> bool:

View File

@ -1385,14 +1385,31 @@
if (!container) return;
try {
const comments = await fetch(`/api/comments?drucksache=${encodeURIComponent(drucksache)}`).then(r => r.json());
if (comments.length === 0) {
// Client-seitig filtern nach Sichtbarkeit
const visible = comments.filter(c => {
if (c.visibility === 'all') return true;
if (!currentUser) return false;
if (c.visibility === 'authenticated') return true;
if (c.visibility === 'private') return c.user_id === currentUser.sub;
// group:XYZ — TODO: gegen Keycloak-Gruppen prüfen
if (c.visibility?.startsWith('group:')) return true; // Platzhalter
return false;
});
if (visible.length === 0) {
container.innerHTML = '<span style="color:#aaa;font-size:0.85rem;">Noch keine Kommentare.</span>';
return;
}
container.innerHTML = comments.map(c => `
const visBadge = (v) => {
if (v === 'private') return '<span style="font-size:0.7rem;background:#f0f0f0;padding:0.1rem 0.3rem;border-radius:2px;" title="Nur für dich sichtbar">👤</span>';
if (v === 'authenticated') return '<span style="font-size:0.7rem;background:#e8f4f8;padding:0.1rem 0.3rem;border-radius:2px;" title="Nur für angemeldete Nutzer">🔒</span>';
if (v?.startsWith('group:')) return `<span style="font-size:0.7rem;background:#f0e6ff;padding:0.1rem 0.3rem;border-radius:2px;" title="Gruppe: ${v.substring(6)}">👥 ${v.substring(6)}</span>`;
return '<span style="font-size:0.7rem;background:#e8ffe8;padding:0.1rem 0.3rem;border-radius:2px;" title="Öffentlich sichtbar">🌐</span>';
};
container.innerHTML = visible.map(c => `
<div style="padding:0.4rem 0;border-bottom:1px solid #f0f0f0;font-size:0.85rem;">
<strong>${c.user_name || 'Anonym'}</strong>
<span style="color:#aaa;font-size:0.75rem;margin-left:0.5rem;">${new Date(c.created_at).toLocaleString('de-DE')}</span>
${visBadge(c.visibility)}
<span style="color:#aaa;font-size:0.75rem;margin-left:0.3rem;">${new Date(c.created_at).toLocaleString('de-DE')}</span>
${currentUser && currentUser.sub === c.user_id ? `<button onclick="deleteCommentUI(${c.id},'${drucksache}')" style="float:right;background:none;border:none;color:#dc3545;cursor:pointer;font-size:0.75rem;"></button>` : ''}
<div style="margin-top:0.2rem;">${c.text}</div>
</div>
@ -1401,12 +1418,15 @@
}
async function addCommentUI(drucksache) {
const input = document.getElementById('comment-input-' + drucksache.replace('/', '-'));
const safeDrs = drucksache.replace('/', '-');
const input = document.getElementById('comment-input-' + safeDrs);
const visSelect = document.getElementById('comment-visibility-' + safeDrs);
if (!input || !input.value.trim()) return;
const visibility = visSelect ? visSelect.value : 'all';
await fetch('/api/comment', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: `drucksache=${encodeURIComponent(drucksache)}&text=${encodeURIComponent(input.value)}`
body: `drucksache=${encodeURIComponent(drucksache)}&text=${encodeURIComponent(input.value)}&visibility=${visibility}`
});
input.value = '';
loadComments(drucksache);
@ -1940,11 +1960,17 @@
<span style="color:#aaa;font-size:0.85rem;">Lade Kommentare...</span>
</div>
${currentUser ? `
<div style="display:flex;gap:0.5rem;">
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:center;">
<input id="comment-input-${item.drucksache.replace('/','-')}" type="text"
placeholder="Kommentar schreiben..."
style="flex:1;padding:0.4rem;border:1px solid #ddd;border-radius:4px;font-size:0.85rem;"
style="flex:1;min-width:150px;padding:0.4rem;border:1px solid #ddd;border-radius:4px;font-size:0.85rem;"
onkeydown="if(event.key==='Enter')addCommentUI('${item.drucksache}')">
<select id="comment-visibility-${item.drucksache.replace('/','-')}"
style="padding:0.4rem;border:1px solid #ddd;border-radius:4px;font-size:0.8rem;color:#666;">
<option value="all">🌐 Öffentlich</option>
<option value="authenticated">🔒 Angemeldete</option>
<option value="private">👤 Nur ich</option>
</select>
<button onclick="addCommentUI('${item.drucksache}')"
style="padding:0.4rem 0.8rem;background:var(--color-blue);color:white;border:none;border-radius:4px;cursor:pointer;font-size:0.85rem;">
Senden