mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:41:09 +02:00
feat(admin): replace mock dashboard stats with real /admin/stats endpoint
The /admin route in the unified Mana web app was rendering hardcoded
mock data (42 users, 156 successful logins, 3 failed) for every
admin who opened it. The previous code had a TODO comment to wire
up a real endpoint and the backend half had been waiting for the
frontend half ever since the consolidation landed.
Backend (mana-auth):
Add GET /api/v1/admin/stats — admin-only, returns the seven counts
the dashboard needs in a single response. Each count is its own
Drizzle query against auth.users / auth.sessions / auth.login_
attempts; they run in parallel via Promise.all so total latency is
dominated by the round-trip to Postgres, not the per-query work.
Stats:
- totalUsers → users where deleted_at IS NULL
- newUsers7d → users created in the last 7 days
- newUsers30d → users created in the last 30 days
- activeSessions → sessions where expires_at > now() AND not revoked
- uniqueUsers24h → distinct user_id from sessions with last_activity
in the last 24h (and not revoked)
- loginSuccess7d → login_attempts where successful=true, last 7d
- loginFailed7d → login_attempts where successful=false, last 7d
Plus a generatedAt ISO timestamp so the client can show staleness
if it ever caches the response.
Frontend (apps/mana/apps/web):
- Add adminService.getStats() in the existing admin API service
(sits next to getUsers / getUserData / deleteUserData; uses the
same authenticated base-client and ApiResult envelope).
- Replace the onMount mock-data block in admin/+page.svelte with
a single adminService.getStats() call. Drop the local Stats
interface in favor of the AdminStats type exported from the
service.
- Guard the Success Rate calculation against division by zero on
fresh deployments — when there have been no login attempts in
the last 7 days, render '—%' instead of NaN%.
Verification:
- mana-auth type-check unchanged (baseline errors only)
- mana-auth runtime tests still 19/19 passing
- svelte-check on the two changed web files: zero errors
Closes item #12 in docs/REFACTORING_AUDIT_2026_04.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7b25f3abfb
commit
fbb71f9366
3 changed files with 109 additions and 36 deletions
|
|
@ -5,10 +5,11 @@
|
|||
*/
|
||||
|
||||
import { Hono } from 'hono';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { and, count, countDistinct, eq, gte, isNull } from 'drizzle-orm';
|
||||
import type { AuthUser } from '../middleware/jwt-auth';
|
||||
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
|
||||
import { users } from '../db/schema/auth';
|
||||
import { users, sessions } from '../db/schema/auth';
|
||||
import { loginAttempts } from '../db/schema/login-attempts';
|
||||
import type { UserDataService } from '../services/user-data';
|
||||
|
||||
const VALID_TIERS = ['guest', 'public', 'beta', 'alpha', 'founder'] as const;
|
||||
|
|
@ -26,6 +27,75 @@ export function createAdminRoutes(db: PostgresJsDatabase<any>, userDataService:
|
|||
await next();
|
||||
});
|
||||
|
||||
// ─── Aggregate stats for the admin dashboard ──────────────
|
||||
//
|
||||
// Replaces hardcoded mock data in apps/mana/apps/web/src/routes/
|
||||
// (app)/admin/+page.svelte. All seven values come from auth.users,
|
||||
// auth.sessions and auth.login_attempts — no other service is
|
||||
// involved, which keeps this endpoint a pure single-DB read.
|
||||
|
||||
app.get('/stats', async (c) => {
|
||||
const now = new Date();
|
||||
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
|
||||
// One query per stat — Postgres handles them in parallel via
|
||||
// the connection pool when wrapped in Promise.all. Each one is
|
||||
// a single indexed count, so total latency is dominated by
|
||||
// round-trip not query work.
|
||||
const [
|
||||
[{ value: totalUsers }],
|
||||
[{ value: newUsers7d }],
|
||||
[{ value: newUsers30d }],
|
||||
[{ value: activeSessions }],
|
||||
[{ value: uniqueUsers24h }],
|
||||
[{ value: loginSuccess7d }],
|
||||
[{ value: loginFailed7d }],
|
||||
] = await Promise.all([
|
||||
db.select({ value: count() }).from(users).where(isNull(users.deletedAt)),
|
||||
db
|
||||
.select({ value: count() })
|
||||
.from(users)
|
||||
.where(and(isNull(users.deletedAt), gte(users.createdAt, sevenDaysAgo))),
|
||||
db
|
||||
.select({ value: count() })
|
||||
.from(users)
|
||||
.where(and(isNull(users.deletedAt), gte(users.createdAt, thirtyDaysAgo))),
|
||||
db
|
||||
.select({ value: count() })
|
||||
.from(sessions)
|
||||
.where(and(gte(sessions.expiresAt, now), isNull(sessions.revokedAt))),
|
||||
db
|
||||
.select({ value: countDistinct(sessions.userId) })
|
||||
.from(sessions)
|
||||
.where(and(isNull(sessions.revokedAt), gte(sessions.lastActivityAt, twentyFourHoursAgo))),
|
||||
db
|
||||
.select({ value: count() })
|
||||
.from(loginAttempts)
|
||||
.where(
|
||||
and(eq(loginAttempts.successful, true), gte(loginAttempts.attemptedAt, sevenDaysAgo))
|
||||
),
|
||||
db
|
||||
.select({ value: count() })
|
||||
.from(loginAttempts)
|
||||
.where(
|
||||
and(eq(loginAttempts.successful, false), gte(loginAttempts.attemptedAt, sevenDaysAgo))
|
||||
),
|
||||
]);
|
||||
|
||||
return c.json({
|
||||
totalUsers,
|
||||
newUsers7d,
|
||||
newUsers30d,
|
||||
activeSessions,
|
||||
uniqueUsers24h,
|
||||
loginSuccess7d,
|
||||
loginFailed7d,
|
||||
generatedAt: now.toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
// ─── List users with pagination and search ────────────────
|
||||
|
||||
app.get('/users', async (c) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue