feat: update contact form client with captcha support
This commit is contained in:
@@ -8,7 +8,8 @@ import { Textarea } from '@/components/ui/textarea';
|
||||
import { Toast } from '@/components/ui/toast';
|
||||
import { sanitizeInput } from '@/lib/sanitize';
|
||||
import { generateCSRFToken, setCSRFTokenToStorage, getCSRFTokenFromStorage } from '@/lib/csrf';
|
||||
import { Mail, Phone, MapPin, Send, Loader2, Clock, HeadphonesIcon, CheckCircle2 } from 'lucide-react';
|
||||
import { generateCaptcha } from '@/lib/security/captcha';
|
||||
import { Mail, Phone, MapPin, Send, Loader2, Clock, HeadphonesIcon, CheckCircle2, RefreshCw } from 'lucide-react';
|
||||
import { COMPANY_INFO } from '@/lib/constants';
|
||||
|
||||
const contactFormSchema = z.object({
|
||||
@@ -25,6 +26,7 @@ interface FormErrors {
|
||||
phone?: string;
|
||||
email?: string;
|
||||
message?: string;
|
||||
captcha?: string;
|
||||
}
|
||||
|
||||
export function ContactSection() {
|
||||
@@ -41,6 +43,8 @@ export function ContactSection() {
|
||||
message: '',
|
||||
});
|
||||
const [errors, setErrors] = useState<FormErrors>({});
|
||||
const [captcha, setCaptcha] = useState(generateCaptcha('simple'));
|
||||
const [captchaAnswer, setCaptchaAnswer] = useState('');
|
||||
const sectionRef = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -89,6 +93,19 @@ export function ContactSection() {
|
||||
validateField(field, value);
|
||||
};
|
||||
|
||||
const handleCaptchaRefresh = () => {
|
||||
setCaptcha(generateCaptcha('simple'));
|
||||
setCaptchaAnswer('');
|
||||
setErrors((prev) => ({ ...prev, captcha: undefined }));
|
||||
};
|
||||
|
||||
const handleCaptchaChange = (value: string) => {
|
||||
setCaptchaAnswer(value);
|
||||
if (errors.captcha) {
|
||||
setErrors((prev) => ({ ...prev, captcha: undefined }));
|
||||
}
|
||||
};
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -99,7 +116,7 @@ export function ContactSection() {
|
||||
setShowToast(true);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const result = contactFormSchema.safeParse(formData);
|
||||
|
||||
if (!result.success) {
|
||||
@@ -112,6 +129,12 @@ export function ContactSection() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!captchaAnswer || parseInt(captchaAnswer) !== captcha.answer) {
|
||||
setErrors((prev) => ({ ...prev, captcha: '验证码错误,请重新计算' }));
|
||||
handleCaptchaRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
@@ -123,13 +146,16 @@ export function ContactSection() {
|
||||
body: JSON.stringify({
|
||||
...formData,
|
||||
csrfToken: storedToken,
|
||||
mathHash: captcha.hash,
|
||||
mathTimestamp: captcha.timestamp,
|
||||
mathAnswer: captcha.answer,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || '提交失败');
|
||||
throw new Error(data.error || '提交失败');
|
||||
}
|
||||
|
||||
const newCsrfToken = generateCSRFToken();
|
||||
@@ -330,6 +356,39 @@ export function ContactSection() {
|
||||
onBlur={(e) => handleBlur('message', e.target.value)}
|
||||
error={errors.message}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="captcha" className="block text-sm font-medium text-[#1A1A2E]">
|
||||
验证码 <span className="text-[#C41E3A]">*</span>
|
||||
</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-[#E2E8F0] px-4 py-2 rounded-md font-mono text-lg text-[#1A1A2E] min-w-[120px] text-center" data-testid="captcha-question">
|
||||
{captcha.question}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
id="captcha"
|
||||
type="number"
|
||||
placeholder="请输入答案"
|
||||
required
|
||||
data-testid="captcha-input"
|
||||
value={captchaAnswer}
|
||||
onChange={(e) => handleCaptchaChange(e.target.value)}
|
||||
error={errors.captcha}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleCaptchaRefresh}
|
||||
disabled={isSubmitting}
|
||||
data-testid="refresh-captcha"
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
|
||||
Reference in New Issue
Block a user