Skip to content

Feat/admin dashboard - #21

Merged
devtofunmi merged 27 commits into
mainfrom
feat/admin-dashboard
Aug 14, 2026
Merged

Feat/admin dashboard#21
devtofunmi merged 27 commits into
mainfrom
feat/admin-dashboard

Conversation

@devtofunmi

Copy link
Copy Markdown
Owner

Operator/admin dashboard + landing-page analytics

Adds a private /admin dashboard for platform operators — analytics across all
workspaces plus light user management — and the lightweight page-view tracking
that powers its traffic metrics.

What's included

  • /admin dashboard (noindex, not in app nav — operator-only):
    • Totals: users, workspaces, Pro/Free split, estimated MRR.
    • Agent activity: reviews, scans, findings.
    • Trends: 12-month signups / reviews / scans.
    • Landing analytics: 30-day views + top visitor countries.
    • Recent activity: latest reviews/scans across all workspaces.
    • User management: a table of every user with plan + usage, and a
      one-click Make Pro / Make Free toggle.
  • Landing page-view tracking — a privacy-light beacon (/api/track) records
    only path, referrer, and the visitor's country (from the edge geo header). No
    cookies, no IP, no user id.

Access & security

  • Every admin server call is gated server-side — the UI gate is never
    trusted.
  • Access is granted by a users.is_admin boolean flag on the account's own
    row (which is keyed to an immutable GitHub id), not a mutable username or
    an env allowlist. Renaming/reclaiming a username can't grant access.
  • The dashboard is noindex and absent from the app navigation.

Database

Two additive migrations (no changes to existing data):

  • page_views table (landing analytics).
  • users.is_admin boolean (default false).

Run npm run db:migrate after pulling. Grant yourself admin with:
update users set is_admin = true where github_id = '<your-github-id>';

Privacy-light tracking: path, country, referrer, created_at, with an index on created_at. No PII.
Incremental migration on top of the squashed baseline; creates page_views and its created_at index.
Adds ADMIN_USERNAMES to OptionalEnvKey so the admin allowlist typechecks.
requireAdmin/isAdminUsername gate every call via ADMIN_USERNAMES. getAdminOverview returns totals (users/workspaces/pro/free/MRR/reviews/scans/findings/views), 12-month trends, top countries, 30-day views, and a cross-workspace recent-activity feed. getAdminUsers lists users with plan + usage. setWorkspacePlanAsAdmin dispatches to billing.
Pure ISO 3166-1 alpha-2 -> flag emoji, with a globe fallback.
useMounted defers recharts rendering until after mount to avoid an SSR/hydration size mismatch.
Lists every user with plan + usage; the Make Pro/Free button posts to /api/admin/set-plan and revalidates.
Fires a single sendBeacon/fetch on mount with just path + referrer.
POST /api/track records path + referrer and derives country from the edge geo header (x-vercel-ip-country / cf-ipcountry). Best-effort, always 204.
POST /api/admin/set-plan re-checks the admin via getCurrentUserFromRequest and flips a workspace between free and pro.
/admin (noindex, gated in beforeLoad; non-admins redirect to /app). Thin route: loads the overview + users and composes the admin components.
OpenTelemetry/SigNoz config is personal operator tooling, not needed by contributors deploying Jargons; drop it from .env.example. ADMIN_USERNAMES is intentionally left undocumented here too.
# Conflicts:
#	src/routeTree.gen.ts
Boolean flag (default false) granting /admin dashboard access. Set on a user's own row; the owner sets their own.
isAdmin() checks the users.is_admin flag on the signed-in account's own row, instead of the mutable-username ADMIN_USERNAMES env allowlist. The flag is tied to the account row (created from an immutable GitHub id), so renaming/reclaiming a username can't grant access.
@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
jargons-8yyz Ready Ready Preview Aug 14, 2026 8:38am

@devtofunmi
devtofunmi merged commit ccc5df3 into main Aug 14, 2026
3 checks passed

@jargons-ai jargons-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jargons Jargons review

Found 5 issues worth a look:

🟠 High — Unprotected Admin Flag in users Table

drizzle/0002_luxuriant_joseph.sql:1

The is_admin column is added to the users table with a default value of false. This flag is critical for authorization. Without robust mechanisms to prevent unauthorized modification of this flag (e.g., through direct database access, insecure API endpoints, or client-side manipulation), an attacker could potentially elevate their privileges to admin.

Suggestion: Ensure that the is_admin flag can only be modified by highly privileged, authenticated, and authorized internal processes or APIs. Implement strict access controls and validation on any API endpoint that could potentially interact with user roles. Consider adding database-level policies (e.g., RLS) if applicable, or ensuring that only trusted backend services can update this field.

🟡 Medium — Potential SQL Injection Vulnerability in page_views Table

drizzle/0001_jazzy_darkstar.sql:3

The path, country, and referrer columns in the page_views table are defined as text and are likely populated from user-controlled input (e.g., HTTP request headers or URL paths). Without proper sanitization or parameterized queries when inserting data, this could lead to SQL injection vulnerabilities.

Suggestion: Ensure that all data inserted into the path, country, and referrer columns is properly sanitized and that parameterized queries are used in the application code to prevent SQL injection. Drizzle ORM typically handles this, but it's crucial to confirm the application layer's usage.

🟡 Medium — Lack of Index on path, country, or referrer in page_views

drizzle/0001_jazzy_darkstar.sql:9

The page_views table has an index on created_at, but not on path, country, or referrer. If queries frequently filter or sort by these columns (e.g., to find page views for a specific path or from a particular country), performance could degrade over time as the table grows.

Suggestion: Consider adding indexes to path, country, or referrer columns if these columns are frequently used in WHERE clauses or ORDER BY clauses for querying page view data. A composite index might also be beneficial depending on common query patterns.

🔵 Low — Missing OpenTelemetry Configuration in .env.example

.env.example:28

The OpenTelemetry environment variables have been removed from .env.example. While these might be optional for some deployments, it's good practice to keep them as examples for users who might want to integrate with tracing systems like SigNoz.

Suggestion: Consider re-adding the OpenTelemetry configuration variables to .env.example with comments explaining their purpose and typical values, even if commented out by default.

🔵 Low — Incomplete HTML Structure in AdminCountries Component

src/components/admin/countries.tsx:54

The AdminCountries component's JSX is incomplete, ending abruptly with <h2 className="text-l. This will result in a syntax error or an improperly rendered component.

Suggestion: Complete the JSX structure of the AdminCountries component, ensuring all tags are properly closed and the component returns valid JSX.

Note: the diff was large and reviewed in part.

🔧 Suggested fixes: open the fix PR →

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant