import React, { useState, useMemo, useCallback, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { BookOpen } from 'lucide-react';
import { useAuth } from '../contexts/AuthContext';
import { getToken } from '../services/authService';
import { getPublicConfig } from '../services/configService';
import { createBetterAuthClient } from '../services/betterAuthClient';
import { getBasePath } from '../utils/runtime';
import ThemeSwitch from '@/components/ui/ThemeSwitch';
import LanguageSwitch from '@/components/ui/LanguageSwitch';
import DefaultPasswordWarningModal from '@/components/ui/DefaultPasswordWarningModal';
const sanitizeReturnUrl = (value: string | null): string | null => {
if (!value) {
return null;
}
try {
// Support both relative paths and absolute URLs on the same origin
const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost';
const url = new URL(value, origin);
if (url.origin !== origin) {
return null;
}
const relativePath = `${url.pathname}${url.search}${url.hash}`;
return relativePath || '/';
} catch {
if (value.startsWith('/') && !value.startsWith('//')) {
return value;
}
return null;
}
};
const LoginPage: React.FC = () => {
const { t } = useTranslation();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [socialLoading, setSocialLoading] = useState<'google' | 'github' | null>(null);
const [socialError, setSocialError] = useState<string | null>(null);
const [betterAuthBasePath, setBetterAuthBasePath] = useState<string | undefined>(undefined);
const [socialProviders, setSocialProviders] = useState({
google: false,
github: false,
});
const [showDefaultPasswordWarning, setShowDefaultPasswordWarning] = useState(false);
const { login, auth } = useAuth();
const location = useLocation();
const navigate = useNavigate();
const returnUrl = useMemo(() => {
const params = new URLSearchParams(location.search);
return sanitizeReturnUrl(params.get('returnUrl'));
}, [location.search]);
const isServerUnavailableError = useCallback((message?: string) => {
if (!message) return false;
const normalized = message.toLowerCase();
return (
normalized.includes('failed to fetch') ||
normalized.includes('networkerror') ||
normalized.includes('network error') ||
normalized.includes('connection refused') ||
normalized.includes('unable to connect') ||
normalized.includes('fetch error') ||
normalized.includes('econnrefused') ||
normalized.includes('http 500') ||
normalized.includes('internal server error') ||
normalized.includes('proxy error')
);
}, []);
const buildRedirectTarget = useCallback(() => {
if (!returnUrl) {
return '/';
}
// Only attach JWT when returning to the OAuth authorize endpoint
if (!returnUrl.startsWith('/oauth/authorize')) {
return returnUrl;
}
const token = getToken();
if (!token) {
return returnUrl;
}
try {
const origin = window.location.origin;
const url = new URL(returnUrl, origin);
url.searchParams.set('token', token);
return `${url.pathname}${url.search}${url.hash}`;
} catch {
const separator = returnUrl.includes('?') ? '&' : '?';
return `${returnUrl}${separator}token=${encodeURIComponent(token)}`;
}
}, [returnUrl]);
const redirectAfterLogin = useCallback(() => {
if (returnUrl) {
window.location.assign(buildRedirectTarget());
} else {
navigate('/');
}
}, [buildRedirectTarget, navigate, returnUrl]);
useEffect(() => {
if (!auth.loading && auth.isAuthenticated) {
redirectAfterLogin();
}
}, [auth.isAuthenticated, auth.loading, redirectAfterLogin]);
useEffect(() => {
const loadAuthProviders = async () => {
const publicConfig = await getPublicConfig();
const betterAuth = publicConfig.betterAuth;
if (!betterAuth?.enabled) {
setSocialProviders({ google: false, github: false });
return;
}
setBetterAuthBasePath(betterAuth.basePath);
setSocialProviders({
google: betterAuth.providers?.google?.enabled === true,
github: betterAuth.providers?.github?.enabled === true,
});
};
loadAuthProviders();
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setSocialError(null);
setLoading(true);
try {
if (!username || !password) {
setError(t('auth.emptyFields'));
setLoading(false);
return;
}
const result = await login(username, password);
if (result.success) {
if (result.isUsingDefaultPassword) {
// Show warning modal instead of navigating immediately
setShowDefaultPasswordWarning(true);
} else {
redirectAfterLogin();
}
} else {
const message = result.message;
if (isServerUnavailableError(message)) {
setError(t('auth.serverUnavailable'));
} else {
setError(t('auth.loginFailed'));
}
}
} catch (err) {
const message = err instanceof Error ? err.message : undefined;
if (isServerUnavailableError(message)) {
setError(t('auth.serverUnavailable'));
} else {
setError(t('auth.loginError'));
}
} finally {
setLoading(false);
}
};
const handleSocialLogin = async (provider: 'google' | 'github') => {
setSocialError(null);
setSocialLoading(provider);
try {
const client = createBetterAuthClient(betterAuthBasePath);
await client.signIn.social({
provider,
callbackURL: returnUrl || '/',
errorCallbackURL: `${getBasePath()}/login`,
});
} catch (err) {
console.error('Social login error:', err);
setSocialError(t('auth.socialLoginFailed'));
setSocialLoading(null);
}
};
const handleCloseWarning = () => {
setShowDefaultPasswordWarning(false);
redirectAfterLogin();
};
return (
<div className="relative min-h-screen w-full overflow-hidden bg-gray-50 dark:bg-gray-950">
{/* Top-right controls */}
<div className="absolute top-4 right-4 z-20 flex items-center gap-2">
<a
href="https://docs.mcphubx.com"
target="_blank"
rel="noopener noreferrer"
className="rounded-md p-2 text-gray-500 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200"
aria-label="Documentation"
>
<BookOpen className="h-5 w-5" />
</a>
<ThemeSwitch />
<LanguageSwitch />
</div>
{/* Tech background layer */}
<div
className="pointer-events-none absolute inset-0 -z-10 opacity-60 dark:opacity-70"
style={{
backgroundImage:
'radial-gradient(60rem 60rem at 20% -10%, rgba(99,102,241,0.25), transparent), radial-gradient(50rem 50rem at 120% 10%, rgba(168,85,247,0.15), transparent)',
}}
/>
<div className="pointer-events-none absolute inset-0 -z-10">
<svg
className="h-full w-full opacity-[0.08] dark:opacity-[0.12]"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<pattern id="grid" width="32" height="32" patternUnits="userSpaceOnUse">
<path d="M 32 0 L 0 0 0 32" fill="none" stroke="currentColor" strokeWidth="0.5" />
</pattern>
</defs>
<rect
width="100%"
height="100%"
fill="url(#grid)"
className="text-gray-400 dark:text-gray-300"
/>
</svg>
</div>
{/* Main content */}
<div className="relative mx-auto flex min-h-screen w-full max-w-md items-center justify-center px-6 py-16">
<div className="w-full space-y-16">
{/* Centered slogan */}
<div className="flex justify-center w-full">
<h1 className="text-5xl sm:text-5xl font-extrabold leading-tight tracking-tight text-gray-900 dark:text-white whitespace-nowrap">
<span className="bg-gradient-to-r from-indigo-400 via-cyan-400 to-emerald-400 bg-clip-text text-transparent">
{t('auth.slogan')}
</span>
</h1>
</div>
{/* Centered login card */}
<div className="login-card relative w-full rounded-2xl border border-white/10 bg-white/60 p-8 shadow-xl backdrop-blur-md transition dark:border-white/10 dark:bg-gray-900/60">
<div className="absolute -top-24 right-12 h-40 w-40 -translate-y-6 rounded-full bg-indigo-500/30 blur-3xl" />
<div className="absolute -bottom-24 -left-12 h-40 w-40 translate-y-6 rounded-full bg-cyan-500/20 blur-3xl" />
<form className="mt-4 space-y-4" onSubmit={handleSubmit}>
<div className="space-y-4">
<div>
<label htmlFor="username" className="sr-only">
{t('auth.username')}
</label>
<input
id="username"
name="username"
type="text"
autoComplete="username"
required
className="login-input appearance-none relative block w-full rounded-md border border-gray-300/60 bg-white/70 px-3 py-3 text-gray-900 shadow-sm outline-none ring-0 transition-all placeholder:text-gray-500 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 dark:border-gray-700/60 dark:bg-gray-800/70 dark:text-white dark:placeholder:text-gray-400"
placeholder={t('auth.username')}
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div>
<label htmlFor="password" className="sr-only">
{t('auth.password')}
</label>
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className="login-input appearance-none relative block w-full rounded-md border border-gray-300/60 bg-white/70 px-3 py-3 text-gray-900 shadow-sm outline-none ring-0 transition-all placeholder:text-gray-500 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 dark:border-gray-700/60 dark:bg-gray-800/70 dark:text-white dark:placeholder:text-gray-400"
placeholder={t('auth.password')}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
</div>
{error && (
<div className="error-box rounded border border-red-500/20 bg-red-500/10 p-2 text-center text-sm text-red-600 dark:text-red-400">
{error}
</div>
)}
<div>
<button
type="submit"
disabled={loading}
className="login-button btn-primary group relative flex w-full items-center justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-all hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-70"
>
{loading ? t('auth.loggingIn') : t('auth.login')}
</button>
</div>
</form>
{(socialProviders.google || socialProviders.github) && (
<div className="mt-6 space-y-4">
<div className="flex items-center gap-3">
<div className="h-px flex-1 bg-gray-200/80 dark:bg-gray-700/80" />
<span className="text-xs uppercase tracking-widest text-gray-500 dark:text-gray-400">
{t('auth.orContinue')}
</span>
<div className="h-px flex-1 bg-gray-200/80 dark:bg-gray-700/80" />
</div>
{socialError && (
<div className="error-box rounded border border-red-500/20 bg-red-500/10 p-2 text-center text-sm text-red-600 dark:text-red-400">
{socialError}
</div>
)}
<div className="space-y-3">
{socialProviders.google && (
<button
type="button"
onClick={() => handleSocialLogin('google')}
disabled={socialLoading !== null}
className="flex w-full items-center justify-center gap-2 rounded-md border border-gray-200 bg-white/80 px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-70 dark:border-gray-700 dark:bg-gray-900/70 dark:text-gray-200"
>
{socialLoading === 'google' ? t('auth.loggingIn') : t('auth.loginWithGoogle')}
</button>
)}
{socialProviders.github && (
<button
type="button"
onClick={() => handleSocialLogin('github')}
disabled={socialLoading !== null}
className="flex w-full items-center justify-center gap-2 rounded-md border border-gray-200 bg-gray-900 px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-70 dark:border-gray-700"
>
{socialLoading === 'github' ? t('auth.loggingIn') : t('auth.loginWithGithub')}
</button>
)}
</div>
</div>
)}
</div>
</div>
</div>
{/* Default Password Warning Modal */}
<DefaultPasswordWarningModal
isOpen={showDefaultPasswordWarning}
onClose={handleCloseWarning}
/>
</div>
);
};
export default LoginPage;