提交 Turnstile 集成源文件改动
This commit is contained in:
@@ -7,6 +7,10 @@ import {
|
|||||||
buildProxyExceptionResponse,
|
buildProxyExceptionResponse,
|
||||||
buildUpstreamErrorResponse,
|
buildUpstreamErrorResponse,
|
||||||
} from "@/lib/api-proxy";
|
} from "@/lib/api-proxy";
|
||||||
|
import {
|
||||||
|
requireTurnstileForRequest,
|
||||||
|
stripTurnstileToken,
|
||||||
|
} from "@/lib/turnstile";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -89,6 +93,8 @@ export async function POST(req: NextRequest) {
|
|||||||
let auth: Awaited<ReturnType<typeof buildBackendRequestHeaders>> | null = null;
|
let auth: Awaited<ReturnType<typeof buildBackendRequestHeaders>> | null = null;
|
||||||
try {
|
try {
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
|
const turnstileError = await requireTurnstileForRequest(req, "feedback_submit", body);
|
||||||
|
if (turnstileError) return turnstileError;
|
||||||
auth = await buildBackendRequestHeaders(req);
|
auth = await buildBackendRequestHeaders(req);
|
||||||
const headers = new Headers(auth.headers);
|
const headers = new Headers(auth.headers);
|
||||||
headers.set("Content-Type", "application/json");
|
headers.set("Content-Type", "application/json");
|
||||||
@@ -96,7 +102,7 @@ export async function POST(req: NextRequest) {
|
|||||||
const res = await fetch(`${API_BASE}/api/feedback`, {
|
const res = await fetch(`${API_BASE}/api/feedback`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(stripTurnstileToken(body)),
|
||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
});
|
});
|
||||||
const raw = await res.text();
|
const raw = await res.text();
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import {
|
|||||||
buildProxyExceptionResponse,
|
buildProxyExceptionResponse,
|
||||||
buildUpstreamErrorResponse,
|
buildUpstreamErrorResponse,
|
||||||
} from "@/lib/api-proxy";
|
} from "@/lib/api-proxy";
|
||||||
|
import {
|
||||||
|
requireTurnstileForRequest,
|
||||||
|
stripTurnstileToken,
|
||||||
|
} from "@/lib/turnstile";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -53,6 +57,10 @@ export async function POST(
|
|||||||
const { intentId } = await context.params;
|
const { intentId } = await context.params;
|
||||||
try {
|
try {
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
|
if (process.env.POLYWEATHER_TURNSTILE_REQUIRE_PAYMENT_SUBMIT === "true") {
|
||||||
|
const turnstileError = await requireTurnstileForRequest(req, "payment_tx_submit", body);
|
||||||
|
if (turnstileError) return turnstileError;
|
||||||
|
}
|
||||||
const auth = await buildBackendRequestHeaders(req);
|
const auth = await buildBackendRequestHeaders(req);
|
||||||
const authError = requireBackendPaymentAuth(auth);
|
const authError = requireBackendPaymentAuth(auth);
|
||||||
if (authError) return authError;
|
if (authError) return authError;
|
||||||
@@ -63,7 +71,7 @@ export async function POST(
|
|||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: proxiedHeaders,
|
headers: proxiedHeaders,
|
||||||
body: JSON.stringify(body ?? {}),
|
body: JSON.stringify(stripTurnstileToken(body ?? {})),
|
||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import {
|
|||||||
buildUpstreamErrorResponse,
|
buildUpstreamErrorResponse,
|
||||||
} from "@/lib/api-proxy";
|
} from "@/lib/api-proxy";
|
||||||
import { isPaymentHostAllowed } from "@/lib/payment-host";
|
import { isPaymentHostAllowed } from "@/lib/payment-host";
|
||||||
|
import {
|
||||||
|
requireTurnstileForRequest,
|
||||||
|
stripTurnstileToken,
|
||||||
|
} from "@/lib/turnstile";
|
||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
|
|
||||||
@@ -35,6 +39,8 @@ export async function POST(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
|
const turnstileError = await requireTurnstileForRequest(req, "payment_intent_create", body);
|
||||||
|
if (turnstileError) return turnstileError;
|
||||||
const auth = await buildBackendRequestHeaders(req);
|
const auth = await buildBackendRequestHeaders(req);
|
||||||
const authError = requireBackendPaymentAuth(auth);
|
const authError = requireBackendPaymentAuth(auth);
|
||||||
if (authError) return authError;
|
if (authError) return authError;
|
||||||
@@ -43,7 +49,7 @@ export async function POST(req: NextRequest) {
|
|||||||
const res = await fetch(`${API_BASE}/api/payments/intents`, {
|
const res = await fetch(`${API_BASE}/api/payments/intents`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: proxiedHeaders,
|
headers: proxiedHeaders,
|
||||||
body: JSON.stringify(body ?? {}),
|
body: JSON.stringify(stripTurnstileToken(body ?? {})),
|
||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import {
|
|||||||
} from "./constants";
|
} from "./constants";
|
||||||
import { InfoRow, PlusIcon } from "./AccountInfoRow";
|
import { InfoRow, PlusIcon } from "./AccountInfoRow";
|
||||||
import { AccountFeedbackPanel } from "./AccountFeedbackPanel";
|
import { AccountFeedbackPanel } from "./AccountFeedbackPanel";
|
||||||
|
import { TurnstileWidget } from "@/components/security/TurnstileWidget";
|
||||||
import {
|
import {
|
||||||
chainIdToDisplayName,
|
chainIdToDisplayName,
|
||||||
clearStoredPaymentRecovery,
|
clearStoredPaymentRecovery,
|
||||||
@@ -65,6 +66,7 @@ import {
|
|||||||
buildTrialValueReplaySummary,
|
buildTrialValueReplaySummary,
|
||||||
readTrialValueReplay,
|
readTrialValueReplay,
|
||||||
} from "@/lib/trial-value-replay";
|
} from "@/lib/trial-value-replay";
|
||||||
|
import { getTurnstileTokenForAction } from "@/lib/turnstile-client";
|
||||||
|
|
||||||
// --- Main Component ---
|
// --- Main Component ---
|
||||||
|
|
||||||
@@ -90,9 +92,25 @@ export function AccountCenter() {
|
|||||||
const [backend, setBackend] = useState<AuthMeResponse | null>(null);
|
const [backend, setBackend] = useState<AuthMeResponse | null>(null);
|
||||||
const [referralCodeInput, setReferralCodeInput] = useState("");
|
const [referralCodeInput, setReferralCodeInput] = useState("");
|
||||||
const [referralApplying, setReferralApplying] = useState(false);
|
const [referralApplying, setReferralApplying] = useState(false);
|
||||||
|
const [paymentTurnstileToken, setPaymentTurnstileToken] = useState("");
|
||||||
|
const [paymentTurnstileResetKey, setPaymentTurnstileResetKey] = useState(0);
|
||||||
|
|
||||||
const supabaseReady = hasSupabasePublicEnv();
|
const supabaseReady = hasSupabasePublicEnv();
|
||||||
const walletConnectEnabled = Boolean(WALLETCONNECT_PROJECT_ID);
|
const walletConnectEnabled = Boolean(WALLETCONNECT_PROJECT_ID);
|
||||||
|
const resetPaymentTurnstile = useCallback(() => {
|
||||||
|
setPaymentTurnstileToken("");
|
||||||
|
setPaymentTurnstileResetKey((value) => value + 1);
|
||||||
|
}, []);
|
||||||
|
const getPaymentTurnstileToken = useCallback(
|
||||||
|
(
|
||||||
|
action: "payment_intent_create" | "payment_tx_submit",
|
||||||
|
options?: { optional?: boolean },
|
||||||
|
) => {
|
||||||
|
if (options?.optional && !paymentTurnstileToken) return undefined;
|
||||||
|
return getTurnstileTokenForAction(paymentTurnstileToken, action);
|
||||||
|
},
|
||||||
|
[paymentTurnstileToken],
|
||||||
|
);
|
||||||
|
|
||||||
// ── Hook ────────────────────────────────────────────────
|
// ── Hook ────────────────────────────────────────────────
|
||||||
const {
|
const {
|
||||||
@@ -189,6 +207,8 @@ export function AccountCenter() {
|
|||||||
setUpdatedAt,
|
setUpdatedAt,
|
||||||
usePoints,
|
usePoints,
|
||||||
setUsePoints,
|
setUsePoints,
|
||||||
|
getPaymentTurnstileToken,
|
||||||
|
resetPaymentTurnstile,
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Auth analytics effect ──────────────────────────────
|
// ── Auth analytics effect ──────────────────────────────
|
||||||
@@ -1370,6 +1390,12 @@ export function AccountCenter() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<TurnstileWidget
|
||||||
|
action="payment_intent_create"
|
||||||
|
onToken={setPaymentTurnstileToken}
|
||||||
|
resetKey={paymentTurnstileResetKey}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Payment Method Tabs */}
|
{/* Payment Method Tabs */}
|
||||||
<div className="border-t border-slate-200 pt-5">
|
<div className="border-t border-slate-200 pt-5">
|
||||||
<p className="mb-3 text-[10px] font-semibold uppercase text-slate-500">
|
<p className="mb-3 text-[10px] font-semibold uppercase text-slate-500">
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ export interface UseAccountPaymentParams {
|
|||||||
setUpdatedAt: (text: string) => void;
|
setUpdatedAt: (text: string) => void;
|
||||||
usePoints: boolean;
|
usePoints: boolean;
|
||||||
setUsePoints: (v: boolean) => void;
|
setUsePoints: (v: boolean) => void;
|
||||||
|
getPaymentTurnstileToken?: (
|
||||||
|
action: "payment_intent_create" | "payment_tx_submit",
|
||||||
|
options?: { optional?: boolean },
|
||||||
|
) => string | undefined;
|
||||||
|
resetPaymentTurnstile?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -73,6 +78,8 @@ export function useAccountPayment(params: UseAccountPaymentParams) {
|
|||||||
setUpdatedAt,
|
setUpdatedAt,
|
||||||
usePoints,
|
usePoints,
|
||||||
setUsePoints,
|
setUsePoints,
|
||||||
|
getPaymentTurnstileToken,
|
||||||
|
resetPaymentTurnstile,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
// ── Base payment state (from usePaymentState) ──────────────
|
// ── Base payment state (from usePaymentState) ──────────────
|
||||||
@@ -532,6 +539,8 @@ export function useAccountPayment(params: UseAccountPaymentParams) {
|
|||||||
allowedPaymentHosts: billing.allowedPaymentHosts,
|
allowedPaymentHosts: billing.allowedPaymentHosts,
|
||||||
authIsAuthenticated,
|
authIsAuthenticated,
|
||||||
hasPayingWallet: walletBind.hasPayingWallet,
|
hasPayingWallet: walletBind.hasPayingWallet,
|
||||||
|
getPaymentTurnstileToken,
|
||||||
|
resetPaymentTurnstile,
|
||||||
getValidAccessToken,
|
getValidAccessToken,
|
||||||
buildAuthedHeaders,
|
buildAuthedHeaders,
|
||||||
loadSnapshot,
|
loadSnapshot,
|
||||||
|
|||||||
@@ -32,9 +32,12 @@ import {
|
|||||||
assertExpectedPaymentReceiver,
|
assertExpectedPaymentReceiver,
|
||||||
EXPECTED_PAYMENT_RECEIVER_ADDRESS,
|
EXPECTED_PAYMENT_RECEIVER_ADDRESS,
|
||||||
} from "@/lib/payment-receiver";
|
} from "@/lib/payment-receiver";
|
||||||
|
import { getTurnstileTokenForAction } from "@/lib/turnstile-client";
|
||||||
import type { PaymentTxValidationState } from "./usePaymentState";
|
import type { PaymentTxValidationState } from "./usePaymentState";
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
type PaymentTurnstileAction = "payment_intent_create" | "payment_tx_submit";
|
||||||
|
|
||||||
export interface UsePaymentFlowParams {
|
export interface UsePaymentFlowParams {
|
||||||
isEn: boolean;
|
isEn: boolean;
|
||||||
copy: Record<string, string>;
|
copy: Record<string, string>;
|
||||||
@@ -97,6 +100,11 @@ export interface UsePaymentFlowParams {
|
|||||||
allowedPaymentHosts: string[];
|
allowedPaymentHosts: string[];
|
||||||
authIsAuthenticated: boolean;
|
authIsAuthenticated: boolean;
|
||||||
hasPayingWallet: boolean;
|
hasPayingWallet: boolean;
|
||||||
|
getPaymentTurnstileToken?: (
|
||||||
|
action: PaymentTurnstileAction,
|
||||||
|
options?: { optional?: boolean },
|
||||||
|
) => string | undefined;
|
||||||
|
resetPaymentTurnstile?: () => void;
|
||||||
|
|
||||||
// Callbacks from master
|
// Callbacks from master
|
||||||
getValidAccessToken: () => Promise<string>;
|
getValidAccessToken: () => Promise<string>;
|
||||||
@@ -159,6 +167,8 @@ export function usePaymentFlow(params: UsePaymentFlowParams) {
|
|||||||
allowedPaymentHosts,
|
allowedPaymentHosts,
|
||||||
authIsAuthenticated,
|
authIsAuthenticated,
|
||||||
hasPayingWallet,
|
hasPayingWallet,
|
||||||
|
getPaymentTurnstileToken,
|
||||||
|
resetPaymentTurnstile,
|
||||||
getValidAccessToken,
|
getValidAccessToken,
|
||||||
buildAuthedHeaders,
|
buildAuthedHeaders,
|
||||||
loadSnapshot,
|
loadSnapshot,
|
||||||
@@ -184,6 +194,16 @@ export function usePaymentFlow(params: UsePaymentFlowParams) {
|
|||||||
trackAppEvent("payment_success", payload);
|
trackAppEvent("payment_success", payload);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const buildPaymentTurnstilePayload = useCallback(
|
||||||
|
(action: PaymentTurnstileAction, optional = false) => {
|
||||||
|
const turnstile_token =
|
||||||
|
getPaymentTurnstileToken?.(action, { optional }) ||
|
||||||
|
(optional ? undefined : getTurnstileTokenForAction("", action));
|
||||||
|
return turnstile_token ? { turnstile_token } : {};
|
||||||
|
},
|
||||||
|
[getPaymentTurnstileToken],
|
||||||
|
);
|
||||||
|
|
||||||
const verifyPaymentAuthReady = useCallback(async () => {
|
const verifyPaymentAuthReady = useCallback(async () => {
|
||||||
const accessToken = await getValidAccessToken();
|
const accessToken = await getValidAccessToken();
|
||||||
const authHeaders: Record<string, string> = {
|
const authHeaders: Record<string, string> = {
|
||||||
@@ -499,6 +519,7 @@ export function usePaymentFlow(params: UsePaymentFlowParams) {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: authHeaders,
|
headers: authHeaders,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
...buildPaymentTurnstilePayload("payment_intent_create"),
|
||||||
plan_code: selectedPlan?.plan_code || "pro_monthly",
|
plan_code: selectedPlan?.plan_code || "pro_monthly",
|
||||||
payment_mode: "strict",
|
payment_mode: "strict",
|
||||||
allowed_wallet: payingWallet,
|
allowed_wallet: payingWallet,
|
||||||
@@ -514,6 +535,7 @@ export function usePaymentFlow(params: UsePaymentFlowParams) {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
resetPaymentTurnstile?.();
|
||||||
if (!createRes.ok) {
|
if (!createRes.ok) {
|
||||||
const raw = await readPaymentApiErrorMessage(
|
const raw = await readPaymentApiErrorMessage(
|
||||||
createRes,
|
createRes,
|
||||||
@@ -596,7 +618,13 @@ export function usePaymentFlow(params: UsePaymentFlowParams) {
|
|||||||
await waitForReceipt(txHashNorm, eth);
|
await waitForReceipt(txHashNorm, eth);
|
||||||
|
|
||||||
const submitRes = await fetch(`/api/payments/intents/${intentId}/submit`, {
|
const submitRes = await fetch(`/api/payments/intents/${intentId}/submit`, {
|
||||||
method: "POST", headers: authHeaders, body: JSON.stringify({ tx_hash: txHashNorm, from_address: payingWallet }),
|
method: "POST",
|
||||||
|
headers: authHeaders,
|
||||||
|
body: JSON.stringify({
|
||||||
|
...buildPaymentTurnstilePayload("payment_tx_submit", true),
|
||||||
|
tx_hash: txHashNorm,
|
||||||
|
from_address: payingWallet,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
if (!submitRes.ok) {
|
if (!submitRes.ok) {
|
||||||
const raw = await readPaymentApiErrorMessage(
|
const raw = await readPaymentApiErrorMessage(
|
||||||
@@ -692,6 +720,7 @@ export function usePaymentFlow(params: UsePaymentFlowParams) {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: authHeaders,
|
headers: authHeaders,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
...buildPaymentTurnstilePayload("payment_intent_create"),
|
||||||
plan_code: selectedPlan?.plan_code || "pro_monthly",
|
plan_code: selectedPlan?.plan_code || "pro_monthly",
|
||||||
payment_mode: "direct",
|
payment_mode: "direct",
|
||||||
chain_id: effectivePaymentChainId,
|
chain_id: effectivePaymentChainId,
|
||||||
@@ -705,6 +734,7 @@ export function usePaymentFlow(params: UsePaymentFlowParams) {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
resetPaymentTurnstile?.();
|
||||||
if (!createRes.ok) {
|
if (!createRes.ok) {
|
||||||
const raw = await readPaymentApiErrorMessage(
|
const raw = await readPaymentApiErrorMessage(
|
||||||
createRes,
|
createRes,
|
||||||
@@ -765,7 +795,12 @@ export function usePaymentFlow(params: UsePaymentFlowParams) {
|
|||||||
try {
|
try {
|
||||||
const authHeaders = await buildAuthedHeaders(true, true);
|
const authHeaders = await buildAuthedHeaders(true, true);
|
||||||
const submitRes = await fetch(`/api/payments/intents/${intentIdVal}/submit`, {
|
const submitRes = await fetch(`/api/payments/intents/${intentIdVal}/submit`, {
|
||||||
method: "POST", headers: authHeaders, body: JSON.stringify({ tx_hash: txHashNorm }),
|
method: "POST",
|
||||||
|
headers: authHeaders,
|
||||||
|
body: JSON.stringify({
|
||||||
|
...buildPaymentTurnstilePayload("payment_tx_submit", true),
|
||||||
|
tx_hash: txHashNorm,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
if (!submitRes.ok) {
|
if (!submitRes.ok) {
|
||||||
const raw = await readPaymentApiErrorMessage(
|
const raw = await readPaymentApiErrorMessage(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FormEvent, useEffect, useState } from "react";
|
import { FormEvent, useCallback, useEffect, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +18,8 @@ import {
|
|||||||
} from "@/lib/supabase/client";
|
} from "@/lib/supabase/client";
|
||||||
import { getConfiguredSiteUrl, PRODUCTION_SITE_URL } from "@/lib/site-url";
|
import { getConfiguredSiteUrl, PRODUCTION_SITE_URL } from "@/lib/site-url";
|
||||||
import { useI18n } from "@/hooks/useI18n";
|
import { useI18n } from "@/hooks/useI18n";
|
||||||
|
import { TurnstileWidget } from "@/components/security/TurnstileWidget";
|
||||||
|
import { getTurnstileTokenForAction } from "@/lib/turnstile-client";
|
||||||
|
|
||||||
type Mode = "login" | "signup";
|
type Mode = "login" | "signup";
|
||||||
|
|
||||||
@@ -72,6 +74,8 @@ export function LoginClient({ nextPath, initialError, initialMode }: LoginClient
|
|||||||
const [infoText, setInfoText] = useState("");
|
const [infoText, setInfoText] = useState("");
|
||||||
const [resetSent, setResetSent] = useState(false);
|
const [resetSent, setResetSent] = useState(false);
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [turnstileToken, setTurnstileToken] = useState("");
|
||||||
|
const [turnstileResetKey, setTurnstileResetKey] = useState(0);
|
||||||
|
|
||||||
const supabaseReady = hasSupabasePublicEnv();
|
const supabaseReady = hasSupabasePublicEnv();
|
||||||
const isLogin = mode === "login";
|
const isLogin = mode === "login";
|
||||||
@@ -173,6 +177,11 @@ export function LoginClient({ nextPath, initialError, initialMode }: LoginClient
|
|||||||
setErrorText(initialError || "");
|
setErrorText(initialError || "");
|
||||||
}, [initialError]);
|
}, [initialError]);
|
||||||
|
|
||||||
|
const resetTurnstile = useCallback(() => {
|
||||||
|
setTurnstileToken("");
|
||||||
|
setTurnstileResetKey((value) => value + 1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const onResetPassword = async () => {
|
const onResetPassword = async () => {
|
||||||
setErrorText("");
|
setErrorText("");
|
||||||
setInfoText("");
|
setInfoText("");
|
||||||
@@ -247,13 +256,19 @@ export function LoginClient({ nextPath, initialError, initialMode }: LoginClient
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const supabase = getSupabaseBrowserClient();
|
const supabase = getSupabaseBrowserClient();
|
||||||
|
const captchaToken = getTurnstileTokenForAction(
|
||||||
|
turnstileToken,
|
||||||
|
isLogin ? "login" : "signup",
|
||||||
|
);
|
||||||
if (mode === "login") {
|
if (mode === "login") {
|
||||||
const { data, error } = await supabase.auth.signInWithPassword({
|
const { data, error } = await supabase.auth.signInWithPassword({
|
||||||
email: email.trim(),
|
email: email.trim(),
|
||||||
password,
|
password,
|
||||||
|
options: captchaToken ? { captchaToken } : undefined,
|
||||||
});
|
});
|
||||||
if (error) {
|
if (error) {
|
||||||
setErrorText(error.message);
|
setErrorText(error.message);
|
||||||
|
resetTurnstile();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const redirectPath = await resolvePostLoginRedirect({
|
const redirectPath = await resolvePostLoginRedirect({
|
||||||
@@ -272,10 +287,12 @@ export function LoginClient({ nextPath, initialError, initialMode }: LoginClient
|
|||||||
password,
|
password,
|
||||||
options: {
|
options: {
|
||||||
emailRedirectTo,
|
emailRedirectTo,
|
||||||
|
...(captchaToken ? { captchaToken } : {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (error) {
|
if (error) {
|
||||||
setErrorText(error.message);
|
setErrorText(error.message);
|
||||||
|
resetTurnstile();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (data.session?.user) {
|
if (data.session?.user) {
|
||||||
@@ -287,6 +304,10 @@ export function LoginClient({ nextPath, initialError, initialMode }: LoginClient
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setInfoText(copy.signupCheckEmail);
|
setInfoText(copy.signupCheckEmail);
|
||||||
|
resetTurnstile();
|
||||||
|
} catch (error) {
|
||||||
|
setErrorText(error instanceof Error ? error.message : String(error));
|
||||||
|
resetTurnstile();
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -407,6 +428,7 @@ export function LoginClient({ nextPath, initialError, initialMode }: LoginClient
|
|||||||
setErrorText("");
|
setErrorText("");
|
||||||
setInfoText("");
|
setInfoText("");
|
||||||
setMode(isLogin ? "signup" : "login");
|
setMode(isLogin ? "signup" : "login");
|
||||||
|
resetTurnstile();
|
||||||
}}
|
}}
|
||||||
className="whitespace-nowrap rounded-xl border border-slate-300 bg-white px-4 py-2 text-xs font-bold text-slate-700 shadow-sm transition hover:border-blue-300 hover:text-blue-700 active:scale-[0.98]"
|
className="whitespace-nowrap rounded-xl border border-slate-300 bg-white px-4 py-2 text-xs font-bold text-slate-700 shadow-sm transition hover:border-blue-300 hover:text-blue-700 active:scale-[0.98]"
|
||||||
>
|
>
|
||||||
@@ -495,6 +517,12 @@ export function LoginClient({ nextPath, initialError, initialMode }: LoginClient
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<TurnstileWidget
|
||||||
|
action={isLogin ? "login" : "signup"}
|
||||||
|
onToken={setTurnstileToken}
|
||||||
|
resetKey={turnstileResetKey}
|
||||||
|
/>
|
||||||
|
|
||||||
{!isLogin ? (
|
{!isLogin ? (
|
||||||
<p className="text-[11px] leading-relaxed text-slate-500">
|
<p className="text-[11px] leading-relaxed text-slate-500">
|
||||||
{copy.termsAgreement}
|
{copy.termsAgreement}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
getSupabaseBrowserClient,
|
getSupabaseBrowserClient,
|
||||||
hasSupabasePublicEnv,
|
hasSupabasePublicEnv,
|
||||||
} from "@/lib/supabase/client";
|
} from "@/lib/supabase/client";
|
||||||
|
import { TurnstileWidget } from "@/components/security/TurnstileWidget";
|
||||||
|
import { getTurnstileTokenForAction, isTurnstileEnabled } from "@/lib/turnstile-client";
|
||||||
import type { UserFeedbackEntry } from "@/types/ops";
|
import type { UserFeedbackEntry } from "@/types/ops";
|
||||||
|
|
||||||
export type FeedbackCategory = "bug" | "data" | "idea" | "payment" | "account" | "other";
|
export type FeedbackCategory = "bug" | "data" | "idea" | "payment" | "account" | "other";
|
||||||
@@ -111,6 +113,8 @@ export function UserFeedbackModal({
|
|||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [submitted, setSubmitted] = useState(false);
|
const [submitted, setSubmitted] = useState(false);
|
||||||
|
const [turnstileToken, setTurnstileToken] = useState("");
|
||||||
|
const [turnstileResetKey, setTurnstileResetKey] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
@@ -120,6 +124,8 @@ export function UserFeedbackModal({
|
|||||||
setLoginEmailContact("");
|
setLoginEmailContact("");
|
||||||
setError("");
|
setError("");
|
||||||
setSubmitted(false);
|
setSubmitted(false);
|
||||||
|
setTurnstileToken("");
|
||||||
|
setTurnstileResetKey((value) => value + 1);
|
||||||
}, [open, draft?.category]);
|
}, [open, draft?.category]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -158,13 +164,20 @@ export function UserFeedbackModal({
|
|||||||
|
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|
||||||
const canSubmit = message.trim().length >= 3 && !submitting;
|
const canSubmit =
|
||||||
|
message.trim().length >= 3 &&
|
||||||
|
!submitting &&
|
||||||
|
(!isTurnstileEnabled() || Boolean(turnstileToken));
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if (!canSubmit) return;
|
if (!canSubmit) return;
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
|
const turnstile_token = getTurnstileTokenForAction(
|
||||||
|
turnstileToken,
|
||||||
|
"feedback_submit",
|
||||||
|
);
|
||||||
const res = await fetch("/api/feedback", {
|
const res = await fetch("/api/feedback", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@@ -174,6 +187,7 @@ export function UserFeedbackModal({
|
|||||||
contact: contact.trim() || undefined,
|
contact: contact.trim() || undefined,
|
||||||
source: draft?.source || "terminal",
|
source: draft?.source || "terminal",
|
||||||
context: buildRuntimeContext(draft?.context || {}),
|
context: buildRuntimeContext(draft?.context || {}),
|
||||||
|
turnstile_token,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -188,6 +202,8 @@ export function UserFeedbackModal({
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
setError(message.slice(0, 220));
|
setError(message.slice(0, 220));
|
||||||
|
setTurnstileToken("");
|
||||||
|
setTurnstileResetKey((value) => value + 1);
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
@@ -296,6 +312,12 @@ export function UserFeedbackModal({
|
|||||||
: "会自动附带终端上下文:城市、槽位、数据源状态、浏览器和会话诊断信息。"}
|
: "会自动附带终端上下文:城市、槽位、数据源状态、浏览器和会话诊断信息。"}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<TurnstileWidget
|
||||||
|
action="feedback_submit"
|
||||||
|
onToken={setTurnstileToken}
|
||||||
|
resetKey={turnstileResetKey}
|
||||||
|
/>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="rounded border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700">
|
<div className="rounded border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700">
|
||||||
{isEn ? "Submit failed: " : "提交失败:"}{error}
|
{isEn ? "Submit failed: " : "提交失败:"}{error}
|
||||||
|
|||||||
Reference in New Issue
Block a user