feat: check-in feature integrates Turnstile security check

This commit is contained in:
Seefs
2026-01-03 11:08:26 +08:00
parent 43c671b8b3
commit c33ac97c71
4 changed files with 70 additions and 13 deletions

View File

@@ -96,7 +96,7 @@ func SetApiRouter(router *gin.Engine) {
// Check-in routes
selfRoute.GET("/checkin", controller.GetCheckinStatus)
selfRoute.POST("/checkin", controller.DoCheckin)
selfRoute.POST("/checkin", middleware.TurnstileCheck(), controller.DoCheckin)
}
adminRoute := userRoute.Group("/")

View File

@@ -451,7 +451,12 @@ const PersonalSetting = () => {
{/* 签到日历 - 仅在启用时显示 */}
{status?.checkin_enabled && (
<div className='mt-4 md:mt-6'>
<CheckinCalendar t={t} status={status} />
<CheckinCalendar
t={t}
status={status}
turnstileEnabled={turnstileEnabled}
turnstileSiteKey={turnstileSiteKey}
/>
</div>
)}

View File

@@ -27,6 +27,7 @@ import {
Spin,
Tooltip,
Collapsible,
Modal,
} from '@douyinfe/semi-ui';
import {
CalendarCheck,
@@ -35,11 +36,14 @@ import {
ChevronDown,
ChevronUp,
} from 'lucide-react';
import Turnstile from 'react-turnstile';
import { API, showError, showSuccess, renderQuota } from '../../../../helpers';
const CheckinCalendar = ({ t, status }) => {
const CheckinCalendar = ({ t, status, turnstileEnabled, turnstileSiteKey }) => {
const [loading, setLoading] = useState(false);
const [checkinLoading, setCheckinLoading] = useState(false);
const [turnstileModalVisible, setTurnstileModalVisible] = useState(false);
const [turnstileWidgetKey, setTurnstileWidgetKey] = useState(0);
const [checkinData, setCheckinData] = useState({
enabled: false,
stats: {
@@ -109,11 +113,23 @@ const CheckinCalendar = ({ t, status }) => {
}
};
// 执行签到
const doCheckin = async () => {
const postCheckin = async (token) => {
const url = token
? `/api/user/checkin?turnstile=${encodeURIComponent(token)}`
: '/api/user/checkin';
return API.post(url);
};
const shouldTriggerTurnstile = (message) => {
if (!turnstileEnabled) return false;
if (typeof message !== 'string') return true;
return message.includes('Turnstile');
};
const doCheckin = async (token) => {
setCheckinLoading(true);
try {
const res = await API.post('/api/user/checkin');
const res = await postCheckin(token);
const { success, data, message } = res.data;
if (success) {
showSuccess(
@@ -121,7 +137,19 @@ const CheckinCalendar = ({ t, status }) => {
);
// 刷新签到状态
fetchCheckinStatus(currentMonth);
setTurnstileModalVisible(false);
} else {
if (!token && shouldTriggerTurnstile(message)) {
if (!turnstileSiteKey) {
showError('Turnstile is enabled but site key is empty.');
return;
}
setTurnstileModalVisible(true);
return;
}
if (token && shouldTriggerTurnstile(message)) {
setTurnstileWidgetKey((v) => v + 1);
}
showError(message || t('签到失败'));
}
} catch (error) {
@@ -186,6 +214,30 @@ const CheckinCalendar = ({ t, status }) => {
return (
<Card className='!rounded-2xl'>
<Modal
title='Security Check'
visible={turnstileModalVisible}
footer={null}
centered
onCancel={() => {
setTurnstileModalVisible(false);
setTurnstileWidgetKey((v) => v + 1);
}}
>
<div className='flex justify-center py-2'>
<Turnstile
key={turnstileWidgetKey}
sitekey={turnstileSiteKey}
onVerify={(token) => {
doCheckin(token);
}}
onExpire={() => {
setTurnstileWidgetKey((v) => v + 1);
}}
/>
</div>
</Modal>
{/* 卡片头部 */}
<div className='flex items-center justify-between'>
<div
@@ -221,7 +273,7 @@ const CheckinCalendar = ({ t, status }) => {
type='primary'
theme='solid'
icon={<Gift size={16} />}
onClick={doCheckin}
onClick={() => doCheckin()}
loading={checkinLoading || !initialLoaded}
disabled={!initialLoaded || checkinData.stats?.checked_in_today}
className='!bg-green-600 hover:!bg-green-700'

View File

@@ -42,12 +42,12 @@ export class SecureVerificationService {
isPasskeySupported(),
]);
console.log('=== DEBUGGING VERIFICATION METHODS ===');
console.log('2FA Response:', JSON.stringify(twoFAResponse, null, 2));
console.log(
'Passkey Response:',
JSON.stringify(passkeyResponse, null, 2),
);
// console.log('=== DEBUGGING VERIFICATION METHODS ===');
// console.log('2FA Response:', JSON.stringify(twoFAResponse, null, 2));
// console.log(
// 'Passkey Response:',
// JSON.stringify(passkeyResponse, null, 2),
// );
const has2FA =
twoFAResponse.data?.success &&