borsa / nextjs-app /src /components /AuthForm.tsx
veteroner's picture
fix: make sync endpoint market-aware — prevent US scan results from overwriting BIST file
ce7f322
'use client';
import { useState, useEffect, useMemo, useCallback } from 'react';
import { useAuth } from '@/contexts/AuthContext';
import { useMarket } from '@/contexts/MarketContext';
import { useSearchParams } from 'next/navigation';
import { Mail, Lock, User, AlertCircle, Loader2 } from 'lucide-react';
import { isSupabaseEnabled } from '@/lib/supabase/client';
import { validatePassword, getStrengthColor, getStrengthLabel, getStrengthPercent } from '@/lib/password-validation';
export default function AuthForm() {
const { market } = useMarket();
const isUS = market === 'us';
const [isLogin, setIsLogin] = useState(true);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const { signIn, signUp } = useAuth();
const searchParams = useSearchParams();
const passwordCheck = useMemo(() => validatePassword(password), [password]);
const t = useCallback((tr: string, en: string) => isUS ? en : tr, [isUS]);
const strengthLabel = isUS
? passwordCheck.strength === 'fair'
? 'Fair'
: passwordCheck.strength === 'good'
? 'Good'
: passwordCheck.strength === 'strong'
? 'Strong'
: getStrengthLabel(passwordCheck.strength)
: getStrengthLabel(passwordCheck.strength)
// Banned user redirect handling
useEffect(() => {
if (searchParams.get('banned') === '1') {
setError(t('Hesabınız engellenmiştir. Yönetici ile iletişime geçin.', 'Your account has been blocked. Contact the administrator.'));
}
}, [searchParams, t]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
// Validate password strength on signup
if (!isLogin && !passwordCheck.valid) {
setError(t('Şifre gereksinimleri: ', 'Password requirements: ') + passwordCheck.errors.join(', '));
setLoading(false);
return;
}
try {
const { error } = isLogin
? await signIn(email, password)
: await signUp(email, password);
if (error) {
setError(error.message);
} else {
if (!isLogin) {
setError(t('Kayıt başarılı! Lütfen email adresinizi doğrulayın.', 'Registration successful! Please verify your email address.'));
} else {
// Wait a moment for @supabase/ssr to persist session cookies before navigating.
// If cookies don't appear, navigating to a protected route will bounce back to /login.
const hasSbCookie = () => typeof document !== 'undefined' && document.cookie.includes('sb-')
let attempts = 0
while (!hasSbCookie() && attempts < 20) {
// 20 * 50ms = 1s max
// eslint-disable-next-line no-await-in-loop
await new Promise((r) => setTimeout(r, 50))
attempts += 1
}
if (!hasSbCookie()) {
setError(
t(
'Giriş başarılı görünüyor ama oturum cookie yazılamadı. Lütfen siteyi sadece http://localhost:3001 üzerinden açın (127.0.0.1 değil) ve tarayıcıda cookie engeli olmadığından emin olun.',
'Sign-in appears successful but the session cookie could not be written. Open the site only via http://localhost:3001 (not 127.0.0.1) and make sure cookies are not blocked in the browser.'
)
)
return
}
window.location.replace('/');
}
}
} catch (err) {
setError(t('Bir hata oluştu. Lütfen tekrar deneyin.', 'An error occurred. Please try again.'));
} finally {
setLoading(false);
}
};
return (
<div className="max-w-md mx-auto">
<div className="bg-white rounded-lg shadow-lg p-8">
<div className="text-center mb-8">
<User className="w-12 h-12 text-primary-600 mx-auto mb-4" />
<h2 className="text-2xl font-bold text-gray-900">
{isLogin ? t('Giriş Yap', 'Sign In') : t('Kayıt Ol', 'Sign Up')}
</h2>
<p className="text-gray-600 mt-2">
{isLogin
? t('Portföyünüze erişmek için giriş yapın', 'Sign in to access your portfolio')
: t('Yeni bir hesap oluşturun', 'Create a new account')}
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{!isSupabaseEnabled && (
<div className="flex items-start gap-2 p-4 bg-amber-50 border border-amber-200 rounded-lg">
<AlertCircle className="w-5 h-5 text-amber-700 flex-shrink-0 mt-0.5" />
<p className="text-sm text-amber-800">
{t('Giriş/Kayıt devre dışı: Supabase yapılandırılmamış. Netlify/yerel ortamda', 'Sign-in/Sign-up disabled: Supabase is not configured. In Netlify/local environments add')}
<span className="font-mono"> NEXT_PUBLIC_SUPABASE_URL</span> ve
<span className="font-mono"> NEXT_PUBLIC_SUPABASE_ANON_KEY</span> {t('değişkenlerini ekleyin.', 'environment variables.')}
</p>
</div>
)}
<div>
<label htmlFor="auth-email" className="block text-sm font-medium text-gray-700 mb-2">
Email
</label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" aria-hidden="true" />
<input
id="auth-email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="ornek@email.com"
required
autoComplete="email"
/>
</div>
</div>
<div>
<label htmlFor="auth-password" className="block text-sm font-medium text-gray-700 mb-2">
{t('Şifre', 'Password')}
</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" aria-hidden="true" />
<input
id="auth-password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="••••••••"
required
minLength={8}
autoComplete={isLogin ? 'current-password' : 'new-password'}
/>
</div>
{!isLogin && password.length > 0 && (
<div className="mt-2">
<div className="flex items-center gap-2">
<div className="flex-1 h-1.5 bg-gray-200 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-300 ${getStrengthColor(passwordCheck.strength)}`}
style={{ width: `${getStrengthPercent(passwordCheck.strength)}%` }}
/>
</div>
<span className="text-xs font-medium text-gray-600">{strengthLabel}</span>
</div>
{passwordCheck.errors.length > 0 && (
<ul className="mt-1 space-y-0.5">
{passwordCheck.errors.map((err, i) => (
<li key={i} className="text-xs text-red-500">• {err}</li>
))}
</ul>
)}
</div>
)}
{!isLogin && password.length === 0 && (
<p className="text-xs text-gray-500 mt-1">{t('En az 8 karakter, büyük harf, küçük harf ve rakam', 'At least 8 characters, uppercase, lowercase, and a number')}</p>
)}
</div>
{error && (
<div className="flex items-start gap-2 p-4 bg-red-50 border border-red-200 rounded-lg">
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-600">{error}</p>
</div>
)}
<button
type="submit"
disabled={loading || !isSupabaseEnabled}
className="w-full bg-primary-600 text-white py-3 rounded-lg hover:bg-primary-700 disabled:bg-gray-300 disabled:cursor-not-allowed flex items-center justify-center gap-2 font-medium"
>
{loading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
{t('İşlem yapılıyor...', 'Processing...')}
</>
) : isLogin ? (
t('Giriş Yap', 'Sign In')
) : (
t('Kayıt Ol', 'Sign Up')
)}
</button>
</form>
<div className="mt-6 text-center">
<button
onClick={() => {
setIsLogin(!isLogin);
setError('');
}}
className="text-primary-600 hover:text-primary-800 font-medium"
>
{isLogin
? t('Hesabınız yok mu? Kayıt olun', 'Don\'t have an account? Sign up')
: t('Zaten hesabınız var mı? Giriş yapın', 'Already have an account? Sign in')}
</button>
</div>
</div>
</div>
);
}