Fix blank page on direct navigation to /admin pages

Agent-Logs-Url: https://github.com/christianlouis/pop_puller_to_gmail/sessions/8bc46694-c28c-4041-af60-8c8b72fe512a

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-26 17:28:24 +00:00
parent 3dcb700e68
commit 74fa460660
5 changed files with 350 additions and 322 deletions
+76 -64
View File
@@ -26,85 +26,97 @@ export default function AdminPage() {
enabled: !!user?.is_superuser,
});
if (!user?.is_superuser) return null;
// AuthGuard must always render so it can fetch the current user and handle
// unauthenticated redirects. The early-return that was here prevented
// AuthGuard from ever mounting on a direct navigation to /admin, leaving a
// permanent blank page. The superuser guard is now applied inside the
// layout so that the auth check always runs first.
return (
<AuthGuard>
<DashboardLayout>
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
<Shield className="h-6 w-6 text-purple-600" />
Admin Overview
</h1>
<p className="mt-1 text-sm text-gray-500">
System-wide statistics and management tools.
</p>
{!user?.is_superuser ? (
// Shown briefly while AuthGuard resolves the current user, or while
// the non-superuser redirect in the useEffect at the top of this
// component fires (router.replace('/dashboard')).
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
</div>
{isLoading ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
) : (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
<Shield className="h-6 w-6 text-purple-600" />
Admin Overview
</h1>
<p className="mt-1 text-sm text-gray-500">
System-wide statistics and management tools.
</p>
</div>
) : (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-3">
<div className="bg-white rounded-lg shadow p-6 flex items-center gap-4">
<div className="p-3 rounded-full bg-purple-100">
{isLoading ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
</div>
) : (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-3">
<div className="bg-white rounded-lg shadow p-6 flex items-center gap-4">
<div className="p-3 rounded-full bg-purple-100">
<Users className="h-6 w-6 text-purple-600" />
</div>
<div>
<p className="text-sm font-medium text-gray-500">Total Users</p>
<p className="text-3xl font-semibold text-gray-900">{stats?.total_users ?? '—'}</p>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6 flex items-center gap-4">
<div className="p-3 rounded-full bg-blue-100">
<Mail className="h-6 w-6 text-blue-600" />
</div>
<div>
<p className="text-sm font-medium text-gray-500">Mail Accounts</p>
<p className="text-3xl font-semibold text-gray-900">{stats?.total_mail_accounts ?? '—'}</p>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6 flex items-center gap-4">
<div className="p-3 rounded-full bg-green-100">
<Activity className="h-6 w-6 text-green-600" />
</div>
<div>
<p className="text-sm font-medium text-gray-500">Processing Runs</p>
<p className="text-3xl font-semibold text-gray-900">{stats?.total_processing_runs ?? '—'}</p>
</div>
</div>
</div>
)}
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<Link
href="/admin/users"
className="bg-white rounded-lg shadow p-6 hover:shadow-md transition-shadow flex items-center gap-4 group"
>
<div className="p-3 rounded-full bg-purple-100 group-hover:bg-purple-200 transition-colors">
<Users className="h-6 w-6 text-purple-600" />
</div>
<div>
<p className="text-sm font-medium text-gray-500">Total Users</p>
<p className="text-3xl font-semibold text-gray-900">{stats?.total_users ?? '—'}</p>
<p className="text-lg font-semibold text-gray-900">Manage Users</p>
<p className="text-sm text-gray-500">View, edit, assign plans, promote to admin</p>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6 flex items-center gap-4">
<div className="p-3 rounded-full bg-blue-100">
</Link>
<Link
href="/admin/plans"
className="bg-white rounded-lg shadow p-6 hover:shadow-md transition-shadow flex items-center gap-4 group"
>
<div className="p-3 rounded-full bg-blue-100 group-hover:bg-blue-200 transition-colors">
<Mail className="h-6 w-6 text-blue-600" />
</div>
<div>
<p className="text-sm font-medium text-gray-500">Mail Accounts</p>
<p className="text-3xl font-semibold text-gray-900">{stats?.total_mail_accounts ?? '—'}</p>
<p className="text-lg font-semibold text-gray-900">Manage Plans</p>
<p className="text-sm text-gray-500">Create and configure subscription plans</p>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6 flex items-center gap-4">
<div className="p-3 rounded-full bg-green-100">
<Activity className="h-6 w-6 text-green-600" />
</div>
<div>
<p className="text-sm font-medium text-gray-500">Processing Runs</p>
<p className="text-3xl font-semibold text-gray-900">{stats?.total_processing_runs ?? '—'}</p>
</div>
</div>
</Link>
</div>
)}
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<Link
href="/admin/users"
className="bg-white rounded-lg shadow p-6 hover:shadow-md transition-shadow flex items-center gap-4 group"
>
<div className="p-3 rounded-full bg-purple-100 group-hover:bg-purple-200 transition-colors">
<Users className="h-6 w-6 text-purple-600" />
</div>
<div>
<p className="text-lg font-semibold text-gray-900">Manage Users</p>
<p className="text-sm text-gray-500">View, edit, assign plans, promote to admin</p>
</div>
</Link>
<Link
href="/admin/plans"
className="bg-white rounded-lg shadow p-6 hover:shadow-md transition-shadow flex items-center gap-4 group"
>
<div className="p-3 rounded-full bg-blue-100 group-hover:bg-blue-200 transition-colors">
<Mail className="h-6 w-6 text-blue-600" />
</div>
<div>
<p className="text-lg font-semibold text-gray-900">Manage Plans</p>
<p className="text-sm text-gray-500">Create and configure subscription plans</p>
</div>
</Link>
</div>
</div>
)}
</DashboardLayout>
</AuthGuard>
);
+142 -135
View File
@@ -270,152 +270,159 @@ export default function AdminPlansPage() {
},
});
if (!user?.is_superuser) return null;
// AuthGuard must always render (see admin/page.tsx for explanation).
return (
<AuthGuard>
<DashboardLayout>
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
<Shield className="h-6 w-6 text-purple-600" />
Manage Plans
</h1>
<p className="mt-1 text-sm text-gray-500">
Configure subscription plans, limits, and pricing.
</p>
</div>
<button
onClick={() => setShowCreate(true)}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-md hover:bg-purple-700"
>
<Plus className="h-4 w-4" />
New Plan
</button>
{!user?.is_superuser ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
</div>
<div className="bg-white rounded-lg shadow overflow-hidden">
{isLoading ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
) : (
<>
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
<Shield className="h-6 w-6 text-purple-600" />
Manage Plans
</h1>
<p className="mt-1 text-sm text-gray-500">
Configure subscription plans, limits, and pricing.
</p>
</div>
<button
onClick={() => setShowCreate(true)}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-md hover:bg-purple-700"
>
<Plus className="h-4 w-4" />
New Plan
</button>
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
{['Tier', 'Name', 'Price/mo', 'Mailboxes', 'Emails/day', 'Interval', 'Support', 'Status', 'Actions'].map((h) => (
<th
key={h}
className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
{h}
</th>
))}
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{(plans ?? []).map((p) => (
<tr key={p.id} className="hover:bg-gray-50">
<td className="px-4 py-3">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 capitalize">
{p.tier}
</span>
</td>
<td className="px-4 py-3 text-sm font-medium text-gray-900">{p.name}</td>
<td className="px-4 py-3 text-sm text-gray-500">
${p.price_monthly.toFixed(2)}
</td>
<td className="px-4 py-3 text-sm text-gray-500 text-center">
{p.max_mail_accounts}
</td>
<td className="px-4 py-3 text-sm text-gray-500 text-center">
{p.max_emails_per_day.toLocaleString()}
</td>
<td className="px-4 py-3 text-sm text-gray-500 text-center">
{p.check_interval_minutes}m
</td>
<td className="px-4 py-3 text-sm text-gray-500 capitalize">
{p.support_level}
</td>
<td className="px-4 py-3">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
p.is_active
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-500'
}`}
>
{p.is_active ? 'Active' : 'Inactive'}
</span>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<button
onClick={() => setEditingPlan(p)}
className="p-1.5 text-gray-400 hover:text-purple-600 rounded hover:bg-purple-50"
title="Edit plan"
<div className="bg-white rounded-lg shadow overflow-hidden">
{isLoading ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
{['Tier', 'Name', 'Price/mo', 'Mailboxes', 'Emails/day', 'Interval', 'Support', 'Status', 'Actions'].map((h) => (
<th
key={h}
className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
<Pencil className="h-4 w-4" />
</button>
{deleteConfirm === p.id ? (
<div className="flex items-center gap-1">
<button
onClick={() => deleteMutation.mutate(p.id)}
className="p-1.5 text-white bg-red-600 rounded hover:bg-red-700"
title="Confirm delete"
>
<Check className="h-4 w-4" />
</button>
<button
onClick={() => setDeleteConfirm(null)}
className="p-1.5 text-gray-400 hover:text-gray-600 rounded hover:bg-gray-100"
title="Cancel"
>
<X className="h-4 w-4" />
</button>
</div>
) : (
<button
onClick={() => setDeleteConfirm(p.id)}
className="p-1.5 text-gray-400 hover:text-red-600 rounded hover:bg-red-50"
title="Delete plan"
{h}
</th>
))}
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{(plans ?? []).map((p) => (
<tr key={p.id} className="hover:bg-gray-50">
<td className="px-4 py-3">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 capitalize">
{p.tier}
</span>
</td>
<td className="px-4 py-3 text-sm font-medium text-gray-900">{p.name}</td>
<td className="px-4 py-3 text-sm text-gray-500">
${p.price_monthly.toFixed(2)}
</td>
<td className="px-4 py-3 text-sm text-gray-500 text-center">
{p.max_mail_accounts}
</td>
<td className="px-4 py-3 text-sm text-gray-500 text-center">
{p.max_emails_per_day.toLocaleString()}
</td>
<td className="px-4 py-3 text-sm text-gray-500 text-center">
{p.check_interval_minutes}m
</td>
<td className="px-4 py-3 text-sm text-gray-500 capitalize">
{p.support_level}
</td>
<td className="px-4 py-3">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
p.is_active
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-500'
}`}
>
<Trash2 className="h-4 w-4" />
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
{(plans ?? []).length === 0 && (
<div className="text-center py-12 text-gray-500 text-sm">
No plans found. Create one to get started.
{p.is_active ? 'Active' : 'Inactive'}
</span>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<button
onClick={() => setEditingPlan(p)}
className="p-1.5 text-gray-400 hover:text-purple-600 rounded hover:bg-purple-50"
title="Edit plan"
>
<Pencil className="h-4 w-4" />
</button>
{deleteConfirm === p.id ? (
<div className="flex items-center gap-1">
<button
onClick={() => deleteMutation.mutate(p.id)}
className="p-1.5 text-white bg-red-600 rounded hover:bg-red-700"
title="Confirm delete"
>
<Check className="h-4 w-4" />
</button>
<button
onClick={() => setDeleteConfirm(null)}
className="p-1.5 text-gray-400 hover:text-gray-600 rounded hover:bg-gray-100"
title="Cancel"
>
<X className="h-4 w-4" />
</button>
</div>
) : (
<button
onClick={() => setDeleteConfirm(p.id)}
className="p-1.5 text-gray-400 hover:text-red-600 rounded hover:bg-red-50"
title="Delete plan"
>
<Trash2 className="h-4 w-4" />
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
{(plans ?? []).length === 0 && (
<div className="text-center py-12 text-gray-500 text-sm">
No plans found. Create one to get started.
</div>
)}
</div>
)}
</div>
)}
</div>
</div>
</div>
{showCreate && (
<PlanFormModal
plan={null}
onClose={() => setShowCreate(false)}
onCreate={(data) => createMutation.mutate(data)}
/>
)}
{editingPlan && (
<PlanFormModal
plan={editingPlan}
onClose={() => setEditingPlan(null)}
onUpdate={(data) =>
updateMutation.mutate({ id: editingPlan.id, data })
}
/>
{showCreate && (
<PlanFormModal
plan={null}
onClose={() => setShowCreate(false)}
onCreate={(data) => createMutation.mutate(data)}
/>
)}
{editingPlan && (
<PlanFormModal
plan={editingPlan}
onClose={() => setEditingPlan(null)}
onUpdate={(data) =>
updateMutation.mutate({ id: editingPlan.id, data })
}
/>
)}
</>
)}
</DashboardLayout>
</AuthGuard>
+130 -123
View File
@@ -162,142 +162,149 @@ export default function AdminUsersPage() {
},
});
if (!currentUser?.is_superuser) return null;
// AuthGuard must always render (see admin/page.tsx for explanation).
return (
<AuthGuard>
<DashboardLayout>
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
<Shield className="h-6 w-6 text-purple-600" />
Manage Users
</h1>
<p className="mt-1 text-sm text-gray-500">
View all registered users, assign plans, and manage admin privileges.
</p>
{!currentUser?.is_superuser ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
</div>
<div className="bg-white rounded-lg shadow overflow-hidden">
{isLoading ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
) : (
<>
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
<Shield className="h-6 w-6 text-purple-600" />
Manage Users
</h1>
<p className="mt-1 text-sm text-gray-500">
View all registered users, assign plans, and manage admin privileges.
</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
{['User', 'Plan', 'Status', 'Accounts', 'Last Login', 'Role', 'Actions'].map((h) => (
<th
key={h}
className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
{h}
</th>
))}
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{(users ?? []).map((u) => (
<tr key={u.id} className="hover:bg-gray-50">
<td className="px-4 py-3">
<div>
<p className="text-sm font-medium text-gray-900">{u.full_name || '—'}</p>
<p className="text-xs text-gray-500">{u.email}</p>
</div>
</td>
<td className="px-4 py-3">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 capitalize">
{u.subscription_tier}
</span>
</td>
<td className="px-4 py-3">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
u.is_active
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}
>
{u.is_active ? 'Active' : 'Inactive'}
</span>
</td>
<td className="px-4 py-3 text-sm text-gray-500 text-center">
{u.mail_account_count}
</td>
<td className="px-4 py-3 text-xs text-gray-500">
{u.last_login_at
? new Date(u.last_login_at).toLocaleDateString()
: '—'}
</td>
<td className="px-4 py-3">
{u.is_superuser ? (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
<Shield className="h-3 w-3" />
Admin
</span>
) : (
<span className="text-xs text-gray-400">User</span>
)}
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<button
onClick={() => setEditingUser(u)}
className="p-1.5 text-gray-400 hover:text-purple-600 rounded hover:bg-purple-50"
title="Edit user"
<div className="bg-white rounded-lg shadow overflow-hidden">
{isLoading ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
{['User', 'Plan', 'Status', 'Accounts', 'Last Login', 'Role', 'Actions'].map((h) => (
<th
key={h}
className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
<Pencil className="h-4 w-4" />
</button>
{u.id !== currentUser?.id && (
deleteConfirm === u.id ? (
<div className="flex items-center gap-1">
<button
onClick={() => deleteMutation.mutate(u.id)}
className="p-1.5 text-white bg-red-600 rounded hover:bg-red-700"
title="Confirm delete"
>
<Check className="h-4 w-4" />
</button>
<button
onClick={() => setDeleteConfirm(null)}
className="p-1.5 text-gray-400 hover:text-gray-600 rounded hover:bg-gray-100"
title="Cancel"
>
<X className="h-4 w-4" />
</button>
</div>
{h}
</th>
))}
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{(users ?? []).map((u) => (
<tr key={u.id} className="hover:bg-gray-50">
<td className="px-4 py-3">
<div>
<p className="text-sm font-medium text-gray-900">{u.full_name || '—'}</p>
<p className="text-xs text-gray-500">{u.email}</p>
</div>
</td>
<td className="px-4 py-3">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 capitalize">
{u.subscription_tier}
</span>
</td>
<td className="px-4 py-3">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
u.is_active
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}
>
{u.is_active ? 'Active' : 'Inactive'}
</span>
</td>
<td className="px-4 py-3 text-sm text-gray-500 text-center">
{u.mail_account_count}
</td>
<td className="px-4 py-3 text-xs text-gray-500">
{u.last_login_at
? new Date(u.last_login_at).toLocaleDateString()
: '—'}
</td>
<td className="px-4 py-3">
{u.is_superuser ? (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
<Shield className="h-3 w-3" />
Admin
</span>
) : (
<span className="text-xs text-gray-400">User</span>
)}
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<button
onClick={() => setDeleteConfirm(u.id)}
className="p-1.5 text-gray-400 hover:text-red-600 rounded hover:bg-red-50"
title="Delete user"
onClick={() => setEditingUser(u)}
className="p-1.5 text-gray-400 hover:text-purple-600 rounded hover:bg-purple-50"
title="Edit user"
>
<Trash2 className="h-4 w-4" />
<Pencil className="h-4 w-4" />
</button>
)
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
{(users ?? []).length === 0 && (
<div className="text-center py-12 text-gray-500 text-sm">No users found.</div>
{u.id !== currentUser?.id && (
deleteConfirm === u.id ? (
<div className="flex items-center gap-1">
<button
onClick={() => deleteMutation.mutate(u.id)}
className="p-1.5 text-white bg-red-600 rounded hover:bg-red-700"
title="Confirm delete"
>
<Check className="h-4 w-4" />
</button>
<button
onClick={() => setDeleteConfirm(null)}
className="p-1.5 text-gray-400 hover:text-gray-600 rounded hover:bg-gray-100"
title="Cancel"
>
<X className="h-4 w-4" />
</button>
</div>
) : (
<button
onClick={() => setDeleteConfirm(u.id)}
className="p-1.5 text-gray-400 hover:text-red-600 rounded hover:bg-red-50"
title="Delete user"
>
<Trash2 className="h-4 w-4" />
</button>
)
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
{(users ?? []).length === 0 && (
<div className="text-center py-12 text-gray-500 text-sm">No users found.</div>
)}
</div>
)}
</div>
)}
</div>
</div>
</div>
{editingUser && (
<EditUserModal
user={editingUser}
onClose={() => setEditingUser(null)}
onSave={(data) => updateMutation.mutate({ id: editingUser.id, data })}
/>
{editingUser && (
<EditUserModal
user={editingUser}
onClose={() => setEditingUser(null)}
onSave={(data) => updateMutation.mutate({ id: editingUser.id, data })}
/>
)}
</>
)}
</DashboardLayout>
</AuthGuard>