Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
dc44a5e
refactor(role): setting up a superadmin role
ayush00git Sep 11, 2026
ff48149
feat: initialize a superadminhandler and login
ayush00git Sep 11, 2026
6d34579
feat: add a email sending service helper
ayush00git Sep 11, 2026
3f8db37
rename files to superadmin instead of _
ayush00git Sep 11, 2026
7eea844
feat: SuperAdminAccess handler added and routes registered
ayush00git Sep 11, 2026
0bd7010
feat: fetch all user type posts
ayush00git Sep 11, 2026
b35a662
feat(db): registered the SuperAdminRoute to gin.Engine
ayush00git Sep 11, 2026
e520fb4
fix: lookup for email in super_admin table
ayush00git Sep 11, 2026
732a1c5
fix: move Find to last
ayush00git Sep 11, 2026
a6f8447
feat: setup dashboard for superadmin
ayush00git Sep 11, 2026
54eb514
fix: set 25 as page offest default size
ayush00git Sep 11, 2026
2dbe166
fix: return the actual total len of posts[]
ayush00git Sep 11, 2026
5f9663e
feat: register a assign superadmin route
ayush00git Sep 11, 2026
0819b25
fix: set TranslateError to get unique index errors
ayush00git Sep 11, 2026
5d9c578
feat: add assign modal
ayush00git Sep 11, 2026
78c7522
fix: check if email exists alr
ayush00git Sep 11, 2026
ca48781
fix: orchesterate admins login btn
ayush00git Sep 11, 2026
c676d58
fix: send invitation email service herlper added
ayush00git Sep 11, 2026
e7d1498
feat: register a go func to send emails
ayush00git Sep 11, 2026
f74277a
fix: /api/profile route donot register for superadmins
ayush00git Sep 11, 2026
dda1183
fix: set exp to 3 days
ayush00git Sep 11, 2026
d74e3ce
fix: use passed para instead of global
ayush00git Sep 11, 2026
0f5916a
fix: superadmin routes share auth context
ayush00git Sep 11, 2026
e10b6db
fix: set httpOnly f and registered a new /profile case
ayush00git Sep 11, 2026
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
6 changes: 6 additions & 0 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import { XENPostView } from './pages/admin/XENPostView';
import { AEPostView } from './pages/admin/AEPostView';
import { JEPostView } from './pages/admin/JEPostView';
import { AdminPostView } from './pages/admin/AdminPostView';
import { SuperAdminLogin } from './pages/superadmin/SuperAdminLogin';
import { SuperAdminAccess } from './pages/superadmin/SuperAdminAccess';
import { SuperAdminDashboard } from './pages/superadmin/SuperAdminDashboard';
import { GuestRoute } from './components/GuestRoute';
import { NotFound } from './pages/NotFound';
import { AuthProvider } from './context/AuthContext';
Expand Down Expand Up @@ -56,6 +59,9 @@ function App() {
<Route path="/admin/ae" element={<AEPostView />} />
<Route path="/admin/je" element={<JEPostView />} />
<Route path="/admin/posts/:role/:post_id" element={<AdminPostView />} />
<Route path="/superadmin" element={<SuperAdminDashboard />} />
<Route path="/superadmin/login" element={<GuestRoute><SuperAdminLogin /></GuestRoute>} />
<Route path="/superadmin/access" element={<SuperAdminAccess />} />
<Route path="*" element={<NotFound />} />
</Routes>
</AuthProvider>
Expand Down
181 changes: 181 additions & 0 deletions app/src/components/AssignAdminModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import React, { useEffect, useState } from 'react';
import { X, UserPlus, CheckCircle2, AlertCircle } from 'lucide-react';
import { Loader } from './Loader';

interface AssignAdminModalProps {
open: boolean;
onClose: () => void;
/** Called after a successful assignment, with the new admin's details. */
onAssigned?: (admin: { name: string; email: string }) => void;
}

// AssignAdminModal lets a logged-in super admin add another super admin by
// name and email. The new admin logs in through the same magic-link flow.
export function AssignAdminModal({ open, onClose, onAssigned }: AssignAdminModalProps) {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [loading, setLoading] = useState(false);
const [status, setStatus] = useState<'success' | 'error' | null>(null);
const [message, setMessage] = useState('');

// Reset the form each time the modal opens.
useEffect(() => {
if (open) {
setName('');
setEmail('');
setStatus(null);
setMessage('');
setLoading(false);
}
}, [open]);

// Close on Escape.
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [open, onClose]);

if (!open) return null;

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setStatus(null);
setMessage('');
try {
const res = await fetch('/api/superadmin/assign', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name.trim(), email: email.trim() }),
credentials: 'include',
});
const data = await res.json().catch(() => ({}));
if (res.ok) {
setStatus('success');
setMessage(`${name.trim()} can now log in at /superadmin/login with ${email.trim()}.`);
onAssigned?.({ name: name.trim(), email: email.trim() });
} else {
setStatus('error');
setMessage(data.error || `Request failed (${res.status}).`);
}
} catch {
setStatus('error');
setMessage('Failed to connect to the server. Please try again.');
} finally {
setLoading(false);
}
};

const inputCls = 'w-full px-3.5 py-2.5 border border-[#CCCCCC] rounded-lg focus:outline-none focus:border-[#111111] text-sm text-[#111111] placeholder-[#999999] bg-white transition-colors';
const labelCls = 'block text-sm font-semibold text-[#111111] mb-1.5';

return (
<div
className="fixed inset-0 z-50 flex items-center justify-center px-4 bg-black/40 backdrop-blur-[2px]"
onMouseDown={e => { if (e.target === e.currentTarget) onClose(); }}
role="dialog"
aria-modal="true"
aria-labelledby="assign-admin-title"
>
<div className="w-full max-w-md bg-white rounded-2xl shadow-xl border border-gray-200 overflow-hidden">
<div className="flex items-center gap-3 px-5 py-4 border-b border-gray-100">
<span className="w-9 h-9 rounded-full bg-amber-50 border border-amber-200 flex items-center justify-center text-[#ff9900]">
<UserPlus className="w-4 h-4" />
</span>
<div>
<h2 id="assign-admin-title" className="text-sm font-bold text-gray-800">Assign a new super admin</h2>
<p className="text-[11px] text-gray-400">They will get the same read-only oversight access as you.</p>
</div>
<button
onClick={onClose}
className="ml-auto text-gray-400 hover:text-gray-700 rounded-md p-1 transition-colors cursor-pointer"
aria-label="Close"
>
<X className="w-4 h-4" />
</button>
</div>

{status === 'success' ? (
<div className="px-5 py-8 flex flex-col items-center text-center gap-3">
<CheckCircle2 className="w-10 h-10 text-emerald-500" />
<p className="text-sm font-semibold text-gray-800">Super admin assigned</p>
<p className="text-xs text-gray-500">{message}</p>
<div className="flex gap-2 mt-2">
<button
onClick={() => { setStatus(null); setName(''); setEmail(''); setMessage(''); }}
className="text-xs font-semibold px-4 py-2 rounded-lg border border-gray-300 text-gray-700 hover:border-gray-500 transition-colors cursor-pointer"
>
Add another
</button>
<button
onClick={onClose}
className="text-xs font-semibold px-4 py-2 rounded-lg bg-[#222222] hover:bg-[#111111] text-white transition-colors cursor-pointer"
>
Done
</button>
</div>
</div>
) : (
<form onSubmit={handleSubmit} className="px-5 py-5 space-y-4">
{status === 'error' && (
<div className="flex items-start gap-2 bg-[#FCEBEA] border border-[#f5c6c4] text-[#b91c1c] text-xs rounded-lg px-3 py-2.5">
<AlertCircle className="w-4 h-4 shrink-0 mt-px" />
<span className="font-medium">{message}</span>
</div>
)}

<div>
<label className={labelCls} htmlFor="assign-admin-name">Full name</label>
<input
id="assign-admin-name"
type="text"
value={name}
onChange={e => setName(e.target.value)}
className={inputCls}
placeholder="e.g. Dr. A. Sharma"
maxLength={50}
required
autoFocus
/>
</div>

<div>
<label className={labelCls} htmlFor="assign-admin-email">Email address</label>
<input
id="assign-admin-email"
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
className={inputCls}
placeholder="name@nith.ac.in"
maxLength={255}
required
/>
<p className="text-[11px] text-gray-400 mt-1.5">Login links will be mailed to this address.</p>
</div>

<div className="flex items-center justify-end gap-2 pt-2 border-t border-gray-100">
<button
type="button"
onClick={onClose}
className="text-xs font-semibold px-4 py-2 rounded-lg border border-gray-300 text-gray-700 hover:border-gray-500 transition-colors cursor-pointer"
>
Cancel
</button>
<button
type="submit"
disabled={loading}
className={`inline-flex items-center gap-2 text-xs font-semibold px-4 py-2 rounded-lg bg-[#16a34a] hover:bg-[#15803d] text-white transition-colors ${loading ? 'opacity-70 cursor-not-allowed' : 'cursor-pointer'}`}
>
{loading && <Loader size="sm" color="white" />}
{loading ? 'Assigning…' : 'Assign super admin'}
</button>
</div>
</form>
)}
</div>
</div>
);
}
4 changes: 2 additions & 2 deletions app/src/components/GuestRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface GuestRouteProps {
}

export function GuestRoute({ children }: GuestRouteProps) {
const { status } = useAuth();
const { status, profile } = useAuth();

if (status === 'loading') {
return (
Expand All @@ -19,7 +19,7 @@ export function GuestRoute({ children }: GuestRouteProps) {
}

if (status === 'authenticated') {
return <Navigate to="/profile" replace />;
return <Navigate to={profile?.role === 'superadmin' ? '/superadmin' : '/profile'} replace />;
}

return <>{children}</>;
Expand Down
Loading
Loading