Files
kakeibo/frontend/src/features/auth/guard.tsx
T

72 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}