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 { useState, useRef, useCallback } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useAuth } from '@/lib/auth';
|
import { useAuth } from '@/lib/auth';
|
||||||
|
import Altcha from '@/components/Altcha';
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [altchaToken, setAltchaToken] = useState('');
|
||||||
|
const [altchaVerified, setAltchaVerified] = useState(false);
|
||||||
const emailRef = useRef('');
|
const emailRef = useRef('');
|
||||||
const passwordRef = useRef('');
|
const passwordRef = useRef('');
|
||||||
const { login } = useAuth();
|
const { login } = useAuth();
|
||||||
const router = useRouter();
|
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 () => {
|
const performLogin = useCallback(async () => {
|
||||||
|
if (!altchaVerified) {
|
||||||
|
setError('Please complete the security verification');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
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);
|
await login(emailRef.current, passwordRef.current, altchaToken);
|
||||||
router.push('/');
|
router.push('/');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|
@ -26,7 +42,7 @@ export default function LoginPage() {
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [login, router]);
|
}, [login, router, altchaToken, altchaVerified]);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -85,10 +101,16 @@ export default function LoginPage() {
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Altcha
|
||||||
|
challengeurl="https://api.sojorn.net/api/v1/admin/altcha-challenge"
|
||||||
|
onStateChange={handleAltchaStateChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn-primary w-full"
|
className="btn-primary w-full"
|
||||||
disabled={loading}
|
disabled={loading || !altchaVerified}
|
||||||
>
|
>
|
||||||
{loading ? 'Signing in...' : 'Sign In'}
|
{loading ? 'Signing in...' : 'Sign In'}
|
||||||
</button>
|
</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