change auth to fastapi
All checks were successful
PR Build Check / build (pull_request) Successful in 2m20s

This commit is contained in:
Syasya 2025-08-21 16:45:21 +08:00
parent 1133e52ec0
commit 79c611d061
5 changed files with 146 additions and 92 deletions

View File

@ -68,28 +68,40 @@ const AdminDashboard = () => {
// near other refs // near other refs
const loggingRef = useRef<HTMLDivElement | null>(null); const loggingRef = useRef<HTMLDivElement | null>(null);
useEffect(() => { const API = process.env.NEXT_PUBLIC_FASTAPI_URL || 'http://127.0.0.1:8000';
const checkAuth = async () => {
try {
const res = await fetch('/api/auth/me', { credentials: 'include' });
if (!res.ok) {
router.replace('/login');
return;
}
const data = await res.json();
if (!data.user) {
router.replace('/login');
return;
}
} catch {
router.replace('/login');
} finally {
setAuthChecked(true);
}
};
checkAuth(); useEffect(() => {
}, [router]); let cancelled = false;
const checkAuth = async () => {
try {
const res = await fetch(`${API}/auth/me`, {
credentials: 'include',
cache: 'no-store',
});
if (!res.ok) {
router.replace('/login');
return;
}
const user = await res.json().catch(() => null);
if (!user?.id) {
router.replace('/login');
return;
}
// authenticated
} catch {
router.replace('/login');
return;
} finally {
if (!cancelled) setAuthChecked(true);
}
};
checkAuth();
return () => { cancelled = true; };
}, [router, API]);

View File

@ -10,30 +10,48 @@ export default function LoginPage() {
const router = useRouter(); const router = useRouter();
const [ready, setReady] = useState(false); // gate to avoid UI flash const [ready, setReady] = useState(false); // gate to avoid UI flash
// Use ONE client-exposed API env var everywhere
const API = process.env.NEXT_PUBLIC_FASTAPI_URL || 'http://127.0.0.1:8000';
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
const controller = new AbortController();
(async () => { (async () => {
try { try {
const res = await fetch('/api/auth/me', { const res = await fetch(`${API}/auth/me`, {
method: 'GET', credentials: 'include',
cache: 'no-store', cache: 'no-store', // don't reuse a cached 401
credentials: 'include', // safe even if same-origin signal: controller.signal,
}); });
if (!cancelled && res.ok) {
if (!res.ok) {
if (!cancelled) setReady(true);
return;
}
const user = await res.json().catch(() => null);
if (user?.id) {
// already logged in -> go straight to dashboard
router.replace('/adminDashboard'); router.replace('/adminDashboard');
return; return;
} }
// not logged in -> show form
if (!cancelled) setReady(true);
} catch { } catch {
// ignore errors; just show the form // network/error -> show form
if (!cancelled) setReady(true);
} }
if (!cancelled) setReady(true);
})(); })();
return () => { cancelled = true; }; return () => {
}, [router]); cancelled = true;
controller.abort();
};
}, [router, API]);
if (!ready) return null; // or a small spinner if you prefer if (!ready) return null; // or a spinner/skeleton
return ( return (
<div className="relative min-h-screen overflow-hidden bg-[#060818] text-white"> <div className="relative min-h-screen overflow-hidden bg-[#060818] text-white">
@ -66,10 +84,8 @@ export default function LoginPage() {
bg-[linear-gradient(45deg,#fffbe6_0%,rgba(255,251,230,0)_25%,rgba(255,251,230,0)_75%,#fffbe6_100%)] bg-[linear-gradient(45deg,#fffbe6_0%,rgba(255,251,230,0)_25%,rgba(255,251,230,0)_75%,#fffbe6_100%)]
dark:bg-[linear-gradient(52.22deg,#facc15_0%,rgba(250,204,21,0)_20%,rgba(250,204,21,0)_80%,#facc15_100%)]" dark:bg-[linear-gradient(52.22deg,#facc15_0%,rgba(250,204,21,0)_20%,rgba(250,204,21,0)_80%,#facc15_100%)]"
> >
{/* Inner card (glassmorphic effect) */}
<div className="relative z-10 rounded-2xl bg-white/10 px-8 py-16 backdrop-blur-lg dark:bg-white/10 lg:min-h-[600px]"> <div className="relative z-10 rounded-2xl bg-white/10 px-8 py-16 backdrop-blur-lg dark:bg-white/10 lg:min-h-[600px]">
<div className="mx-auto w-full max-w-[440px] text-center"> <div className="mx-auto w-full max-w-[440px] text-center">
{/* Header */}
<h1 className="text-4xl font-extrabold uppercase tracking-wide text-yellow-400 mb-2"> <h1 className="text-4xl font-extrabold uppercase tracking-wide text-yellow-400 mb-2">
Sign In Sign In
</h1> </h1>
@ -77,10 +93,8 @@ export default function LoginPage() {
Enter your email and password to access your account. Enter your email and password to access your account.
</p> </p>
{/* Login form */}
<ComponentsAuthLoginForm /> <ComponentsAuthLoginForm />
{/* Footer link */}
<div className="mt-6 text-sm text-gray-200 dark:text-gray-300"> <div className="mt-6 text-sm text-gray-200 dark:text-gray-300">
Dont have an account?{' '} Dont have an account?{' '}
<Link <Link
@ -98,3 +112,4 @@ export default function LoginPage() {
); );
} }

View File

@ -3,31 +3,46 @@ import IconLockDots from '@/components/icon/icon-lock-dots';
import IconMail from '@/components/icon/icon-mail'; import IconMail from '@/components/icon/icon-mail';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import axios from 'axios';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
type User = { id: string; email: string; is_active: boolean };
const ComponentsAuthLoginForm = () => { const ComponentsAuthLoginForm = () => {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const router = useRouter(); const router = useRouter();
const API = process.env.NEXT_PUBLIC_FASTAPI_URL; // e.g. http://localhost:8000
const submitForm = async (e: React.FormEvent) => { const submitForm = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
setLoading(true); setLoading(true);
try { try {
const res = await axios.post('/api/login', { email, password }); const res = await fetch(`${API}/auth/login`, {
toast.success(res.data?.message || 'Login successful!'); method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
credentials: 'include', // cookie from FastAPI
});
let data: any = null;
try {
data = await res.json();
} catch {
// non-JSON error
}
if (!res.ok) {
const msg = data?.detail || data?.message || 'Invalid credentials';
throw new Error(msg);
}
const user: User = data;
toast.success(`Welcome ${user.email}`);
router.push('/adminDashboard'); router.push('/adminDashboard');
router.refresh(); router.refresh();
// token cookie is already set by the server:
} catch (err: any) { } catch (err: any) {
console.error('Login error:', err); toast.error(err?.message ?? 'Login failed');
const msg =
err?.response?.data?.message ||
err?.message ||
'Invalid credentials';
toast.error(msg);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -52,6 +67,7 @@ const ComponentsAuthLoginForm = () => {
</span> </span>
</div> </div>
</div> </div>
<div className="pb-2"> <div className="pb-2">
<label htmlFor="Password" className="text-yellow-400 text-left">Password</label> <label htmlFor="Password" className="text-yellow-400 text-left">Password</label>
<div className="relative text-white-dark"> <div className="relative text-white-dark">
@ -70,6 +86,7 @@ const ComponentsAuthLoginForm = () => {
</span> </span>
</div> </div>
</div> </div>
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
@ -83,3 +100,4 @@ const ComponentsAuthLoginForm = () => {
export default ComponentsAuthLoginForm; export default ComponentsAuthLoginForm;

View File

@ -33,12 +33,14 @@ export default function ComponentsAuthRegisterForm({ redirectTo = "/dashboard" }
return; return;
} }
const API = process.env.NEXT_PUBLIC_FASTAPI_URL; // e.g. http://localhost:8000
try { try {
setLoading(true); setLoading(true);
const res = await fetch("/api/register", { const res = await fetch(`${API}/auth/register`, {
method: "POST", method: 'POST',
headers: { "Content-Type": "application/json" }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }), body: JSON.stringify({ email, password }),
credentials: 'include',
}); });
const data = await res.json(); const data = await res.json();

View File

@ -3,19 +3,18 @@ import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import Link from 'next/link'; import Link from 'next/link';
import { IRootState } from '@/store'; import { IRootState } from '@/store';
import { toggleTheme, toggleSidebar, toggleRTL } from '@/store/themeConfigSlice'; import { toggleTheme, toggleSidebar } from '@/store/themeConfigSlice';
import Image from 'next/image'; import Image from 'next/image';
import Dropdown from '@/components/dropdown'; import Dropdown from '@/components/dropdown';
import IconMenu from '@/components/icon/icon-menu'; import IconMenu from '@/components/icon/icon-menu';
import IconSun from '@/components/icon/icon-sun'; import IconSun from '@/components/icon/icon-sun';
import IconMoon from '@/components/icon/icon-moon'; import IconMoon from '@/components/icon/icon-moon';
import IconUser from '@/components/icon/icon-user'; import IconUser from '@/components/icon/icon-user';
import IconMail from '@/components/icon/icon-mail';
import IconLockDots from '@/components/icon/icon-lock-dots'; import IconLockDots from '@/components/icon/icon-lock-dots';
import IconLogout from '@/components/icon/icon-logout'; import IconLogout from '@/components/icon/icon-logout';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
type UserData = { id: string; email: string; createdAt: string }; type UserData = { id: string; email: string; is_active: boolean };
export default function Header() { export default function Header() {
const pathname = usePathname(); const pathname = usePathname();
@ -27,17 +26,16 @@ export default function Header() {
const [user, setUser] = useState<UserData | null>(null); const [user, setUser] = useState<UserData | null>(null);
const [loadingUser, setLoadingUser] = useState(true); const [loadingUser, setLoadingUser] = useState(true);
// Highlight active menu (your original effect) const API = process.env.NEXT_PUBLIC_FASTAPI_URL || 'http://127.0.0.1:8000';
// highlight active menu
useEffect(() => { useEffect(() => {
const selector = document.querySelector( const selector = document.querySelector(
'ul.horizontal-menu a[href="' + window.location.pathname + '"]' `ul.horizontal-menu a[href="${window.location.pathname}"]`
); );
if (selector) { if (selector) {
document document
.querySelectorAll('ul.horizontal-menu .nav-link.active') .querySelectorAll('ul.horizontal-menu .nav-link.active, ul.horizontal-menu a.active')
.forEach((el) => el.classList.remove('active'));
document
.querySelectorAll('ul.horizontal-menu a.active')
.forEach((el) => el.classList.remove('active')); .forEach((el) => el.classList.remove('active'));
selector.classList.add('active'); selector.classList.add('active');
const ul: any = selector.closest('ul.sub-menu'); const ul: any = selector.closest('ul.sub-menu');
@ -48,16 +46,16 @@ export default function Header() {
} }
}, [pathname]); }, [pathname]);
async function loadUser() { async function loadUser(signal?: AbortSignal) {
try { try {
const res = await fetch('/api/auth/me', { const res = await fetch(`${API}/auth/me`, {
method: 'GET', credentials: 'include',
credentials: 'include', // send cookie cache: 'no-store',
cache: 'no-store', // avoid stale cached responses signal,
}); });
if (!res.ok) throw new Error(); if (!res.ok) throw new Error();
const data = await res.json(); const data = await res.json().catch(() => null);
setUser(data.user); setUser(data?.id ? (data as UserData) : null);
} catch { } catch {
setUser(null); setUser(null);
} finally { } finally {
@ -65,44 +63,53 @@ export default function Header() {
} }
} }
useEffect(() => { useEffect(() => {
setLoadingUser(true); setLoadingUser(true);
loadUser(); const controller = new AbortController();
// eslint-disable-next-line react-hooks/exhaustive-deps loadUser(controller.signal);
}, [pathname]); // re-fetch on route change (after login redirect) return () => controller.abort();
}, [pathname, API]);
const handleLogout = async () => { const handleLogout = async () => {
await fetch('/api/logout', { method: 'POST' }); try {
await fetch(`${API}/auth/logout`, {
method: 'POST',
credentials: 'include',
cache: 'no-store',
});
} catch (_) {
// ignore
} finally {
setUser(null); setUser(null);
router.push('/login'); // go to login window.location.href = '/login';
}
}; };
return ( return (
<header className={`z-40 ${themeConfig.semidark && themeConfig.menu === 'horizontal' ? 'dark' : ''}`}> <header className={`z-40 ${themeConfig.semidark && themeConfig.menu === 'horizontal' ? 'dark' : ''}`}>
<div className="shadow-sm"> <div className="shadow-sm">
<div className="relative flex w-full items-center bg-white px-5 py-2.5 dark:bg-rtgray-900"> <div className="relative flex w-full items-center bg-white px-5 py-2.5 dark:bg-rtgray-900">
{/* Logo */} {/* Logo + mobile toggler */}
<div className="horizontal-logo flex items-center justify-between ltr:mr-2 rtl:ml-2 lg:hidden"> <div className="horizontal-logo flex items-center justify-between ltr:mr-2 rtl:ml-2 lg:hidden">
<div className="relative h-10 w-32 sm:h-11 sm:w-36 md:h-12 md:w-27 shrink-0 max-h-12"> <div className="relative h-10 w-32 sm:h-11 sm:w-36 md:h-12 md:w-27 shrink-0 max-h-12">
<Image <Image
src="/assets/images/newfulllogo.png" src="/assets/images/newfulllogo.png"
alt="logo" alt="logo"
fill fill
className="object-cover" className="object-cover"
priority priority
sizes="(max-width: 640px) 8rem, (max-width: 768px) 9rem, (max-width: 1024px) 10rem, 10rem" sizes="(max-width: 640px) 8rem, (max-width: 768px) 9rem, (max-width: 1024px) 10rem, 10rem"
/> />
</div> </div>
<button <button
type="button" type="button"
onClick={() => dispatch(toggleSidebar())} onClick={() => dispatch(toggleSidebar())}
className="collapse-icon flex p-2 rounded-full hover:bg-rtgray-200 dark:text-white dark:hover:bg-rtgray-700" className="collapse-icon flex p-2 rounded-full hover:bg-rtgray-200 dark:text-white dark:hover:bg-rtgray-700"
> >
<IconMenu className="h-6 w-6" /> <IconMenu className="h-6 w-6" />
</button> </button>
</div> </div>
{/* Right-side actions */} {/* Right-side actions */}
<div className="flex items-center justify-end space-x-1.5 ltr:ml-auto rtl:mr-auto rtl:space-x-reverse dark:text-[#d0d2d6] lg:space-x-2"> <div className="flex items-center justify-end space-x-1.5 ltr:ml-auto rtl:mr-auto rtl:space-x-reverse dark:text-[#d0d2d6] lg:space-x-2">
@ -124,21 +131,21 @@ useEffect(() => {
)} )}
{/* User dropdown */} {/* User dropdown */}
<div className="dropdown flex shrink-0 "> <div className="dropdown flex shrink-0">
{loadingUser ? ( {loadingUser ? (
<div className="h-9 w-9 rounded-full animate-pulse bg-gray-300 dark:bg-rtgray-800" /> <div className="h-9 w-9 rounded-full animate-pulse bg-gray-300 dark:bg-rtgray-800" />
) : user ? ( ) : user ? (
<Dropdown <Dropdown
placement={isRtl ? 'bottom-start' : 'bottom-end'} placement={isRtl ? 'bottom-start' : 'bottom-end'}
btnClassName="relative group block" btnClassName="relative group block"
panelClassName="rounded-lg shadow-lg border border-white/10 bg-rtgray-100 dark:bg-rtgray-800 p-2" // ✅ panelClassName="rounded-lg shadow-lg border border-white/10 bg-rtgray-100 dark:bg-rtgray-800 p-2"
button={ button={
<div className="h-9 w-9 rounded-full bg-rtgray-200 dark:bg-rtgray-800 flex items-center justify-center group-hover:bg-rtgray-300 dark:group-hover:bg-rtgray-700"> <div className="h-9 w-9 rounded-full bg-rtgray-200 dark:bg-rtgray-800 flex items-center justify-center group-hover:bg-rtgray-300 dark:group-hover:bg-rtgray-700">
<IconUser className="h-5 w-5 text-gray-600 dark:text-gray-300" /> <IconUser className="h-5 w-5 text-gray-600 dark:text-gray-300" />
</div> </div>
} }
> >
<ul className="w-[230px] font-semibold text-dark"> {/* make sure this stays transparent */} <ul className="w-[230px] font-semibold text-dark">
<li className="px-4 py-4 flex items-center"> <li className="px-4 py-4 flex items-center">
<div className="truncate ltr:pl-1.5 rtl:pr-4"> <div className="truncate ltr:pl-1.5 rtl:pr-4">
<h4 className="text-sm text-left">{user.email}</h4> <h4 className="text-sm text-left">{user.email}</h4>