Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { WardenForgotPassword } from './pages/auth/WardenForgotPassword';
import { CentreHeadForgotPassword } from './pages/auth/CentreHeadForgotPassword';
import { AccountResetPass } from './pages/auth/AccountResetPass';
import { StaffLogin } from './pages/auth/StaffLogin';
import { AdminAccess } from './pages/auth/AdminAccess';
import { VerifyAccount } from './pages/auth/VerifyAccount';
import { FacultyPost } from './pages/post/FacultyPost';
import { WardenPost } from './pages/post/WardenPost';
Expand Down Expand Up @@ -42,6 +43,7 @@ function App() {
<Route path="/centre-head/forgot-password" element={<CentreHeadForgotPassword />} />
<Route path="/account/reset-password" element={<AccountResetPass />} />
<Route path="/staff/login" element={<GuestRoute><StaffLogin /></GuestRoute>} />
<Route path="/admin/access" element={<GuestRoute><AdminAccess /></GuestRoute>} />
<Route path="/faculty/posts" element={<FacultyPost />} />
<Route path="/warden/posts" element={<WardenPost />} />
<Route path="/centre-head/posts" element={<CentreHeadPost />} />
Expand Down
158 changes: 87 additions & 71 deletions app/src/components/layout/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@ import { useState } from 'react';
import { ChevronDown, Menu, X, User, LogOut } from 'lucide-react';
import { Link } from 'react-router-dom';
import { useAuth } from '../../context/auth-context';
import { adminDashboardFor } from '../../constants/adminDashboard';

export function Navbar() {
const [mobileOpen, setMobileOpen] = useState(false);
const [lodgeOpen, setLodgeOpen] = useState(false);
const [adminOpen, setAdminOpen] = useState(false);
const [loginDropdownOpen, setLoginDropdownOpen] = useState(false);
const { status } = useAuth();
const { status, profile } = useAuth();
const isAuth = status === 'loading' ? null : status === 'authenticated';

// admins have no /profile page — their "Profile" entry points at the dashboard
const isAdmin = Boolean(profile?.position);
const profileHref = isAdmin ? adminDashboardFor(profile!.position!) : '/profile';
const profileLabel = isAdmin ? 'Dashboard' : 'Profile';

const closeMobile = () => {
setMobileOpen(false);
setLodgeOpen(false);
Expand All @@ -36,37 +42,42 @@ export function Navbar() {
</Link>
</li>

<li className="relative group">
<button className="flex items-center gap-1 px-4 py-3 hover:bg-white/10 transition-colors w-full">
Lodge Complaint <ChevronDown className="w-3.5 h-3.5 opacity-60" />
</button>
<div className="absolute top-full left-0 hidden group-hover:block bg-white text-[#111111] shadow-lg border border-[#E5E5E5] rounded-b-lg min-w-[180px] z-50">
<Link to="/faculty/signup" className="block px-4 py-2.5 text-sm hover:bg-[#F5F5F5] transition-colors">Employee</Link>
<Link to="/warden/signup" className="block px-4 py-2.5 text-sm hover:bg-[#F5F5F5] transition-colors">Warden</Link>
<Link to="/centre-head/signup" className="block px-4 py-2.5 text-sm hover:bg-[#F5F5F5] transition-colors">Centre Head</Link>
</div>
</li>
{/* complaint-side items are for users, not admin staff */}
{!isAdmin && (
<>
<li className="relative group">
<button className="flex items-center gap-1 px-4 py-3 hover:bg-white/10 transition-colors w-full">
Lodge Complaint <ChevronDown className="w-3.5 h-3.5 opacity-60" />
</button>
<div className="absolute top-full left-0 hidden group-hover:block bg-white text-[#111111] shadow-lg border border-[#E5E5E5] rounded-b-lg min-w-[180px] z-50">
<Link to="/faculty/signup" className="block px-4 py-2.5 text-sm hover:bg-[#F5F5F5] transition-colors">Employee</Link>
<Link to="/warden/signup" className="block px-4 py-2.5 text-sm hover:bg-[#F5F5F5] transition-colors">Warden</Link>
<Link to="/centre-head/signup" className="block px-4 py-2.5 text-sm hover:bg-[#F5F5F5] transition-colors">Centre Head</Link>
</div>
</li>

<li>
<button className="block px-4 py-3 hover:bg-white/10 transition-colors">
Track Status
</button>
</li>
<li>
<button className="block px-4 py-3 hover:bg-white/10 transition-colors">
Track Status
</button>
</li>

<li className="relative group">
<button className="flex items-center gap-1 px-4 py-3 hover:bg-white/10 transition-colors">
Administration <ChevronDown className="w-3.5 h-3.5 opacity-60" />
</button>
<div className="absolute top-full left-0 hidden group-hover:block bg-white text-[#111111] shadow-lg border border-[#E5E5E5] rounded-b-lg min-w-[180px] z-50">
<Link to="/staff/login" className="block px-4 py-2.5 text-sm hover:bg-[#F5F5F5] transition-colors">Staff Login</Link>
</div>
</li>
<li className="relative group">
<button className="flex items-center gap-1 px-4 py-3 hover:bg-white/10 transition-colors">
Administration <ChevronDown className="w-3.5 h-3.5 opacity-60" />
</button>
<div className="absolute top-full left-0 hidden group-hover:block bg-white text-[#111111] shadow-lg border border-[#E5E5E5] rounded-b-lg min-w-[180px] z-50">
<Link to="/staff/login" className="block px-4 py-2.5 text-sm hover:bg-[#F5F5F5] transition-colors">Staff Login</Link>
</div>
</li>

<li>
<button className="block px-4 py-3 hover:bg-white/10 transition-colors">
Guidelines
</button>
</li>
<li>
<button className="block px-4 py-3 hover:bg-white/10 transition-colors">
Guidelines
</button>
</li>
</>
)}

<li>
<a href="mailto:admin.cccms@nith.ac.in" className="block px-4 py-3 hover:bg-white/10 transition-colors">
Expand All @@ -86,9 +97,9 @@ export function Navbar() {
<LogOut className="w-4 h-4" />
<span>Logout</span>
</button>
<Link to="/profile" className="flex items-center gap-2 px-4 py-3 hover:bg-white/10 transition-colors text-white">
<Link to={profileHref} className="flex items-center gap-2 px-4 py-3 hover:bg-white/10 transition-colors text-white">
<User className="w-4 h-4" />
<span>Profile</span>
<span>{profileLabel}</span>
</Link>
</>
) : (
Expand Down Expand Up @@ -123,9 +134,9 @@ export function Navbar() {
<LogOut className="w-3.5 h-3.5" />
Logout
</button>
<Link to="/profile" className="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-semibold bg-white/10 text-white hover:bg-white/20 transition-colors">
<Link to={profileHref} className="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-semibold bg-white/10 text-white hover:bg-white/20 transition-colors">
<User className="w-3.5 h-3.5" />
Profile
{profileLabel}
</Link>
</>
) : (
Expand Down Expand Up @@ -162,9 +173,9 @@ export function Navbar() {
<LogOut className="w-4 h-4" />
<span>Logout</span>
</button>
<Link to="/profile" onClick={closeMobile} className="flex items-center gap-2 px-5 py-3 hover:bg-white/10 transition-colors text-[#16a34a] font-semibold">
<Link to={profileHref} onClick={closeMobile} className="flex items-center gap-2 px-5 py-3 hover:bg-white/10 transition-colors text-[#16a34a] font-semibold">
<User className="w-4 h-4" />
<span>Profile</span>
<span>{profileLabel}</span>
</Link>
</div>
) : (
Expand All @@ -190,47 +201,52 @@ export function Navbar() {
</div>
)}

{/* Lodge Complaint accordion */}
<div className="border-b border-white/5">
<button
onClick={() => setLodgeOpen(prev => !prev)}
className="flex items-center justify-between w-full px-5 py-3 hover:bg-white/10 transition-colors"
>
<span>Lodge Complaint</span>
<ChevronDown className={`w-4 h-4 opacity-60 transition-transform ${lodgeOpen ? 'rotate-180' : ''}`} />
</button>
{lodgeOpen && (
<div className="bg-white/5">
<Link to="/faculty/signup" onClick={closeMobile} className="block px-8 py-2.5 hover:bg-white/10 transition-colors">Employee</Link>
<Link to="/warden/signup" onClick={closeMobile} className="block px-8 py-2.5 hover:bg-white/10 transition-colors">Warden</Link>
<Link to="/centre-head/signup" onClick={closeMobile} className="block px-8 py-2.5 hover:bg-white/10 transition-colors">Centre Head</Link>
{/* complaint-side items are for users, not admin staff */}
{!isAdmin && (
<>
{/* Lodge Complaint accordion */}
<div className="border-b border-white/5">
<button
onClick={() => setLodgeOpen(prev => !prev)}
className="flex items-center justify-between w-full px-5 py-3 hover:bg-white/10 transition-colors"
>
<span>Lodge Complaint</span>
<ChevronDown className={`w-4 h-4 opacity-60 transition-transform ${lodgeOpen ? 'rotate-180' : ''}`} />
</button>
{lodgeOpen && (
<div className="bg-white/5">
<Link to="/faculty/signup" onClick={closeMobile} className="block px-8 py-2.5 hover:bg-white/10 transition-colors">Employee</Link>
<Link to="/warden/signup" onClick={closeMobile} className="block px-8 py-2.5 hover:bg-white/10 transition-colors">Warden</Link>
<Link to="/centre-head/signup" onClick={closeMobile} className="block px-8 py-2.5 hover:bg-white/10 transition-colors">Centre Head</Link>
</div>
)}
</div>
)}
</div>

<button className="block w-full text-left px-5 py-3 hover:bg-white/10 transition-colors border-b border-white/5">
Track Status
</button>
<button className="block w-full text-left px-5 py-3 hover:bg-white/10 transition-colors border-b border-white/5">
Track Status
</button>

{/* Administration accordion */}
<div className="border-b border-white/5">
<button
onClick={() => setAdminOpen(prev => !prev)}
className="flex items-center justify-between w-full px-5 py-3 hover:bg-white/10 transition-colors"
>
<span>Administration</span>
<ChevronDown className={`w-4 h-4 opacity-60 transition-transform ${adminOpen ? 'rotate-180' : ''}`} />
</button>
{adminOpen && (
<div className="bg-white/5">
<Link to="/staff/login" onClick={closeMobile} className="block px-8 py-2.5 hover:bg-white/10 transition-colors">Staff Login</Link>
{/* Administration accordion */}
<div className="border-b border-white/5">
<button
onClick={() => setAdminOpen(prev => !prev)}
className="flex items-center justify-between w-full px-5 py-3 hover:bg-white/10 transition-colors"
>
<span>Administration</span>
<ChevronDown className={`w-4 h-4 opacity-60 transition-transform ${adminOpen ? 'rotate-180' : ''}`} />
</button>
{adminOpen && (
<div className="bg-white/5">
<Link to="/staff/login" onClick={closeMobile} className="block px-8 py-2.5 hover:bg-white/10 transition-colors">Staff Login</Link>
</div>
)}
</div>
)}
</div>

<button className="block w-full text-left px-5 py-3 hover:bg-white/10 transition-colors border-b border-white/5">
Guidelines
</button>
<button className="block w-full text-left px-5 py-3 hover:bg-white/10 transition-colors border-b border-white/5">
Guidelines
</button>
</>
)}

<a href="mailto:admin.cccms@nith.ac.in" className="block w-full text-left px-5 py-3 hover:bg-white/10 transition-colors">
Contact Us
Expand Down
8 changes: 8 additions & 0 deletions app/src/constants/adminDashboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// adminDashboardFor maps an admin position (XEN_* / AE_* / JE_*) to its
// dashboard route; '/' means the position is unknown.
export function adminDashboardFor(position: string): string {
if (position.startsWith('XEN')) return '/admin/xen';
if (position.startsWith('AE')) return '/admin/ae';
if (position.startsWith('JE')) return '/admin/je';
return '/';
}
2 changes: 2 additions & 0 deletions app/src/context/auth-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export interface ProfileData {
type?: string;
hostel?: string;
building?: string;
/** Only present for admin (XEN / AE / JE) sessions. */
position?: string;
}

export type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated' | 'rate-limited' | 'error';
Expand Down
114 changes: 114 additions & 0 deletions app/src/pages/auth/AdminAccess.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { useState } from 'react';
import { Link, useSearchParams, useNavigate } from 'react-router-dom';
import { MainLayout } from '../../components/layout/MainLayout';
import { Loader } from '../../components/Loader';
import { useAuth } from '../../context/auth-context';
import { adminDashboardFor } from '../../constants/adminDashboard';

type AccessStatus = 'idle' | 'loading' | 'error' | 'no-token';

// AdminAccess is the page the emailed login link lands on. The explicit
// button keeps automated email link scanners from triggering the login;
// clicking it exchanges the link token for a session cookie, then redirects
// to the admin's dashboard.
export function AdminAccess() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const { refetch } = useAuth();
const token = searchParams.get('token');

const [status, setStatus] = useState<AccessStatus>(token ? 'idle' : 'no-token');
const [message, setMessage] = useState(token ? '' : 'No login token found in the link.');

const handleLogin = async () => {
if (!token) return;
setStatus('loading');
setMessage('');

try {
const response = await fetch(`/api/auth/admin/access?token=${encodeURIComponent(token)}`, {
method: 'GET',
credentials: 'include',
});

const data = await response.json();

if (response.ok) {
const dest = adminDashboardFor(data.position ?? '');
if (dest === '/') {
setStatus('error');
setMessage(`Unknown position "${data.position}" — contact admin.`);
} else {
refetch();
navigate(dest, { replace: true });
}
} else {
setStatus('error');
setMessage(data.error || 'Login failed. The link may be expired or invalid.');
}
} catch {
setStatus('error');
setMessage('Failed to connect to the server. Please try again.');
}
};

return (
<MainLayout>
<div
className="flex-1 flex flex-col items-center justify-center px-4 py-16 min-h-[60vh]"
style={{
backgroundImage: `linear-gradient(rgba(17,17,17,0.06) 1px, transparent 1px), linear-gradient(90deg, rgba(17,17,17,0.06) 1px, transparent 1px)`,
backgroundSize: '32px 32px',
}}
>

{/* Idle / Loading — ready to complete login */}
{(status === 'idle' || status === 'loading') && (
<div className="flex flex-col items-center gap-5 text-center max-w-sm">
<div className="w-20 h-20 rounded-full bg-emerald-50 border-2 border-emerald-200 flex items-center justify-center">
<svg className="w-10 h-10 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
</div>
<div>
<h2 className="text-2xl font-extrabold text-gray-800">Staff Portal Login</h2>
<p className="text-gray-500 mt-1 text-sm">
Continue to log in to your staff account.
</p>
</div>
<button
onClick={handleLogin}
disabled={status === 'loading'}
className={`inline-flex items-center gap-2 bg-[#16a34a] hover:bg-[#15803d] text-white font-bold py-2.5 px-8 rounded-lg transition-colors text-sm active:scale-[0.98] ${status === 'loading' ? 'opacity-70 cursor-not-allowed' : 'cursor-pointer'}`}
>
{status === 'loading' && <Loader size="sm" color="white" />}
{status === 'loading' ? 'Logging in…' : 'Log in to Portal'}
</button>
</div>
)}

{/* Error / No token */}
{(status === 'error' || status === 'no-token') && (
<div className="flex flex-col items-center gap-5 text-center max-w-sm">
<div className="w-20 h-20 rounded-full bg-rose-50 border-2 border-rose-200 flex items-center justify-center">
<svg className="w-10 h-10 text-rose-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<h2 className="text-2xl font-extrabold text-gray-800">Login Failed</h2>
<p className="text-gray-500 mt-1 text-sm">{message}</p>
</div>
<Link
to="/staff/login"
className="bg-[#222222] hover:bg-[#111111] text-white font-bold py-2.5 px-8 rounded-lg transition-colors text-sm"
>
Request a New Link
</Link>
</div>
)}

</div>
</MainLayout>
);
}
Loading
Loading