change auth to fastapi
All checks were successful
PR Build Check / build (pull_request) Successful in 2m20s
All checks were successful
PR Build Check / build (pull_request) Successful in 2m20s
This commit is contained in:
parent
1133e52ec0
commit
79c611d061
@ -68,28 +68,40 @@ const AdminDashboard = () => {
|
||||
// near other refs
|
||||
const loggingRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const API = process.env.NEXT_PUBLIC_FASTAPI_URL || 'http://127.0.0.1:8000';
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/auth/me', { credentials: 'include' });
|
||||
const res = await fetch(`${API}/auth/me`, {
|
||||
credentials: 'include',
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
router.replace('/login');
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
if (!data.user) {
|
||||
|
||||
const user = await res.json().catch(() => null);
|
||||
if (!user?.id) {
|
||||
router.replace('/login');
|
||||
return;
|
||||
}
|
||||
// authenticated
|
||||
} catch {
|
||||
router.replace('/login');
|
||||
return;
|
||||
} finally {
|
||||
setAuthChecked(true);
|
||||
if (!cancelled) setAuthChecked(true);
|
||||
}
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
}, [router]);
|
||||
return () => { cancelled = true; };
|
||||
}, [router, API]);
|
||||
|
||||
|
||||
|
||||
|
||||
@ -10,30 +10,48 @@ export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
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(() => {
|
||||
let cancelled = false;
|
||||
const controller = new AbortController();
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/auth/me', {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
credentials: 'include', // safe even if same-origin
|
||||
const res = await fetch(`${API}/auth/me`, {
|
||||
credentials: 'include',
|
||||
cache: 'no-store', // don't reuse a cached 401
|
||||
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');
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// ignore errors; just show the form
|
||||
}
|
||||
|
||||
// not logged in -> show form
|
||||
if (!cancelled) setReady(true);
|
||||
} catch {
|
||||
// network/error -> show form
|
||||
if (!cancelled) setReady(true);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => { cancelled = true; };
|
||||
}, [router]);
|
||||
return () => {
|
||||
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 (
|
||||
<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%)]
|
||||
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="mx-auto w-full max-w-[440px] text-center">
|
||||
{/* Header */}
|
||||
<h1 className="text-4xl font-extrabold uppercase tracking-wide text-yellow-400 mb-2">
|
||||
Sign In
|
||||
</h1>
|
||||
@ -77,10 +93,8 @@ export default function LoginPage() {
|
||||
Enter your email and password to access your account.
|
||||
</p>
|
||||
|
||||
{/* Login form */}
|
||||
<ComponentsAuthLoginForm />
|
||||
|
||||
{/* Footer link */}
|
||||
<div className="mt-6 text-sm text-gray-200 dark:text-gray-300">
|
||||
Don’t have an account?{' '}
|
||||
<Link
|
||||
@ -98,3 +112,4 @@ export default function LoginPage() {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -3,31 +3,46 @@ import IconLockDots from '@/components/icon/icon-lock-dots';
|
||||
import IconMail from '@/components/icon/icon-mail';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
type User = { id: string; email: string; is_active: boolean };
|
||||
|
||||
const ComponentsAuthLoginForm = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
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();
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await axios.post('/api/login', { email, password });
|
||||
toast.success(res.data?.message || 'Login successful!');
|
||||
const res = await fetch(`${API}/auth/login`, {
|
||||
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.refresh();
|
||||
// token cookie is already set by the server:
|
||||
} catch (err: any) {
|
||||
console.error('Login error:', err);
|
||||
const msg =
|
||||
err?.response?.data?.message ||
|
||||
err?.message ||
|
||||
'Invalid credentials';
|
||||
toast.error(msg);
|
||||
toast.error(err?.message ?? 'Login failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -52,6 +67,7 @@ const ComponentsAuthLoginForm = () => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pb-2">
|
||||
<label htmlFor="Password" className="text-yellow-400 text-left">Password</label>
|
||||
<div className="relative text-white-dark">
|
||||
@ -70,6 +86,7 @@ const ComponentsAuthLoginForm = () => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
@ -83,3 +100,4 @@ const ComponentsAuthLoginForm = () => {
|
||||
|
||||
export default ComponentsAuthLoginForm;
|
||||
|
||||
|
||||
|
||||
@ -33,12 +33,14 @@ export default function ComponentsAuthRegisterForm({ redirectTo = "/dashboard" }
|
||||
return;
|
||||
}
|
||||
|
||||
const API = process.env.NEXT_PUBLIC_FASTAPI_URL; // e.g. http://localhost:8000
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await fetch("/api/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
const res = await fetch(`${API}/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
@ -3,19 +3,18 @@ import { useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Link from 'next/link';
|
||||
import { IRootState } from '@/store';
|
||||
import { toggleTheme, toggleSidebar, toggleRTL } from '@/store/themeConfigSlice';
|
||||
import { toggleTheme, toggleSidebar } from '@/store/themeConfigSlice';
|
||||
import Image from 'next/image';
|
||||
import Dropdown from '@/components/dropdown';
|
||||
import IconMenu from '@/components/icon/icon-menu';
|
||||
import IconSun from '@/components/icon/icon-sun';
|
||||
import IconMoon from '@/components/icon/icon-moon';
|
||||
import IconUser from '@/components/icon/icon-user';
|
||||
import IconMail from '@/components/icon/icon-mail';
|
||||
import IconLockDots from '@/components/icon/icon-lock-dots';
|
||||
import IconLogout from '@/components/icon/icon-logout';
|
||||
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() {
|
||||
const pathname = usePathname();
|
||||
@ -27,17 +26,16 @@ export default function Header() {
|
||||
const [user, setUser] = useState<UserData | null>(null);
|
||||
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(() => {
|
||||
const selector = document.querySelector(
|
||||
'ul.horizontal-menu a[href="' + window.location.pathname + '"]'
|
||||
`ul.horizontal-menu a[href="${window.location.pathname}"]`
|
||||
);
|
||||
if (selector) {
|
||||
document
|
||||
.querySelectorAll('ul.horizontal-menu .nav-link.active')
|
||||
.forEach((el) => el.classList.remove('active'));
|
||||
document
|
||||
.querySelectorAll('ul.horizontal-menu a.active')
|
||||
.querySelectorAll('ul.horizontal-menu .nav-link.active, ul.horizontal-menu a.active')
|
||||
.forEach((el) => el.classList.remove('active'));
|
||||
selector.classList.add('active');
|
||||
const ul: any = selector.closest('ul.sub-menu');
|
||||
@ -48,16 +46,16 @@ export default function Header() {
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
async function loadUser() {
|
||||
async function loadUser(signal?: AbortSignal) {
|
||||
try {
|
||||
const res = await fetch('/api/auth/me', {
|
||||
method: 'GET',
|
||||
credentials: 'include', // send cookie
|
||||
cache: 'no-store', // avoid stale cached responses
|
||||
const res = await fetch(`${API}/auth/me`, {
|
||||
credentials: 'include',
|
||||
cache: 'no-store',
|
||||
signal,
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
const data = await res.json();
|
||||
setUser(data.user);
|
||||
const data = await res.json().catch(() => null);
|
||||
setUser(data?.id ? (data as UserData) : null);
|
||||
} catch {
|
||||
setUser(null);
|
||||
} finally {
|
||||
@ -67,21 +65,31 @@ export default function Header() {
|
||||
|
||||
useEffect(() => {
|
||||
setLoadingUser(true);
|
||||
loadUser();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pathname]); // re-fetch on route change (after login redirect)
|
||||
const controller = new AbortController();
|
||||
loadUser(controller.signal);
|
||||
return () => controller.abort();
|
||||
}, [pathname, API]);
|
||||
|
||||
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);
|
||||
router.push('/login'); // go to login
|
||||
window.location.href = '/login';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<header className={`z-40 ${themeConfig.semidark && themeConfig.menu === 'horizontal' ? 'dark' : ''}`}>
|
||||
<div className="shadow-sm">
|
||||
<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="relative h-10 w-32 sm:h-11 sm:w-36 md:h-12 md:w-27 shrink-0 max-h-12">
|
||||
<Image
|
||||
@ -103,7 +111,6 @@ useEffect(() => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
{/* 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">
|
||||
{/* Theme toggle */}
|
||||
@ -131,14 +138,14 @@ useEffect(() => {
|
||||
<Dropdown
|
||||
placement={isRtl ? 'bottom-start' : 'bottom-end'}
|
||||
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={
|
||||
<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" />
|
||||
</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">
|
||||
<div className="truncate ltr:pl-1.5 rtl:pr-4">
|
||||
<h4 className="text-sm text-left">{user.email}</h4>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user