chahinebrini 2c33ba55a4 fix(backend): username (Login-Identifikator) aus öffentlichen Payloads entfernt
community/posts.get.ts + social/profile/[userId].get.ts lieferten neben
nickname auch username an fremde Clients — username ist der Login-
Identifikator ({username}@rebreak.internal) und verletzt die Nickname-
Anonymitäts-Invariante (REQ-COMM-005 / R-DATA-07) + exponiert das halbe
Login-Credential-Paar. Frontend rendert das Feld nirgends (verifiziert);
totes Typ-Feld in stores/community.ts entfernt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 06:36:16 +02:00

138 lines
5.0 KiB
TypeScript

import { getPosts } from "../../db/community";
import { getFollowingSet } from "../../db/social";
/** GET /api/community/posts?category=all&page=1&limit=20 */
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig();
const lyraBotUserId = config.lyraBotUserId || null;
const rebreakBotUserId = config.rebreakBotUserId || null;
const query = getQuery(event);
const category = (query.category as string) || "all";
const page = Math.max(1, parseInt((query.page as string) || "1"));
const limit = Math.min(50, parseInt((query.limit as string) || "20"));
const userIdParam = (query.userId as string) || null;
// Lyra / ReBreak → nach userId filtern; expliziter userIdParam hat Vorrang
let filterUserId: string | null = userIdParam;
let dbCategory = category;
if (userIdParam) {
// Explizite userId: Bot-Kategorie-Mapping irrelevant, category-Filter weglassen außer echte Kategorien
if (category === "lyra" || category === "rebreak") {
dbCategory = "all";
}
} else if (category === "lyra") {
filterUserId = lyraBotUserId;
dbCategory = "all";
} else if (category === "rebreak") {
filterUserId = rebreakBotUserId;
dbCategory = "all";
}
// Auth-User optional (Gäste können auch lesen)
let currentUserId: string | null = null;
try {
const u = await requireUser(event);
currentUserId = u.id;
} catch {}
const {
posts,
userLikes,
challengeStatuses,
domainSubmissions,
userDomainVotes,
userScores,
submissionVoters,
} = await getPosts(dbCategory, page, limit, currentUserId, filterUserId);
// Batch: isFollowing für alle Autoren
const authorIds = [
...new Set(posts.map((p) => p.userId).filter((id): id is string => !!id)),
];
const followingSet =
currentUserId && authorIds.length > 0
? await getFollowingSet(currentUserId, authorIds)
: new Set<string>();
return posts.map((p) => {
const a = p.author;
return {
id: p.id,
category: p.category,
content:
p.category === "game_share"
? p.content.split("\n").slice(1).join("\n").trim()
: p.content,
imageUrl: (p as any).imageUrl ?? null,
challengeId: (p as any).challengeId ?? null,
challengeStatus: (p as any).challengeId
? challengeStatuses[(p as any).challengeId]?.status ?? "OPEN"
: null,
gameName: (p as any).challengeId
? challengeStatuses[(p as any).challengeId]?.gameType ?? null
: (p as any).gameName ?? (p.category === "game_share"
? p.content.split("\n")[0] ?? null
: null),
opponentName: (p as any).challengeId
? challengeStatuses[(p as any).challengeId]?.opponentName ?? null
: null,
isLive: (p as any).challengeId
? challengeStatuses[(p as any).challengeId]?.isLive ?? false
: false,
likesCount: p.likesCount ?? 0,
dislikesCount: p.dislikesCount ?? 0,
commentsCount: p.commentsCount ?? 0,
repostsCount: (p as any).repostsCount ?? 0,
isAnonymous: p.isAnonymous,
createdAt: p.createdAt,
userLike: userLikes[p.id] ?? null,
submission: domainSubmissions[p.id]
? {
...domainSubmissions[p.id],
yesVoters: submissionVoters[domainSubmissions[p.id].id]?.yes ?? [],
noVoters: submissionVoters[domainSubmissions[p.id].id]?.no ?? [],
}
: null,
userVote: userDomainVotes[p.id] ?? null,
i18nKey: (p as any).i18nKey ?? null,
repostOfId: (p as any).repostOfId ?? null,
repostOf: (p as any).repostOf
? {
id: (p as any).repostOf.id,
content: (p as any).repostOf.content,
imageUrl: (p as any).repostOf.imageUrl ?? null,
author: {
id: (p as any).repostOf.userId ?? null,
nickname:
(p as any).repostOf.author?.nickname ??
(p as any).repostOf.author?.username ??
"Nutzer",
avatar: (p as any).repostOf.author?.avatar ?? null,
plan: (p as any).repostOf.author?.plan ?? "free",
tier: userScores[(p as any).repostOf.userId ?? ""] ?? "beginner",
},
}
: null,
author: {
id: p.userId ?? null,
// username ist der Login-Identifikator (→ {username}@rebreak.internal)
// und darf NIE an fremde Clients gehen — Anonymitäts-Invariante (nickname-only).
nickname: a?.nickname ?? a?.username ?? "Nutzer",
avatar: a?.avatar ?? null,
plan: (a as any)?.plan ?? "free",
tier: userScores[p.userId ?? ""] ?? "beginner",
isFollowing: p.userId ? followingSet.has(p.userId) : false,
},
isBot:
!!(lyraBotUserId && p.userId === lyraBotUserId) ||
!!(rebreakBotUserId && p.userId === rebreakBotUserId),
botType:
lyraBotUserId && p.userId === lyraBotUserId
? "lyra"
: rebreakBotUserId && p.userId === rebreakBotUserId
? "rebreak"
: undefined,
};
});
});