From 3332344ca04c5a35edc4c608a50383904b1b6ba3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:02:12 +0000 Subject: [PATCH 1/3] Initial plan From 3dcb700e68774efb7a00c4be155be75c710545c5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:14:03 +0000 Subject: [PATCH 2/3] Fix: auto-promote ADMIN_EMAIL user to superuser on application startup Agent-Logs-Url: https://github.com/christianlouis/pop_puller_to_gmail/sessions/1cafe07b-4978-4609-992d-59bffb30b208 Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- CHANGELOG.md | 1 + backend/app/main.py | 28 ++++++++++++++++++++++++++++ docs/TODO.md | 2 +- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a19da68..cd3418e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed `TypeError: can't subtract offset-naive and offset-aware datetimes` in `process_mail_account` task when computing `duration_seconds`. After a database refresh, `started_at` may be returned as a naive datetime; it is now normalized to UTC before subtraction. +- **Admin user not seeing admin dashboard**: Added startup auto-promotion in `main.py` lifespan handler — on every application start, if the user matching `ADMIN_EMAIL` exists in the database but does not yet have `is_superuser=True`, they are promoted immediately. This fixes accounts created before the auto-promotion-on-login code was deployed (e.g. `christianlouis@gmail.com` was logged in but saw no admin section). ### Security - Upgraded `python-jose` from 3.3.0 to 3.5.0 to fix CVE: algorithm confusion vulnerability with OpenSSH ECDSA keys (affected versions < 3.4.0). diff --git a/backend/app/main.py b/backend/app/main.py index ed40431..cd0e036 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -58,6 +58,34 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: except Exception as exc: logger.warning("Could not seed default settings: %s", exc, exc_info=True) + # Ensure the configured ADMIN_EMAIL user has is_superuser=True. + # This runs on every startup so that existing accounts created before the + # auto-promotion login logic existed are also promoted correctly. + if settings.ADMIN_EMAIL: + try: + from sqlalchemy import select, func + from app.core.database import async_session_maker + from app.models.database_models import User + + async with async_session_maker() as db: + result = await db.execute( + select(User).where( + func.lower(User.email) == settings.ADMIN_EMAIL.lower() + ) + ) + admin_user = result.scalar_one_or_none() + if admin_user and not admin_user.is_superuser: + admin_user.is_superuser = True # type: ignore[assignment] + await db.commit() + logger.info( + "Auto-promoted admin user to superuser on startup: %s", + admin_user.email, + ) + except Exception as exc: + logger.warning( + "Could not auto-promote admin user on startup: %s", exc, exc_info=True + ) + yield # Shutdown logger.info("Shutting down application") diff --git a/docs/TODO.md b/docs/TODO.md index 87e711b..d13dc6d 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -245,7 +245,7 @@ because the API client layer is missing. - [x] Admin overview page (`/admin`) with system-wide stats - [x] User management page (`/admin/users`) — list, edit, delete users; assign plans; promote/demote admin - [x] Plan management page (`/admin/plans`) — full CRUD for subscription plans (mailboxes, emails/day, interval, pricing) -- [x] `ADMIN_EMAIL` env var with default `christianlouis@gmail.com`; admin auto-promoted on login +- [x] `ADMIN_EMAIL` env var with default `christianlouis@gmail.com`; admin auto-promoted on login and on every application startup (fixes pre-existing accounts) - [x] `is_superuser` exposed in `/users/me` response - [x] Admin badge (purple shield) shown in top bar for superusers From 74fa4606606c144a8c4035c7834b5085b3b72984 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:28:24 +0000 Subject: [PATCH 3/3] 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> --- CHANGELOG.md | 1 + docs/TODO.md | 1 + frontend/src/app/admin/page.tsx | 140 +++++++------ frontend/src/app/admin/plans/page.tsx | 277 +++++++++++++------------- frontend/src/app/admin/users/page.tsx | 253 +++++++++++------------ 5 files changed, 350 insertions(+), 322 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd3418e..5b96c68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed `TypeError: can't subtract offset-naive and offset-aware datetimes` in `process_mail_account` task when computing `duration_seconds`. After a database refresh, `started_at` may be returned as a naive datetime; it is now normalized to UTC before subtraction. - **Admin user not seeing admin dashboard**: Added startup auto-promotion in `main.py` lifespan handler — on every application start, if the user matching `ADMIN_EMAIL` exists in the database but does not yet have `is_superuser=True`, they are promoted immediately. This fixes accounts created before the auto-promotion-on-login code was deployed (e.g. `christianlouis@gmail.com` was logged in but saw no admin section). +- **Blank page on direct navigation to `/admin`, `/admin/users`, `/admin/plans`**: All three admin pages had `if (!user?.is_superuser) return null` before the `` was ever rendered. On a direct page load or refresh the Zustand store initialises with `user = null`, so the guard fired immediately and returned an empty render — `AuthGuard` was never mounted, its `checkAuth` effect never ran, and the user data was never fetched. Fixed by removing the early return and moving the superuser guard inside the `/` tree, so authentication always runs first. ### Security - Upgraded `python-jose` from 3.3.0 to 3.5.0 to fix CVE: algorithm confusion vulnerability with OpenSSH ECDSA keys (affected versions < 3.4.0). diff --git a/docs/TODO.md b/docs/TODO.md index d13dc6d..a71b347 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -248,6 +248,7 @@ because the API client layer is missing. - [x] `ADMIN_EMAIL` env var with default `christianlouis@gmail.com`; admin auto-promoted on login and on every application startup (fixes pre-existing accounts) - [x] `is_superuser` exposed in `/users/me` response - [x] Admin badge (purple shield) shown in top bar for superusers +- [x] Fix blank page on direct navigation to `/admin*`: moved superuser guard inside `` so auth check always runs on fresh load --- diff --git a/frontend/src/app/admin/page.tsx b/frontend/src/app/admin/page.tsx index 953d71a..3e80638 100644 --- a/frontend/src/app/admin/page.tsx +++ b/frontend/src/app/admin/page.tsx @@ -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 ( -
-
-

- - Admin Overview -

-

- System-wide statistics and management tools. -

+ {!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')). +
+
- - {isLoading ? ( -
-
+ ) : ( +
+
+

+ + Admin Overview +

+

+ System-wide statistics and management tools. +

- ) : ( -
-
-
+ + {isLoading ? ( +
+
+
+ ) : ( +
+
+
+ +
+
+

Total Users

+

{stats?.total_users ?? '—'}

+
+
+
+
+ +
+
+

Mail Accounts

+

{stats?.total_mail_accounts ?? '—'}

+
+
+
+
+ +
+
+

Processing Runs

+

{stats?.total_processing_runs ?? '—'}

+
+
+
+ )} + +
+ +
-

Total Users

-

{stats?.total_users ?? '—'}

+

Manage Users

+

View, edit, assign plans, promote to admin

-
-
-
+ + +
-

Mail Accounts

-

{stats?.total_mail_accounts ?? '—'}

+

Manage Plans

+

Create and configure subscription plans

-
-
-
- -
-
-

Processing Runs

-

{stats?.total_processing_runs ?? '—'}

-
-
+
- )} - -
- -
- -
-
-

Manage Users

-

View, edit, assign plans, promote to admin

-
- - -
- -
-
-

Manage Plans

-

Create and configure subscription plans

-
-
-
+ )} ); diff --git a/frontend/src/app/admin/plans/page.tsx b/frontend/src/app/admin/plans/page.tsx index 4e4b208..c0f536a 100644 --- a/frontend/src/app/admin/plans/page.tsx +++ b/frontend/src/app/admin/plans/page.tsx @@ -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 ( -
-
-
-

- - Manage Plans -

-

- Configure subscription plans, limits, and pricing. -

-
- + {!user?.is_superuser ? ( +
+
- -
- {isLoading ? ( -
-
+ ) : ( + <> +
+
+
+

+ + Manage Plans +

+

+ Configure subscription plans, limits, and pricing. +

+
+
- ) : ( -
- - - - {['Tier', 'Name', 'Price/mo', 'Mailboxes', 'Emails/day', 'Interval', 'Support', 'Status', 'Actions'].map((h) => ( - - ))} - - - - {(plans ?? []).map((p) => ( - - - - - - - - - - + + + ))} + +
- {h} -
- - {p.tier} - - {p.name} - ${p.price_monthly.toFixed(2)} - - {p.max_mail_accounts} - - {p.max_emails_per_day.toLocaleString()} - - {p.check_interval_minutes}m - - {p.support_level} - - - {p.is_active ? 'Active' : 'Inactive'} - - -
-
+
+ + {deleteConfirm === p.id ? ( +
+ + +
+ ) : ( + + )} +
+
+ {(plans ?? []).length === 0 && ( +
+ No plans found. Create one to get started. +
+ )}
)}
- )} -
-
+
- {showCreate && ( - setShowCreate(false)} - onCreate={(data) => createMutation.mutate(data)} - /> - )} - {editingPlan && ( - setEditingPlan(null)} - onUpdate={(data) => - updateMutation.mutate({ id: editingPlan.id, data }) - } - /> + {showCreate && ( + setShowCreate(false)} + onCreate={(data) => createMutation.mutate(data)} + /> + )} + {editingPlan && ( + setEditingPlan(null)} + onUpdate={(data) => + updateMutation.mutate({ id: editingPlan.id, data }) + } + /> + )} + )} diff --git a/frontend/src/app/admin/users/page.tsx b/frontend/src/app/admin/users/page.tsx index 1c6d3ae..1263c1e 100644 --- a/frontend/src/app/admin/users/page.tsx +++ b/frontend/src/app/admin/users/page.tsx @@ -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 ( -
-
-

- - Manage Users -

-

- View all registered users, assign plans, and manage admin privileges. -

+ {!currentUser?.is_superuser ? ( +
+
- -
- {isLoading ? ( -
-
+ ) : ( + <> +
+
+

+ + Manage Users +

+

+ View all registered users, assign plans, and manage admin privileges. +

- ) : ( -
- - - - {['User', 'Plan', 'Status', 'Accounts', 'Last Login', 'Role', 'Actions'].map((h) => ( - - ))} - - - - {(users ?? []).map((u) => ( - - - - - - - - + + ))} + +
- {h} -
-
-

{u.full_name || '—'}

-

{u.email}

-
-
- - {u.subscription_tier} - - - - {u.is_active ? 'Active' : 'Inactive'} - - - {u.mail_account_count} - - {u.last_login_at - ? new Date(u.last_login_at).toLocaleDateString() - : '—'} - - {u.is_superuser ? ( - - - Admin - - ) : ( - User - )} - -
- + +
+ ) : ( + + ) + )} + +
+ {(users ?? []).length === 0 && ( +
No users found.
+ )} +
)}
- )} -
-
+
- {editingUser && ( - setEditingUser(null)} - onSave={(data) => updateMutation.mutate({ id: editingUser.id, data })} - /> + {editingUser && ( + setEditingUser(null)} + onSave={(data) => updateMutation.mutate({ id: editingUser.id, data })} + /> + )} + )}