a2eff1a984
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
72 lines
2.5 KiB
TypeScript
72 lines
2.5 KiB
TypeScript
import { useEffect, useRef, useState } from 'react'
|
||
import { Turnstile, type TurnstileHandle } from '@/components/turnstile'
|
||
import { ApiError } from '@/lib/api'
|
||
import { useAuthGuard } from '@/lib/auth'
|
||
import { t } from '@/lib/strings'
|
||
|
||
const s = t.auth.guard
|
||
|
||
/**
|
||
* Giriş / kayıt güvenliği (ortak): bot doğrulama jetonu + IP kilidi.
|
||
* Sunucu: üst üste 3 hatalı deneme → bu bağlantıya giriş ve kayıt 15 dk kapalı.
|
||
*/
|
||
export function useAuthSecurity() {
|
||
const guard = useAuthGuard()
|
||
const widget = useRef<TurnstileHandle>(null)
|
||
const [token, setToken] = useState<string | null>(null)
|
||
const [scriptFailed, setScriptFailed] = useState(false)
|
||
const siteKey = guard.data?.captcha_site_key ?? null
|
||
// Bitiş anı: yanıtın alındığı an + sunucunun bildirdiği kalan süre
|
||
const lockedSecs = guard.data?.locked_seconds ?? 0
|
||
const until = lockedSecs > 0 ? guard.dataUpdatedAt + lockedSecs * 1000 : 0
|
||
const locked = useCountdown(until, () => void guard.refetch())
|
||
|
||
return {
|
||
loading: guard.isPending,
|
||
locked,
|
||
/** Gönder düğmesi: doğrulama açıksa jeton gelmeden olmaz */
|
||
ready: !guard.isPending && locked === 0 && (!siteKey || !!token),
|
||
token: token ?? undefined,
|
||
/** Her başarısız gönderimden sonra: jeton tek kullanımlık; kilit geldiyse sayaç başlar */
|
||
afterError(e: unknown) {
|
||
widget.current?.reset()
|
||
if (e instanceof ApiError && (e.code === 'locked' || e.code === 'invalid_credentials'))
|
||
void guard.refetch()
|
||
},
|
||
field: siteKey ? (
|
||
<div className="space-y-1.5">
|
||
<Turnstile
|
||
ref={widget}
|
||
siteKey={siteKey}
|
||
onToken={setToken}
|
||
onLoadError={() => setScriptFailed(true)}
|
||
/>
|
||
{scriptFailed && <p className="text-sm text-expense">{s.scriptFailed}</p>}
|
||
</div>
|
||
) : null,
|
||
}
|
||
}
|
||
|
||
/** `until` anına (ms) kadar kalan saniye, saniye saniye; bitince `onDone` */
|
||
function useCountdown(until: number, onDone: () => void): number {
|
||
const [now, setNow] = useState(() => Date.now())
|
||
const done = useRef(onDone)
|
||
useEffect(() => {
|
||
done.current = onDone
|
||
})
|
||
useEffect(() => {
|
||
if (!until) return
|
||
const id = setInterval(() => {
|
||
const n = Date.now()
|
||
setNow(n)
|
||
if (n >= until) {
|
||
clearInterval(id)
|
||
done.current()
|
||
}
|
||
}, 1000)
|
||
return () => clearInterval(id)
|
||
}, [until])
|
||
// `now` bileşen açılırken alındı; yanıt daha sonra geldiyse süre 15 dk'yı aşmasın
|
||
return until ? Math.max(0, Math.ceil((until - Math.max(now, until - 15 * 60_000)) / 1000)) : 0
|
||
}
|