feat: implement authentication flow with login page, middleware protection, and session-based role management

This commit is contained in:
sazzadulalambd
2026-05-07 16:08:18 +06:00
parent 1ec8882ab7
commit 9687a71570
10 changed files with 629 additions and 77 deletions

View File

@@ -1,8 +1,8 @@
import type { Metadata, Viewport } from "next"; import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google"; import { Inter } from "next/font/google";
import "./globals.css"; import "./globals.css";
import Sidebar from "@/components/Sidebar";
import { Toaster } from "react-hot-toast"; import { Toaster } from "react-hot-toast";
import LayoutContent from "@/components/LayoutContent";
const inter = Inter({ const inter = Inter({
variable: "--font-inter", variable: "--font-inter",
@@ -36,10 +36,7 @@ export default function RootLayout({
return ( return (
<html lang="en" className={inter.variable} suppressHydrationWarning> <html lang="en" className={inter.variable} suppressHydrationWarning>
<body className="min-h-screen bg-slate-50 antialiased" suppressHydrationWarning> <body className="min-h-screen bg-slate-50 antialiased" suppressHydrationWarning>
<Sidebar /> <LayoutContent>{children}</LayoutContent>
<main className="lg:ml-64 min-h-screen pb-20 lg:pb-0">
{children}
</main>
<Toaster position="top-right" /> <Toaster position="top-right" />
</body> </body>
</html> </html>

29
src/app/login/layout.tsx Normal file
View File

@@ -0,0 +1,29 @@
import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google";
import "../globals.css";
const inter = Inter({
variable: "--font-inter",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "JAIBEN Mobility - Login",
description: "Login to JAIBEN Mobility EV Rental Platform",
};
export const viewport: Viewport = {
themeColor: "#ffffff",
width: "device-width",
initialScale: 1,
maximumScale: 1,
userScalable: false,
};
export default function LoginLayout({ children }: { children: React.ReactNode }) {
return (
<section className="min-h-screen bg-slate-50 antialiased flex items-center justify-center">
{children}
</section>
);
}

227
src/app/login/page.tsx Normal file
View File

@@ -0,0 +1,227 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { users } from '@/data/mockData';
import { Zap, ArrowRight, Bike, Wallet, Shield, Users, Calculator, Store, Truck } from 'lucide-react';
const demoUsers = [
{ email: 'superadmin@jaiben.com', role: 'admin', label: 'Super Admin', icon: Shield, color: 'bg-accent' },
{ email: 'admin@jaiben.com', role: 'manager', label: 'Admin Manager', icon: Users, color: 'bg-blue-500' },
{ email: 'staff@jaiben.com', role: 'staff', label: 'Front Desk', icon: Users, color: 'bg-purple-500' },
{ email: 'accountant@jaiben.com', role: 'accountant', label: 'Accountant', icon: Calculator, color: 'bg-green-500' },
{ email: 'investor@email.com', role: 'investor', label: 'Investor', icon: Wallet, color: 'bg-amber-500' },
];
export default function LoginPage() {
const router = useRouter();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
await new Promise(resolve => setTimeout(resolve, 800));
const user = users.find(u => u.email === email);
if (user && password === 'demo123') {
sessionStorage.setItem('authToken', 'demo-token');
sessionStorage.setItem('userRole', user.role);
sessionStorage.setItem('userName', user.name);
switch (user.role) {
case 'admin':
case 'manager':
case 'staff':
router.push('/admin');
break;
case 'accountant':
router.push('/admin');
break;
case 'investor':
router.push('/investor');
break;
case 'biker':
router.push('/biker');
break;
case 'swap-station':
router.push('/swapstation');
break;
case 'merchant':
router.push('/merchant');
break;
default:
router.push('/');
}
} else {
setError('Invalid email or password. Try demo123');
}
setLoading(false);
};
const handleQuickLogin = async (userEmail: string) => {
setLoading(true);
await new Promise(resolve => setTimeout(resolve, 500));
const user = users.find(u => u.email === userEmail);
if (user) {
sessionStorage.setItem('authToken', 'demo-token');
sessionStorage.setItem('userRole', user.role);
sessionStorage.setItem('userName', user.name);
switch (user.role) {
case 'admin':
case 'manager':
case 'staff':
router.push('/admin');
break;
case 'accountant':
router.push('/admin');
break;
case 'investor':
router.push('/investor');
break;
case 'biker':
router.push('/biker');
break;
case 'swap-station':
router.push('/swapstation');
break;
case 'merchant':
router.push('/merchant');
break;
default:
router.push('/');
}
}
setLoading(false);
};
return (
<div className="w-full min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 flex items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<button
onClick={() => router.push('/')}
className="inline-block"
>
<h1 className="text-3xl font-extrabold text-white">JAIBEN</h1>
<p className="text-xs text-slate-400">Mobility Ltd</p>
</button>
</div>
<div className="bg-slate-800/50 border border-slate-700 rounded-2xl p-6 lg:p-8 backdrop-blur">
<div className="text-center mb-6">
<h2 className="text-xl font-bold text-white mb-1">Welcome Back</h2>
<p className="text-slate-400 text-sm">Sign in to continue to your dashboard</p>
</div>
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium text-slate-300 mb-2">
Email Address
</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full px-4 py-3 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-accent focus:border-accent transition-all"
placeholder="Enter your email"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-slate-300 mb-2">
Password
</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="w-full px-4 py-3 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-accent focus:border-accent transition-all"
placeholder="Enter your password"
/>
</div>
{error && (
<div className="bg-red-500/10 border border-red-500/30 text-red-400 px-4 py-3 rounded-lg text-sm">
{error}
</div>
)}
<button
type="submit"
disabled={loading}
className="w-full py-3 bg-accent text-white rounded-lg font-semibold hover:bg-accent-dark transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{loading ? (
<>
<svg className="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path>
</svg>
Signing in...
</>
) : (
<>
Sign In <ArrowRight className="w-5 h-5" />
</>
)}
</button>
</form>
<div className="mt-6 pt-6 border-t border-slate-700">
<p className="text-slate-400 text-sm mb-4 text-center">Quick Login as Demo User:</p>
<div className="grid grid-cols-1 gap-2">
{demoUsers.map((user) => {
const Icon = user.icon;
return (
<button
key={user.email}
onClick={() => handleQuickLogin(user.email)}
disabled={loading}
className="flex items-center gap-3 px-4 py-2.5 bg-slate-700/30 border border-slate-600 rounded-lg hover:border-accent hover:bg-slate-700/50 transition-all text-left"
>
<div className={`w-8 h-8 ${user.color} rounded-lg flex items-center justify-center`}>
<Icon className="w-4 h-4 text-white" />
</div>
<div className="flex-1">
<p className="text-white font-medium text-sm">{user.label}</p>
<p className="text-slate-400 text-xs">{user.email}</p>
</div>
<ArrowRight className="w-4 h-4 text-slate-500" />
</button>
);
})}
</div>
</div>
</div>
<div className="mt-6 text-center">
<button
onClick={() => router.push('/')}
className="text-slate-400 hover:text-white text-sm transition-colors flex items-center justify-center gap-2"
>
<ArrowRight className="w-4 h-4 rotate-180" /> Back to Home
</button>
</div>
<div className="mt-6 text-center">
<p className="text-slate-500 text-xs">
© 2024 JAIBEN Mobility Ltd. All rights reserved.
</p>
</div>
</div>
</div>
);
}

View File

@@ -1,80 +1,182 @@
import Image from 'next/image'; 'use client';
import StatCard from '@/components/StatCard';
import BikeCard from '@/components/BikeCard';
import TransactionList from '@/components/TransactionList';
import { bikes, rentals, transactions, stats, users } from '@/data/mockData';
import { Bike, Wallet, Footprints, Clock, Zap, RotateCcw, CreditCard, FileText, Share2 } from 'lucide-react';
export default function Home() { import { useState } from 'react';
const activeRental = rentals.find(r => r.status === 'active'); import { useRouter } from 'next/navigation';
const currentBike = bikes.find(b => b.id === activeRental?.bikeId); import { users } from '@/data/mockData';
const currentUser = users[0]; import { Zap, ArrowRight, Bike, Wallet } from 'lucide-react';
export default function LandingPage() {
const router = useRouter();
const [loading, setLoading] = useState(false);
const handleLogin = async (email: string) => {
setLoading(true);
await new Promise(resolve => setTimeout(resolve, 500));
const user = users.find(u => u.email === email);
if (user) {
sessionStorage.setItem('authToken', 'demo-token');
sessionStorage.setItem('userRole', user.role);
sessionStorage.setItem('userName', user.name);
switch (user.role) {
case 'admin':
case 'manager':
case 'staff':
router.push('/admin');
break;
case 'accountant':
router.push('/admin');
break;
case 'investor':
router.push('/investor');
break;
case 'biker':
router.push('/biker');
break;
case 'swap-station':
router.push('/swapstation');
break;
case 'merchant':
router.push('/merchant');
break;
default:
router.push('/login');
}
}
setLoading(false);
};
return ( return (
<div className="p-4 lg:p-6"> <div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900">
<div className="mb-6"> <header className="p-4 lg:p-6">
<div className="flex items-center gap-4">
<div className="w-14 h-14 rounded-full bg-biker-light flex items-center justify-center text-2xl font-bold text-biker">
{currentUser.name.charAt(0)}
</div>
<div>
<h1 className="text-xl lg:text-2xl font-extrabold text-slate-800">Welcome back, {currentUser.name.split(' ')[0]}!</h1>
<p className="text-sm text-slate-500">Here's your rental status</p>
</div>
</div>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<StatCard label="Active Rental" value={stats.biker.activeRental} icon={Bike} color="text-biker" />
<StatCard label="Wallet Balance" value={`৳${stats.biker.walletBalance}`} icon={Wallet} color="text-green-600" />
<StatCard label="Total Rides" value={stats.biker.totalRides} icon={Footprints} color="text-purple-600" />
<StatCard label="Days Remaining" value={stats.biker.daysRemaining} icon={Clock} color="text-amber-600" />
</div>
{currentBike && (
<div className="mb-6">
<h2 className="text-lg font-bold text-slate-800 mb-3">Current Bike</h2>
<BikeCard bike={currentBike} />
</div>
)}
<div className="grid lg:grid-cols-2 gap-6 mb-6">
<div>
<h2 className="text-lg font-bold text-slate-800 mb-3">Quick Actions</h2>
<div className="grid grid-cols-2 gap-3">
<button className="py-3 bg-accent text-white rounded-lg font-semibold hover:bg-accent-dark transition-colors flex items-center justify-center gap-2">
<Zap className="w-4 h-4" /> Rent Bike
</button>
<button className="py-3 bg-biker text-white rounded-lg font-semibold hover:bg-biker-dark transition-colors flex items-center justify-center gap-2">
<RotateCcw className="w-4 h-4" /> Extend Rental
</button>
<button className="py-3 bg-green-600 text-white rounded-lg font-semibold hover:bg-green-700 transition-colors flex items-center justify-center gap-2">
<CreditCard className="w-4 h-4" /> Top Up
</button>
<button className="py-3 border-2 border-slate-200 text-slate-600 rounded-lg font-semibold hover:bg-slate-50 transition-colors flex items-center justify-center gap-2">
<FileText className="w-4 h-4" /> End Rental
</button>
</div>
</div>
<div>
<h2 className="text-lg font-bold text-slate-800 mb-3">Recent Transactions</h2>
<TransactionList transactions={transactions.filter(t => t.userId === 'u1')} compact />
</div>
</div>
<div className="bg-gradient-to-r from-accent to-accent-dark rounded-xl p-6 text-white">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm opacity-80">Refer a friend</p> <h1 className="text-2xl lg:text-3xl font-extrabold text-white">JAIBEN</h1>
<h3 className="text-xl font-bold mt-1">Earn 500 Credit</h3> <p className="text-xs text-slate-400">Mobility Ltd</p>
<p className="text-xs opacity-80 mt-2">Share your referral link and earn free rides</p>
</div> </div>
<button className="px-4 py-2 bg-white text-accent rounded-lg font-semibold text-sm flex items-center gap-2"> <button
<Share2 className="w-4 h-4" /> Share Link onClick={() => router.push('/login')}
className="px-4 py-2 bg-accent text-white rounded-lg font-semibold hover:bg-accent-dark transition-colors flex items-center gap-2"
>
Sign In <ArrowRight className="w-4 h-4" />
</button> </button>
</div> </div>
</div> </header>
<main className="p-4 lg:p-6">
<div className="max-w-4xl mx-auto">
<div className="text-center mb-8 lg:mb-12">
<div className="inline-flex items-center gap-2 px-4 py-1.5 bg-accent/20 rounded-full text-accent text-sm font-medium mb-4">
<Zap className="w-4 h-4" /> Bangladesh&apos;s Leading EV Platform
</div>
<h2 className="text-3xl lg:text-5xl font-extrabold text-white mb-4 leading-tight">
Electric Vehicle
<br className="lg:hidden" /> Rental
<span className="text-accent"> Made Simple</span>
</h2>
<p className="text-slate-400 text-lg lg:text-xl max-w-2xl mx-auto">
Rent, Rent-to-Own, or Invest in EVs. Join Bangladesh&apos;s fastest growing
electric mobility ecosystem with FOCO model for investors.
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 mb-8">
<button
onClick={() => handleLogin('rahim@email.com')}
disabled={loading}
className="p-6 bg-slate-800/50 border border-slate-700 rounded-2xl hover:border-biker hover:bg-biker/10 transition-all group text-left"
>
<div className="w-12 h-12 bg-biker/20 rounded-xl flex items-center justify-center mb-4">
<Bike className="w-6 h-6 text-biker" />
</div>
<h3 className="text-xl font-bold text-white mb-1">Biker</h3>
<p className="text-slate-400 text-sm mb-3">Rent electric bikes for daily commute</p>
<span className="text-biker text-sm font-medium flex items-center gap-1">
Login as Biker <ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</span>
</button>
<button
onClick={() => handleLogin('investor@email.com')}
disabled={loading}
className="p-6 bg-slate-800/50 border border-slate-700 rounded-2xl hover:border-green-500 hover:bg-green-500/10 transition-all group text-left"
>
<div className="w-12 h-12 bg-green-500/20 rounded-xl flex items-center justify-center mb-4">
<Wallet className="w-6 h-6 text-green-500" />
</div>
<h3 className="text-xl font-bold text-white mb-1">Investor</h3>
<p className="text-slate-400 text-sm mb-3">FOCO model with guaranteed returns</p>
<span className="text-green-500 text-sm font-medium flex items-center gap-1">
Login as Investor <ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</span>
</button>
<button
onClick={() => handleLogin('swap@jaiben.com')}
disabled={loading}
className="p-6 bg-slate-800/50 border border-slate-700 rounded-2xl hover:border-purple-500 hover:bg-purple-500/10 transition-all group text-left"
>
<div className="w-12 h-12 bg-purple-500/20 rounded-xl flex items-center justify-center mb-4">
<Zap className="w-6 h-6 text-purple-500" />
</div>
<h3 className="text-xl font-bold text-white mb-1">Swap Station</h3>
<p className="text-slate-400 text-sm mb-3">Battery swap and charging services</p>
<span className="text-purple-500 text-sm font-medium flex items-center gap-1">
Login as Swap Station <ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</span>
</button>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<div className="text-center p-4">
<div className="text-2xl lg:text-3xl font-bold text-accent mb-1">500+</div>
<div className="text-slate-400 text-sm">Active Bikers</div>
</div>
<div className="text-center p-4">
<div className="text-2xl lg:text-3xl font-bold text-accent mb-1">200+</div>
<div className="text-slate-400 text-sm">EV Fleet</div>
</div>
<div className="text-center p-4">
<div className="text-2xl lg:text-3xl font-bold text-accent mb-1">50+</div>
<div className="text-slate-400 text-sm">Investors</div>
</div>
<div className="text-center p-4">
<div className="text-2xl lg:text-3xl font-bold text-accent mb-1">18%</div>
<div className="text-slate-400 text-sm">Avg. ROI</div>
</div>
</div>
<div className="bg-gradient-to-r from-accent to-accent-dark rounded-2xl p-6 lg:p-8 text-white">
<div className="grid lg:grid-cols-2 gap-6 items-center">
<div>
<h3 className="text-xl lg:text-2xl font-bold mb-2">Ready to join the EV revolution?</h3>
<p className="text-white/80 mb-4">Start renting or investing today and be part of Bangladesh&apos;s sustainable future.</p>
</div>
<div className="flex flex-col gap-3">
<button
onClick={() => router.push('/login')}
className="w-full py-3 bg-white text-accent rounded-lg font-semibold hover:bg-slate-100 transition-colors"
>
Get Started
</button>
<button
onClick={() => router.push('/login')}
className="w-full py-3 border-2 border-white text-white rounded-lg font-semibold hover:bg-white/10 transition-colors"
>
Learn More
</button>
</div>
</div>
</div>
</div>
</main>
<footer className="p-4 lg:p-6 text-center">
<p className="text-slate-500 text-sm">
&copy; 2024 JAIBEN Mobility Ltd. All rights reserved.
</p>
</footer>
</div> </div>
); );
} }

View File

@@ -0,0 +1,21 @@
"use client";
import { usePathname } from "next/navigation";
import Sidebar from "@/components/Sidebar";
interface LayoutContentProps {
children: React.ReactNode;
}
export default function LayoutContent({ children }: LayoutContentProps) {
const pathname = usePathname();
const showSidebar = pathname !== "/" && pathname !== "/login";
return (
<>
{showSidebar && <Sidebar />}
<main className={showSidebar ? "lg:ml-64 min-h-screen pb-20 lg:pb-0" : "min-h-screen pb-20 lg:pb-0"}>
{children}
</main>
</>
);
}

View File

@@ -154,7 +154,20 @@ export default function Sidebar() {
<p className="text-sm font-medium text-slate-700 truncate">Admin User</p> <p className="text-sm font-medium text-slate-700 truncate">Admin User</p>
<p className="text-xs text-slate-400">admin@jaiben.com</p> <p className="text-xs text-slate-400">admin@jaiben.com</p>
</div> </div>
<button className="p-1.5 hover:bg-slate-100 rounded-lg"> <button
onClick={() => {
// Import and call logout function
// Note: We can't use client-only imports in server components directly
// For now, we'll just clear sessionStorage and redirect
if (typeof window !== 'undefined') {
window.sessionStorage.removeItem('authToken');
window.sessionStorage.removeItem('userRole');
window.sessionStorage.removeItem('userName');
window.location.href = '/login';
}
}}
className="p-1.5 hover:bg-slate-100 rounded-lg"
>
<LogOut className="w-4 h-4 text-slate-400" /> <LogOut className="w-4 h-4 text-slate-400" />
</button> </button>
</Link> </Link>

View File

@@ -3,7 +3,7 @@ export interface User {
name: string; name: string;
email: string; email: string;
phone: string; phone: string;
role: 'biker' | 'admin' | 'manager' | 'staff' | 'accountant' | 'investor' | 'shop' | 'merchant'; role: 'biker' | 'admin' | 'manager' | 'staff' | 'accountant' | 'investor' | 'swap-station' | 'merchant';
avatar?: string; avatar?: string;
status: 'active' | 'pending' | 'inactive'; status: 'active' | 'pending' | 'inactive';
createdAt: string; createdAt: string;
@@ -164,7 +164,7 @@ export const users: User[] = [
{ id: 'u5', name: 'Staff User', email: 'staff@jaiben.com', phone: '01710000003', role: 'staff', status: 'active', createdAt: '2023-07-01' }, { id: 'u5', name: 'Staff User', email: 'staff@jaiben.com', phone: '01710000003', role: 'staff', status: 'active', createdAt: '2023-07-01' },
{ id: 'u6', name: 'Accountant User', email: 'accountant@jaiben.com', phone: '01710000004', role: 'accountant', status: 'active', createdAt: '2023-07-01' }, { id: 'u6', name: 'Accountant User', email: 'accountant@jaiben.com', phone: '01710000004', role: 'accountant', status: 'active', createdAt: '2023-07-01' },
{ id: 'u7', name: 'Investor User', email: 'investor@email.com', phone: '01720000001', role: 'investor', status: 'active', createdAt: '2023-08-01' }, { id: 'u7', name: 'Investor User', email: 'investor@email.com', phone: '01720000001', role: 'investor', status: 'active', createdAt: '2023-08-01' },
{ id: 'u8', name: 'Shop Owner', email: 'shop@email.com', phone: '01730000001', role: 'shop', status: 'active', createdAt: '2023-09-01' }, { id: 'u8', name: 'Swap Station Owner', email: 'swap@jaiben.com', phone: '01730000001', role: 'swap-station', status: 'active', createdAt: '2023-09-01' },
{ id: 'u9', name: 'Merchant User', email: 'merchant@email.com', phone: '01740000001', role: 'merchant', status: 'active', createdAt: '2023-10-01' }, { id: 'u9', name: 'Merchant User', email: 'merchant@email.com', phone: '01740000001', role: 'merchant', status: 'active', createdAt: '2023-10-01' },
]; ];

19
src/lib/auth.ts Normal file
View File

@@ -0,0 +1,19 @@
export const isAuthenticated = (): boolean => {
return typeof window !== 'undefined' && !!sessionStorage.getItem('authToken');
};
export const getUserRole = (): string | null => {
return typeof window !== 'undefined' ? sessionStorage.getItem('userRole') : null;
};
export const getUserName = (): string | null => {
return typeof window !== 'undefined' ? sessionStorage.getItem('userName') : null;
};
export const logout = () => {
if (typeof window !== 'undefined') {
sessionStorage.removeItem('authToken');
sessionStorage.removeItem('userRole');
sessionStorage.removeItem('userName');
}
};

115
src/middleware.ts Normal file
View File

@@ -0,0 +1,115 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { isAuthenticated, getUserRole } from '@/lib/auth';
// Define protected routes and their corresponding roles
const protectedRoutes: { [key: string]: string[] } = {
'/admin': ['admin', 'manager', 'staff', 'accountant'],
'/investor': ['investor'],
'/swapstation': ['swapstation'],
'/merchant': ['merchant'],
'/biker': ['biker'],
};
// Define public routes that don't require authentication
const publicRoutes = ['/login', '/', '/api/', '/_next/'];
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Check if the route is public
const isPublicRoute = publicRoutes.some(route =>
pathname.startsWith(route)
);
if (isPublicRoute) {
return NextResponse.next();
}
// Check if the user is authenticated
const isAuth = isAuthenticated();
// If trying to access login page while authenticated, redirect to appropriate dashboard
if (pathname === '/login' && isAuth) {
const role = getUserRole();
if (!role) {
// If no role found, logout and redirect to login
// In a real app, you'd clear the session and redirect
// For demo, we'll just redirect to login (but this would cause a loop, so we'll go to home)
return NextResponse.redirect(new URL('/', request.url));
}
// Redirect based on role
let redirectUrl = '/'; // default
switch (role) {
case 'admin':
case 'manager':
case 'staff':
redirectUrl = '/admin';
break;
case 'accountant':
redirectUrl = '/admin/accounting';
break;
case 'investor':
redirectUrl = '/investor';
break;
case 'biker':
redirectUrl = '/';
break;
case 'swapstation':
redirectUrl = '/swapstation';
break;
case 'merchant':
redirectUrl = '/merchant';
break;
default:
redirectUrl = '/';
}
return NextResponse.redirect(new URL(redirectUrl, request.url));
}
// If not authenticated and trying to access a protected route, redirect to login
if (!isAuth) {
// Check if the route is protected
const isProtectedRoute = Object.keys(protectedRoutes).some(prefix =>
pathname.startsWith(prefix)
);
if (isProtectedRoute) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
// If authenticated, check if the user has access to the specific route
if (isAuth) {
const role = getUserRole();
if (role) {
// Check if the route is protected and if the user's role is allowed
for (const [prefix, allowedRoles] of Object.entries(protectedRoutes)) {
if (pathname.startsWith(prefix) && !allowedRoles.includes(role)) {
// User is authenticated but doesn't have permission for this route
// Redirect to home or show an error - for demo, we'll redirect to home
return NextResponse.redirect(new URL('/', request.url));
}
}
}
}
return NextResponse.next();
}
// Configure middleware to run on specific paths
export const config = {
matcher: [
/*
* Match all request paths except:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - public folder
*/
'/((?!_next/static|_next/image|favicon.ico|public).*)',
],
};

29
tailwind.config.js Normal file
View File

@@ -0,0 +1,29 @@
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx}",
"./app/**/*.{js,ts,jsx,tsx}"
],
darkMode: "class", // enable class-based dark mode
theme: {
extend: {
colors: {
// Brand-specific colors used in UI components
biker: "#1e3a8a", // deep blue for Biker theme
swapstation: "#7c3aed", // purple for Shop/Merchant theme
accent: "#6366f1", // primary accent (indigo)
"accent-dark": "#4f46e5", // darker accent for hover states
// Additional subtle shades for UI elements
"bg-dark": "#1f2937",
"bg-darker": "#111827",
"text-muted": "#9ca3af"
},
backgroundImage: {
// optional gradient for hero background
"hero-gradient": "radial-gradient(circle at top left, #1e293b, #111827)"
}
}
},
plugins: []
};