Skip to content

Commit e31f483

Browse files
fix(auth): resolve login redirection and persistent session issues Send email (#3)
* fixes * fix-2 * fix-3 * fix-4 * fix-4 * fix(auth): resolve login redirection and persistent session issues This commit addresses the following: - Adds @tanstack/react-query and react-error-boundary for state management and resilience. - Refactors App.tsx to use React Router v6.4+ Data API (createBrowserRouter). - Fixes an issue where successful login did not redirect to the dashboard. - Modifies authMiddleware to drop the Secure flag on localhost to prevent proxy issues. - Adds Cache-Control headers to /api/session to avoid browser caching of 401s.
1 parent 2ee5c23 commit e31f483

12 files changed

Lines changed: 228 additions & 42 deletions

File tree

env.example

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# ------------------------------------------------------------------
2+
# Environment Variables for Local Development
3+
# ------------------------------------------------------------------
4+
# Save or copy this file as `.dev.vars` to use these variables locally.
5+
# `wrangler dev` automatically reads `.dev.vars` for secrets and variables.
6+
#
7+
# Note: D1 Database connections (like database_id) are configured
8+
# in `wrangler.toml`. To test locally while connecting to your remote
9+
# Cloudflare D1 database and other cloud resources, run:
10+
# npm run dev:backend:remote
11+
# (which executes `wrangler dev --remote`)
12+
# ------------------------------------------------------------------
13+
14+
# [Required Secrets]
15+
# The password used to log into the admin dashboard
16+
ADMIN_PASSWORD="your_secure_admin_password_here"
17+
18+
# A secure random string used for signing JWT tokens
19+
# You can generate one via: openssl rand -hex 32
20+
JWT_TOKEN="your_secure_jwt_secret_token_here"
21+
22+
# [Optional Secrets]
23+
# Resend API Key for sending outbound emails (if you use this feature)
24+
RESEND_API_KEY="re_123456789"
25+
26+
# [Variables]
27+
# You can also override variables defined in wrangler.toml here for local testing
28+
MAIL_DOMAIN="example.com"
29+
ADMIN_NAME="admin"

package-lock.json

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@
2323
"@radix-ui/react-slot": "^1.2.4",
2424
"@radix-ui/react-tabs": "^1.1.13",
2525
"@tailwindcss/vite": "^4.1.18",
26+
"@tanstack/react-query": "^5.90.21",
2627
"class-variance-authority": "^0.7.1",
2728
"clsx": "^2.1.1",
2829
"date-fns": "^4.1.0",
2930
"lucide-react": "^0.563.0",
3031
"react": "^19.2.0",
3132
"react-dom": "^19.2.0",
33+
"react-error-boundary": "^6.1.1",
3234
"react-hot-toast": "^2.6.0",
3335
"react-router-dom": "^7.13.0",
3436
"tailwind-merge": "^3.4.0",
@@ -48,4 +50,4 @@
4850
"typescript-eslint": "^8.46.4",
4951
"vite": "^7.2.4"
5052
}
51-
}
53+
}

src/App.tsx

Lines changed: 62 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
1+
import { createBrowserRouter, RouterProvider, Navigate, Outlet } from 'react-router-dom';
22
import { Toaster } from 'react-hot-toast';
3+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4+
import { GlobalErrorBoundary } from '@/components/GlobalErrorBoundary';
35
import { AuthProvider, useAuth } from '@/context/AuthContext';
46
import Login from '@/pages/auth/Login';
57
import Dashboard from '@/pages/dashboard';
@@ -9,6 +11,8 @@ import SentMailbox from "@/pages/mailbox/Sent";
911
import Layout from '@/components/layout/Layout';
1012
import { Loader2 } from 'lucide-react';
1113

14+
const queryClient = new QueryClient();
15+
1216
const PrivateRoute = ({ children, roles = [] }: { children: React.ReactNode, roles?: string[] }) => {
1317
const { user, isLoading } = useAuth();
1418

@@ -33,9 +37,9 @@ const PrivateRoute = ({ children, roles = [] }: { children: React.ReactNode, rol
3337
return <>{children}</>;
3438
};
3539

36-
function AppRoutes() {
40+
const AuthenticatedRoute = () => {
3741
const { user, isLoading } = useAuth();
38-
42+
3943
if (isLoading) {
4044
return (
4145
<div className="min-h-screen flex items-center justify-center bg-background">
@@ -44,31 +48,65 @@ function AppRoutes() {
4448
);
4549
}
4650

47-
return (
48-
<Routes>
49-
<Route path="/login" element={!user ? <Login /> : <Navigate to="/dashboard" />} />
51+
if (!user) return <Navigate to="/login" replace />;
5052

51-
<Route element={user ? <Layout /> : <Navigate to="/login" />}>
52-
<Route path="/dashboard" element={<Dashboard />} />
53-
<Route path="/mailbox" element={<PrivateRoute roles={['admin', 'user', 'mailbox']}><Mailbox /></PrivateRoute>} />
54-
<Route path="/compose" element={<PrivateRoute roles={['admin', 'user', 'mailbox']}><ComposePage /></PrivateRoute>} />
55-
<Route path="/sent" element={<PrivateRoute roles={['admin', 'user', 'mailbox']}><SentMailbox /></PrivateRoute>} />
56-
<Route path="/settings" element={<PrivateRoute roles={['admin', 'user']}><div className="p-8">Settings (Work in Progress)</div></PrivateRoute>} />
57-
<Route index element={<Navigate to="/dashboard" />} />
58-
</Route>
59-
60-
<Route path="*" element={<Navigate to={user ? "/dashboard" : "/login"} />} />
61-
</Routes>
53+
return (
54+
<Layout>
55+
<Outlet />
56+
</Layout>
6257
);
63-
}
58+
};
59+
60+
const router = createBrowserRouter([
61+
{
62+
path: "/login",
63+
element: <Login />,
64+
},
65+
{
66+
path: "/",
67+
element: <AuthenticatedRoute />,
68+
children: [
69+
{
70+
index: true,
71+
element: <Navigate to="/dashboard" replace />,
72+
},
73+
{
74+
path: "dashboard",
75+
element: <Dashboard />,
76+
},
77+
{
78+
path: "mailbox",
79+
element: <PrivateRoute roles={['admin', 'user', 'mailbox']}><Mailbox /></PrivateRoute>,
80+
},
81+
{
82+
path: "compose",
83+
element: <PrivateRoute roles={['admin', 'user', 'mailbox']}><ComposePage /></PrivateRoute>,
84+
},
85+
{
86+
path: "sent",
87+
element: <PrivateRoute roles={['admin', 'user', 'mailbox']}><SentMailbox /></PrivateRoute>,
88+
},
89+
{
90+
path: "settings",
91+
element: <PrivateRoute roles={['admin', 'user']}><div className="p-8">Settings (Work in Progress)</div></PrivateRoute>,
92+
}
93+
]
94+
},
95+
{
96+
path: "*",
97+
element: <Navigate to="/dashboard" replace />
98+
}
99+
]);
64100

65101
export default function App() {
66102
return (
67-
<BrowserRouter>
68-
<AuthProvider>
69-
<AppRoutes />
70-
<Toaster position="top-right" />
71-
</AuthProvider>
72-
</BrowserRouter>
103+
<GlobalErrorBoundary>
104+
<QueryClientProvider client={queryClient}>
105+
<AuthProvider>
106+
<RouterProvider router={router} />
107+
<Toaster position="top-right" />
108+
</AuthProvider>
109+
</QueryClientProvider>
110+
</GlobalErrorBoundary>
73111
);
74112
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { ErrorBoundary } from 'react-error-boundary';
2+
import type { FallbackProps } from 'react-error-boundary';
3+
import { Button } from '@/components/ui/button';
4+
import { AlertCircle } from 'lucide-react';
5+
6+
interface GlobalErrorBoundaryProps {
7+
children: React.ReactNode;
8+
}
9+
10+
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
11+
return (
12+
<div className="min-h-screen flex flex-col items-center justify-center bg-background p-4 text-center">
13+
<div className="max-w-md space-y-6">
14+
<div className="flex justify-center flex-col items-center space-y-4">
15+
<div className="bg-destructive/10 p-4 rounded-full">
16+
<AlertCircle className="h-12 w-12 text-destructive" />
17+
</div>
18+
<h1 className="text-3xl font-bold tracking-tight">Something went wrong</h1>
19+
</div>
20+
21+
<div className="bg-muted p-4 rounded-md text-left overflow-auto max-h-[300px]">
22+
<p className="text-sm font-mono text-muted-foreground break-all">
23+
{error instanceof Error ? error.message : String(error)}
24+
</p>
25+
</div>
26+
27+
<div className="flex gap-4 justify-center">
28+
<Button onClick={resetErrorBoundary} variant="default">
29+
Try again
30+
</Button>
31+
<Button onClick={() => window.location.href = '/'} variant="outline">
32+
Go home
33+
</Button>
34+
</div>
35+
</div>
36+
</div>
37+
);
38+
}
39+
40+
export function GlobalErrorBoundary({ children }: GlobalErrorBoundaryProps) {
41+
return (
42+
<ErrorBoundary
43+
FallbackComponent={ErrorFallback}
44+
onReset={() => {
45+
// Reset the state of your app so the error doesn't happen again
46+
}}
47+
>
48+
{children}
49+
</ErrorBoundary>
50+
);
51+
}

src/components/layout/Layout.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ import {
1515
} from 'lucide-react';
1616
import { cn } from '@/lib/utils';
1717

18-
export default function Layout() {
18+
interface LayoutProps {
19+
children?: React.ReactNode;
20+
}
21+
22+
export default function Layout({ children }: LayoutProps) {
1923
const { user, logout } = useAuth();
2024
const [isSidebarOpen, setSidebarOpen] = useState(false);
2125
const toggleSidebar = () => setSidebarOpen(!isSidebarOpen);
@@ -100,7 +104,7 @@ export default function Layout() {
100104
</header>
101105

102106
<div className="flex-1">
103-
<Outlet />
107+
{children || <Outlet />}
104108
</div>
105109
</main>
106110
</div>

src/context/AuthContext.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,6 @@ export function AuthProvider({ children }: { children: ReactNode }) {
3535
} else {
3636
setUser(null);
3737
}
38-
} catch (error: any) {
39-
// 401 Unauthorized is expected if not logged in
40-
if (error.status === 401) {
41-
setUser(null);
42-
} else {
43-
console.error('Session check failed:', error);
44-
setUser(null);
45-
}
4638
} finally {
4739
setIsLoading(false);
4840
}

src/pages/auth/Login.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useState } from 'react';
2+
import { useNavigate } from 'react-router-dom';
23
import { useAuth } from '@/context/AuthContext';
34
import { Button } from '@/components/ui/button';
45
import { Input } from '@/components/ui/input';
@@ -11,13 +12,15 @@ export default function Login() {
1112
const [username, setUsername] = useState('');
1213
const [password, setPassword] = useState('');
1314
const [isLoading, setIsLoading] = useState(false);
15+
const navigate = useNavigate();
1416

1517
const handleSubmit = async (e: React.FormEvent) => {
1618
e.preventDefault();
1719
setIsLoading(true);
1820
try {
1921
await login({ username, password });
2022
toast.success('Login successful');
23+
navigate('/dashboard', { replace: true });
2124
} catch (error) {
2225
const msg = error instanceof Error ? error.message : 'Login failed';
2326
toast.error(msg);

worker/src/assets/manager.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,8 @@ export class AssetManager {
6666

6767
// Allow .html files to fall through to specific handlers or asset fetch
6868
// But map SPA routes to index.html
69-
7069
if (pathname === '/admin.html' || pathname === '/admin') {
71-
return await this.handleAdminPage(this.handlePathMapping(request, url), env, JWT_TOKEN);
70+
return await this.handleAdminPage(mappedRequest, env, JWT_TOKEN);
7271
}
7372

7473
if (pathname === '/mailbox.html' || pathname === '/html/mailbox.html') {
@@ -169,7 +168,11 @@ export class AssetManager {
169168
handlePathMapping(request, url) {
170169
let targetUrl = url.toString();
171170

172-
// Mapping logic for specific legacy/static references
171+
const spaRoutes = ['/login', '/dashboard', '/mailbox', '/compose', '/sent', '/settings'];
172+
if (spaRoutes.includes(url.pathname)) {
173+
// SPA route: serve index.html
174+
targetUrl = new URL('/index.html', url).toString();
175+
}
173176
if (url.pathname === '/admin') {
174177
targetUrl = new URL('/html/admin.html', url).toString();
175178
}

0 commit comments

Comments
 (0)