diff --git a/app/teknav-auth.jsx b/app/teknav-auth.jsx
new file mode 100644
index 0000000..ec69436
--- /dev/null
+++ b/app/teknav-auth.jsx
@@ -0,0 +1,988 @@
+// teknav-auth.jsx — Login / Signup page
+import { useState, useEffect } from 'react';
+import { useAuth, useNav, useToast } from './teknav-ui.jsx';
+import { ApiError } from './src/lib/api.js';
+import { api } from './src/lib/api.js';
+import TeknavCAP from './src/lib/TeknavCAP.jsx';
+
+// ── Hooks ─────────────────────────────────────────────────────────────────────
+function useTimeGreeting() {
+ const h = new Date().getHours();
+ if (h < 5) return 'شب بخیر';
+ if (h < 12) return 'صبح بخیر';
+ if (h < 17) return 'روز بخیر';
+ if (h < 21) return 'عصر بخیر';
+ return 'شب بخیر';
+}
+
+function useCountUp(target, delayMs = 0) {
+ const [count, setCount] = useState(0);
+ useEffect(() => {
+ const t = setTimeout(() => {
+ const steps = 36;
+ let step = 0;
+ const iv = setInterval(() => {
+ step++;
+ const ease = 1 - Math.pow(1 - step / steps, 3);
+ setCount(Math.floor(ease * target));
+ if (step >= steps) { setCount(target); clearInterval(iv); }
+ }, 1600 / steps);
+ return () => clearInterval(iv);
+ }, delayMs);
+ return () => clearTimeout(t);
+ }, [target, delayMs]);
+ return count;
+}
+
+// ── Page-scoped CSS ───────────────────────────────────────────────────────────
+function AuthStyles() {
+ return (
+
+ );
+}
+
+// ── Icons ─────────────────────────────────────────────────────────────────────
+function EyeIcon({ open }) {
+ return open ? (
+
+ ) : (
+
+ );
+}
+
+function GoogleIcon() {
+ return (
+
+ );
+}
+
+function GithubIcon() {
+ return (
+
+ );
+}
+
+// ── Brand Panel ───────────────────────────────────────────────────────────────
+const TICKER_TOPICS = [
+ 'هوش مصنوعی و آینده توسعه نرمافزار',
+ 'امنیت سایبری در دوران پس از کوانتوم',
+ 'پردازش ابری بومیسازیشده در ایران',
+ 'متنباز در اکوسیستم فارسیزبان',
+ 'زیرساختهای توزیعشده مقیاسپذیر',
+ 'یادگیری عمیق با مجموعهدادههای فارسی',
+ 'معماری میکروسرویس در محیط تولید',
+ 'مدیریت داده در مقیاس پتابایت',
+];
+
+const BRAND_WORDS = [
+ { text: 'هوش مصنوعی', s: 12, x: 6, y: 12, spd: 3.2 },
+ { text: 'ابر', s: 10, x: 72, y: 22, spd: 4.1 },
+ { text: 'امنیت', s: 11, x: 18, y: 74, spd: 3.6 },
+ { text: 'متنباز', s: 10, x: 62, y: 84, spd: 4.5 },
+ { text: 'پردازش', s: 11, x: 4, y: 48, spd: 3.9 },
+ { text: 'شبکه', s: 10, x: 76, y: 58, spd: 3.3 },
+ { text: 'داده', s: 12, x: 38, y: 8, spd: 4.8 },
+ { text: 'زیرساخت', s: 10, x: 48, y: 91, spd: 3.7 },
+];
+
+function BrandTicker() {
+ const doubled = [...TICKER_TOPICS, ...TICKER_TOPICS];
+ return (
+
+
+ {doubled.map((item, i) => (
+
+ ◉
+ {item}
+
+ ))}
+
+
+ );
+}
+
+function BrandPanel() {
+ const [mouse, setMouse] = useState({ x: 0, y: 0 });
+ const articles = useCountUp(23, 700);
+ const readers = useCountUp(8400, 1000);
+ const series = useCountUp(6, 1300);
+
+ useEffect(() => {
+ const h = (e) => setMouse({
+ x: e.clientX / window.innerWidth - 0.5,
+ y: e.clientY / window.innerHeight - 0.5,
+ });
+ window.addEventListener('mousemove', h, { passive: true });
+ return () => window.removeEventListener('mousemove', h);
+ }, []);
+
+ return (
+
+ {/* Giant watermark letter */}
+
ت
+
+ {/* Dot grid */}
+
+
+ {/* Parallax glow — follows cursor */}
+
+
+ {/* Terracotta accent glow */}
+
+
+ {/* Floating word bubbles — subtle parallax */}
+
+ {BRAND_WORDS.map((w, i) => (
+
{w.text}
+ ))}
+
+
+ {/* ── Content ── */}
+
+
+ {/* Orbital logo */}
+
+ {/* Outer dashed ring */}
+
+ {/* Middle ring + orbiting dot */}
+
+ {/* Inner ring + dot */}
+
+ {/* Logo square */}
+
+ ت
+
+
+
+ {/* Wordmark */}
+
تکناو
+
TEKNAV · IR
+
+ {/* Animated divider */}
+
+
+ {/* Tagline */}
+
+ آخرین دانش فناوری،{' '}
+ اول به فارسی
+
+
+ {/* Live stats — count-up */}
+
+ {[
+ { n: articles, label: 'مقاله', delay: '0s', suffix: '+' },
+ { n: readers, label: 'خواننده', delay: '0.18s', suffix: '' },
+ { n: series, label: 'سری', delay: '0.34s', suffix: '' },
+ ].map(({ n, label, delay, suffix }) => (
+
+
+ {n.toLocaleString('fa-IR')}{suffix}
+
+
{label}
+
+ ))}
+
+
+ {/* Divider + ticker */}
+
+
+ {/* Online pulse indicator */}
+
+
+ در حال حاضر آنلاین
+
+
+
+ );
+}
+
+// ── AuthInput ─────────────────────────────────────────────────────────────────
+function AuthInput({ label, hint, error, suffix, inputProps = {} }) {
+ const { style: inputStyle, ...rest } = inputProps;
+ return (
+
+ {label && (
+
+ )}
+
+
+ {suffix && (
+
{suffix}
+ )}
+
+ {hint && !error &&
{hint}
}
+ {error &&
{error}
}
+
+ );
+}
+
+// ── Password strength ─────────────────────────────────────────────────────────
+function PasswordStrength({ pw }) {
+ const checks = [
+ { label: '۸+ کاراکتر', ok: pw.length >= 8 },
+ { label: 'حرف کوچک', ok: /[a-z]/.test(pw) },
+ { label: 'حرف بزرگ', ok: /[A-Z]/.test(pw) },
+ { label: 'عدد', ok: /[0-9]/.test(pw) },
+ { label: 'نماد', ok: /[^a-zA-Z0-9]/.test(pw) },
+ ];
+ const score = checks.filter(c => c.ok).length;
+ const barColor = score <= 2 ? '#C94C4C' : score === 3 ? '#D08A22' : '#2F8F6B';
+ return (
+
+
+ {[1,2,3,4,5].map(i => (
+
+ ))}
+
+
+ {checks.map(({ label, ok }) => (
+ {ok ? '✓' : '·'} {label}
+ ))}
+
+
+ );
+}
+
+// ── Forgot password modal ─────────────────────────────────────────────────────
+function ForgotPasswordModal({ onClose }) {
+ const toast = useToast();
+ const [email, setEmail] = useState('');
+ const [sent, setSent] = useState(false);
+ const [loading, setLoading] = useState(false);
+
+ const send = async (e) => {
+ e.preventDefault();
+ if (!email.trim()) { toast('ایمیل را وارد کنید', 'error'); return; }
+ setLoading(true);
+ try {
+ await api.post('/api/auth/forgot-password', { email: email.trim().toLowerCase() });
+ setSent(true);
+ } catch {
+ toast('خطا در ارسال ایمیل. لطفاً دوباره تلاش کنید', 'error');
+ } finally { setLoading(false); }
+ };
+
+ return (
+
+
+
+
بازیابی رمز عبور
+
+ {sent
+ ? 'اگر این ایمیل در سیستم ثبت شده باشد، لینک بازنشانی برای شما ارسال خواهد شد.'
+ : 'آدرس ایمیل ثبتشده خود را وارد کنید.'}
+
+ {sent ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
+// ── Login form ────────────────────────────────────────────────────────────────
+function LoginForm({ onSuccess }) {
+ const { login } = useAuth();
+ const toast = useToast();
+ const [identifier, setIdentifier] = useState('');
+ const [password, setPassword] = useState('');
+ const [captcha, setCaptcha] = useState({ captchaId: '', userSolution: '' });
+ const [loading, setLoading] = useState(false);
+ const [showPass, setShowPass] = useState(false);
+ const [showForgot, setShowForgot] = useState(false);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (!identifier || !password) { toast('نام کاربری/ایمیل و رمز عبور الزامی است', 'error'); return; }
+ if (!captcha.captchaId || !captcha.userSolution.trim()) { toast('کد امنیتی را وارد کنید', 'error'); return; }
+ setLoading(true);
+ try {
+ const user = await login(identifier, password, captcha.captchaId, captcha.userSolution);
+ if (user) onSuccess(user);
+ else toast('ایمیل یا رمز عبور نادرست است', 'error');
+ } catch (err) {
+ if (err instanceof ApiError) {
+ if (err.status === 429) toast('تعداد تلاشهای ناموفق از حد مجاز گذشت. چند دقیقه صبر کنید', 'error');
+ else if (err.status === 401) toast('نام کاربری، ایمیل یا رمز عبور نادرست است', 'error');
+ else if (err.status === 403) toast('این حساب غیرفعال است', 'error');
+ else if (err.status === 400) toast('کد امنیتی نامعتبر است — کد جدید دریافت کنید', 'error');
+ else toast('خطا در اتصال به سرور', 'error');
+ } else toast('خطای غیرمنتظره', 'error');
+ } finally { setLoading(false); }
+ };
+
+ return (
+
+ );
+}
+
+// ── Signup form ───────────────────────────────────────────────────────────────
+function SignupForm({ onSuccess }) {
+ const { signup } = useAuth();
+ const toast = useToast();
+ const [name, setName] = useState('');
+ const [username, setUsername] = useState('');
+ const [identifier, setIdentifier] = useState('');
+ const [identifierType, setIdentifierType] = useState('email');
+ const [password, setPassword] = useState('');
+ const [confirmPw, setConfirmPw] = useState('');
+ const [captcha, setCaptcha] = useState({ captchaId: '', userSolution: '' });
+ const [loading, setLoading] = useState(false);
+ const [showPass, setShowPass] = useState(false);
+
+ const isPhone = identifierType === 'phone';
+ const pwMatch = confirmPw && password === confirmPw;
+ const pwMismatch = confirmPw && password !== confirmPw;
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (!name.trim()) { toast('نام الزامی است', 'error'); return; }
+ if (!/^[a-z0-9_][a-z0-9_.]{1,28}[a-z0-9_]$/.test(username.trim().toLowerCase().replace(/^@/, ''))) {
+ toast('نام کاربری معتبر وارد کنید', 'error'); return;
+ }
+ if (!identifier.trim()) { toast(isPhone ? 'شماره موبایل الزامی است' : 'ایمیل الزامی است', 'error'); return; }
+ if (password.length < 8) { toast('رمز عبور باید حداقل ۸ کاراکتر باشد', 'error'); return; }
+ if (password !== confirmPw) { toast('رمزهای عبور تطابق ندارند', 'error'); return; }
+ if (!captcha.captchaId || !captcha.userSolution.trim()) { toast('کد امنیتی را وارد کنید', 'error'); return; }
+ setLoading(true);
+ try {
+ const payload = {
+ name: name.trim(),
+ username: username.trim().toLowerCase().replace(/^@/, ''),
+ password, captchaId: captcha.captchaId, userSolution: captcha.userSolution,
+ ...(isPhone ? { phone: identifier.trim() } : { email: identifier.trim().toLowerCase() }),
+ };
+ const user = await signup(payload);
+ if (user) onSuccess(user);
+ } catch (err) {
+ if (err instanceof ApiError) {
+ if (err.status === 409) {
+ const msg = err.body?.error;
+ toast(msg === 'username_taken' ? 'این نام کاربری قبلاً گرفته شده است' : isPhone ? 'این شماره قبلاً ثبت شده است' : 'این ایمیل قبلاً ثبت شده است', 'error');
+ } else if (err.status === 429) {
+ toast('تعداد ثبتنام از این آیپی زیاد است. کمی صبر کنید', 'error');
+ } else if (err.status === 400) {
+ toast(err.body?.error === 'invalid_captcha' ? 'کد امنیتی نامعتبر است' : 'اطلاعات واردشده معتبر نیست', 'error');
+ } else toast('خطا در اتصال به سرور', 'error');
+ } else toast('خطای غیرمنتظره', 'error');
+ } finally { setLoading(false); }
+ };
+
+ return (
+
+ );
+}
+
+// ── OAuth buttons ─────────────────────────────────────────────────────────────
+function OAuthButtons({ oauthError }) {
+ const { startOAuth } = useAuth();
+ const [providers, setProviders] = useState([]);
+ useEffect(() => {
+ api.get('/api/auth/oauth/providers')
+ .then(d => setProviders(d?.providers ?? []))
+ .catch(() => {});
+ }, []);
+ if (providers.length === 0 && !oauthError) return null;
+ return (
+
+ {oauthError && (
+
+ {oauthError === 'account_suspended' ? 'این حساب معلق شده است' : 'ورود با این روش ممکن نبود. دوباره تلاش کنید'}
+
+ )}
+ {providers.length > 0 && (
+ <>
+
+ {providers.includes('google') && (
+
+ )}
+ {providers.includes('github') && (
+
+ )}
+
+
+ >
+ )}
+
+ );
+}
+
+// ── Main LoginPage ────────────────────────────────────────────────────────────
+function LoginPage() {
+ const { navigate } = useNav();
+ const toast = useToast();
+ const { user, refreshMe } = useAuth();
+ const [tab, setTab] = useState('login');
+ const greeting = useTimeGreeting();
+
+ const hashParams = new URLSearchParams(window.location.hash.split('?')[1] || '');
+ const oauthError = hashParams.get('error') || null;
+ const emailVerifyToken = new URLSearchParams(window.location.search).get('verifyEmail');
+
+ useEffect(() => {
+ if (!emailVerifyToken || !user) return;
+ api.post('/api/auth/email/verify/confirm', { token: emailVerifyToken })
+ .then(async () => {
+ const freshUser = await refreshMe?.();
+ toast('ایمیل شما تایید شد');
+ window.history.replaceState(null, '', window.location.pathname + window.location.hash);
+ navigate((freshUser?.role ?? user.role) === 'reader' ? '/' : '/admin');
+ })
+ .catch(() => toast('تایید ایمیل ناموفق بود یا لینک منقضی شده است', 'error'));
+ }, [emailVerifyToken, user]);
+
+ useEffect(() => {
+ if (user && !emailVerifyToken) navigate(user.role === 'reader' ? '/' : '/admin');
+ }, [user, emailVerifyToken]);
+
+ const handleSuccess = (u) => {
+ toast(`خوش آمدید، ${u.name}!`);
+ navigate(u.role === 'reader' ? '/' : '/admin');
+ };
+
+ return (
+ <>
+
+
+
+ {/* ── Form column (right in RTL) ──────────────────────────────── */}
+
+ {/* Radial vignette overlay */}
+
+
+
+
+ {/* Mobile logo */}
+
+
+ {/* Heading with vertical accent bar + time-based greeting */}
+
+
+
+
+ {tab === 'login' ? greeting : 'عضو تکناو شوید'}
+
+
+
+ {tab === 'login'
+ ? 'با حساب کاربری خود وارد شوید'
+ : 'در چند ثانیه حساب رایگان بسازید'}
+
+
+
+ {/* Pill tab switcher */}
+
+ {[{ key: 'login', label: 'ورود' }, { key: 'signup', label: 'ثبتنام' }].map(t => (
+
+ ))}
+
+
+ {/* OAuth */}
+
+
+ {/* Form */}
+
+ {tab === 'login'
+ ?
+ : }
+
+
+ {/* Switch prompt */}
+
+ {tab === 'login' ? (
+ <>حساب ندارید؟{' '}>
+ ) : (
+ <>قبلاً حساب ساختهاید؟{' '}>
+ )}
+
+
+
+ TEKNAV · نشریه فناوری فارسی
+
+
+
+
+ {/* ── Brand column (left in RTL) ──────────────────────────────── */}
+
+
+ >
+ );
+}
+
+// ── Shared styles ─────────────────────────────────────────────────────────────
+const goldBtn = (loading) => ({
+ width: '100%', padding: '13px 0', borderRadius: 10, border: 'none',
+ background: loading ? '#E8E2D8' : 'linear-gradient(135deg, #C8951C 0%, #D49A2A 60%, #C8951C 100%)',
+ color: loading ? '#B0B8BA' : '#0F2A2E',
+ fontSize: 14, fontWeight: 700, cursor: loading ? 'not-allowed' : 'pointer',
+ fontFamily: 'Vazirmatn,sans-serif',
+ boxShadow: loading ? 'none' : '0 4px 16px rgba(200,149,28,0.22)',
+ transition: 'opacity 0.2s, transform 0.15s, box-shadow 0.2s',
+ letterSpacing: '0.02em', marginTop: 4,
+});
+
+const cancelBtn = {
+ flex: 1, padding: '12px 0', border: '1.5px solid #E4DDD2', borderRadius: 10,
+ background: 'transparent', color: '#7B8C8E', cursor: 'pointer',
+ fontSize: 13, fontWeight: 600, fontFamily: 'Vazirmatn,sans-serif',
+};
+
+const oauthBtnStyle = {
+ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 10,
+ width: '100%', padding: '11px 0', borderRadius: 10,
+ border: '1.5px solid #E4DDD2', background: '#fff', cursor: 'pointer',
+ fontFamily: 'Vazirmatn,sans-serif', fontSize: 13, fontWeight: 600, color: '#1A2E30',
+ transition: 'border-color 0.18s, background 0.18s',
+};
+
+export { LoginPage };