diff --git a/frontend/messages/en.json b/frontend/messages/en.json
index 4e517d3..f1b749e 100644
--- a/frontend/messages/en.json
+++ b/frontend/messages/en.json
@@ -29,6 +29,10 @@
"signin": "Sign in",
"signout": "Sign out"
},
+ "Toast": {
+ "need_signed_in": "You need signed in",
+ "session_expired": "Session expired"
+ },
"Auth": {
"account": "Account",
"signup": "Sign up",
diff --git a/frontend/messages/ja.json b/frontend/messages/ja.json
index d290e65..b98b987 100644
--- a/frontend/messages/ja.json
+++ b/frontend/messages/ja.json
@@ -29,6 +29,10 @@
"signin": "サインイン",
"signout": "サインアウト"
},
+ "Toast": {
+ "need_signed_in": "サインインが必要です",
+ "session_expired": "セッションが切れました"
+ },
"Auth": {
"account": "アカウント",
"signup": "新規登録",
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 2e214f8..1c45227 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -32,6 +32,7 @@
"react": "^18.3.1",
"react-apexcharts": "^1.4.1",
"react-dom": "18.2.0",
+ "react-toastify": "^10.0.5",
"tailwind-variants": "^0.1.18",
"tailwindcss": "^3.4.3",
"typescript": "5.0.4"
@@ -3672,9 +3673,9 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
"node_modules/clsx": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
- "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"engines": {
"node": ">=6"
}
@@ -6173,6 +6174,18 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/react-toastify": {
+ "version": "10.0.5",
+ "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz",
+ "integrity": "sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==",
+ "dependencies": {
+ "clsx": "^2.1.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 5abc551..e380034 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -33,6 +33,7 @@
"react": "^18.3.1",
"react-apexcharts": "^1.4.1",
"react-dom": "18.2.0",
+ "react-toastify": "^10.0.5",
"tailwind-variants": "^0.1.18",
"tailwindcss": "^3.4.3",
"typescript": "5.0.4"
diff --git a/frontend/src/app/[locale]/layout.tsx b/frontend/src/app/[locale]/layout.tsx
index 180375f..74d1992 100644
--- a/frontend/src/app/[locale]/layout.tsx
+++ b/frontend/src/app/[locale]/layout.tsx
@@ -4,6 +4,7 @@ import { Providers } from './providers';
import Header from './Header';
import clsx from 'clsx';
import { getTranslations } from 'next-intl/server';
+import { useTranslations } from 'next-intl';
export async function generateMetadata({ params: { locale } }) {
const t = await getTranslations({ locale, namespace: 'Header' });
@@ -25,11 +26,20 @@ export default function RootLayout({
children: React.ReactNode;
params: { locale: string };
}) {
+ const t = useTranslations('Toast');
+ const toastMessages = {
+ needSignedIn: t('need_signed_in'),
+ sessionExpired: t('session_expired'),
+ };
+
return (
-
+
{children}
diff --git a/frontend/src/app/[locale]/providers.tsx b/frontend/src/app/[locale]/providers.tsx
index 26e7adf..cd2e573 100644
--- a/frontend/src/app/[locale]/providers.tsx
+++ b/frontend/src/app/[locale]/providers.tsx
@@ -5,22 +5,27 @@ import { NextUIProvider } from '@nextui-org/react';
import { useRouter } from 'next/navigation';
import { ThemeProvider as NextThemesProvider } from 'next-themes';
import { ThemeProviderProps } from 'next-themes/dist/types';
+import ToastProvider from '@/utils/ToastProvider';
+import { ToastProps } from '@/types/toast';
import TokenProvider from '@/utils/TokenProvider';
import { TokenProps } from '@/types/user';
export interface ProvidersProps {
children: React.ReactNode;
themeProps?: ThemeProviderProps;
+ toastProps?: ToastProps;
tokenProps?: TokenProps;
}
-export function Providers({ children, themeProps, tokenProps }: ProvidersProps) {
+export function Providers({ children, themeProps, toastProps, tokenProps }: ProvidersProps) {
const router = useRouter();
return (
- {children}
+
+ {children}
+
);
diff --git a/frontend/types/toast.ts b/frontend/types/toast.ts
new file mode 100644
index 0000000..973610a
--- /dev/null
+++ b/frontend/types/toast.ts
@@ -0,0 +1,12 @@
+export type ToastProps = {
+ children?: React.ReactNode;
+};
+
+export type ToastMessages = {
+ needSignedIn: string;
+ sessionExpired: string;
+};
+
+export type ToastContextType = {
+ showToast: (text: string, mode: string) => {};
+};
diff --git a/frontend/types/user.ts b/frontend/types/user.ts
index 970d6c5..ec4cb9b 100644
--- a/frontend/types/user.ts
+++ b/frontend/types/user.ts
@@ -1,3 +1,5 @@
+import { ToastMessages } from './toast';
+
export type UserType = {
id: number | null;
email: string;
@@ -8,6 +10,7 @@ export type UserType = {
} | null;
export type TokenProps = {
+ toastMessages: ToastMessages;
locale: string;
children?: React.ReactNode;
};
diff --git a/frontend/utils/ToastProvider.tsx b/frontend/utils/ToastProvider.tsx
new file mode 100644
index 0000000..c5ba500
--- /dev/null
+++ b/frontend/utils/ToastProvider.tsx
@@ -0,0 +1,31 @@
+import { createContext } from 'react';
+import { ToastContextType, ToastProps } from '@/types/toast';
+
+// TODO Temporary use until NextUI's Toast is released.
+import { ToastContainer, toast } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
+
+const defaultContext = {
+ showToast: (text: string, mode: string) => {},
+};
+const ToastContext = createContext(defaultContext);
+
+const ToastProvider = ({ children }: ToastProps) => {
+ const showToast = (text: string, mode: string) => {
+ toast(text, { theme: 'light' });
+ };
+
+ const toastContext = {
+ showToast,
+ };
+
+ return (
+
+
+ {children}
+
+ );
+};
+
+export { ToastContext };
+export default ToastProvider;
diff --git a/frontend/utils/TokenProvider.tsx b/frontend/utils/TokenProvider.tsx
index 031f7a8..8fa21ef 100644
--- a/frontend/utils/TokenProvider.tsx
+++ b/frontend/utils/TokenProvider.tsx
@@ -1,9 +1,10 @@
'use client';
-import { createContext, useState, useEffect } from 'react';
+import { createContext, useState, useEffect, useContext } from 'react';
import { TokenContextType, TokenType } from '@/types/user';
import { TokenProps } from '@/types/user';
import { useRouter, usePathname } from '@/src/navigation';
import { roles } from '@/config/selection';
+import { ToastContext } from './ToastProvider';
const LOCAL_STORAGE_KEY = 'testplat-auth-token';
function storeTokenToLocalStorage(token: TokenType) {
@@ -27,9 +28,10 @@ const defaultContext = {
};
const TokenContext = createContext(defaultContext);
-const TokenProvider = ({ locale, children }: TokenProps) => {
+const TokenProvider = ({ toastMessages, locale, children }: TokenProps) => {
const router = useRouter();
const pathname = usePathname();
+ const toastContext = useContext(ToastContext);
const [hasRestoreFinished, setHasRestoreFinished] = useState(false);
const [token, setToken] = useState({
@@ -38,34 +40,39 @@ const TokenProvider = ({ locale, children }: TokenProps) => {
user: null,
});
- const isSignedIn = () => {
- // check token
+ const tokenExists = () => {
if (token && token.user && token.user.username) {
- // check expire date
- if (Date.now() < token.expires_at) {
- return true;
- } else {
- console.error('session expired');
- }
+ return true;
+ } else {
+ return false;
}
+ };
- return false;
+ const isTokenValid = () => {
+ if (Date.now() < token.expires_at) {
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ const isSignedIn = () => {
+ if (tokenExists() && isTokenValid()) {
+ return true;
+ } else {
+ return false;
+ }
};
const isAdmin = () => {
- // check token
- if (token && token.user && token.user.username) {
- // check expire date
- if (Date.now() < token.expires_at) {
- // check role
- const adminRoleIndex = roles.findIndex((entry) => entry.uid === 'administrator');
- if (token.user.role === adminRoleIndex) {
- return true;
- }
+ if (tokenExists() && isTokenValid()) {
+ const adminRoleIndex = roles.findIndex((entry) => entry.uid === 'administrator');
+ if (token.user.role === adminRoleIndex) {
+ return true;
}
+ } else {
+ return false;
}
-
- return false;
};
const tokenContext = {
@@ -91,9 +98,11 @@ const TokenProvider = ({ locale, children }: TokenProps) => {
}, []);
useEffect(() => {
- // check current path is private. pravate path is '/account' or '/projects/*'
+ // pravate paths are '/account', '/admin', '/projects/*'
const isPrivatePath = (pathname: string) => {
- return /^\/account(\/)?$/.test(pathname) || /^\/projects(\/)?$/.test(pathname);
+ return (
+ /^\/account(\/)?$/.test(pathname) || /^\/admin(\/)?$/.test(pathname) || /^\/projects(\/.*)?$/.test(pathname)
+ );
};
const checkSignInPage = () => {
@@ -101,7 +110,16 @@ const TokenProvider = ({ locale, children }: TokenProps) => {
return;
}
if (isPrivatePath(pathname) && !isSignedIn()) {
- router.push(`/account/signin`, { locale: locale });
+ if (tokenExists()) {
+ toastContext.showToast(toastMessages.needSignedIn, 'default');
+ router.push(`/account/signin`, { locale: locale });
+ return;
+ }
+ if (isTokenValid()) {
+ toastContext.showToast(toastMessages.sessionExpired, 'default');
+ router.push(`/account/signin`, { locale: locale });
+ return;
+ }
}
};