feat: admin dashboard -- user management and tier control (OPE-102)#273
Conversation
Backend (backend/routes/admin.py, 162 lines):
- GET /admin/users: lists all users with tier, repo count, last sign-in
Joins Supabase auth users + user_profiles + repo counts
- PATCH /admin/users/{id}/tier: upgrade/downgrade user tier
Upserts to user_profiles, clears Redis cache for immediate effect
- Protected by ADMIN_EMAILS env var (email allowlist)
Frontend (frontend/src/pages/AdminPage.tsx, 216 lines):
- User table: email, tier badge (color-coded), repo count, join date
- One-click upgrade/downgrade buttons per user
- 403 error state with clear 'Admin access required' message
- Refresh button, loading states, toast notifications
Wiring:
- Route at /dashboard/admin in Dashboard.tsx
- Shield icon + 'Admin' link in Sidebar
- admin_router registered in main.py at /api/v1/admin
- ADMIN_EMAILS added to startup_checks and .env.example
Set ADMIN_EMAILS=your@email.com on Railway to enable.
|
@DevanshuNEU is attempting to deploy a commit to the Dev's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds an admin feature: environment flag and startup check for ADMIN_EMAILS, a backend admin router with admin-only endpoints to list users and update tiers (clears cache), and frontend navigation and AdminPage UI to manage users and tiers. Changes
Sequence Diagram(s)sequenceDiagram
participant Admin as Admin User
participant Frontend as Frontend App
participant Backend as Backend API
participant Supabase as Supabase
participant Redis as Redis Cache
Admin->>Frontend: Navigate to /admin
Frontend->>Backend: GET /admin/users (Authorization)
rect rgba(100,150,200,0.5)
Backend->>Supabase: Fetch user_profiles, repos, auth users
end
Supabase-->>Backend: Return aggregated user data
Backend-->>Frontend: Return consolidated user list
Frontend->>Admin: Render user list
Admin->>Frontend: Click change tier
Frontend->>Backend: PATCH /admin/users/{id}/tier
rect rgba(150,100,150,0.5)
Backend->>Supabase: Upsert user_profiles tier
Backend->>Redis: Clear user cache
end
Supabase-->>Backend: Confirm update
Redis-->>Backend: Confirm cache cleared
Backend-->>Frontend: Return updated tier & limits
Frontend->>Admin: Show success, update UI
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
.env.example (1)
71-73: Consider using a placeholder email in the example.Using
admin@example.comor similar placeholder is a common convention for example configuration files.Proposed change
# Admin access (comma-separated emails) # Users with these emails can access /api/v1/admin/* routes -ADMIN_EMAILS=devanshurajesh@gmail.com +ADMIN_EMAILS=admin@example.com🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.env.example around lines 71 - 73, Replace the real email in the example env by using a placeholder value for ADMIN_EMAILS; update the ADMIN_EMAILS entry (the ADMIN_EMAILS variable) to a neutral placeholder such as admin@example.com (or a comma-separated list of placeholder addresses) so the example file does not contain a real personal email.backend/routes/admin.py (2)
34-35: Consider usingasync deffor I/O-bound endpoints.Per coding guidelines, use
async/awaitfor I/O operations in Python backend. Bothlist_usersandupdate_user_tierperform Supabase database calls and Redis operations. While FastAPI handles sync functions in a thread pool, async is preferred for consistency and better resource utilization under load.Example refactor for list_users
`@router.get`("/users") -def list_users(auth: AuthContext = Depends(require_admin)) -> dict: +async def list_users(auth: AuthContext = Depends(require_admin)) -> dict:Note: This would require the Supabase client calls to also be async-compatible, which may need additional changes depending on the client configuration.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/routes/admin.py` around lines 34 - 35, Change the synchronous endpoints list_users and update_user_tier to async functions (use "async def list_users(...)" and "async def update_user_tier(...)"), update all internal Supabase and Redis calls inside those functions to their async/await variants (await supabase.client... and await redis_client... or swap to an async Supabase/Redis client), preserve the AuthContext dependency usage (Depends(require_admin)) and the same return types, and ensure any helper functions invoked are also async or awaited so no sync blocking calls remain in list_users and update_user_tier.
139-145: Consider moving the import to the top of the file.The late import of
redis_clienton line 140 works but is unconventional. Moving it to the top with other imports improves readability.Proposed change
At the top of the file (around line 14):
from dependencies import redis_clientThen simplify the usage:
# Clear Redis cache so new tier takes effect immediately - from dependencies import redis_client if redis_client:🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/routes/admin.py` around lines 139 - 145, Move the late import of redis_client to the module imports at the top of admin.py (alongside other imports) and update the usage in the block that clears cache to reference the top-level redis_client directly (redis_client.delete(f"user:tier:{user_id}")); keep or tighten error handling around redis_client.delete if needed but remove the in-line import from inside the function so the symbol redis_client is resolved from the module scope.frontend/src/pages/AdminPage.tsx (1)
43-62: Consider using React Query for data fetching.Per coding guidelines: "Use React Query (
useQuery) for all server data fetching, never raw fetch in useEffect." React Query provides automatic caching, background refetching, and better loading/error state management.Example refactor using useQuery
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' export function AdminPage() { const { session } = useAuth() const queryClient = useQueryClient() const { data, isLoading, error, refetch } = useQuery({ queryKey: ['admin', 'users'], queryFn: async () => { const resp = await fetch(`${API_URL}/admin/users`, { headers: { Authorization: `Bearer ${session?.access_token}` }, }) if (resp.status === 403) { throw new Error('Admin access required. Your email is not in the ADMIN_EMAILS list.') } if (!resp.ok) throw new Error('Failed to fetch users') return resp.json() }, enabled: !!session?.access_token, }) const users = data?.users ?? [] // ... rest of component }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/AdminPage.tsx` around lines 43 - 62, Replace the manual fetchUsers function and its useEffect call in AdminPage with a React Query useQuery hook: remove the useCallback fetchUsers and useEffect, import useQuery/useQueryClient from `@tanstack/react-query`, and implement a query with queryKey ['admin','users'] whose queryFn performs the same fetch to `${API_URL}/admin/users` using the session access token (the existing headers logic) but throws on resp.status === 403 with the same message and throws on !resp.ok; set enabled: !!session?.access_token so it waits for the token, map data?.users ?? [] to users, and use useQuery's isLoading and error instead of setLoading/setError to manage UI state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/routes/admin.py`:
- Line 10: Remove the unused import by deleting the `user_limits` import from
the top of admin.py (the line `from dependencies import user_limits`) so the
module no longer imports an unused symbol; ensure no other code in admin.py
references `user_limits` before removing it.
---
Nitpick comments:
In @.env.example:
- Around line 71-73: Replace the real email in the example env by using a
placeholder value for ADMIN_EMAILS; update the ADMIN_EMAILS entry (the
ADMIN_EMAILS variable) to a neutral placeholder such as admin@example.com (or a
comma-separated list of placeholder addresses) so the example file does not
contain a real personal email.
In `@backend/routes/admin.py`:
- Around line 34-35: Change the synchronous endpoints list_users and
update_user_tier to async functions (use "async def list_users(...)" and "async
def update_user_tier(...)"), update all internal Supabase and Redis calls inside
those functions to their async/await variants (await supabase.client... and
await redis_client... or swap to an async Supabase/Redis client), preserve the
AuthContext dependency usage (Depends(require_admin)) and the same return types,
and ensure any helper functions invoked are also async or awaited so no sync
blocking calls remain in list_users and update_user_tier.
- Around line 139-145: Move the late import of redis_client to the module
imports at the top of admin.py (alongside other imports) and update the usage in
the block that clears cache to reference the top-level redis_client directly
(redis_client.delete(f"user:tier:{user_id}")); keep or tighten error handling
around redis_client.delete if needed but remove the in-line import from inside
the function so the symbol redis_client is resolved from the module scope.
In `@frontend/src/pages/AdminPage.tsx`:
- Around line 43-62: Replace the manual fetchUsers function and its useEffect
call in AdminPage with a React Query useQuery hook: remove the useCallback
fetchUsers and useEffect, import useQuery/useQueryClient from
`@tanstack/react-query`, and implement a query with queryKey ['admin','users']
whose queryFn performs the same fetch to `${API_URL}/admin/users` using the
session access token (the existing headers logic) but throws on resp.status ===
403 with the same message and throws on !resp.ok; set enabled:
!!session?.access_token so it waits for the token, map data?.users ?? [] to
users, and use useQuery's isLoading and error instead of setLoading/setError to
manage UI state.
ℹ️ Review info
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
.env.examplebackend/config/startup_checks.pybackend/main.pybackend/routes/admin.pyfrontend/src/components/Dashboard.tsxfrontend/src/components/dashboard/Sidebar.tsxfrontend/src/pages/AdminPage.tsx
…l placeholder 1. Remove unused 'user_limits' import (CI lint failure) 2. Move redis_client import to module top, remove late import in function 3. Replace real email with admin@example.com in .env.example 4. Convert AdminPage from useEffect+fetch to React Query (useQuery) per CLAUDE.md: 'Use React Query for all server data fetching' - queryKey: ['admin', 'users'] - changeTier invalidates query instead of manual state update - refetch replaces fetchUsers callback Skipped: sync->async route conversion. Supabase client is sync, FastAPI correctly runs sync handlers in threadpool. Converting to async def with sync DB calls would block the event loop. flake8 passes. Build passes.
|
@coberabbit review |
There was a problem hiding this comment.
🧹 Nitpick comments (4)
backend/routes/admin.py (4)
141-144: Log Redis cache clear failures for observability.The silent
passhides potential Redis connectivity issues. Consider logging at debug/warning level for operational visibility.try: redis_client.delete(f"user:tier:{user_id}") - except Exception: - pass + except Exception as e: + logger.warning("Failed to clear tier cache", user_id=user_id, error=str(e))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/routes/admin.py` around lines 141 - 144, The except block silently swallows Redis errors when clearing the user tier cache; update the try/except around redis_client.delete(f"user:tier:{user_id}") to catch Exception as before but log the failure (use the module/logger used in this file, e.g., logger or current_app.logger) with a warning or debug level and include the exception details and the user_id in the message to aid observability.
34-35: Consider async handler for I/O operations.Per coding guidelines, I/O operations should use async/await. This endpoint makes multiple Supabase calls synchronously. Since Supabase's sync client blocks the event loop, consider wrapping calls with
asyncio.to_thread()or migrating to the async Supabase client for better concurrency.The PR notes acknowledge this is intentional due to client limitations, so this can be deferred.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/routes/admin.py` around lines 34 - 35, The list_users endpoint is currently a synchronous handler (function list_users with Depends(require_admin)) and performs blocking Supabase I/O; change it to an async handler (async def list_users(...)) and either migrate the Supabase usage to the async Supabase client or wrap the blocking calls in asyncio.to_thread(...) so the event loop isn't blocked—update any subsequent calls inside list_users that invoke the Supabase client accordingly (or replace them with awaitable async client methods).
107-108: Consider using Literal or Enum for stricter typing.Using
Literal["free", "pro", "enterprise"]orUserTierdirectly in the Pydantic model provides automatic validation with cleaner error messages:from typing import Literal class UpdateTierRequest(BaseModel): tier: Literal["free", "pro", "enterprise"]This eliminates the manual validation block and provides better OpenAPI documentation.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/routes/admin.py` around lines 107 - 108, The UpdateTierRequest model currently types tier as str which allows invalid values; replace that with a stricter type (use Literal["free","pro","enterprise"] or the existing UserTier Enum) on the UpdateTierRequest.tier field so Pydantic performs validation and OpenAPI reflects the allowed values; update any validation or manual checks that validate tier to rely on the model (e.g., remove the manual validation block that checks tier) and adjust usages of UpdateTierRequest.tier accordingly.
50-57: Consider aggregated query for repo counts.Fetching all repository rows to count in Python is inefficient for large datasets. Consider using Supabase's RPC or a view with aggregation:
SELECT user_id, COUNT(*) as count FROM repositories GROUP BY user_idThis reduces data transfer and processing overhead. For now, this works for small user bases.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/routes/admin.py` around lines 50 - 57, Currently the code iterates all repository rows via sb.client.table("repositories").select(...).execute() and tallies counts in Python (result, repo_counts); replace this with a single aggregated query to let Supabase compute counts (e.g. "SELECT user_id, COUNT(*) AS count FROM repositories GROUP BY user_id") via the Supabase client or an RPC/view, then iterate the aggregated result to populate repo_counts; keep existing error handling around the call (the except block using logger.warning) and map each row's user_id and count into repo_counts instead of incrementing per-row.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@backend/routes/admin.py`:
- Around line 141-144: The except block silently swallows Redis errors when
clearing the user tier cache; update the try/except around
redis_client.delete(f"user:tier:{user_id}") to catch Exception as before but log
the failure (use the module/logger used in this file, e.g., logger or
current_app.logger) with a warning or debug level and include the exception
details and the user_id in the message to aid observability.
- Around line 34-35: The list_users endpoint is currently a synchronous handler
(function list_users with Depends(require_admin)) and performs blocking Supabase
I/O; change it to an async handler (async def list_users(...)) and either
migrate the Supabase usage to the async Supabase client or wrap the blocking
calls in asyncio.to_thread(...) so the event loop isn't blocked—update any
subsequent calls inside list_users that invoke the Supabase client accordingly
(or replace them with awaitable async client methods).
- Around line 107-108: The UpdateTierRequest model currently types tier as str
which allows invalid values; replace that with a stricter type (use
Literal["free","pro","enterprise"] or the existing UserTier Enum) on the
UpdateTierRequest.tier field so Pydantic performs validation and OpenAPI
reflects the allowed values; update any validation or manual checks that
validate tier to rely on the model (e.g., remove the manual validation block
that checks tier) and adjust usages of UpdateTierRequest.tier accordingly.
- Around line 50-57: Currently the code iterates all repository rows via
sb.client.table("repositories").select(...).execute() and tallies counts in
Python (result, repo_counts); replace this with a single aggregated query to let
Supabase compute counts (e.g. "SELECT user_id, COUNT(*) AS count FROM
repositories GROUP BY user_id") via the Supabase client or an RPC/view, then
iterate the aggregated result to populate repo_counts; keep existing error
handling around the call (the except block using logger.warning) and map each
row's user_id and count into repo_counts instead of incrementing per-row.
ℹ️ Review info
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
.env.examplebackend/routes/admin.pyfrontend/src/pages/AdminPage.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- frontend/src/pages/AdminPage.tsx
1. UpdateTierRequest.tier typed as UserTier enum instead of str -- Pydantic validates automatically, OpenAPI shows allowed values, manual try/except validation removed (7 lines less) 2. Redis cache clear now logs warning on failure instead of silently passing -- aids debugging when cache isn't cleared Skipped: sync->async (Supabase client is sync, threadpool is correct), repo count aggregation (Supabase REST doesn't support GROUP BY).
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
backend/routes/admin.py (2)
59-71: No pagination for user listing.
list_users()returns all users from the auth provider in a single request. For applications with a large user base, this could lead to performance issues and timeouts.Consider adding pagination parameters (e.g.,
page,per_page) and usingsb.client.auth.admin.list_users(page=..., per_page=...)if the Supabase client supports it.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/routes/admin.py` around lines 59 - 71, The current call to sb.client.auth.admin.list_users() fetches all users at once; replace it with a paginated fetch using sb.client.auth.admin.list_users(page=..., per_page=...) (or the client's supported pagination params) and iterate pages starting from page=1, accumulating results into raw_users until an empty page is returned (or total count reached), keeping the existing try/except and logger/error handling around the loop; update the variable raw_users to be the aggregated list and ensure the pagination loop breaks correctly when no more users are returned.
34-35: Consider usingasync deffor I/O-bound endpoints.This endpoint performs multiple Supabase I/O operations but is defined as a synchronous function. Per coding guidelines, async/await should be used for I/O operations. While the commit notes mention keeping Supabase sync with threadpool, FastAPI will run sync endpoints in a threadpool anyway, potentially limiting concurrency.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/routes/admin.py` around lines 34 - 35, The endpoint function list_users (decorated with `@router.get`("/users") and depending on require_admin) is synchronous but performs Supabase I/O; change its signature to async def list_users(...) and convert internal Supabase calls to awaitable calls (await client methods or wrapper async helpers) so the I/O uses async/await; ensure any helper functions used by list_users are async or called via an async wrapper, and update imports/type hints if needed so FastAPI treats it as an async endpoint.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/routes/admin.py`:
- Around line 89-91: The tier value assigned via profile = profiles.get(uid, {})
and tier = profile.get("tier", meta.get("tier", "free")) must be validated
against the UserTier enum before returning to the frontend; update the code so
after computing tier you check if tier is a valid UserTier member (e.g., try
UserTier(tier) or check tier in {t.value for t in UserTier}) and if not, set
tier to the safe default (e.g., UserTier.FREE.value or "free"); ensure you
reference the computed tier variable and replace any direct usage with the
validated/normalized value so only known UserTier values are returned.
---
Nitpick comments:
In `@backend/routes/admin.py`:
- Around line 59-71: The current call to sb.client.auth.admin.list_users()
fetches all users at once; replace it with a paginated fetch using
sb.client.auth.admin.list_users(page=..., per_page=...) (or the client's
supported pagination params) and iterate pages starting from page=1,
accumulating results into raw_users until an empty page is returned (or total
count reached), keeping the existing try/except and logger/error handling around
the loop; update the variable raw_users to be the aggregated list and ensure the
pagination loop breaks correctly when no more users are returned.
- Around line 34-35: The endpoint function list_users (decorated with
`@router.get`("/users") and depending on require_admin) is synchronous but
performs Supabase I/O; change its signature to async def list_users(...) and
convert internal Supabase calls to awaitable calls (await client methods or
wrapper async helpers) so the I/O uses async/await; ensure any helper functions
used by list_users are async or called via an async wrapper, and update
imports/type hints if needed so FastAPI treats it as an async endpoint.
Unknown tier values in user_profiles or user_metadata now fall back to 'free' instead of passing through to the frontend unchecked. _VALID_TIERS set computed once at module level.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/routes/admin.py`:
- Around line 20-30: ADMIN_EMAILS is built and matched case-sensitively so
require_admin may reject equivalent emails with different casing; update the
normalization to store lowercase addresses and compare against a lowercased
auth.email: when constructing ADMIN_EMAILS (symbol: ADMIN_EMAILS) call .lower()
on each stripped env entry and in require_admin (symbols: require_admin,
AuthContext, require_auth) use auth.email.lower() (after confirming auth.email
is present) to check membership against ADMIN_EMAILS.
- Around line 44-68: auth.admin.list_users() is paginated (defaults to 50/day),
so replace the single call to sb.client.auth.admin.list_users() with a loop that
pages through all results, accumulating every page into raw_users; for example,
call sb.client.auth.admin.list_users(...) with the pagination parameters
supported by the client (page/token or page/per_page) and keep requesting until
no further pages, merging each response's .users (or list items) into raw_users,
while preserving the existing handling of both list and object responses and
error handling around auth_response and raw_users.
- Around line 37-40: Convert the synchronous route handlers list_users and
update_user_tier to async def and move blocking Supabase/Redis calls into
run_in_threadpool (or equivalent) to avoid thread blocking; locate the Supabase
client usage in get_supabase_service(), calls using select(...).execute(), and
any Redis calls in update_user_tier and wrap those calls with run_in_threadpool.
While converting, add explicit pagination/limits (use .range(start,end) or
.limit(n) and consider query parameters for page/size) when calling
select("*").execute() to avoid the Supabase 1000-row silent truncation. Ensure
function signatures (list_users, update_user_tier) and any
Depends(require_admin) usage remain unchanged except for async and await
handling.
ADMIN_EMAILS stored lowercase, auth.email compared lowercase. Prevents rejection when Supabase returns 'Dev@Example.com' but env var has 'dev@example.com'.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
backend/routes/admin.py (2)
37-40:⚠️ Potential issue | 🟠 MajorConvert blocking admin handlers to async with awaited I/O.
Lines 37 and 115 use synchronous handlers while performing external I/O (Supabase + Redis). This violates backend async guideline and can degrade request concurrency.
Proposed fix pattern
+from starlette.concurrency import run_in_threadpool ... -@router.get("/users") -def list_users(auth: AuthContext = Depends(require_admin)) -> dict: +@router.get("/users") +async def list_users(auth: AuthContext = Depends(require_admin)) -> dict: ... - result = sb.client.table("user_profiles").select("*").execute() + result = await run_in_threadpool( + lambda: sb.client.table("user_profiles").select("*").execute() + ) ... - result = sb.client.table("repositories").select("user_id").execute() + result = await run_in_threadpool( + lambda: sb.client.table("repositories").select("user_id").execute() + ) ... - auth_response = sb.client.auth.admin.list_users() + auth_response = await run_in_threadpool(sb.client.auth.admin.list_users) ... -@router.patch("/users/{user_id}/tier") -def update_user_tier( +@router.patch("/users/{user_id}/tier") +async def update_user_tier( ... - sb.client.table("user_profiles").upsert(...).execute() + await run_in_threadpool( + lambda: sb.client.table("user_profiles").upsert(...).execute() + ) ... - redis_client.delete(f"user:tier:{user_id}") + await run_in_threadpool(redis_client.delete, f"user:tier:{user_id}")#!/bin/bash set -euo pipefail # Verify sync handlers + blocking I/O callsites in this module. rg -nP --type=py '^\s*def\s+(list_users|update_user_tier)\s*\(' backend/routes/admin.py rg -n --type=py 'execute\(|auth\.admin\.list_users|redis_client\.delete' backend/routes/admin.pyAs per coding guidelines: "Use async/await for I/O operations in Python backend".
Also applies to: 44-45, 53-54, 63-64, 115-119, 127-130, 138-138
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/routes/admin.py` around lines 37 - 40, The admin handlers (e.g., list_users and update_user_tier) are currently synchronous but call blocking I/O (Supabase and Redis); change their signatures to async def and await all I/O: use an async Supabase client from get_supabase_service or wrap blocking calls (supabase queries, auth.admin.list_users) in asyncio.to_thread/run_in_executor, and switch Redis calls (redis_client.delete) to the async redis client API or await an async wrapper; ensure Depends(require_admin) remains compatible with async handlers and update any internal helper calls to their async equivalents so all external I/O is awaited.
63-68:⚠️ Potential issue | 🟠 MajorPaginate auth user retrieval to avoid partial admin results.
Line 63 fetches a single page from auth admin users. If pagination applies,
/admin/userscan silently omit users.Proposed fix
- auth_response = sb.client.auth.admin.list_users() - raw_users = ( - auth_response - if isinstance(auth_response, list) - else getattr(auth_response, "users", []) - ) + raw_users = [] + page = 1 + per_page = 100 + while True: + auth_response = sb.client.auth.admin.list_users(page=page, per_page=per_page) + batch = ( + auth_response + if isinstance(auth_response, list) + else getattr(auth_response, "users", []) + ) + raw_users.extend(batch) + if len(batch) < per_page: + break + page += 1For the current supabase-py version used by this repository, what are the pagination parameters and default page size/limit for `auth.admin.list_users()`?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/routes/admin.py` around lines 63 - 68, auth.admin.list_users() currently only fetches a single page (auth_response) which can omit users when pagination applies; update the code that sets auth_response/raw_users to iterate over all pages by calling sb.client.auth.admin.list_users() with the pagination parameters (e.g., page/per_page or next_page token as supported by the installed supabase-py) and aggregating results into raw_users until no more pages remain; specifically modify the block around auth_response and raw_users to perform a paginated loop using sb.client.auth.admin.list_users() and append each page's .users (or list response) to raw_users, handling whichever pagination keys (page/per_page, limit/offset, or next/continuation token) the repo's supabase-py exposes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/routes/admin.py`:
- Around line 104-105: The audit logs in admin.py call logger.info with raw
identifiers (e.g., the "Admin listed users" log using auth.email and the other
logger.info at the later block) which exposes PII; replace those raw identifiers
with a reversible or one-way masked form (e.g., hash or mask the email) before
logging. Locate the logger.info calls (search for "Admin listed users" and the
other info log around the user listing/management functions) and compute a
consistent scrubbed identifier (e.g., SHA-256 hex of auth.email or mask
local-part) and pass that scrubbed value as the admin/user field to logger.info
instead of the raw auth.email/user id to preserve auditability while minimizing
PII exposure.
- Around line 85-93: The code assumes u.user_metadata is a dict but can be None;
change the meta assignment so that meta is always a dict (coalesce None to {})
before calling meta.get, e.g., ensure the expression that sets meta (the
variable derived from u.user_metadata or u.get("user_metadata", {})) treats a
present but None u.user_metadata as {}. Then keep the existing lookup logic for
profile, raw_tier, and tier (references: u, user_metadata, meta, profiles,
profile.get, raw_tier, _VALID_TIERS, UserTier.FREE.value) so meta.get(...)
cannot raise.
---
Duplicate comments:
In `@backend/routes/admin.py`:
- Around line 37-40: The admin handlers (e.g., list_users and update_user_tier)
are currently synchronous but call blocking I/O (Supabase and Redis); change
their signatures to async def and await all I/O: use an async Supabase client
from get_supabase_service or wrap blocking calls (supabase queries,
auth.admin.list_users) in asyncio.to_thread/run_in_executor, and switch Redis
calls (redis_client.delete) to the async redis client API or await an async
wrapper; ensure Depends(require_admin) remains compatible with async handlers
and update any internal helper calls to their async equivalents so all external
I/O is awaited.
- Around line 63-68: auth.admin.list_users() currently only fetches a single
page (auth_response) which can omit users when pagination applies; update the
code that sets auth_response/raw_users to iterate over all pages by calling
sb.client.auth.admin.list_users() with the pagination parameters (e.g.,
page/per_page or next_page token as supported by the installed supabase-py) and
aggregating results into raw_users until no more pages remain; specifically
modify the block around auth_response and raw_users to perform a paginated loop
using sb.client.auth.admin.list_users() and append each page's .users (or list
response) to raw_users, handling whichever pagination keys (page/per_page,
limit/offset, or next/continuation token) the repo's supabase-py exposes.
user_metadata can be explicitly None (not just missing), which
would crash meta.get('tier', 'free'). Added 'or {}' fallback.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
What
Admin dashboard at /dashboard/admin for user management and tier control. Protected by ADMIN_EMAILS env var.
Backend
Frontend
Setup
Set on Railway: ADMIN_EMAILS=your@email.com
Closes OPE-102 (partial -- user management and tier control)
Summary by CodeRabbit