register/login cookie
This commit is contained in:
parent
837aee67fc
commit
0885771131
@ -19,6 +19,7 @@ const ComponentsAuthLoginForm = () => {
|
||||
const res = await axios.post('/api/login', { email, password });
|
||||
toast.success(res.data?.message || 'Login successful!');
|
||||
router.push('/adminDashboard');
|
||||
router.refresh();
|
||||
// token cookie is already set by the server:
|
||||
} catch (err: any) {
|
||||
console.error('Login error:', err);
|
||||
|
@ -1,57 +1,73 @@
|
||||
// Dropdown.tsx
|
||||
'use client';
|
||||
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { usePopper } from 'react-popper';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
const Dropdown = (props: any, forwardedRef: any) => {
|
||||
const [visibility, setVisibility] = useState<any>(false);
|
||||
type DropdownProps = {
|
||||
button?: ReactNode; // 👈 make optional
|
||||
children: ReactNode;
|
||||
btnClassName?: string;
|
||||
placement?: any;
|
||||
offset?: [number, number];
|
||||
panelClassName?: string;
|
||||
closeOnItemClick?: boolean;
|
||||
};
|
||||
|
||||
const referenceRef = useRef<any>();
|
||||
const popperRef = useRef<any>();
|
||||
const Dropdown = (props: DropdownProps, forwardedRef: any) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const referenceRef = useRef<HTMLButtonElement | null>(null);
|
||||
const popperRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { styles, attributes } = usePopper(referenceRef.current, popperRef.current, {
|
||||
placement: props.placement || 'bottom-end',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: props.offset || [0],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const { styles, attributes } = usePopper(referenceRef.current, popperRef.current, {
|
||||
placement: props.placement || 'bottom-end',
|
||||
modifiers: [{ name: 'offset', options: { offset: props.offset ?? [0, 8] } }],
|
||||
});
|
||||
|
||||
const handleDocumentClick = (event: any) => {
|
||||
if (referenceRef.current.contains(event.target) || popperRef.current.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setVisibility(false);
|
||||
useEffect(() => {
|
||||
const onDoc = (e: MouseEvent) => {
|
||||
if (!referenceRef.current || !popperRef.current) return;
|
||||
if (referenceRef.current.contains(e.target as Node)) return;
|
||||
if (popperRef.current.contains(e.target as Node)) return;
|
||||
setVisible(false);
|
||||
};
|
||||
document.addEventListener('mousedown', onDoc);
|
||||
return () => document.removeEventListener('mousedown', onDoc);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleDocumentClick);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleDocumentClick);
|
||||
};
|
||||
}, []);
|
||||
useImperativeHandle(forwardedRef, () => ({ close: () => setVisible(false) }));
|
||||
|
||||
useImperativeHandle(forwardedRef, () => ({
|
||||
close() {
|
||||
setVisibility(false);
|
||||
},
|
||||
}));
|
||||
const defaultButton = (
|
||||
<span className="inline-flex h-9 w-9 items-center justify-center rounded-full bg-gray-200 dark:bg-gray-700" />
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button ref={referenceRef} type="button" className={props.btnClassName} onClick={() => setVisibility(!visibility)}>
|
||||
{props.button}
|
||||
</button>
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
ref={referenceRef}
|
||||
type="button"
|
||||
className={props.btnClassName}
|
||||
onClick={() => setVisible((v) => !v)}
|
||||
>
|
||||
{props.button ?? defaultButton} {/* 👈 fallback */}
|
||||
</button>
|
||||
|
||||
<div ref={popperRef} style={styles.popper} {...attributes.popper} className="z-50" onClick={() => setVisibility(!visibility)}>
|
||||
{visibility && props.children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
<div
|
||||
ref={popperRef}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
className="z-50"
|
||||
>
|
||||
{visible && (
|
||||
<div className={props.panelClassName ?? 'rounded-lg bg-white dark:bg-neutral-900 shadow-lg ring-1 ring-black/5'}>
|
||||
{props.children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(Dropdown);
|
||||
|
||||
|
||||
|
@ -6,251 +6,171 @@ import { IRootState } from '@/store';
|
||||
import { toggleTheme, toggleSidebar, toggleRTL } from '@/store/themeConfigSlice';
|
||||
import Dropdown from '@/components/dropdown';
|
||||
import IconMenu from '@/components/icon/icon-menu';
|
||||
import IconCalendar from '@/components/icon/icon-calendar';
|
||||
import IconEdit from '@/components/icon/icon-edit';
|
||||
import IconChatNotification from '@/components/icon/icon-chat-notification';
|
||||
import IconSearch from '@/components/icon/icon-search';
|
||||
import IconXCircle from '@/components/icon/icon-x-circle';
|
||||
import IconSun from '@/components/icon/icon-sun';
|
||||
import IconMoon from '@/components/icon/icon-moon';
|
||||
import IconLaptop from '@/components/icon/icon-laptop';
|
||||
import IconMailDot from '@/components/icon/icon-mail-dot';
|
||||
import IconArrowLeft from '@/components/icon/icon-arrow-left';
|
||||
import IconInfoCircle from '@/components/icon/icon-info-circle';
|
||||
import IconBellBing from '@/components/icon/icon-bell-bing';
|
||||
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 IconMenuDashboard from '@/components/icon/menu/icon-menu-dashboard';
|
||||
import IconCaretDown from '@/components/icon/icon-caret-down';
|
||||
import IconMenuApps from '@/components/icon/menu/icon-menu-apps';
|
||||
import IconMenuComponents from '@/components/icon/menu/icon-menu-components';
|
||||
import IconMenuElements from '@/components/icon/menu/icon-menu-elements';
|
||||
import IconMenuDatatables from '@/components/icon/menu/icon-menu-datatables';
|
||||
import IconMenuForms from '@/components/icon/menu/icon-menu-forms';
|
||||
import IconMenuPages from '@/components/icon/menu/icon-menu-pages';
|
||||
import IconMenuMore from '@/components/icon/menu/icon-menu-more';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { getTranslation } from '@/i18n';
|
||||
|
||||
const Header = () => {
|
||||
const pathname = usePathname();
|
||||
const dispatch = useDispatch();
|
||||
const router = useRouter();
|
||||
const { t, i18n } = getTranslation();
|
||||
type UserData = { id: string; email: string; createdAt: string };
|
||||
|
||||
useEffect(() => {
|
||||
const selector = document.querySelector('ul.horizontal-menu a[href="' + window.location.pathname + '"]');
|
||||
if (selector) {
|
||||
const all: any = document.querySelectorAll('ul.horizontal-menu .nav-link.active');
|
||||
for (let i = 0; i < all.length; i++) {
|
||||
all[0]?.classList.remove('active');
|
||||
}
|
||||
export default function Header() {
|
||||
const pathname = usePathname();
|
||||
const dispatch = useDispatch();
|
||||
const router = useRouter();
|
||||
const themeConfig = useSelector((state: IRootState) => state.themeConfig);
|
||||
const isRtl = themeConfig.rtlClass === 'rtl';
|
||||
|
||||
let allLinks = document.querySelectorAll('ul.horizontal-menu a.active');
|
||||
for (let i = 0; i < allLinks.length; i++) {
|
||||
const element = allLinks[i];
|
||||
element?.classList.remove('active');
|
||||
}
|
||||
selector?.classList.add('active');
|
||||
const [user, setUser] = useState<UserData | null>(null);
|
||||
const [loadingUser, setLoadingUser] = useState(true);
|
||||
|
||||
const ul: any = selector.closest('ul.sub-menu');
|
||||
if (ul) {
|
||||
let ele: any = ul.closest('li.menu').querySelectorAll('.nav-link');
|
||||
if (ele) {
|
||||
ele = ele[0];
|
||||
setTimeout(() => {
|
||||
ele?.classList.add('active');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
const isRtl = useSelector((state: IRootState) => state.themeConfig.rtlClass) === 'rtl';
|
||||
|
||||
const themeConfig = useSelector((state: IRootState) => state.themeConfig);
|
||||
const setLocale = (flag: string) => {
|
||||
if (flag.toLowerCase() === 'ae') {
|
||||
dispatch(toggleRTL('rtl'));
|
||||
} else {
|
||||
dispatch(toggleRTL('ltr'));
|
||||
}
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
function createMarkup(messages: any) {
|
||||
return { __html: messages };
|
||||
}
|
||||
const [messages, setMessages] = useState([
|
||||
{
|
||||
id: 1,
|
||||
image: '<span class="grid place-content-center w-9 h-9 rounded-full bg-success-light dark:bg-success text-success dark:text-success-light"><svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg></span>',
|
||||
title: 'Congratulations!',
|
||||
message: 'Your OS has been updated.',
|
||||
time: '1hr',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
image: '<span class="grid place-content-center w-9 h-9 rounded-full bg-info-light dark:bg-info text-info dark:text-info-light"><svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></span>',
|
||||
title: 'Did you know?',
|
||||
message: 'You can switch between artboards.',
|
||||
time: '2hr',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
image: '<span class="grid place-content-center w-9 h-9 rounded-full bg-danger-light dark:bg-danger text-danger dark:text-danger-light"> <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></span>',
|
||||
title: 'Something went wrong!',
|
||||
message: 'Send Reposrt',
|
||||
time: '2days',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
image: '<span class="grid place-content-center w-9 h-9 rounded-full bg-warning-light dark:bg-warning text-warning dark:text-warning-light"><svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> <circle cx="12" cy="12" r="10"></circle> <line x1="12" y1="8" x2="12" y2="12"></line> <line x1="12" y1="16" x2="12.01" y2="16"></line></svg></span>',
|
||||
title: 'Warning',
|
||||
message: 'Your password strength is low.',
|
||||
time: '5days',
|
||||
},
|
||||
]);
|
||||
|
||||
const removeMessage = (value: number) => {
|
||||
setMessages(messages.filter((user) => user.id !== value));
|
||||
};
|
||||
|
||||
const [notifications, setNotifications] = useState([
|
||||
{
|
||||
id: 1,
|
||||
profile: 'user-profile.jpeg',
|
||||
message: '<strong class="text-sm mr-1">John Doe</strong>invite you to <strong>Prototyping</strong>',
|
||||
time: '45 min ago',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
profile: 'profile-34.jpeg',
|
||||
message: '<strong class="text-sm mr-1">Adam Nolan</strong>mentioned you to <strong>UX Basics</strong>',
|
||||
time: '9h Ago',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
profile: 'profile-16.jpeg',
|
||||
message: '<strong class="text-sm mr-1">Anna Morgan</strong>Upload a file',
|
||||
time: '9h Ago',
|
||||
},
|
||||
]);
|
||||
|
||||
const removeNotification = (value: number) => {
|
||||
setNotifications(notifications.filter((user) => user.id !== value));
|
||||
};
|
||||
|
||||
const [search, setSearch] = useState(false);
|
||||
|
||||
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-black">
|
||||
<div className="horizontal-logo flex items-center justify-between ltr:mr-2 rtl:ml-2 lg:hidden">
|
||||
<Link href="/" className="main-logo flex shrink-0 items-center">
|
||||
<img className="inline w-8 ltr:-ml-1 rtl:-mr-1" src="/assets/images/newfulllogo.png" alt="logo" />
|
||||
<span className="hidden align-middle text-2xl font-semibold transition-all duration-300 ltr:ml-1.5 rtl:mr-1.5 dark:text-white-light md:inline">Rooftop Energy</span>
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
className="collapse-icon flex flex-none rounded-full bg-white-light/40 p-2 hover:bg-white-light/90 hover:text-primary ltr:ml-2 rtl:mr-2 dark:bg-dark/40 dark:text-[#d0d2d6] dark:hover:bg-dark/60 dark:hover:text-primary lg:hidden"
|
||||
onClick={() => dispatch(toggleSidebar())}
|
||||
>
|
||||
<IconMenu className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end space-x-1.5 ltr:ml-auto rtl:mr-auto rtl:space-x-reverse dark:text-[#d0d2d6] sm:flex-1 ltr:sm:ml-0 sm:rtl:mr-0 lg:space-x-2">
|
||||
|
||||
{/* ------------------- Start Theme Switch ------------------- */}
|
||||
<div>
|
||||
{themeConfig.theme === 'light' ? (
|
||||
<button
|
||||
className={`${
|
||||
themeConfig.theme === 'light' &&
|
||||
'flex items-center rounded-full bg-white-light/40 p-2 hover:bg-white-light/90 hover:text-primary dark:bg-dark/40 dark:hover:bg-dark/60'
|
||||
}`}
|
||||
onClick={() => dispatch(toggleTheme('dark'))}
|
||||
>
|
||||
<IconSun />
|
||||
</button>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{themeConfig.theme === 'dark' && (
|
||||
<button
|
||||
className={`${
|
||||
themeConfig.theme === 'dark' &&
|
||||
'flex items-center rounded-full bg-white-light/40 p-2 hover:bg-white-light/90 hover:text-primary dark:bg-dark/40 dark:hover:bg-dark/60'
|
||||
}`}
|
||||
onClick={() => dispatch(toggleTheme('light'))}
|
||||
>
|
||||
<IconMoon />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{/* ------------------- End Theme Switch ------------------- */}
|
||||
|
||||
|
||||
<div className="dropdown flex shrink-0">
|
||||
<Dropdown
|
||||
offset={[0, 8]}
|
||||
placement={`${isRtl ? 'bottom-start' : 'bottom-end'}`}
|
||||
btnClassName="relative group block"
|
||||
button={<img className="h-9 w-9 rounded-full object-cover saturate-50 group-hover:saturate-100" src="/assets/images/user-profile.jpeg" alt="userProfile" />}
|
||||
>
|
||||
<ul className="w-[230px] !py-0 font-semibold text-dark dark:text-white-dark dark:text-white-light/90">
|
||||
<li>
|
||||
<div className="flex items-center px-4 py-4">
|
||||
<img className="h-10 w-10 rounded-md object-cover" src="/assets/images/user-profile.jpeg" alt="userProfile" />
|
||||
<div className="truncate ltr:pl-4 rtl:pr-4">
|
||||
<h4 className="text-base">
|
||||
John Doe
|
||||
<span className="rounded bg-success-light px-1 text-xs text-success ltr:ml-2 rtl:ml-2">Pro</span>
|
||||
</h4>
|
||||
<button type="button" className="text-black/60 hover:text-primary dark:text-dark-light/60 dark:hover:text-white">
|
||||
johndoe@gmail.com
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/users/profile" className="dark:hover:text-white">
|
||||
<IconUser className="h-4.5 w-4.5 shrink-0 ltr:mr-2 rtl:ml-2" />
|
||||
Profile
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/apps/mailbox" className="dark:hover:text-white">
|
||||
<IconMail className="h-4.5 w-4.5 shrink-0 ltr:mr-2 rtl:ml-2" />
|
||||
Inbox
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/auth/boxed-lockscreen" className="dark:hover:text-white">
|
||||
<IconLockDots className="h-4.5 w-4.5 shrink-0 ltr:mr-2 rtl:ml-2" />
|
||||
Lock Screen
|
||||
</Link>
|
||||
</li>
|
||||
<li className="border-t border-white-light dark:border-white-light/10">
|
||||
<Link href="/auth/boxed-signin" className="!py-3 text-danger">
|
||||
<IconLogout className="h-4.5 w-4.5 shrink-0 rotate-90 ltr:mr-2 rtl:ml-2" />
|
||||
Sign Out
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</header>
|
||||
// Highlight active menu (your original effect)
|
||||
useEffect(() => {
|
||||
const selector = document.querySelector(
|
||||
'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')
|
||||
.forEach((el) => el.classList.remove('active'));
|
||||
selector.classList.add('active');
|
||||
const ul: any = selector.closest('ul.sub-menu');
|
||||
if (ul) {
|
||||
const ele: any = ul.closest('li.menu')?.querySelector('.nav-link');
|
||||
setTimeout(() => ele?.classList.add('active'));
|
||||
}
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
export default Header;
|
||||
async function loadUser() {
|
||||
try {
|
||||
const res = await fetch('/api/auth/me', {
|
||||
method: 'GET',
|
||||
credentials: 'include', // send cookie
|
||||
cache: 'no-store', // avoid stale cached responses
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
const data = await res.json();
|
||||
setUser(data.user);
|
||||
} catch {
|
||||
setUser(null);
|
||||
} finally {
|
||||
setLoadingUser(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setLoadingUser(true);
|
||||
loadUser();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pathname]); // re-fetch on route change (after login redirect)
|
||||
|
||||
const handleLogout = async () => {
|
||||
await fetch('/api/auth/logout', { method: 'POST' });
|
||||
setUser(null);
|
||||
router.push('/login'); // go to 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 */}
|
||||
<div className="horizontal-logo flex items-center justify-between ltr:mr-2 rtl:ml-2 lg:hidden">
|
||||
<Link href="/" className="main-logo flex shrink-0 items-center">
|
||||
<img className="inline w-8" src="/assets/images/newfulllogo.png" alt="logo" />
|
||||
<span className="hidden text-2xl font-semibold ltr:ml-1.5 dark:text-white-light md:inline">
|
||||
Rooftop Energy
|
||||
</span>
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => dispatch(toggleSidebar())}
|
||||
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" />
|
||||
</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 */}
|
||||
{themeConfig.theme === 'light' ? (
|
||||
<button
|
||||
onClick={() => dispatch(toggleTheme('dark'))}
|
||||
className="flex items-center p-2 rounded-full bg-white-light/40 hover:bg-white-light/90 dark:bg-rtgray-800 dark:hover:bg-rtgray-700"
|
||||
>
|
||||
<IconSun />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => dispatch(toggleTheme('light'))}
|
||||
className="flex items-center p-2 rounded-full bg-white-light/40 hover:bg-white-light/90 dark:bg-rtgray-800 dark:hover:bg-rtgray-700"
|
||||
>
|
||||
<IconMoon />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* User dropdown */}
|
||||
<div className="dropdown flex shrink-0 ">
|
||||
{loadingUser ? (
|
||||
<div className="h-9 w-9 rounded-full animate-pulse bg-gray-300 dark:bg-rtgray-800" />
|
||||
) : user ? (
|
||||
<Dropdown
|
||||
placement={isRtl ? 'bottom-start' : 'bottom-end'}
|
||||
btnClassName="relative group block"
|
||||
panelClassName="rounded-lg shadow-lg border border-white/10 bg-gray-100 dark:bg-gray-800 p-2" // ✅
|
||||
button={
|
||||
<div className="h-9 w-9 rounded-full bg-gray-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 dark:text-white-dark bg-transparent"> {/* make sure this stays transparent */}
|
||||
<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>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/users/profile" className="dark:hover:text-white">
|
||||
<IconUser className="h-4.5 w-4.5 mr-2" /> Profile
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/apps/mailbox" className="dark:hover:text-white">
|
||||
<IconMail className="h-4.5 w-4.5 mr-2" /> Inbox
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/auth/boxed-lockscreen" className="dark:hover:text-white">
|
||||
<IconLockDots className="h-4.5 w-4.5 mr-2" /> Lock Screen
|
||||
</Link>
|
||||
</li>
|
||||
<li className="border-t border-white-light dark:border-white-light/10">
|
||||
<button onClick={handleLogout} className="flex w-full items-center py-3 text-danger">
|
||||
<IconLogout className="h-4.5 w-4.5 mr-2 rotate-90" /> Sign Out
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<Link
|
||||
href="/login"
|
||||
className="rounded-md bg-yellow-400 px-3 py-1.5 text-black font-semibold hover:brightness-95"
|
||||
>
|
||||
Sign In
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
5891
package-lock.json
generated
5891
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@headlessui/react": "^1.7.8",
|
||||
"@heroui/react": "^2.8.2",
|
||||
"@prisma/client": "^6.8.2",
|
||||
"@reduxjs/toolkit": "^1.9.1",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
@ -27,6 +28,7 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"eslint": "8.32.0",
|
||||
"eslint-config-next": "13.1.2",
|
||||
"framer-motion": "^12.23.12",
|
||||
"he": "^1.2.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"i18next": "^22.4.10",
|
||||
|
@ -1,27 +1,46 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import jwt from "jsonwebtoken";
|
||||
// pages/api/auth/me.ts
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import jwt, { JwtPayload } from "jsonwebtoken";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const SECRET_KEY = process.env.JWT_SECRET as string;
|
||||
|
||||
function readCookieToken(req: NextApiRequest) {
|
||||
const cookie = req.headers.cookie || "";
|
||||
const match = cookie.split("; ").find((c) => c.startsWith("token="));
|
||||
return match?.split("=")[1];
|
||||
}
|
||||
|
||||
function readAuthBearer(req: NextApiRequest) {
|
||||
const auth = req.headers.authorization;
|
||||
if (!auth?.startsWith("Bearer ")) return undefined;
|
||||
return auth.slice("Bearer ".length);
|
||||
}
|
||||
|
||||
function hasEmail(payload: string | JwtPayload): payload is JwtPayload & { email: string } {
|
||||
return typeof payload === "object" && payload !== null && typeof (payload as any).email === "string";
|
||||
}
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
const token = authHeader.split(" ")[1]; // Extract token
|
||||
if (req.method !== "GET") return res.status(405).json({ message: "Method not allowed" });
|
||||
|
||||
try {
|
||||
const decoded: any = jwt.verify(token, SECRET_KEY);
|
||||
const user = await prisma.user.findUnique({ where: { id: decoded.userId } });
|
||||
const token = readAuthBearer(req) ?? readCookieToken(req);
|
||||
if (!token) return res.status(401).json({ message: "Unauthorized" });
|
||||
|
||||
const decoded = jwt.verify(token, SECRET_KEY);
|
||||
if (!hasEmail(decoded)) return res.status(401).json({ message: "Invalid token" });
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: decoded.email },
|
||||
select: { id: true, email: true, createdAt: true },
|
||||
});
|
||||
|
||||
if (!user) return res.status(401).json({ message: "User not found" });
|
||||
|
||||
res.json({ user });
|
||||
} catch (error) {
|
||||
res.status(401).json({ message: "Invalid token" });
|
||||
return res.status(200).json({ user });
|
||||
} catch {
|
||||
return res.status(401).json({ message: "Invalid token" });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const isMatch = await bcrypt.compare(password, user.password);
|
||||
if (!isMatch) return res.status(401).json({ message: "Invalid credentials" });
|
||||
|
||||
const token = jwt.sign({ sub: user.id, email: user.email }, SECRET_KEY, { expiresIn: "1d" });
|
||||
const token = jwt.sign({ sub: String(user.id), email: user.email }, SECRET_KEY, { expiresIn: "1d" });
|
||||
|
||||
const isProd = process.env.NODE_ENV === "production";
|
||||
const cookie = [
|
||||
|
20
pages/api/logout.ts
Normal file
20
pages/api/logout.ts
Normal file
@ -0,0 +1,20 @@
|
||||
// pages/api/auth/logout.ts
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const isProd = process.env.NODE_ENV === "production";
|
||||
res.setHeader(
|
||||
"Set-Cookie",
|
||||
[
|
||||
"token=", // empty token
|
||||
"HttpOnly",
|
||||
"Path=/",
|
||||
"SameSite=Strict",
|
||||
"Max-Age=0", // expire immediately
|
||||
isProd ? "Secure" : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("; ")
|
||||
);
|
||||
return res.status(200).json({ message: "Logged out" });
|
||||
}
|
@ -24,7 +24,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
select: { id: true, email: true, createdAt: true }, // do NOT expose password
|
||||
});
|
||||
|
||||
const token = jwt.sign({ sub: user.id, email: user.email }, SECRET_KEY, { expiresIn: "1d" });
|
||||
const token = jwt.sign({ sub: String(user.id), email: user.email }, SECRET_KEY, { expiresIn: "1d" });
|
||||
|
||||
// Set a secure, httpOnly cookie
|
||||
const maxAge = 60 * 60 * 24; // 1 day
|
||||
|
@ -449,6 +449,7 @@ hover:text-primary hover:before:!bg-primary ltr:before:mr-2 rtl:before:ml-2 dark
|
||||
/* dropdown */
|
||||
.dropdown {
|
||||
@apply relative;
|
||||
@apply z-50;
|
||||
}
|
||||
.dropdown > button {
|
||||
@apply flex;
|
||||
|
Loading…
x
Reference in New Issue
Block a user