Skip to content

Commit c77b736

Browse files
committed
fix: tighten feed and topic visibility
1 parent add0b23 commit c77b736

12 files changed

Lines changed: 210 additions & 112 deletions

File tree

‎apps/api/src/routes/topic.ts‎

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,24 @@ function isAdminUser(user: any) {
5757
return (process.env.APP_ADMINS || "").split(",").filter(Boolean).includes(user.loginname);
5858
}
5959

60+
export function shouldIncludeInternalTabsInTopicList(tab: string | undefined, isAdmin: boolean) {
61+
return !!(isAdmin && tab && INTERNAL_TABS.has(tab));
62+
}
63+
64+
export function buildTopicListQuery(tab: string | undefined, isAdmin: boolean) {
65+
if (tab && INTERNAL_TABS.has(tab) && !isAdmin) return null;
66+
const query: any = { publicVisible: true };
67+
if (!tab || tab === "all") {
68+
query.excludeTabs = ["job"];
69+
} else if (tab === "good") {
70+
query.good = 1;
71+
} else {
72+
query.tab = tab;
73+
}
74+
query.includeInternalTabs = shouldIncludeInternalTabsInTopicList(tab, isAdmin);
75+
return query;
76+
}
77+
6078
async function canPostJob(user: any) {
6179
return canPostJobFromRoles(isAdminUser(user), await roleQueries.listByUserId(user.id));
6280
}
@@ -89,20 +107,11 @@ const listTopicsRoute = createRoute({
89107
topic.openapi(listTopicsRoute, async (c) => {
90108
const { page, limit, tab, mdrender } = c.req.valid("query");
91109

92-
const query: any = {};
93110
const isAdmin = c.get("isAdmin");
94-
if (tab && INTERNAL_TABS.has(tab) && !isAdmin) {
111+
const query = buildTopicListQuery(tab, isAdmin);
112+
if (!query) {
95113
return c.json({ success: true as const, data: [], total: 0 }, 200);
96114
}
97-
if (!tab || tab === "all") {
98-
query.excludeTabs = ["job"];
99-
} else if (tab === "good") {
100-
query.good = 1;
101-
} else {
102-
query.tab = tab;
103-
}
104-
query.publicVisible = true;
105-
query.includeInternalTabs = isAdmin;
106115

107116
const topicsList = await topicQueries.getByQuery(query, {
108117
limit,
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { expect, test } from "vitest";
2+
import { mdrenderQuerySchema } from "@cnode/shared";
3+
4+
test("mdrender query parses explicit false strings", () => {
5+
expect(mdrenderQuerySchema.parse({}).mdrender).toBe(true);
6+
expect(mdrenderQuerySchema.parse({ mdrender: "false" }).mdrender).toBe(false);
7+
expect(mdrenderQuerySchema.parse({ mdrender: "0" }).mdrender).toBe(false);
8+
});
9+
10+
test("mdrender query parses explicit true strings", () => {
11+
expect(mdrenderQuerySchema.parse({ mdrender: "true" }).mdrender).toBe(true);
12+
expect(mdrenderQuerySchema.parse({ mdrender: "1" }).mdrender).toBe(true);
13+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { expect, test } from "vitest";
2+
import { buildTopicListQuery, shouldIncludeInternalTabsInTopicList } from "../src/routes/topic";
3+
4+
test("public all topic list excludes internal tabs for admins too", () => {
5+
expect(shouldIncludeInternalTabsInTopicList(undefined, true)).toBe(false);
6+
expect(shouldIncludeInternalTabsInTopicList("all", true)).toBe(false);
7+
expect(shouldIncludeInternalTabsInTopicList("share", true)).toBe(false);
8+
});
9+
10+
test("internal tabs are only available when explicitly selected by admins", () => {
11+
expect(shouldIncludeInternalTabsInTopicList("dev", true)).toBe(true);
12+
expect(shouldIncludeInternalTabsInTopicList("test", true)).toBe(true);
13+
expect(shouldIncludeInternalTabsInTopicList("dev", false)).toBe(false);
14+
});
15+
16+
test("topic list query keeps all and normal tab filters outside internal tabs", () => {
17+
expect(buildTopicListQuery(undefined, false)).toMatchObject({ excludeTabs: ["job"], publicVisible: true, includeInternalTabs: false });
18+
expect(buildTopicListQuery("all", true)).toMatchObject({ excludeTabs: ["job"], publicVisible: true, includeInternalTabs: false });
19+
expect(buildTopicListQuery("share", true)).toMatchObject({ tab: "share", publicVisible: true, includeInternalTabs: false });
20+
expect(buildTopicListQuery("ask", false)).toMatchObject({ tab: "ask", publicVisible: true, includeInternalTabs: false });
21+
expect(buildTopicListQuery("good", true)).toMatchObject({ good: 1, publicVisible: true, includeInternalTabs: false });
22+
});
23+
24+
test("topic list query only allows explicit internal tabs for admins", () => {
25+
expect(buildTopicListQuery("dev", false)).toBeNull();
26+
expect(buildTopicListQuery("test", false)).toBeNull();
27+
expect(buildTopicListQuery("dev", true)).toMatchObject({ tab: "dev", publicVisible: true, includeInternalTabs: true });
28+
expect(buildTopicListQuery("test", true)).toMatchObject({ tab: "test", publicVisible: true, includeInternalTabs: true });
29+
});

‎apps/web/app/components/Layout.tsx‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -478,9 +478,15 @@ function FooterGroup({ title, links }: { title: string; links: Array<[string, st
478478
<ul className="mt-4 space-y-3 text-sm text-muted-foreground">
479479
{links.map(([label, href]) => (
480480
<li key={href}>
481-
<Link to={href} className="hover:text-primary">
482-
{label}
483-
</Link>
481+
{href === "/rss" ? (
482+
<a href={href} className="hover:text-primary">
483+
{label}
484+
</a>
485+
) : (
486+
<Link to={href} className="hover:text-primary">
487+
{label}
488+
</Link>
489+
)}
484490
</li>
485491
))}
486492
</ul>

‎apps/web/app/routes.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export default [
3636
route("app/download", "routes/app.download.tsx"),
3737
route("admin", "routes/admin/_index.tsx"),
3838
route("admin/topics", "routes/admin/topics.tsx"),
39+
route("admin/mod", "routes/admin/mod-redirect.tsx"),
3940
route("admin/moderation", "routes/admin/mod.tsx"),
4041
route("admin/users", "routes/admin/users.tsx"),
4142
route("admin/bans", "routes/admin/bans.tsx"),

‎apps/web/app/routes/admin/audit.tsx‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ function targetHref(log: AuditLog) {
6565
}
6666

6767
function DetailBlock({ detail }: { detail: string | null }) {
68-
if (!detail) return <span className="text-muted-foreground">无</span>;
68+
if (!detail) return <div className="rounded-xl border border-dashed border-border bg-card p-3 text-xs text-muted-foreground">该审计事件没有附加详情。</div>;
6969
try {
7070
return <pre className="overflow-auto rounded-xl bg-surface-subtle p-3 text-xs">{JSON.stringify(JSON.parse(detail), null, 2)}</pre>;
7171
} catch {
@@ -139,16 +139,20 @@ export default function AdminAudit({ loaderData }: any) {
139139
</div>
140140

141141
<details className="mt-3 rounded-xl bg-surface-subtle p-3 text-sm">
142-
<summary className="cursor-pointer font-medium text-cnode-ink">展开原始记录</summary>
142+
<summary className="cursor-pointer font-medium text-cnode-ink">查看审计字段</summary>
143143
<div className="mt-3 grid gap-3 lg:grid-cols-[16rem_minmax(0,1fr)]">
144144
<dl className="space-y-1 text-xs text-muted-foreground">
145+
<div className="mb-2 font-semibold text-cnode-ink">基础字段</div>
145146
<div><dt className="inline font-semibold">id: </dt><dd className="inline">{log.id}</dd></div>
146147
<div><dt className="inline font-semibold">action: </dt><dd className="inline">{log.action}</dd></div>
147148
<div><dt className="inline font-semibold">operator_id: </dt><dd className="inline">{log.operator_id ?? ""}</dd></div>
148149
<div><dt className="inline font-semibold">target_type: </dt><dd className="inline">{log.target_type || ""}</dd></div>
149150
<div><dt className="inline font-semibold">target_id: </dt><dd className="inline">{log.target_id || ""}</dd></div>
150151
</dl>
151-
<DetailBlock detail={log.detail} />
152+
<div className="min-w-0 space-y-2">
153+
<div className="text-xs font-semibold text-cnode-ink">附加详情 detail</div>
154+
<DetailBlock detail={log.detail} />
155+
</div>
152156
</div>
153157
</details>
154158
</article>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { redirect } from "react-router";
2+
3+
export function loader({ request }: { request: Request }) {
4+
const url = new URL(request.url);
5+
return redirect(`/admin/moderation${url.search}`);
6+
}

‎apps/web/app/routes/admin/mod.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ export default function AdminMod({ loaderData }: any) {
180180
</div>
181181
<div className="flex gap-2">
182182
<Button size="sm" variant={jobId === job.id ? "default" : "outline"} asChild>
183-
<Link to={`/admin/mod?status=${encodeURIComponent(status)}${type ? `&type=${encodeURIComponent(type)}` : ""}&job_id=${job.id}`}>查看命中</Link>
183+
<Link to={`/admin/moderation?status=${encodeURIComponent(status)}${type ? `&type=${encodeURIComponent(type)}` : ""}&job_id=${job.id}`}>查看命中</Link>
184184
</Button>
185185
{(job.pendingHitCount || 0) > 0 ? (
186186
<Button size="sm" variant="destructive" onClick={() => setConfirmJobId(job.id)}>批量确认删除</Button>
@@ -244,7 +244,7 @@ export default function AdminMod({ loaderData }: any) {
244244
</DialogContent>
245245
</Dialog>
246246
</AdminPanel>
247-
<AdminPanel title={jobId > 0 ? `任务 #${jobId} 待复核内容` : "待复核内容"} description={`当前页 ${results.length} 条 / 共 ${total} 条`} action={jobId > 0 ? <Button size="sm" variant="outline" asChild><Link to="/admin/mod">查看全部</Link></Button> : null} contentClassName="p-4">
247+
<AdminPanel title={jobId > 0 ? `任务 #${jobId} 待复核内容` : "待复核内容"} description={`当前页 ${results.length} 条 / 共 ${total} 条`} action={jobId > 0 ? <Button size="sm" variant="outline" asChild><Link to="/admin/moderation">查看全部</Link></Button> : null} contentClassName="p-4">
248248
<div className="mb-4 flex flex-wrap items-center justify-between gap-3 rounded-2xl border border-border bg-surface-subtle p-3 text-sm">
249249
<div className="flex flex-wrap gap-2">
250250
<Badge variant="outline">话题 {summary.by_type?.topic || 0}</Badge>
@@ -309,7 +309,7 @@ export default function AdminMod({ loaderData }: any) {
309309
</div>
310310
</div>
311311
))}
312-
<Pagination page={page} total={total} limit={limit} basePath="/admin/mod" searchParams={{ status, ...(type ? { type } : {}), ...(jobId > 0 ? { job_id: String(jobId) } : {}) }} />
312+
<Pagination page={page} total={total} limit={limit} basePath="/admin/moderation" searchParams={{ status, ...(type ? { type } : {}), ...(jobId > 0 ? { job_id: String(jobId) } : {}) }} />
313313
</div>
314314
)}
315315
</AdminPanel>

0 commit comments

Comments
 (0)