完善站点运营设置:分路由管理、登录可见评论、邮件模板内联与关闭注册 SSR。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-23 00:17:00 +08:00
parent acde7ee288
commit 6f054a903c
98 changed files with 8239 additions and 2073 deletions

View File

@@ -10,11 +10,13 @@ import {
type BrandMark,
type BrandSlot,
type FooterLink,
type FooterLinksAlign,
} from "./brand";
import { CSRF_COOKIE } from "./cookies";
import { publishPublicMetaSnapshot } from "./publicMetaSnapshot";
import { emitForceLogout } from "./userEvents";
export type { FooterLink };
export type { FooterLink, FooterLinksAlign };
// API 基础配置
// 注意:客户端请求使用相对路径 /api/*,走 Next.js rewrite 代理到后端,
@@ -684,6 +686,8 @@ export interface PublicSettings {
allow_register: boolean;
/** 全站是否开放评论;关闭后前台不展示评论能力 */
allow_comments: boolean;
/** 评论仅登录可见;缺省 false(游客可读) */
comments_require_login: boolean;
/** 全站是否开放私聊/群聊;关闭后前台不展示消息入口 */
allow_messages: boolean;
post_cooldown_hours: number;
@@ -749,6 +753,7 @@ const DEFAULT_PUBLIC_SETTINGS: PublicSettings = {
footer_links: [],
allow_register: true,
allow_comments: true,
comments_require_login: false,
allow_messages: true,
post_cooldown_hours: 24,
code_block_auto_fold: true,
@@ -821,6 +826,7 @@ function normalizePublicSettings(data: Partial<PublicSettings> | null | undefine
footer_links: normalizeFooterLinks(data?.footer_links),
allow_register: data?.allow_register !== false,
allow_comments: data?.allow_comments !== false,
comments_require_login: data?.comments_require_login === true,
allow_messages: data?.allow_messages !== false,
post_cooldown_hours:
typeof data?.post_cooldown_hours === "number" && data.post_cooldown_hours >= 0
@@ -857,10 +863,52 @@ function normalizePublicSettings(data: Partial<PublicSettings> | null | undefine
export async function fetchPublicSettings(): Promise<PublicSettings> {
try {
const res = await fetch(`${API_BASE}/api/settings`, ssrInit());
if (!res.ok) return { ...DEFAULT_PUBLIC_SETTINGS };
return normalizePublicSettings((await res.json()) as Partial<PublicSettings>);
if (!res.ok) {
const fallback = { ...DEFAULT_PUBLIC_SETTINGS };
publishPublicMetaSnapshot(fallback);
return fallback;
}
const normalized = normalizePublicSettings((await res.json()) as Partial<PublicSettings>);
publishPublicMetaSnapshot(normalized);
return normalized;
} catch {
return { ...DEFAULT_PUBLIC_SETTINGS };
const fallback = { ...DEFAULT_PUBLIC_SETTINGS };
publishPublicMetaSnapshot(fallback);
return fallback;
}
}
/** 公开站点运行态(注册/找回/维护等);SSR 与客户端同源字段 */
export type PublicSiteState = {
allow_register: boolean;
register_notice: string;
verify_email: boolean;
password_reset: boolean;
site_url: string;
};
const DEFAULT_SITE_STATE: PublicSiteState = {
allow_register: true,
register_notice: "",
verify_email: false,
password_reset: false,
site_url: "",
};
export async function fetchSiteState(): Promise<PublicSiteState> {
try {
const res = await fetch(`${API_BASE}/api/site-state`, ssrInit());
if (!res.ok) return { ...DEFAULT_SITE_STATE };
const data = (await res.json()) as Partial<PublicSiteState>;
return {
allow_register: data.allow_register !== false,
register_notice: typeof data.register_notice === "string" ? data.register_notice : "",
verify_email: data.verify_email === true,
password_reset: data.password_reset === true,
site_url: typeof data.site_url === "string" ? data.site_url : "",
};
} catch {
return { ...DEFAULT_SITE_STATE };
}
}
@@ -954,12 +1002,12 @@ export async function apiLogin(username: string, password: string) {
return data;
}
export async function apiRegister(username: string, email: string, password: string) {
export async function apiRegister(username: string, email: string, password: string, code = "") {
const res = await fetch("/api/register", {
method: "POST",
credentials: "include",
headers: clientHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify({ username, email, password }),
body: JSON.stringify({ username, email, password, code }),
});
return res.json();
}
@@ -1878,19 +1926,6 @@ export async function apiAdminDeleteSitePage(id: number): Promise<void> {
}
}
export async function apiGetAdminSettings(): Promise<
PublicSettings & { timeline_git_import?: string }
> {
const res = await fetchWithRefresh("/api/admin/settings", { headers: clientHeaders() });
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || "获取设置失败");
return {
...normalizePublicSettings(data as Partial<PublicSettings>),
timeline_git_import:
typeof data.timeline_git_import === "string" ? data.timeline_git_import : undefined,
};
}
// ===== 用户管理(管理员) =====
// 后台用户列表项:email/最近活跃/登录 IP 仅管理员接口返回
@@ -2223,6 +2258,7 @@ export type UpdateSiteSettingsBody = {
footer_links?: FooterLink[];
allow_register?: boolean;
allow_comments?: boolean;
comments_require_login?: boolean;
allow_messages?: boolean;
post_cooldown_hours?: number;
code_block_auto_fold?: boolean;
@@ -2241,7 +2277,6 @@ export type UpdateSiteSettingsBody = {
bg_site_mode?: string;
bg_admin_url?: string;
bg_admin_mode?: string;
timeline_git_import?: string;
};
// 更新站点设置(字段可选);accent 传空串恢复默认主题色
@@ -2980,3 +3015,10 @@ export async function apiChatUnreadSummary(): Promise<ChatUnreadSummaryResponse>
if (!res.ok) return { total: 0, rooms: [] };
return res.json();
}
export async function apiOperations<T = Record<string, unknown>>(path: string, method = "GET", body?: unknown): Promise<T> {
const res = await fetchWithRefresh(path, { method, cache: "no-store", headers: clientHeaders({ "Content-Type": "application/json" }), ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
const data = await res.json();
if (!res.ok) throw new Error(data.error || "操作失败,请重试");
return data as T;
}