From 8a3ba8ff40e747db5c089fb7e1b1571b58685f43 Mon Sep 17 00:00:00 2001 From: adevopg Date: Mon, 13 Jul 2026 01:07:18 +0000 Subject: [PATCH] Foro: restaurar posts y temas borrados (moderadores) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lib/forum.ts: getTopic/getPosts/countPosts aceptan includeDeleted (mods ven lo borrado); Post.deleted y TopicFull.deleted. - API: PUT /api/forum/post restaura post (mod-only); moderate action 'restore' restaura tema (usa includeDeleted al leer el tema). - UI: posts borrados atenuados con badge y botón Restaurar (PostActions); TopicModBar muestra banner + Restaurar tema cuando el tema está borrado. - i18n es/en: Forum.restore, restoreTopic, deletedMark. Verificado: build OK, PUT post y moderate restore 401 sin sesión, /forum 200. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../[locale]/forum/topic/[topicId]/page.tsx | 30 +++++++++----- web-next/app/api/forum/moderate/route.ts | 7 +++- web-next/app/api/forum/post/route.ts | 15 +++++++ web-next/components/PostActions.tsx | 41 ++++++++++++++++++- web-next/components/TopicModBar.tsx | 18 ++++++++ web-next/lib/forum.ts | 19 ++++++--- web-next/messages/en.json | 5 ++- web-next/messages/es.json | 5 ++- 8 files changed, 119 insertions(+), 21 deletions(-) diff --git a/web-next/app/[locale]/forum/topic/[topicId]/page.tsx b/web-next/app/[locale]/forum/topic/[topicId]/page.tsx index 2f5a5df..83d60f7 100644 --- a/web-next/app/[locale]/forum/topic/[topicId]/page.tsx +++ b/web-next/app/[locale]/forum/topic/[topicId]/page.tsx @@ -25,14 +25,14 @@ export default async function TopicPage({ const t = await getTranslations('Forum') const id = Number(topicId) - const topic = await getTopic(id) - if (!topic) notFound() - const total = await countPosts(id) - const totalPages = Math.max(1, Math.ceil(total / PER_PAGE)) - const page = Math.min(Math.max(1, Number((await searchParams).page) || 1), totalPages) - const posts = await getPosts(id, (page - 1) * PER_PAGE, PER_PAGE) const session = await getSession() const isMod = await forumIsModerator(session) + const topic = await getTopic(id, isMod) + if (!topic) notFound() + const total = await countPosts(id, isMod) + const totalPages = Math.max(1, Math.ceil(total / PER_PAGE)) + const page = Math.min(Math.max(1, Number((await searchParams).page) || 1), totalPages) + const posts = await getPosts(id, (page - 1) * PER_PAGE, PER_PAGE, isMod) const moveForums = isMod ? (await listForumsForMove()).filter((f) => f.id !== topic!.forum_id) : [] const canReply = Boolean(session.username) && (!topic!.locked || isMod) @@ -46,18 +46,28 @@ export default async function TopicPage({

{topic!.sticky && [{t('pinned')}]} {topic!.locked && [{t('locked')}]} + {topic!.deleted && [{t('deletedMark')}]} {topic!.name}

{isMod && ( - + )}
{posts.map((post) => ( -
+
- {post.poster} + + {post.poster} + {post.deleted && [{t('deletedMark')}]} + {post.time && ( {new Date(post.time).toLocaleString(locale)} @@ -70,7 +80,7 @@ export default async function TopicPage({ dangerouslySetInnerHTML={{ __html: post.text }} /> {canEditPost(session, isMod, post.poster_id) && ( - + )}
))} diff --git a/web-next/app/api/forum/moderate/route.ts b/web-next/app/api/forum/moderate/route.ts index f395dec..8b4a7dd 100644 --- a/web-next/app/api/forum/moderate/route.ts +++ b/web-next/app/api/forum/moderate/route.ts @@ -3,7 +3,7 @@ import { getTopic, getForum } from '@/lib/forum' import { setTopicFlag, moveTopic } from '@/lib/forum-write' import { forumIsModerator } from '@/lib/forum-perm' -type Action = 'lock' | 'unlock' | 'sticky' | 'unsticky' | 'delete' | 'move' +type Action = 'lock' | 'unlock' | 'sticky' | 'unsticky' | 'delete' | 'restore' | 'move' /** Moderación de temas: bloquear, fijar, borrar. Solo moderadores. */ export async function POST(request: Request) { @@ -17,7 +17,7 @@ export async function POST(request: Request) { const action = String(b.action ?? '') as Action if (!topicId) return Response.json({ success: false, error: 'invalidRequest' }, { status: 400 }) - const topic = await getTopic(topicId) + const topic = await getTopic(topicId, true) // incluye borrados (ya verificado que es mod) if (!topic) return Response.json({ success: false, error: 'topicNotFound' }, { status: 404 }) switch (action) { @@ -36,6 +36,9 @@ export async function POST(request: Request) { case 'delete': await setTopicFlag(topicId, 'deleted', true) return Response.json({ success: true, forumId: topic.forum_id }) + case 'restore': + await setTopicFlag(topicId, 'deleted', false) + break case 'move': { const newForumId = Number(b.forumId) if (!newForumId || newForumId === topic.forum_id) { diff --git a/web-next/app/api/forum/post/route.ts b/web-next/app/api/forum/post/route.ts index a160520..49e1311 100644 --- a/web-next/app/api/forum/post/route.ts +++ b/web-next/app/api/forum/post/route.ts @@ -6,6 +6,21 @@ import { plainLength } from '@/lib/forum-sanitize' const MIN_TEXT_LEN = 5 +/** Restaurar un post borrado. Solo moderadores. */ +export async function PUT(request: Request) { + const session = await getSession() + if (!session.accountId) return Response.json({ success: false, error: 'notAuthenticated' }, { status: 401 }) + if (!(await forumIsModerator(session))) return Response.json({ success: false, error: 'forbidden' }, { status: 403 }) + + let b: Record = {} + try { b = await request.json() } catch { return Response.json({ success: false, error: 'invalidRequest' }, { status: 400 }) } + const postId = Number(b.postId) + if (!postId) return Response.json({ success: false, error: 'invalidRequest' }, { status: 400 }) + + await setPostDeleted(postId, false) + return Response.json({ success: true }) +} + /** Editar el texto de un post propio (o cualquiera si es moderador). */ export async function PATCH(request: Request) { const session = await getSession() diff --git a/web-next/components/PostActions.tsx b/web-next/components/PostActions.tsx index d96de74..9f0e435 100644 --- a/web-next/components/PostActions.tsx +++ b/web-next/components/PostActions.tsx @@ -5,7 +5,15 @@ import { useTranslations } from 'next-intl' import { useRouter } from '@/i18n/navigation' import { RichTextArea } from './RichTextArea' -export function PostActions({ postId, initialText }: { postId: number; initialText: string }) { +export function PostActions({ + postId, + initialText, + deleted = false, +}: { + postId: number + initialText: string + deleted?: boolean +}) { const t = useTranslations('Forum') const router = useRouter() const [editing, setEditing] = useState(false) @@ -38,6 +46,26 @@ export function PostActions({ postId, initialText }: { postId: number; initialTe } } + async function restore() { + if (busy) return + setBusy(true) + setError(null) + try { + const res = await fetch('/api/forum/post', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify({ postId }), + }) + if ((await res.json()).success) router.refresh() + else setError(t('genericError')) + } catch { + setError(t('genericError')) + } finally { + setBusy(false) + } + } + async function remove() { if (busy || !confirm(t('confirmDeletePost'))) return setBusy(true) @@ -80,6 +108,17 @@ export function PostActions({ postId, initialText }: { postId: number; initialTe ) } + if (deleted) { + return ( +
+ + {error && {error}} +
+ ) + } + return (
+
+ ) + } + return (
{t('moderation')}: diff --git a/web-next/lib/forum.ts b/web-next/lib/forum.ts index 67d57b7..a03e722 100644 --- a/web-next/lib/forum.ts +++ b/web-next/lib/forum.ts @@ -37,6 +37,7 @@ export interface Post { text: string time: string | null edited: boolean + deleted: boolean } export interface TopicFull { @@ -46,6 +47,7 @@ export interface TopicFull { poster_id: number | null locked: boolean sticky: boolean + deleted: boolean } export interface SearchResult { @@ -148,10 +150,11 @@ export async function getTopics(forumId: number, offset = 0, limit = 20): Promis } } -export async function getTopic(id: number): Promise { +export async function getTopic(id: number, includeDeleted = false): Promise { try { + const where = includeDeleted ? 'id = ?' : 'id = ? AND deleted = 0' const [rows] = await db(DB.web).query( - 'SELECT id, name, forum_id, poster_id, locked, sticky FROM forum_topics WHERE id = ? AND deleted = 0', + `SELECT id, name, forum_id, poster_id, locked, sticky, deleted FROM forum_topics WHERE ${where}`, [id], ) const r = rows[0] @@ -163,6 +166,7 @@ export async function getTopic(id: number): Promise { poster_id: r.poster_id ?? null, locked: r.locked === 1, sticky: r.sticky === 1, + deleted: r.deleted === 1, } : null } catch { @@ -170,10 +174,11 @@ export async function getTopic(id: number): Promise { } } -export async function countPosts(topicId: number): Promise { +export async function countPosts(topicId: number, includeDeleted = false): Promise { try { + const del = includeDeleted ? '' : 'AND deleted = 0' const [rows] = await db(DB.web).query( - 'SELECT COUNT(*) AS n FROM forum_posts WHERE topic_id = ? AND deleted = 0', + `SELECT COUNT(*) AS n FROM forum_posts WHERE topic_id = ? ${del}`, [topicId], ) return Number(rows[0]?.n ?? 0) @@ -182,10 +187,11 @@ export async function countPosts(topicId: number): Promise { } } -export async function getPosts(topicId: number, offset = 0, limit = 15): Promise { +export async function getPosts(topicId: number, offset = 0, limit = 15, includeDeleted = false): Promise { try { + const del = includeDeleted ? '' : 'AND deleted = 0' const [rows] = await db(DB.web).query( - 'SELECT id, poster, poster_id, text, time, created_at, updated_at FROM forum_posts WHERE topic_id = ? AND deleted = 0 ORDER BY time ASC, id ASC LIMIT ? OFFSET ?', + `SELECT id, poster, poster_id, text, time, created_at, updated_at, deleted FROM forum_posts WHERE topic_id = ? ${del} ORDER BY time ASC, id ASC LIMIT ? OFFSET ?`, [topicId, limit, offset], ) return rows.map((r) => ({ @@ -198,6 +204,7 @@ export async function getPosts(topicId: number, offset = 0, limit = 15): Promise edited: Boolean( r.updated_at && r.created_at && new Date(r.updated_at).getTime() - new Date(r.created_at).getTime() > 2000, ), + deleted: r.deleted === 1, })) } catch { return [] diff --git a/web-next/messages/en.json b/web-next/messages/en.json index 9e2a32e..9d09ca3 100644 --- a/web-next/messages/en.json +++ b/web-next/messages/en.json @@ -294,7 +294,10 @@ "resultsCount": "{count} result(s) for “{query}”.", "in": "in", "replyOnLastPage": "Go to the last page to reply", - "moveTopic": "Move to" + "moveTopic": "Move to", + "restore": "Restore", + "restoreTopic": "Restore topic", + "deletedMark": "Deleted" }, "Admin": { "title": "Admin panel", diff --git a/web-next/messages/es.json b/web-next/messages/es.json index 4f10a73..45d6a2c 100644 --- a/web-next/messages/es.json +++ b/web-next/messages/es.json @@ -294,7 +294,10 @@ "resultsCount": "{count} resultado(s) para «{query}».", "in": "en", "replyOnLastPage": "Ir a la última página para responder", - "moveTopic": "Mover a" + "moveTopic": "Mover a", + "restore": "Restaurar", + "restoreTopic": "Restaurar tema", + "deletedMark": "Borrado" }, "Admin": { "title": "Panel de administración",