mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
feat(admin): show reported message content in report detail (#3149)
## Summary - include the reported event's complete stored content, author, creation time, and deletion state in the admin report detail response - resolve the event through a community-scoped join so an event ID collision cannot cross tenant boundaries - render the message only on report detail, with an explicit unavailable state when retention has removed it - preserve the existing report list contract so message bodies are not returned during queue browsing ## Security - the existing admin host/origin authorization runs before the detail database read; a route test pins that ordering - the target event is selected using both `events.community_id = moderation_reports.community_id` and `events.id = moderation_reports.target_event_id` - the client supplies only the report UUID; it cannot choose a community or arbitrary event ID - soft-deleted content is visible only through this restricted admin detail route and is labeled deleted - responses retain the admin API's `no-store`, CSP, `nosniff`, frame denial, and referrer policy middleware ## Testing - `pnpm -C admin-web check` - `pnpm -C admin-web test:e2e` (10 passed) - `cargo test -p buzz-db` (84 passed, 130 ignored) - `cargo clippy -p buzz-db -p buzz-relay --all-targets -- -D warnings` - focused admin authorization tests - pre-push Rust and desktop/Tauri suites passed The new Postgres integration test is ignored under the repository convention and will run when explicitly enabled against migrated Postgres. Local Postgres and Redis were unavailable, so the full `buzz-relay --lib` run had 8 existing infrastructure-dependent failures after 749 tests passed. --------- Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Signed-off-by: Kalvin Chau <kalvin@block.xyz> Signed-off-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz> Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Co-authored-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -7,7 +7,12 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
import { ApiFailure, request } from "./api";
|
||||
import type { FeedbackDetail, FeedbackSummary, Report } from "./types";
|
||||
import type {
|
||||
FeedbackDetail,
|
||||
FeedbackSummary,
|
||||
Report,
|
||||
ReportDetail as ReportDetailData,
|
||||
} from "./types";
|
||||
import { useResource } from "./useResource";
|
||||
|
||||
function usePath() {
|
||||
@@ -131,7 +136,10 @@ function Reports() {
|
||||
}
|
||||
|
||||
function ReportDetail({ id }: { id: string }) {
|
||||
const resource = useResource(() => request<Report>(`/reports/${id}`), id);
|
||||
const resource = useResource(
|
||||
() => request<ReportDetailData>(`/reports/${id}`),
|
||||
id,
|
||||
);
|
||||
return (
|
||||
<Page
|
||||
eyebrow="Moderation"
|
||||
@@ -164,6 +172,32 @@ function ReportDetail({ id }: { id: string }) {
|
||||
<dd>
|
||||
<code>{report.target}</code>
|
||||
</dd>
|
||||
{report.targetKind === "event" ? (
|
||||
<>
|
||||
<dt>Message</dt>
|
||||
<dd>
|
||||
{report.message ? (
|
||||
<div className="reported-message">
|
||||
{report.message.deletedAt ? (
|
||||
<span className="status">Deleted</span>
|
||||
) : null}
|
||||
<p>{report.message.content}</p>
|
||||
<div className="reported-message-meta">
|
||||
<span>Author</span>
|
||||
<code>{report.message.authorPubkey}</code>
|
||||
<span>Created</span>
|
||||
<time>{date(report.message.createdAt)}</time>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="message-unavailable">
|
||||
Message content is unavailable. It may have expired or
|
||||
been removed from event storage.
|
||||
</p>
|
||||
)}
|
||||
</dd>
|
||||
</>
|
||||
) : null}
|
||||
<dt>Note</dt>
|
||||
<dd className="sensitive">
|
||||
{report.note ?? "No note provided."}
|
||||
|
||||
@@ -496,6 +496,38 @@ dd {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.reported-message {
|
||||
display: grid;
|
||||
justify-items: start;
|
||||
gap: 1rem;
|
||||
border-left: 3px solid #d7d72e;
|
||||
border-radius: 0 0.8rem 0.8rem 0;
|
||||
background: #f6f6f1;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.reported-message p,
|
||||
.message-unavailable {
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.reported-message-meta {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 0.4rem 0.75rem;
|
||||
width: 100%;
|
||||
color: rgb(35 30 30 / 48%);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.message-unavailable {
|
||||
color: rgb(35 30 30 / 60%);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.sensitive {
|
||||
border-left: 3px solid #d7d72e;
|
||||
border-radius: 0 0.8rem 0.8rem 0;
|
||||
|
||||
@@ -12,6 +12,17 @@ export interface Report {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ReportedMessage {
|
||||
authorPubkey: string;
|
||||
content: string;
|
||||
createdAt: string;
|
||||
deletedAt: string | null;
|
||||
}
|
||||
|
||||
export interface ReportDetail extends Report {
|
||||
message: ReportedMessage | null;
|
||||
}
|
||||
|
||||
export interface FeedbackSummary {
|
||||
id: string;
|
||||
communityId: string;
|
||||
|
||||
@@ -58,6 +58,72 @@ test("report rows render the relay response contract", async ({ page }) => {
|
||||
await expect(page.getByText("Unknown date")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("event report detail renders the reported message content", async ({
|
||||
page,
|
||||
}) => {
|
||||
const id = "0e6caad8-1e18-4cd7-84fa-7264103f0a08";
|
||||
await page.route(`**/api/admin/v1/reports/${id}`, (route) =>
|
||||
route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
communityId: "6d474feb-c50a-44e4-a0b5-f30532df49bc",
|
||||
communityHost: "design.buzz.xyz",
|
||||
reporterPubkey: "21".repeat(32),
|
||||
targetKind: "event",
|
||||
target: "12".repeat(32),
|
||||
reportType: "spam",
|
||||
status: "open",
|
||||
createdAt: "2026-07-17T17:30:00Z",
|
||||
message: {
|
||||
authorPubkey: "31".repeat(32),
|
||||
content:
|
||||
"This is the complete reported message.\nIt preserves lines.",
|
||||
createdAt: "2026-07-17T17:25:00Z",
|
||||
deletedAt: null,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
await page.goto(`/reports/${id}`);
|
||||
await expect(
|
||||
page.getByText("This is the complete reported message.", { exact: false }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("31".repeat(32))).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("Message content is unavailable", { exact: false }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("event report detail explains when message content is unavailable", async ({
|
||||
page,
|
||||
}) => {
|
||||
const id = "0e6caad8-1e18-4cd7-84fa-7264103f0a09";
|
||||
await page.route(`**/api/admin/v1/reports/${id}`, (route) =>
|
||||
route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
communityId: "6d474feb-c50a-44e4-a0b5-f30532df49bc",
|
||||
communityHost: "design.buzz.xyz",
|
||||
reporterPubkey: "21".repeat(32),
|
||||
targetKind: "event",
|
||||
target: "12".repeat(32),
|
||||
reportType: "spam",
|
||||
status: "open",
|
||||
createdAt: "2026-07-17T17:30:00Z",
|
||||
message: null,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
await page.goto(`/reports/${id}`);
|
||||
await expect(
|
||||
page.getByText("Message content is unavailable", { exact: false }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("feedback cards open the complete submission", async ({ page }) => {
|
||||
const id = "feed0000-0000-4000-8000-000000000001";
|
||||
const fullBody = `${"Long feedback ".repeat(30)}end of feedback`;
|
||||
|
||||
Reference in New Issue
Block a user