| @@ -48,6 +48,7 @@ | |||||
| "react-dom": "^18", | "react-dom": "^18", | ||||
| "react-hook-form": "^7.49.2", | "react-hook-form": "^7.49.2", | ||||
| "react-i18next": "^13.5.0", | "react-i18next": "^13.5.0", | ||||
| "react-idle-timer": "^5.7.2", | |||||
| "react-intl": "^6.5.5", | "react-intl": "^6.5.5", | ||||
| "react-number-format": "^5.3.4", | "react-number-format": "^5.3.4", | ||||
| "react-select": "^5.8.0", | "react-select": "^5.8.0", | ||||
| @@ -1,4 +1,5 @@ | |||||
| import AppBar from "@/components/AppBar"; | import AppBar from "@/components/AppBar"; | ||||
| import AutoLogoutProvider from "@/components/AutoLogoutProvider"; | |||||
| import { getServerSession } from "next-auth"; | import { getServerSession } from "next-auth"; | ||||
| import { authOptions } from "@/config/authConfig"; | import { authOptions } from "@/config/authConfig"; | ||||
| import { redirect } from "next/navigation"; | import { redirect } from "next/navigation"; | ||||
| @@ -33,6 +34,7 @@ export default async function MainLayout({ | |||||
| }} | }} | ||||
| > | > | ||||
| <I18nProvider namespaces={["common"]}> | <I18nProvider namespaces={["common"]}> | ||||
| <AutoLogoutProvider/> | |||||
| <Stack spacing={2}> | <Stack spacing={2}> | ||||
| <Breadcrumb /> | <Breadcrumb /> | ||||
| {children} | {children} | ||||
| @@ -0,0 +1,92 @@ | |||||
| 'use client' | |||||
| import React, { createContext, useState, useEffect, ReactNode } from 'react'; | |||||
| import { useIdleTimer } from "react-idle-timer"; | |||||
| import { signOut } from "next-auth/react"; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| interface TimerContextProps { | |||||
| lastRequestTime: number; | |||||
| setLastRequestTime: React.Dispatch<React.SetStateAction<number>>; | |||||
| } | |||||
| export const TimerContext = createContext<TimerContextProps | undefined>(undefined); | |||||
| interface AutoLogoutProviderProps { | |||||
| children?: ReactNode; | |||||
| isUserLoggedIn: boolean; | |||||
| } | |||||
| const AutoLogoutProvider: React.FC<AutoLogoutProviderProps> = ({ children, isUserLoggedIn }) => { | |||||
| const { t } = useTranslation("common") | |||||
| const [lastRequestTime, setLastRequestTime] = useState(Date.now()); | |||||
| const [logoutInterval, setLogoutInterval] = useState(1); // minute | |||||
| const [state, setState] = useState('Active'); | |||||
| const onIdle = () => { | |||||
| setLastRequestTime(Date.now()); | |||||
| setState('Idle') | |||||
| } | |||||
| const onActive = () => { | |||||
| setLastRequestTime(Date.now()); | |||||
| setState('Active') | |||||
| } | |||||
| const { | |||||
| isLastActiveTab, | |||||
| } = useIdleTimer({ | |||||
| onIdle, | |||||
| onActive, | |||||
| timeout: 1_000, | |||||
| throttle: 500, | |||||
| crossTab: true, | |||||
| syncTimers: 200, | |||||
| }) | |||||
| const lastActiveTab = isLastActiveTab() === null ? 'loading' : isLastActiveTab() | |||||
| const getLogoutInterval = () => { | |||||
| if (isUserLoggedIn && logoutInterval === 1) { | |||||
| //TODO: get auto logout time here | |||||
| setLogoutInterval(60); | |||||
| } | |||||
| else { | |||||
| if (!isUserLoggedIn && logoutInterval > 1) { | |||||
| setLogoutInterval(1); | |||||
| } | |||||
| } | |||||
| } | |||||
| useEffect(() => { | |||||
| getLogoutInterval() | |||||
| const interval = setInterval(async () => { | |||||
| const currentTime = Date.now(); | |||||
| // if (isPasswordExpiry()) { | |||||
| // navigate('/user/changePassword'); | |||||
| // } | |||||
| if (state !== "Active" && lastActiveTab) { | |||||
| const timeElapsed = currentTime - lastRequestTime; | |||||
| if (timeElapsed >= logoutInterval * 60 * 1000) { | |||||
| alert(t("Your session has expired, please log in again.")) | |||||
| // console.log(timeElapsed / 1000); | |||||
| // console.log(logoutInterval* 60); | |||||
| // console.log(logoutInterval * 60 * 1000 - timeElapsed) | |||||
| signOut() | |||||
| } | |||||
| } | |||||
| }, 1000); // Check every second | |||||
| return () => { | |||||
| clearInterval(interval); | |||||
| }; | |||||
| }, [lastRequestTime, logoutInterval]); | |||||
| return ( | |||||
| <TimerContext.Provider value={{ lastRequestTime, setLastRequestTime }}> | |||||
| {children} | |||||
| </TimerContext.Provider> | |||||
| ); | |||||
| }; | |||||
| export default AutoLogoutProvider; | |||||
| @@ -0,0 +1,14 @@ | |||||
| import React from "react"; | |||||
| import AutoLogoutProvider from "./AutoLogoutProvider"; | |||||
| import { getServerSession } from "next-auth"; | |||||
| import { authOptions } from "@/config/authConfig"; | |||||
| const AutoLogoutProviderWrapper: React.FC = async () => { | |||||
| const session = await getServerSession(authOptions) | |||||
| const isUserLoggedIn = session?.user !== null | |||||
| return <AutoLogoutProvider isUserLoggedIn={isUserLoggedIn}/>; | |||||
| }; | |||||
| export default AutoLogoutProviderWrapper; | |||||
| @@ -0,0 +1 @@ | |||||
| export { default } from "./AutoLogoutProviderWrapper"; | |||||
| @@ -10,7 +10,9 @@ interface SubComponents { | |||||
| const ProgressByTeamSearchWrapper: React.FC & SubComponents = async () => { | const ProgressByTeamSearchWrapper: React.FC & SubComponents = async () => { | ||||
| const teamprojects = await fetchTeamProjects(); | const teamprojects = await fetchTeamProjects(); | ||||
| return <ProgressByTeamSearch projects={teamprojects} />; | |||||
| const _teamprojects = teamprojects.length > 1 ? [{id: 0, teamId: 0, teamLeadId: 0, teamCode: "All", teamName: "", projectNo: teamprojects.reduce((acc, cur) => acc + cur.projectNo, 0)}, ...teamprojects] : teamprojects | |||||
| return <ProgressByTeamSearch projects={_teamprojects} />; | |||||
| }; | }; | ||||
| ProgressByTeamSearchWrapper.Loading = ProgressByTeamSearchLoading; | ProgressByTeamSearchWrapper.Loading = ProgressByTeamSearchLoading; | ||||
| @@ -6,6 +6,8 @@ | |||||
| "All": "All", | "All": "All", | ||||
| "Your session has expired, please log in again.": "Your session has expired, please log in again.", | |||||
| "Petty Cash": "Petty Cash", | "Petty Cash": "Petty Cash", | ||||
| "Expense": "Expense", | "Expense": "Expense", | ||||
| @@ -1,6 +1,8 @@ | |||||
| { | { | ||||
| "All": "全部", | "All": "全部", | ||||
| "Your session has expired, please log in again.": "登入驗證已過期, 請重新登入.", | |||||
| "Petty Cash": "小額開支", | "Petty Cash": "小額開支", | ||||
| "Expense": "普通開支", | "Expense": "普通開支", | ||||