feat: add ALTCHA widget to admin login page
- Create Altcha component for admin frontend - Add ALTCHA verification to login flow - Disable login button until ALTCHA is verified - Use admin ALTCHA challenge endpoint
This commit is contained in:
parent
ace3b33344
commit
d190afbd19
|
|
@ -3,22 +3,38 @@
|
|||
import { useState, useRef, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/lib/auth';
|
||||
import Altcha from '@/components/Altcha';
|
||||
|
||||
export default function LoginPage() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [altchaToken, setAltchaToken] = useState('');
|
||||
const [altchaVerified, setAltchaVerified] = useState(false);
|
||||
const emailRef = useRef('');
|
||||
const passwordRef = useRef('');
|
||||
const { login } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
const handleAltchaStateChange = useCallback((state: any) => {
|
||||
if (state.state === 'verified' && state.payload) {
|
||||
setAltchaToken(state.payload);
|
||||
setAltchaVerified(true);
|
||||
} else {
|
||||
setAltchaToken('');
|
||||
setAltchaVerified(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const performLogin = useCallback(async () => {
|
||||
if (!altchaVerified) {
|
||||
setError('Please complete the security verification');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// Use development bypass if in development mode
|
||||
const altchaToken = process.env.NODE_ENV === 'development' ? 'BYPASS_DEV_MODE' : '';
|
||||
await login(emailRef.current, passwordRef.current, altchaToken);
|
||||
router.push('/');
|
||||
} catch (err: any) {
|
||||
|
|
@ -26,7 +42,7 @@ export default function LoginPage() {
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [login, router]);
|
||||
}, [login, router, altchaToken, altchaVerified]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -85,10 +101,16 @@ export default function LoginPage() {
|
|||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Altcha
|
||||
challengeurl="https://api.sojorn.net/api/v1/admin/altcha-challenge"
|
||||
onStateChange={handleAltchaStateChange}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-primary w-full"
|
||||
disabled={loading}
|
||||
disabled={loading || !altchaVerified}
|
||||
>
|
||||
{loading ? 'Signing in...' : 'Sign In'}
|
||||
</button>
|
||||
|
|
|
|||
58
admin/src/components/Altcha.tsx
Normal file
58
admin/src/components/Altcha.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface AltchaProps {
|
||||
challengeurl: string;
|
||||
onStateChange?: (state: any) => void;
|
||||
}
|
||||
|
||||
export default function Altcha({ challengeurl, onStateChange }: AltchaProps) {
|
||||
const widgetRef = useRef<HTMLDivElement>(null);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Load ALTCHA widget script
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/altcha@0.5.0/dist/altcha.min.js';
|
||||
script.type = 'module';
|
||||
script.async = true;
|
||||
script.onload = () => setLoaded(true);
|
||||
document.head.appendChild(script);
|
||||
|
||||
return () => {
|
||||
if (script.parentNode) {
|
||||
script.parentNode.removeChild(script);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loaded || !widgetRef.current) return;
|
||||
|
||||
const widget = widgetRef.current.querySelector('altcha-widget');
|
||||
if (!widget) return;
|
||||
|
||||
const handleStateChange = (event: any) => {
|
||||
if (onStateChange) {
|
||||
onStateChange(event.detail);
|
||||
}
|
||||
};
|
||||
|
||||
widget.addEventListener('statechange', handleStateChange);
|
||||
|
||||
return () => {
|
||||
widget.removeEventListener('statechange', handleStateChange);
|
||||
};
|
||||
}, [loaded, onStateChange]);
|
||||
|
||||
return (
|
||||
<div ref={widgetRef}>
|
||||
<altcha-widget
|
||||
challengeurl={challengeurl}
|
||||
hidefooter="true"
|
||||
hidelogo="true"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue