mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
fix: include actionable error details
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { getErrorMessage } from "@/lib/errors";
|
||||
|
||||
export default function AdminError({
|
||||
error,
|
||||
@@ -10,13 +11,18 @@ export default function AdminError({
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
const message = getErrorMessage(error, "管理后台页面加载失败");
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardContent className="py-10 text-center space-y-5">
|
||||
<h1 className="text-xl font-semibold tracking-tight">出了点问题</h1>
|
||||
<p className="text-sm text-destructive">
|
||||
{error.message || "页面加载失败,请稍后重试。"}
|
||||
<p className="text-sm leading-6 text-destructive break-words">
|
||||
{message}
|
||||
</p>
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
如果重试后仍失败,请复制上面的错误详情给管理员排查。
|
||||
</p>
|
||||
<Button onClick={reset} className="h-10">重试</Button>
|
||||
</CardContent>
|
||||
|
||||
@@ -72,7 +72,7 @@ export function NodeActions({ node, siteUrl }: { node: NodeActionValue; siteUrl:
|
||||
try {
|
||||
const res = await testNodeConnection(node.id);
|
||||
if (res.success) toast.success(res.message);
|
||||
else toast.error(res.message);
|
||||
else toast.error(getErrorMessage(res.message, "节点测试失败"));
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error, "测试失败"));
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { ConfirmActionButton } from "@/components/shared/confirm-action-button";
|
||||
import { deletePlanPermanently, togglePlan } from "@/actions/admin/plans";
|
||||
import { toast } from "sonner";
|
||||
import { getErrorMessage } from "@/lib/errors";
|
||||
import {
|
||||
PlanForm,
|
||||
type PlanFormValue,
|
||||
@@ -39,8 +40,7 @@ export function PlanActions({
|
||||
toast.success(isActive ? "套餐已下架" : "套餐已上架");
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "切换失败";
|
||||
toast.error(message);
|
||||
toast.error(getErrorMessage(error, "切换套餐上下架状态失败"));
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -59,7 +59,7 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
|
||||
try {
|
||||
const result = await saveAppSettings(new FormData(form));
|
||||
if (!result.ok) {
|
||||
toast.error(result.error);
|
||||
toast.error(getErrorMessage(result.error, "保存设置失败"));
|
||||
return;
|
||||
}
|
||||
clearPasswordField(form);
|
||||
@@ -86,8 +86,8 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
|
||||
}
|
||||
toast.error(
|
||||
result.settingsSaved
|
||||
? `设置已保存,但测试邮件没有发出:${result.error}`
|
||||
: result.error,
|
||||
? `设置已保存,但测试邮件没有发出:${getErrorMessage(result.error, "测试邮件发送失败")}`
|
||||
: getErrorMessage(result.error, "测试邮件发送失败"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { TurnstileWidget } from "@/components/shared/turnstile-widget";
|
||||
import { getErrorMessage } from "@/lib/errors";
|
||||
import { AuthCard, AuthErrorMessage, AuthShell } from "../_components/auth-shell";
|
||||
|
||||
export function RegisterPageClient({ siteKey }: { siteKey?: string | null }) {
|
||||
@@ -44,13 +45,15 @@ export function RegisterPageClient({ siteKey }: { siteKey?: string | null }) {
|
||||
turnstileToken,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
const data = await response.json().catch(() => null) as { error?: unknown; requiresEmailVerification?: unknown } | null;
|
||||
if (!response.ok) {
|
||||
setError(data.error || "注册失败");
|
||||
setError(getErrorMessage(data, `注册失败 (HTTP ${response.status})`));
|
||||
} else {
|
||||
setRequiresEmailVerification(Boolean(data.requiresEmailVerification));
|
||||
setRequiresEmailVerification(Boolean(data?.requiresEmailVerification));
|
||||
setSuccess(true);
|
||||
}
|
||||
} catch (error) {
|
||||
setError(getErrorMessage(error, "注册失败"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { getErrorMessage } from "@/lib/errors";
|
||||
|
||||
export default function UserError({
|
||||
error,
|
||||
@@ -10,13 +11,18 @@ export default function UserError({
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
const message = getErrorMessage(error, "页面加载失败");
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardContent className="py-10 text-center space-y-5">
|
||||
<h1 className="text-xl font-semibold tracking-tight">出了点问题</h1>
|
||||
<p className="text-sm text-destructive">
|
||||
{error.message || "页面加载失败,请稍后重试。"}
|
||||
<p className="text-sm leading-6 text-destructive break-words">
|
||||
{message}
|
||||
</p>
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
如果重试后仍失败,请复制上面的错误详情给管理员排查。
|
||||
</p>
|
||||
<Button onClick={reset} className="h-10">重试</Button>
|
||||
</CardContent>
|
||||
|
||||
@@ -15,7 +15,7 @@ export function usePlanAvailabilityCheck(planId: string) {
|
||||
if (result.available) {
|
||||
toast.success("这款套餐现在可以购买");
|
||||
} else {
|
||||
toast.error(result.message);
|
||||
toast.error(getErrorMessage(result.message, "这款套餐暂时不可购买"));
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error, "暂时无法确认补位时间"));
|
||||
|
||||
@@ -60,7 +60,7 @@ export function RenewalButton({
|
||||
try {
|
||||
const result = await purchaseRenewal(subscriptionId, renewalDays);
|
||||
if (!result.ok) {
|
||||
toast.error(result.error);
|
||||
toast.error(getErrorMessage(result.error, "创建续费订单失败"));
|
||||
return;
|
||||
}
|
||||
router.push(`/pay/${result.orderId}`);
|
||||
|
||||
@@ -19,11 +19,11 @@ export async function POST(req: Request) {
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
|
||||
return NextResponse.json({ error: "请求体不是有效 JSON,期望格式:{ latencies: [{ carrier, latencyMs }] }" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!Array.isArray(body.latencies) || body.latencies.length === 0) {
|
||||
return NextResponse.json({ error: "Missing latencies" }, { status: 400 });
|
||||
return NextResponse.json({ error: "缺少延迟数据:latencies 必须是非空数组" }, { status: 400 });
|
||||
}
|
||||
|
||||
const validCarriers = new Set(["telecom", "unicom", "mobile"]);
|
||||
|
||||
@@ -20,11 +20,11 @@ export async function POST(req: Request) {
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
|
||||
return NextResponse.json({ error: "请求体不是有效 JSON,期望格式:{ traces: [{ carrier, hops }] }" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!Array.isArray(body.traces) || body.traces.length === 0) {
|
||||
return NextResponse.json({ error: "Missing traces" }, { status: 400 });
|
||||
return NextResponse.json({ error: "缺少路由追踪数据:traces 必须是非空数组" }, { status: 400 });
|
||||
}
|
||||
|
||||
const validCarriers = new Set(["telecom", "unicom", "mobile"]);
|
||||
|
||||
@@ -8,13 +8,22 @@ import { rateLimit } from "@/lib/rate-limit";
|
||||
import { normalizeEmailAddress, sendRegistrationVerificationEmail } from "@/services/email";
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(6),
|
||||
email: z.string().email("邮箱格式不正确"),
|
||||
password: z.string().min(6, "密码至少需要 6 位"),
|
||||
name: z.string().optional(),
|
||||
inviteCode: z.string().optional(),
|
||||
turnstileToken: z.string().optional(),
|
||||
});
|
||||
|
||||
function formatValidationErrors(error: z.ZodError) {
|
||||
return error.issues
|
||||
.map((issue) => {
|
||||
const field = issue.path.join(".") || "请求体";
|
||||
return `${field}:${issue.message}`;
|
||||
})
|
||||
.join(";");
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim()
|
||||
|| req.headers.get("x-real-ip")?.trim()
|
||||
@@ -31,12 +40,12 @@ export async function POST(req: Request) {
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "参数错误" }, { status: 400 });
|
||||
return NextResponse.json({ error: "注册参数错误:请求体不是有效 JSON" }, { status: 400 });
|
||||
}
|
||||
|
||||
const parsed = schema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: "参数错误" }, { status: 400 });
|
||||
return NextResponse.json({ error: `注册参数错误:${formatValidationErrors(parsed.error)}` }, { status: 400 });
|
||||
}
|
||||
|
||||
const { password, name, inviteCode, turnstileToken } = parsed.data;
|
||||
@@ -45,7 +54,7 @@ export async function POST(req: Request) {
|
||||
|
||||
if (config.turnstileSecretKey) {
|
||||
if (!turnstileToken || !(await verifyTurnstile(turnstileToken, config.turnstileSecretKey))) {
|
||||
return NextResponse.json({ error: "人机验证失败" }, { status: 403 });
|
||||
return NextResponse.json({ error: "人机验证失败:Turnstile token 缺失、已过期或校验未通过" }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +62,7 @@ export async function POST(req: Request) {
|
||||
return NextResponse.json({ error: "当前站点暂未开放注册" }, { status: 403 });
|
||||
}
|
||||
if (config.requireInviteCode && !inviteCode) {
|
||||
return NextResponse.json({ error: "当前注册必须填写邀请码" }, { status: 400 });
|
||||
return NextResponse.json({ error: "当前注册必须填写邀请码:请向管理员或邀请人获取有效邀请码" }, { status: 400 });
|
||||
}
|
||||
|
||||
const existing = await prisma.user.findUnique({ where: { email } });
|
||||
@@ -65,7 +74,7 @@ export async function POST(req: Request) {
|
||||
if (inviteCode) {
|
||||
const inviter = await prisma.user.findUnique({ where: { inviteCode } });
|
||||
if (!inviter) {
|
||||
return NextResponse.json({ error: "邀请码无效" }, { status: 400 });
|
||||
return NextResponse.json({ error: "邀请码无效:没有找到对应邀请人,请检查大小写或重新复制" }, { status: 400 });
|
||||
}
|
||||
inviterId = inviter.id;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@ export async function GET(req: Request) {
|
||||
const range = searchParams.get("range") ?? "1d";
|
||||
|
||||
if (!nodeId || nodeId.length > 128 || !RANGES[range]) {
|
||||
return NextResponse.json({ error: "Invalid params" }, { status: 400 });
|
||||
return NextResponse.json(
|
||||
{ error: "参数错误:nodeId 不能为空且长度不超过 128,range 只能是 1d、7d 或 30d" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const { ms, bucketMs } = RANGES[range];
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function GET(
|
||||
const token = url.searchParams.get("token");
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: "Missing token" }, { status: 401 });
|
||||
return NextResponse.json({ error: "订阅链接缺少 token 参数,请从订阅页面重新复制完整链接" }, { status: 401 });
|
||||
}
|
||||
|
||||
const sub = await prisma.userSubscription.findUnique({
|
||||
@@ -25,11 +25,11 @@ export async function GET(
|
||||
});
|
||||
|
||||
if (!sub || sub.downloadToken !== token) {
|
||||
return NextResponse.json({ error: "Invalid token" }, { status: 401 });
|
||||
return NextResponse.json({ error: "订阅 token 无效或已被重置,请在订阅详情页重新复制链接" }, { status: 401 });
|
||||
}
|
||||
|
||||
if (sub.status !== "ACTIVE") {
|
||||
return NextResponse.json({ error: "Subscription inactive" }, { status: 403 });
|
||||
return NextResponse.json({ error: `订阅当前状态为 ${sub.status},只有 ACTIVE 状态可以拉取配置` }, { status: 403 });
|
||||
}
|
||||
|
||||
const format = resolveSubscriptionFormat(url.searchParams, req.headers.get("user-agent"));
|
||||
|
||||
@@ -15,11 +15,11 @@ export async function GET(req: Request) {
|
||||
const token = url.searchParams.get("token");
|
||||
|
||||
if (!userId || !token) {
|
||||
return NextResponse.json({ error: "Missing subscription token" }, { status: 401 });
|
||||
return NextResponse.json({ error: "总订阅链接缺少 userId 或 token 参数,请从订阅页面重新复制完整链接" }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!verifyAggregateSubscriptionToken(userId, token)) {
|
||||
return NextResponse.json({ error: "Invalid subscription token" }, { status: 401 });
|
||||
return NextResponse.json({ error: "总订阅 token 无效,请登录后在订阅页面重新复制链接" }, { status: 401 });
|
||||
}
|
||||
|
||||
const format = resolveSubscriptionFormat(url.searchParams, req.headers.get("user-agent"));
|
||||
|
||||
@@ -8,7 +8,7 @@ export async function GET(
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
return new Response("附件访问失败:你尚未登录,请登录后重新打开附件", { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
@@ -24,11 +24,11 @@ export async function GET(
|
||||
});
|
||||
|
||||
if (!attachment) {
|
||||
return new Response("Not found", { status: 404 });
|
||||
return new Response("附件访问失败:附件不存在,可能已被删除或链接不完整", { status: 404 });
|
||||
}
|
||||
|
||||
if (session.user.role !== "ADMIN" && attachment.ticket.userId !== session.user.id) {
|
||||
return new Response("Forbidden", { status: 403 });
|
||||
return new Response("附件访问失败:你没有权限查看这个工单附件", { status: 403 });
|
||||
}
|
||||
|
||||
const requestUrl = new URL(req.url);
|
||||
|
||||
Reference in New Issue
Block a user