fix: harden secrets and session checks

This commit is contained in:
JetSprow
2026-04-29 17:18:36 +10:00
parent 69be1d6fcc
commit 58fa4fefa4
44 changed files with 454 additions and 154 deletions

View File

@@ -1,8 +1,13 @@
import type { Prisma } from "@prisma/client";
import { prisma } from "@/lib/prisma";
import { notFound } from "next/navigation";
import { sanitizeInboundSettings, sanitizeStreamSettings } from "@/services/node-inbound-sanitize";
const nodeDetailInclude = {
const nodeDetailSelect = {
id: true,
name: true,
panelUrl: true,
status: true,
inbounds: {
where: { isActive: true },
orderBy: { updatedAt: "desc" },
@@ -12,17 +17,25 @@ const nodeDetailInclude = {
},
},
},
} satisfies Prisma.NodeServerInclude;
} satisfies Prisma.NodeServerSelect;
export type NodeDetail = Prisma.NodeServerGetPayload<{
include: typeof nodeDetailInclude;
select: typeof nodeDetailSelect;
}>;
export async function getNodeDetail(id: string): Promise<NodeDetail> {
const node = await prisma.nodeServer.findUnique({
where: { id },
include: nodeDetailInclude,
select: nodeDetailSelect,
});
if (!node) notFound();
return node;
return {
...node,
inbounds: node.inbounds.map((inbound) => ({
...inbound,
settings: sanitizeInboundSettings(inbound.settings),
streamSettings: sanitizeStreamSettings(inbound.streamSettings),
})),
};
}

View File

@@ -60,7 +60,6 @@ function NodeCard({ node, siteUrl }: { node: NodeServerRow; siteUrl: string | nu
name: node.name,
panelUrl: node.panelUrl,
panelUsername: node.panelUsername,
panelPassword: node.panelPassword,
}}
triggerLabel="编辑"
triggerVariant="outline"

View File

@@ -22,7 +22,6 @@ interface NodeFormValue {
name: string;
panelUrl: string | null;
panelUsername: string | null;
panelPassword: string | null;
}
export function NodeForm({
@@ -92,7 +91,13 @@ export function NodeForm({
</div>
<div>
<Label></Label>
<Input name="panelPassword" type="password" defaultValue={node?.panelPassword ?? ""} required />
<Input
name="panelPassword"
type="password"
placeholder={isEdit ? "留空则沿用当前密码" : "请输入面板密码"}
required={!isEdit}
autoComplete="new-password"
/>
</div>
</div>

View File

@@ -2,8 +2,15 @@ import type { Prisma } from "@prisma/client";
import { prisma } from "@/lib/prisma";
import { parsePage } from "@/lib/utils";
import { getConfiguredSiteUrl } from "@/services/site-url";
import { sanitizeInboundSettings } from "@/services/node-inbound-sanitize";
const nodeInclude = {
const nodeSelect = {
id: true,
name: true,
panelUrl: true,
panelUsername: true,
status: true,
agentToken: true,
_count: { select: { inbounds: true } },
inbounds: {
where: { isActive: true },
@@ -16,10 +23,10 @@ const nodeInclude = {
},
orderBy: { updatedAt: "desc" },
},
} satisfies Prisma.NodeServerInclude;
} satisfies Prisma.NodeServerSelect;
export type NodeServerRow = Prisma.NodeServerGetPayload<{
include: typeof nodeInclude;
select: typeof nodeSelect;
}>;
export async function getNodeServers(
@@ -44,7 +51,7 @@ export async function getNodeServers(
const [nodes, total, siteUrl] = await Promise.all([
prisma.nodeServer.findMany({
where,
include: nodeInclude,
select: nodeSelect,
orderBy: { createdAt: "desc" },
skip,
take: pageSize,
@@ -53,5 +60,14 @@ export async function getNodeServers(
getConfiguredSiteUrl(),
]);
return { nodes, total, page, pageSize, filters: { q, status }, siteUrl };
const safeNodes = nodes.map((node) => ({
...node,
agentToken: node.agentToken ? "configured" : null,
inbounds: node.inbounds.map((inbound) => ({
...inbound,
settings: sanitizeInboundSettings(inbound.settings),
})),
}));
return { nodes: safeNodes, total, page, pageSize, filters: { q, status }, siteUrl };
}

View File

@@ -22,10 +22,17 @@ interface Props {
provider: string;
fields: Field[];
currentConfig?: Record<string, string>;
secretConfigured?: Record<string, boolean>;
enabled: boolean;
}
export function PaymentConfigForm({ provider, fields, currentConfig, enabled: initialEnabled }: Props) {
export function PaymentConfigForm({
provider,
fields,
currentConfig,
secretConfigured = {},
enabled: initialEnabled,
}: Props) {
const [enabled, setEnabled] = useState(initialEnabled);
const [saving, setSaving] = useState(false);
@@ -65,6 +72,13 @@ export function PaymentConfigForm({ provider, fields, currentConfig, enabled: in
try {
await savePaymentConfig(provider, config, enabled);
for (const field of fields) {
if (!field.secret) continue;
const input = e.currentTarget.elements.namedItem(field.key);
if (input instanceof HTMLInputElement) {
input.value = "";
}
}
toast.success("保存成功");
} catch (error) {
toast.error(getErrorMessage(error, "保存失败"));
@@ -99,8 +113,8 @@ export function PaymentConfigForm({ provider, fields, currentConfig, enabled: in
<Input
name={field.key}
type={field.secret ? "password" : "text"}
placeholder={field.placeholder}
defaultValue={currentConfig?.[field.key] || ""}
placeholder={field.secret && secretConfigured[field.key] ? "留空保持不变" : field.placeholder}
defaultValue={field.secret ? "" : currentConfig?.[field.key] || ""}
/>
</div>
),

View File

@@ -20,7 +20,7 @@ export default async function PaymentsPage() {
title="支付配置"
/>
<div className="grid gap-5">
{providerConfigs.map(({ provider, config }) => (
{providerConfigs.map(({ provider, config, secretConfigured }) => (
<section key={provider.id} className="surface-card overflow-hidden rounded-xl p-4">
<div className="mb-4 flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="flex items-start gap-3">
@@ -37,7 +37,8 @@ export default async function PaymentsPage() {
<PaymentConfigForm
provider={provider.id}
fields={provider.fields}
currentConfig={config?.config as Record<string, string> | undefined}
currentConfig={config?.config}
secretConfigured={secretConfigured}
enabled={config?.enabled ?? false}
/>
</section>

View File

@@ -1,12 +1,29 @@
import { prisma } from "@/lib/prisma";
import { PAYMENT_PROVIDER_DEFINITIONS } from "@/services/payment/catalog";
import {
getPaymentSecretConfiguredState,
PAYMENT_PROVIDER_DEFINITIONS,
redactPaymentConfigForClient,
} from "@/services/payment/catalog";
export async function getPaymentProviderConfigs() {
const configs = await prisma.paymentConfig.findMany();
const configMap = new Map(configs.map((config) => [config.provider, config]));
return PAYMENT_PROVIDER_DEFINITIONS.map((provider) => ({
provider,
config: configMap.get(provider.id),
}));
return PAYMENT_PROVIDER_DEFINITIONS.map((provider) => {
const config = configMap.get(provider.id);
const configValue = config?.config as Record<string, unknown> | undefined;
return {
provider,
config: config
? {
enabled: config.enabled,
config: redactPaymentConfigForClient(provider.id, configValue ?? {}),
}
: null,
secretConfigured: configValue
? getPaymentSecretConfiguredState(provider.id, configValue)
: {},
};
});
}

View File

@@ -52,7 +52,7 @@ export default async function AdminSettingsPage() {
inviteRewardRate: Number(config.inviteRewardRate),
inviteRewardCouponId: config.inviteRewardCouponId,
turnstileSiteKey: config.turnstileSiteKey,
turnstileSecretKey: config.turnstileSecretKey,
turnstileSecretConfigured: Boolean(config.turnstileSecretKey),
smtpEnabled: config.smtpEnabled,
smtpHost: config.smtpHost,
smtpPort: config.smtpPort,

View File

@@ -41,7 +41,7 @@ interface AppConfig {
inviteRewardRate: number;
inviteRewardCouponId: string | null;
turnstileSiteKey: string | null;
turnstileSecretKey: string | null;
turnstileSecretConfigured: boolean;
smtpEnabled: boolean;
smtpHost: string | null;
smtpPort: number;
@@ -123,6 +123,11 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
if (password instanceof HTMLInputElement) {
password.value = "";
}
const turnstileSecret = form.elements.namedItem("turnstileSecretKey");
if (turnstileSecret instanceof HTMLInputElement) {
turnstileSecret.value = "";
}
}
return (
@@ -553,7 +558,16 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
</div>
<div className="space-y-2">
<Label htmlFor="turnstileSecretKey">Secret Key</Label>
<Input id="turnstileSecretKey" name="turnstileSecretKey" type="password" defaultValue={config.turnstileSecretKey ?? ""} placeholder="0x4AAAAAAA..." />
<Input
id="turnstileSecretKey"
name="turnstileSecretKey"
type="password"
placeholder={config.turnstileSecretConfigured ? "留空保持不变" : "0x4AAAAAAA..."}
autoComplete="new-password"
/>
{config.turnstileSecretConfigured && (
<p className="text-xs leading-5 text-muted-foreground">Secret Key Site Key Turnstile</p>
)}
</div>
</div>
</section>

View File

@@ -1,5 +1,6 @@
import Link from "next/link";
import type { SubscriptionRiskEvent } from "@prisma/client";
import { ChevronDown } from "lucide-react";
import {
SubscriptionStatusBadge,
SubscriptionTypeBadge,
@@ -155,60 +156,97 @@ function ReviewState({ event }: { event: SubscriptionRiskEventRow }) {
);
}
function RiskEventCard({ event }: { event: SubscriptionRiskEventRow }) {
function RiskStat({ label, value }: { label: string; value: string | number }) {
return (
<article className="surface-card overflow-hidden rounded-xl">
<div className="grid xl:grid-cols-[minmax(0,0.9fr)_minmax(25rem,1.2fr)_minmax(18rem,0.65fr)]">
<section className="space-y-5 p-5">
<span className="min-w-0 rounded-lg border border-border/70 bg-muted/20 px-2.5 py-1.5">
<span className="block text-[0.68rem] leading-none text-muted-foreground">{label}</span>
<span className="mt-1 block truncate font-mono text-sm font-semibold leading-none">{value}</span>
</span>
);
}
function RiskEventCard({ event }: { event: SubscriptionRiskEventRow }) {
const summary = event.geoSummary;
const userLabel = event.user?.email ?? "未知用户";
const scopeLabel = event.subscription?.plan.name ?? "总订阅";
return (
<details className="surface-card group overflow-hidden rounded-xl">
<summary className="flex cursor-pointer list-none items-start gap-4 p-4 text-left [&::-webkit-details-marker]:hidden sm:p-5">
<div className="min-w-0 flex-1 space-y-3">
<div className="flex flex-wrap items-center gap-2">
<StatusBadge tone={event.level === "SUSPENDED" ? "danger" : "warning"}>
{reasonLabel(event.reason)}
</StatusBadge>
<StatusBadge tone="neutral">{kindLabel(event.kind)}</StatusBadge>
<StatusBadge tone={reviewStatusTone(event.reviewStatus)}>{reviewStatusLabel(event.reviewStatus)}</StatusBadge>
{event.userRestrictionActive && <StatusBadge tone="danger"></StatusBadge>}
{event.reportSentAt && <StatusBadge tone="info"></StatusBadge>}
<span className="text-xs text-muted-foreground">{formatDate(event.createdAt)}</span>
</div>
<div className="space-y-2">
<p className="text-sm font-semibold leading-6">{event.message}</p>
<p className="break-all font-mono text-xs text-muted-foreground"> IP{event.ip || "未知 IP"}</p>
</div>
<div className="grid gap-4 border-t border-border/60 pt-4 md:grid-cols-2 xl:grid-cols-1 2xl:grid-cols-2">
<div className="space-y-1">
<p className="text-xs font-medium text-muted-foreground"></p>
<UserBlock event={event} />
<div className="grid gap-3 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-end">
<div className="min-w-0 space-y-1.5">
<p className="line-clamp-2 text-sm font-semibold leading-6">{event.message}</p>
<p className="truncate text-xs text-muted-foreground">
{userLabel} · {scopeLabel} · IP{event.ip || "未知 IP"}
</p>
</div>
<div className="space-y-1">
<p className="text-xs font-medium text-muted-foreground"></p>
<EventScope event={event} />
<div className="grid grid-cols-4 gap-2 text-right sm:flex sm:justify-end">
<RiskStat label="国家" value={summary.uniqueCountryCount} />
<RiskStat label="省区" value={summary.uniqueRegionCount} />
<RiskStat label="城市" value={summary.uniqueCityCount} />
<RiskStat label="IP" value={summary.uniqueIpCount} />
</div>
</div>
</section>
</div>
<section className="border-y border-border/70 bg-muted/10 p-5 xl:border-x xl:border-y-0">
<SubscriptionRiskGeoDetails summary={event.geoSummary} />
</section>
<span className="mt-1 flex shrink-0 items-center gap-1.5 rounded-md border border-border bg-muted/30 px-2.5 py-1.5 text-xs font-medium text-muted-foreground transition-colors group-hover:text-foreground">
<span className="hidden sm:inline"></span>
<ChevronDown className="size-4 transition-transform group-open:rotate-180" />
</span>
</summary>
<aside className="space-y-5 p-5">
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground"></p>
<ReviewState event={event} />
</div>
<div className="border-t border-border/60 pt-4">
<SubscriptionRiskReviewActions
eventId={event.id}
reviewStatus={event.reviewStatus}
canRestoreSubscription={event.canRestoreSubscription}
restorableSubscriptionCount={event.restorableSubscriptionCount}
riskReport={event.riskReport}
reportSentAt={event.reportSentAt}
userRestrictionActive={event.userRestrictionActive}
finalAction={event.finalAction}
/>
</div>
</aside>
<div className="border-t border-border/70">
<div className="grid xl:grid-cols-[minmax(0,0.85fr)_minmax(24rem,1.15fr)_minmax(18rem,0.7fr)]">
<section className="space-y-5 p-5">
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-1 2xl:grid-cols-2">
<div className="space-y-1">
<p className="text-xs font-medium text-muted-foreground"></p>
<UserBlock event={event} />
</div>
<div className="space-y-1">
<p className="text-xs font-medium text-muted-foreground"></p>
<EventScope event={event} />
</div>
</div>
</section>
<section className="border-y border-border/70 bg-muted/10 p-5 xl:border-x xl:border-y-0">
<SubscriptionRiskGeoDetails summary={summary} />
</section>
<aside className="space-y-5 p-5">
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground"></p>
<ReviewState event={event} />
</div>
<div className="border-t border-border/60 pt-4">
<SubscriptionRiskReviewActions
eventId={event.id}
reviewStatus={event.reviewStatus}
canRestoreSubscription={event.canRestoreSubscription}
restorableSubscriptionCount={event.restorableSubscriptionCount}
riskReport={event.riskReport}
reportSentAt={event.reportSentAt}
userRestrictionActive={event.userRestrictionActive}
finalAction={event.finalAction}
/>
</div>
</aside>
</div>
</div>
</article>
</details>
);
}

View File

@@ -1,8 +1,7 @@
import type { Metadata } from "next";
import { Suspense } from "react";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { redirect } from "next/navigation";
import { getActiveSession } from "@/lib/require-auth";
import { AdminSidebar } from "@/components/admin/sidebar";
import { AdminMobileNav } from "@/components/admin/mobile-nav";
import { AnnouncementLoader } from "@/components/announcements/announcement-loader";
@@ -21,7 +20,7 @@ export default async function AdminLayout({
}: {
children: React.ReactNode;
}) {
const session = await getServerSession(authOptions);
const session = await getActiveSession();
if (!session) {
redirect("/login");
}

View File

@@ -1,8 +1,7 @@
import type { Metadata } from "next";
import { Suspense } from "react";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { redirect } from "next/navigation";
import { getActiveSession } from "@/lib/require-auth";
import { AnnouncementLoader } from "@/components/announcements/announcement-loader";
import { PageTransition } from "@/components/shared/page-transition";
@@ -19,7 +18,7 @@ export default async function AuthLayout({
}: {
children: React.ReactNode;
}) {
const session = await getServerSession(authOptions);
const session = await getActiveSession();
if (session) {
redirect(session.user.role === "ADMIN" ? "/admin/dashboard" : "/dashboard");
}

View File

@@ -1,7 +1,6 @@
import type { Metadata } from "next";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { authOptions } from "@/lib/auth";
import { getActiveSession } from "@/lib/require-auth";
import { getActiveSubscriptionRiskRestriction } from "@/services/subscription-risk-review";
export const metadata: Metadata = {
@@ -17,7 +16,7 @@ export default async function PaymentLayout({
}: {
children: React.ReactNode;
}) {
const session = await getServerSession(authOptions);
const session = await getActiveSession();
if (!session) {
redirect("/login");

View File

@@ -1,6 +1,5 @@
import type { Metadata } from "next";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { getActiveSession } from "@/lib/require-auth";
import { PageHeader, PageShell } from "@/components/shared/page-shell";
import { AccountPanel } from "./account-panel";
import { getAccountPageData } from "./account-data";
@@ -12,7 +11,7 @@ export const metadata: Metadata = {
};
export default async function AccountPage() {
const session = await getServerSession(authOptions);
const session = await getActiveSession();
const { user, siteNotice } = await getAccountPageData(session!.user.id);
return (

View File

@@ -1,8 +1,7 @@
import type { Metadata } from "next";
import { getActiveSession } from "@/lib/require-auth";
import Link from "next/link";
import { getServerSession } from "next-auth";
import { ShoppingBag, ShoppingCart } from "lucide-react";
import { authOptions } from "@/lib/auth";
import { EmptyState, PageHeader, PageShell } from "@/components/shared/page-shell";
import { buttonVariants } from "@/components/ui/button";
import { CartClient } from "./cart-client";
@@ -14,7 +13,7 @@ export const metadata: Metadata = {
};
export default async function CartPage() {
const session = await getServerSession(authOptions);
const session = await getActiveSession();
const data = await getCartPageData(session!.user.id);
return (

View File

@@ -1,6 +1,5 @@
import type { Metadata } from "next";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { getActiveSession } from "@/lib/require-auth";
import { PageHeader, PageShell } from "@/components/shared/page-shell";
import { getDashboardData, getDashboardTrafficTrend } from "./dashboard-data";
import {
@@ -24,7 +23,7 @@ export const metadata: Metadata = {
};
export default async function UserDashboard() {
const session = await getServerSession(authOptions);
const session = await getActiveSession();
const userId = session!.user.id;
const { activeSubs, pendingOrderCount, paidOrderCount, config } =

View File

@@ -1,8 +1,7 @@
import type { Metadata } from "next";
import { Suspense } from "react";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { redirect } from "next/navigation";
import { getActiveSession } from "@/lib/require-auth";
import { UserSidebar } from "@/components/user/sidebar";
import { UserMobileNav } from "@/components/user/mobile-nav";
import { AnnouncementLoader } from "@/components/announcements/announcement-loader";
@@ -24,7 +23,7 @@ export default async function UserLayout({
}: {
children: React.ReactNode;
}) {
const session = await getServerSession(authOptions);
const session = await getActiveSession();
if (!session) {
redirect("/login");
}

View File

@@ -1,6 +1,5 @@
import type { Metadata } from "next";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { getActiveSession } from "@/lib/require-auth";
import { PageHeader, PageShell } from "@/components/shared/page-shell";
import { NotificationBulkAction } from "./notification-actions";
import { NotificationList } from "./_components/notification-list";
@@ -12,7 +11,7 @@ export const metadata: Metadata = {
};
export default async function NotificationsPage() {
const session = await getServerSession(authOptions);
const session = await getActiveSession();
const { notifications, unreadCount, readCount } = await getUserNotifications(session!.user.id);
return (

View File

@@ -1,6 +1,5 @@
import type { Metadata } from "next";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { getActiveSession } from "@/lib/require-auth";
import { Pagination } from "@/components/shared/pagination";
import { PageHeader, PageShell } from "@/components/shared/page-shell";
import { UserOrdersTable } from "./_components/user-orders-table";
@@ -16,7 +15,7 @@ export default async function UserOrdersPage({
}: {
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
const session = await getServerSession(authOptions);
const session = await getActiveSession();
const { orders, total, page, pageSize } = await getUserOrders({
userId: session!.user.id,
searchParams: await searchParams,

View File

@@ -1,9 +1,8 @@
import type { Metadata } from "next";
import { getActiveSession } from "@/lib/require-auth";
import Link from "next/link";
import { getServerSession } from "next-auth";
import { Film, LifeBuoy, Radio } from "lucide-react";
import { authOptions } from "@/lib/auth";
import { EmptyState, PageShell } from "@/components/shared/page-shell";
import { buttonVariants } from "@/components/ui/button";
import { PendingOrderBanner } from "./pending-order-banner";
@@ -30,7 +29,7 @@ export const metadata: Metadata = {
};
export default async function StorePage() {
const session = await getServerSession(authOptions);
const session = await getActiveSession();
const { plans, availabilityMap, pendingOrder, latencyRecommendations } = await getStorePageData(session?.user.id);
const proxyPlans = getProxyPlans(plans);
const streamingPlans = getStreamingPlans(plans);

View File

@@ -1,8 +1,7 @@
import type { Metadata } from "next";
import { getActiveSession } from "@/lib/require-auth";
import { headers } from "next/headers";
import { notFound } from "next/navigation";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { PageHeader, PageShell, SectionHeader } from "@/components/shared/page-shell";
import { SubscriptionDetailCards } from "@/components/subscriptions/subscription-detail-cards";
import { SubscriptionTimelineSection } from "@/components/subscriptions/subscription-timeline-section";
@@ -22,7 +21,7 @@ export default async function UserSubscriptionDetailPage({
}: {
params: Promise<{ id: string }>;
}) {
const session = await getServerSession(authOptions);
const session = await getActiveSession();
const { id } = await params;
const requestHeaders = await headers();
const [data, baseUrl] = await Promise.all([

View File

@@ -1,6 +1,5 @@
import type { Metadata } from "next";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { getActiveSession } from "@/lib/require-auth";
import { PageHeader, PageShell } from "@/components/shared/page-shell";
import {
getActiveSubscriptions,
@@ -22,7 +21,7 @@ export const metadata: Metadata = {
};
export default async function SubscriptionsPage() {
const session = await getServerSession(authOptions);
const session = await getActiveSession();
const [subs, baseUrl] = await Promise.all([
getUserSubscriptions(session!.user.id),
getSubscriptionBaseUrl(),

View File

@@ -1,7 +1,6 @@
import type { Metadata } from "next";
import { getActiveSession } from "@/lib/require-auth";
import { notFound } from "next/navigation";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { PageHeader, PageShell } from "@/components/shared/page-shell";
import {
SupportTicketPriorityBadge,
@@ -23,7 +22,7 @@ export default async function SupportTicketDetailPage({
}: {
params: Promise<{ id: string }>;
}) {
const session = await getServerSession(authOptions);
const session = await getActiveSession();
const { id } = await params;
const ticket = await getUserSupportTicketDetail({
ticketId: id,

View File

@@ -1,6 +1,5 @@
import type { Metadata } from "next";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { getActiveSession } from "@/lib/require-auth";
import { PageHeader, PageShell } from "@/components/shared/page-shell";
import { prisma } from "@/lib/prisma";
import { getAppConfig } from "@/services/app-config";
@@ -19,7 +18,7 @@ export default async function SupportPage({
}: {
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
const session = await getServerSession(authOptions);
const session = await getActiveSession();
const resolvedSearchParams = await searchParams;
const riskEventId = typeof resolvedSearchParams.riskEventId === "string" ? resolvedSearchParams.riskEventId : "";
const [tickets, openTicketCount, config, riskEvent] = await Promise.all([

View File

@@ -7,6 +7,7 @@ import { verifyTurnstile } from "@/lib/turnstile";
import { rateLimit } from "@/lib/rate-limit";
import { getClientIp } from "@/lib/request-context";
import { isSmtpConfigured, normalizeEmailAddress, sendRegistrationVerificationEmail } from "@/services/email";
import { decryptIfEncrypted } from "@/lib/crypto";
const schema = z.object({
email: z.string().email("邮箱格式不正确"),
@@ -58,8 +59,11 @@ export async function POST(req: Request) {
);
}
if (config.turnstileSecretKey) {
if (!turnstileToken || !(await verifyTurnstile(turnstileToken, config.turnstileSecretKey))) {
const turnstileSecretKey = config.turnstileSecretKey
? decryptIfEncrypted(config.turnstileSecretKey)
: "";
if (turnstileSecretKey) {
if (!turnstileToken || !(await verifyTurnstile(turnstileToken, turnstileSecretKey))) {
return NextResponse.json({ error: "人机验证失败Turnstile token 缺失、已过期或校验未通过" }, { status: 403 });
}
}

View File

@@ -1,10 +1,9 @@
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { getActiveSession } from "@/lib/require-auth";
import { jsonError, jsonOk } from "@/lib/api-response";
import { getUserNotifications } from "@/app/(user)/notifications/notifications-data";
export async function GET() {
const session = await getServerSession(authOptions);
const session = await getActiveSession();
if (!session) return jsonError("未登录", { status: 401 });
const data = await getUserNotifications(session.user.id);

View File

@@ -1,7 +1,6 @@
import { getServerSession } from "next-auth";
import { z } from "zod";
import { authOptions } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { getActiveSession } from "@/lib/require-auth";
import { jsonError, jsonOk } from "@/lib/api-response";
import { getPaymentAdapter } from "@/services/payment/factory";
import { rateLimit } from "@/lib/rate-limit";
@@ -28,7 +27,7 @@ function isSafePaymentUrl(value: string | undefined) {
export async function POST(req: Request) {
try {
const session = await getServerSession(authOptions);
const session = await getActiveSession();
if (!session) {
return jsonError("未登录", { status: 401 });
}

View File

@@ -1,13 +1,12 @@
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { getActiveSession } from "@/lib/require-auth";
import { jsonError, jsonOk } from "@/lib/api-response";
export async function GET(
_req: Request,
{ params }: { params: Promise<{ orderId: string }> },
) {
const session = await getServerSession(authOptions);
const session = await getActiveSession();
if (!session) {
return jsonError("未登录", { status: 401 });
}

View File

@@ -1,6 +1,5 @@
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { getActiveSession } from "@/lib/require-auth";
import { jsonError, jsonOk } from "@/lib/api-response";
import { getPaymentAdapter } from "@/services/payment/factory";
import { handleVerifiedPaymentSuccess } from "@/services/payment/process";
@@ -9,7 +8,7 @@ export async function GET(
_req: Request,
{ params }: { params: Promise<{ tradeNo: string }> }
) {
const session = await getServerSession(authOptions);
const session = await getActiveSession();
if (!session) {
return jsonError("未登录", { status: 401 });
}

View File

@@ -1,12 +1,11 @@
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { getActiveSession } from "@/lib/require-auth";
import { prisma } from "@/lib/prisma";
export async function GET(
req: Request,
{ params }: { params: Promise<{ id: string }> },
) {
const session = await getServerSession(authOptions);
const session = await getActiveSession();
if (!session) {
return new Response("附件访问失败:你尚未登录,请登录后重新打开附件", { status: 401 });
}

View File

@@ -1,7 +1,6 @@
import type { Metadata } from "next";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { redirect } from "next/navigation";
import { getActiveSession } from "@/lib/require-auth";
export const metadata: Metadata = {
title: "首页",
@@ -9,7 +8,7 @@ export const metadata: Metadata = {
};
export default async function Home() {
const session = await getServerSession(authOptions);
const session = await getActiveSession();
if (!session) redirect("/login");
if (session.user.role === "ADMIN") redirect("/admin/dashboard");
redirect("/dashboard");