118 lines
3.7 KiB
TypeScript
118 lines
3.7 KiB
TypeScript
import React, { ReactNode, useEffect, useState } from 'react'
|
||
import jwt from 'jsonwebtoken';
|
||
import menuNavBar from '../menuNavBar'
|
||
import NavBar from '../components/NavBar'
|
||
import FooterBar from '../components/FooterBar'
|
||
import { useAppDispatch, useAppSelector } from '../stores/hooks'
|
||
import Search from '../components/Search';
|
||
import { useRouter } from 'next/router'
|
||
import {findMe, logoutUser} from "../stores/authSlice";
|
||
import axios from 'axios';
|
||
import { mdiAlertCircle } from '@mdi/js';
|
||
import BaseIcon from '../components/BaseIcon';
|
||
import NavBarItemPlain from '../components/NavBarItemPlain';
|
||
import moment from 'moment';
|
||
|
||
import {hasPermission} from "../helpers/userPermissions";
|
||
|
||
|
||
type Props = {
|
||
children: ReactNode
|
||
|
||
permission?: string
|
||
|
||
}
|
||
|
||
export default function LayoutAuthenticated({
|
||
children,
|
||
|
||
permission
|
||
|
||
}: Props) {
|
||
const dispatch = useAppDispatch()
|
||
const router = useRouter()
|
||
const { token, currentUser } = useAppSelector((state) => state.auth)
|
||
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
||
const [lockoutBanner, setLockoutBanner] = useState(null);
|
||
|
||
let localToken
|
||
if (typeof window !== 'undefined') {
|
||
// Perform localStorage action
|
||
localToken = localStorage.getItem('token')
|
||
}
|
||
|
||
const isTokenValid = () => {
|
||
const token = localStorage.getItem('token');
|
||
if (!token) return;
|
||
const date = new Date().getTime() / 1000;
|
||
const data = jwt.decode(token);
|
||
if (!data) return;
|
||
return date < data.exp;
|
||
};
|
||
|
||
useEffect(() => {
|
||
dispatch(findMe());
|
||
if (!isTokenValid()) {
|
||
dispatch(logoutUser());
|
||
router.push('/login');
|
||
}
|
||
}, [token, localToken]);
|
||
|
||
|
||
useEffect(() => {
|
||
if (!permission || !currentUser) return;
|
||
|
||
if (!hasPermission(currentUser, permission)) router.push('/error');
|
||
}, [currentUser, permission]);
|
||
|
||
useEffect(() => {
|
||
const checkLockout = async () => {
|
||
try {
|
||
const response = await axios.get('/app_settings');
|
||
const settings = response.data;
|
||
if (settings.lockoutEnabled && settings.lockoutUntil && moment(settings.lockoutUntil).isAfter(moment())) {
|
||
const allowed = settings.allowedUserIds?.includes(currentUser?.id);
|
||
setLockoutBanner({
|
||
message: settings.lockoutMessage || `System is currently locked for reconciliation until ${moment(settings.lockoutUntil).format('LLL')}.`,
|
||
until: settings.lockoutUntil
|
||
});
|
||
}
|
||
} catch (err) {
|
||
// console.error(err); // Fail silently on frontend if fetch fails
|
||
}
|
||
};
|
||
if (currentUser) {
|
||
checkLockout();
|
||
}
|
||
}, [currentUser]);
|
||
|
||
const darkMode = useAppSelector((state) => state.style.darkMode)
|
||
|
||
return (
|
||
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
|
||
{lockoutBanner && (
|
||
<div className="bg-red-600 text-white p-2 text-center fixed top-0 left-0 w-full z-[100] flex items-center justify-center space-x-2 shadow-lg h-12">
|
||
<BaseIcon path={mdiAlertCircle} size="24" className="w-6 h-6" />
|
||
<span className="font-bold">SYSTEM LOCKOUT:</span>
|
||
<span>{lockoutBanner.message}</span>
|
||
</div>
|
||
)}
|
||
<div
|
||
className={`min-h-screen w-screen transition-position lg:w-auto ${bgColor} dark:bg-dark-800 dark:text-slate-100 ${lockoutBanner ? 'pt-26' : 'pt-14'}`}
|
||
>
|
||
<NavBar
|
||
menu={menuNavBar}
|
||
className={`${lockoutBanner ? 'top-12' : ''}`}
|
||
>
|
||
<NavBarItemPlain useMargin>
|
||
<Search />
|
||
</NavBarItemPlain>
|
||
</NavBar>
|
||
<div className="max-w-7xl mx-auto">
|
||
{children}
|
||
</div>
|
||
<FooterBar>Hand-crafted & Made with ❤️</FooterBar>
|
||
</div>
|
||
</div>
|
||
)
|
||
} |