init
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import { RouteObject } from 'react-router-dom';
|
||||
import AccountSetup from '@/pages/account-setup/account-setup';
|
||||
|
||||
const accountSetupRoute: RouteObject = {
|
||||
path: '/worklenz/setup',
|
||||
element: <AccountSetup />,
|
||||
};
|
||||
|
||||
export default accountSetupRoute;
|
||||
32
worklenz-frontend/src/app/routes/admin-center-routes.tsx
Normal file
32
worklenz-frontend/src/app/routes/admin-center-routes.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { RouteObject } from 'react-router-dom';
|
||||
import AdminCenterLayout from '@/layouts/admin-center-layout';
|
||||
import { adminCenterItems } from '@/pages/admin-center/admin-center-constants';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
|
||||
const AdminCenterGuard = ({ children }: { children: React.ReactNode }) => {
|
||||
const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
|
||||
|
||||
if (!isOwnerOrAdmin) {
|
||||
return <Navigate to="/worklenz/unauthorized" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
const adminCenterRoutes: RouteObject[] = [
|
||||
{
|
||||
path: 'admin-center',
|
||||
element: (
|
||||
<AdminCenterGuard>
|
||||
<AdminCenterLayout />
|
||||
</AdminCenterGuard>
|
||||
),
|
||||
children: adminCenterItems.map(item => ({
|
||||
path: item.endpoint,
|
||||
element: item.element,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
export default adminCenterRoutes;
|
||||
51
worklenz-frontend/src/app/routes/auth-routes.tsx
Normal file
51
worklenz-frontend/src/app/routes/auth-routes.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import AuthLayout from '@/layouts/AuthLayout';
|
||||
import LoginPage from '@/pages/auth/login-page';
|
||||
import SignupPage from '@/pages/auth/signup-page';
|
||||
import ForgotPasswordPage from '@/pages/auth/forgot-password-page';
|
||||
import LoggingOutPage from '@/pages/auth/logging-out';
|
||||
import AuthenticatingPage from '@/pages/auth/authenticating';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import VerifyResetEmailPage from '@/pages/auth/verify-reset-email';
|
||||
import { Suspense } from 'react';
|
||||
import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';
|
||||
|
||||
const authRoutes = [
|
||||
{
|
||||
path: '/auth',
|
||||
element: (
|
||||
<AuthLayout />
|
||||
),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
element: <Navigate to="login" replace />,
|
||||
},
|
||||
{
|
||||
path: 'login',
|
||||
element: <LoginPage />,
|
||||
},
|
||||
{
|
||||
path: 'signup',
|
||||
element: <SignupPage />,
|
||||
},
|
||||
{
|
||||
path: 'forgot-password',
|
||||
element: <ForgotPasswordPage />,
|
||||
},
|
||||
{
|
||||
path: 'logging-out',
|
||||
element: <LoggingOutPage />,
|
||||
},
|
||||
{
|
||||
path: 'authenticating',
|
||||
element: <AuthenticatingPage />,
|
||||
},
|
||||
{
|
||||
path: 'verify-reset-email/:user/:hash',
|
||||
element: <VerifyResetEmailPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default authRoutes;
|
||||
230
worklenz-frontend/src/app/routes/index.tsx
Normal file
230
worklenz-frontend/src/app/routes/index.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
import { createBrowserRouter, Navigate, RouteObject, useLocation } from 'react-router-dom';
|
||||
import rootRoutes from './root-routes';
|
||||
import authRoutes from './auth-routes';
|
||||
import mainRoutes, { licenseExpiredRoute } from './main-routes';
|
||||
import notFoundRoute from './not-found-route';
|
||||
import accountSetupRoute from './account-setup-routes';
|
||||
import reportingRoutes from './reporting-routes';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
import { AuthenticatedLayout } from '@/layouts/AuthenticatedLayout';
|
||||
import ErrorBoundary from '@/components/ErrorBoundary';
|
||||
import NotFoundPage from '@/pages/404-page/404-page';
|
||||
import { ISUBSCRIPTION_TYPE } from '@/shared/constants';
|
||||
import LicenseExpired from '@/pages/license-expired/license-expired';
|
||||
|
||||
interface GuardProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AuthGuard = ({ children }: GuardProps) => {
|
||||
const isAuthenticated = useAuthService().isAuthenticated();
|
||||
const location = useLocation();
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/auth" state={{ from: location }} replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export const AdminGuard = ({ children }: GuardProps) => {
|
||||
const isAuthenticated = useAuthService().isAuthenticated();
|
||||
const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
|
||||
const currentSession = useAuthService().getCurrentSession();
|
||||
const isFreePlan = currentSession?.subscription_type === ISUBSCRIPTION_TYPE.FREE;
|
||||
const location = useLocation();
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/auth" state={{ from: location }} replace />;
|
||||
}
|
||||
|
||||
if (!isOwnerOrAdmin || isFreePlan) {
|
||||
return <Navigate to="/worklenz/unauthorized" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export const LicenseExpiryGuard = ({ children }: GuardProps) => {
|
||||
const isAuthenticated = useAuthService().isAuthenticated();
|
||||
const currentSession = useAuthService().getCurrentSession();
|
||||
const location = useLocation();
|
||||
const isAdminCenterRoute = location.pathname.includes('/worklenz/admin-center');
|
||||
const isLicenseExpiredRoute = location.pathname === '/worklenz/license-expired';
|
||||
|
||||
// Don't check or redirect if we're already on the license-expired page
|
||||
if (isLicenseExpiredRoute) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
// Check if trial is expired more than 7 days or if is_expired flag is set
|
||||
const isLicenseExpiredMoreThan7Days = () => {
|
||||
// Quick bail if no session data is available
|
||||
if (!currentSession) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check is_expired flag first
|
||||
if (currentSession.is_expired) {
|
||||
// If no trial_expire_date exists but is_expired is true, defer to backend check
|
||||
if (!currentSession.trial_expire_date) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If there is a trial_expire_date, check if it's more than 7 days past
|
||||
const today = new Date();
|
||||
const expiryDate = new Date(currentSession.trial_expire_date);
|
||||
const diffTime = today.getTime() - expiryDate.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
// Redirect if more than 7 days past expiration
|
||||
return diffDays > 7;
|
||||
}
|
||||
|
||||
// If not marked as expired but has trial_expire_date, do a date check
|
||||
if (currentSession.subscription_type === ISUBSCRIPTION_TYPE.TRIAL && currentSession.trial_expire_date) {
|
||||
const today = new Date();
|
||||
const expiryDate = new Date(currentSession.trial_expire_date);
|
||||
|
||||
const diffTime = today.getTime() - expiryDate.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
// If expired more than 7 days, redirect
|
||||
return diffDays > 7;
|
||||
}
|
||||
|
||||
// No expiration data found
|
||||
return false;
|
||||
};
|
||||
|
||||
// Add this explicit check and log the result
|
||||
const shouldRedirect = isAuthenticated && isLicenseExpiredMoreThan7Days() && !isAdminCenterRoute;
|
||||
if (shouldRedirect) {
|
||||
return <Navigate to="/worklenz/license-expired" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export const SetupGuard = ({ children }: GuardProps) => {
|
||||
const isAuthenticated = useAuthService().isAuthenticated();
|
||||
const location = useLocation();
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/auth" state={{ from: location }} replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
// Helper to wrap routes with guards
|
||||
const wrapRoutes = (
|
||||
routes: RouteObject[],
|
||||
Guard: React.ComponentType<{ children: React.ReactNode }>
|
||||
): RouteObject[] => {
|
||||
return routes.map(route => {
|
||||
const wrappedRoute = {
|
||||
...route,
|
||||
element: <Guard>{route.element}</Guard>,
|
||||
};
|
||||
|
||||
if (route.children) {
|
||||
wrappedRoute.children = wrapRoutes(route.children, Guard);
|
||||
}
|
||||
|
||||
if (route.index) {
|
||||
delete wrappedRoute.children;
|
||||
}
|
||||
|
||||
return wrappedRoute;
|
||||
});
|
||||
};
|
||||
|
||||
// Static license expired component that doesn't rely on translations or authentication
|
||||
const StaticLicenseExpired = () => {
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
marginTop: 65,
|
||||
minHeight: '90vh',
|
||||
backgroundColor: '#f5f5f5',
|
||||
padding: '20px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<div style={{
|
||||
background: 'white',
|
||||
padding: '30px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||
textAlign: 'center',
|
||||
maxWidth: '600px'
|
||||
}}>
|
||||
<h1 style={{ fontSize: '24px', color: '#faad14', marginBottom: '16px' }}>
|
||||
Your Worklenz trial has expired!
|
||||
</h1>
|
||||
<p style={{ fontSize: '16px', color: '#555', marginBottom: '24px' }}>
|
||||
Please upgrade now to continue using Worklenz.
|
||||
</p>
|
||||
<button
|
||||
style={{
|
||||
backgroundColor: '#1890ff',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
padding: '8px 16px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '16px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => window.location.href = '/worklenz/admin-center/billing'}
|
||||
>
|
||||
Upgrade now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const publicRoutes = [
|
||||
...rootRoutes,
|
||||
...authRoutes,
|
||||
notFoundRoute
|
||||
];
|
||||
const protectedMainRoutes = wrapRoutes(mainRoutes, AuthGuard);
|
||||
const adminRoutes = wrapRoutes(reportingRoutes, AdminGuard);
|
||||
const setupRoutes = wrapRoutes([accountSetupRoute], SetupGuard);
|
||||
|
||||
// Apply LicenseExpiryGuard to all protected routes
|
||||
const withLicenseExpiryCheck = (routes: RouteObject[]): RouteObject[] => {
|
||||
return routes.map(route => {
|
||||
const wrappedRoute = {
|
||||
...route,
|
||||
element: <LicenseExpiryGuard>{route.element}</LicenseExpiryGuard>,
|
||||
};
|
||||
|
||||
if (route.children) {
|
||||
wrappedRoute.children = withLicenseExpiryCheck(route.children);
|
||||
}
|
||||
|
||||
return wrappedRoute;
|
||||
});
|
||||
};
|
||||
|
||||
const licenseCheckedMainRoutes = withLicenseExpiryCheck(protectedMainRoutes);
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
element: <ErrorBoundary><AuthenticatedLayout /></ErrorBoundary>,
|
||||
errorElement: <ErrorBoundary><NotFoundPage /></ErrorBoundary>,
|
||||
children: [
|
||||
...licenseCheckedMainRoutes,
|
||||
...adminRoutes,
|
||||
...setupRoutes,
|
||||
licenseExpiredRoute,
|
||||
],
|
||||
},
|
||||
...publicRoutes,
|
||||
]);
|
||||
|
||||
export default router;
|
||||
64
worklenz-frontend/src/app/routes/main-routes.tsx
Normal file
64
worklenz-frontend/src/app/routes/main-routes.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { RouteObject } from 'react-router-dom';
|
||||
import MainLayout from '@/layouts/MainLayout';
|
||||
import HomePage from '@/pages/home/home-page';
|
||||
import ProjectList from '@/pages/projects/project-list';
|
||||
import settingsRoutes from './settings-routes';
|
||||
import adminCenterRoutes from './admin-center-routes';
|
||||
import Schedule from '@/pages/schedule/schedule';
|
||||
import ProjectTemplateEditView from '@/pages/settings/project-templates/projectTemplateEditView/ProjectTemplateEditView';
|
||||
import LicenseExpired from '@/pages/license-expired/license-expired';
|
||||
import ProjectView from '@/pages/projects/projectView/project-view';
|
||||
import Unauthorized from '@/pages/unauthorized/unauthorized';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
|
||||
// Define AdminGuard component first
|
||||
const AdminGuard = ({ children }: { children: React.ReactNode }) => {
|
||||
const isAuthenticated = useAuthService().isAuthenticated();
|
||||
const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
|
||||
const location = useLocation();
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/auth" state={{ from: location }} replace />;
|
||||
}
|
||||
|
||||
if (!isOwnerOrAdmin) {
|
||||
return <Navigate to="/worklenz/unauthorized" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
const mainRoutes: RouteObject[] = [
|
||||
{
|
||||
path: '/worklenz',
|
||||
element: <MainLayout />,
|
||||
children: [
|
||||
{ path: 'home', element: <HomePage /> },
|
||||
{ path: 'projects', element: <ProjectList /> },
|
||||
{
|
||||
path: 'schedule',
|
||||
element: <AdminGuard><Schedule /></AdminGuard>
|
||||
},
|
||||
{ path: `projects/:projectId`, element: <ProjectView /> },
|
||||
{
|
||||
path: `settings/project-templates/edit/:templateId/:templateName`,
|
||||
element: <ProjectTemplateEditView />,
|
||||
},
|
||||
{ path: 'unauthorized', element: <Unauthorized /> },
|
||||
...settingsRoutes,
|
||||
...adminCenterRoutes,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// License expired route should be separate to avoid being wrapped in LicenseExpiryGuard
|
||||
export const licenseExpiredRoute: RouteObject = {
|
||||
path: '/worklenz',
|
||||
element: <MainLayout />,
|
||||
children: [
|
||||
{ path: 'license-expired', element: <LicenseExpired /> }
|
||||
]
|
||||
};
|
||||
|
||||
export default mainRoutes;
|
||||
10
worklenz-frontend/src/app/routes/not-found-route.tsx
Normal file
10
worklenz-frontend/src/app/routes/not-found-route.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import { RouteObject } from 'react-router-dom';
|
||||
import NotFoundPage from '@/pages/404-page/404-page';
|
||||
|
||||
const notFoundRoute: RouteObject = {
|
||||
path: '*',
|
||||
element: <NotFoundPage />,
|
||||
};
|
||||
|
||||
export default notFoundRoute;
|
||||
21
worklenz-frontend/src/app/routes/protected-routes.tsx
Normal file
21
worklenz-frontend/src/app/routes/protected-routes.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: React.ReactNode;
|
||||
isAllowed: boolean;
|
||||
redirectPath?: string;
|
||||
}
|
||||
|
||||
const ProtectedRoute = ({
|
||||
children,
|
||||
isAllowed,
|
||||
redirectPath = '/worklenz/login',
|
||||
}: ProtectedRouteProps) => {
|
||||
const location = useLocation();
|
||||
|
||||
if (!isAllowed) {
|
||||
return <Navigate to={redirectPath} replace state={{ from: location }} />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
28
worklenz-frontend/src/app/routes/reporting-routes.tsx
Normal file
28
worklenz-frontend/src/app/routes/reporting-routes.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { RouteObject } from 'react-router-dom';
|
||||
import ReportingLayout from '@/layouts/ReportingLayout';
|
||||
import { ReportingMenuItems, reportingsItems } from '@/lib/reporting/reporting-constants';
|
||||
|
||||
// function to flatten nested menu items
|
||||
const flattenItems = (items: ReportingMenuItems[]): ReportingMenuItems[] => {
|
||||
return items.reduce<ReportingMenuItems[]>((acc, item) => {
|
||||
if (item.children) {
|
||||
return [...acc, ...flattenItems(item.children)];
|
||||
}
|
||||
return [...acc, item];
|
||||
}, []);
|
||||
};
|
||||
|
||||
const flattenedItems = flattenItems(reportingsItems);
|
||||
|
||||
const reportingRoutes: RouteObject[] = [
|
||||
{
|
||||
path: 'worklenz/reporting',
|
||||
element: <ReportingLayout />,
|
||||
children: flattenedItems.map(item => ({
|
||||
path: item.endpoint,
|
||||
element: item.element,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
export default reportingRoutes;
|
||||
10
worklenz-frontend/src/app/routes/root-routes.tsx
Normal file
10
worklenz-frontend/src/app/routes/root-routes.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Navigate, RouteObject } from 'react-router-dom';
|
||||
|
||||
const rootRoutes: RouteObject[] = [
|
||||
{
|
||||
path: '/',
|
||||
element: <Navigate to="/auth/login" replace />,
|
||||
},
|
||||
];
|
||||
|
||||
export default rootRoutes;
|
||||
32
worklenz-frontend/src/app/routes/settings-routes.tsx
Normal file
32
worklenz-frontend/src/app/routes/settings-routes.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { RouteObject } from 'react-router-dom';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import SettingsLayout from '@/layouts/SettingsLayout';
|
||||
import { settingsItems } from '@/lib/settings/settings-constants';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
|
||||
const SettingsGuard = ({ children, adminRequired }: { children: React.ReactNode; adminRequired: boolean }) => {
|
||||
const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
|
||||
|
||||
if (adminRequired && !isOwnerOrAdmin) {
|
||||
return <Navigate to="/worklenz/unauthorized" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
const settingsRoutes: RouteObject[] = [
|
||||
{
|
||||
path: 'settings',
|
||||
element: <SettingsLayout />,
|
||||
children: settingsItems.map(item => ({
|
||||
path: item.endpoint,
|
||||
element: (
|
||||
<SettingsGuard adminRequired={!!item.adminOnly}>
|
||||
{item.element}
|
||||
</SettingsGuard>
|
||||
),
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
export default settingsRoutes;
|
||||
Reference in New Issue
Block a user