feat(api): admin backoffice router — meshes, sessions, invites, audit

Extends the Hono adminRouter with four new read-only mesh admin modules:
meshes, sessions, invites, audit. Each ships {queries,router}.ts following
the existing users/organizations/customers pattern (paginated Drizzle
transactions, getOrderByFromSort sorting, ilike search, enum filters).

- GET /admin/meshes — paginated list with owner join + member count subquery
- GET /admin/meshes/:id — detail: members, presences, invites, last 50 audit
  events (returns {mesh: null,...} shell on not-found to stay single-shape
  for Hono RPC inference)
- GET /admin/sessions — live WS presences across every mesh, joined to
  member/mesh for display, status + active/disconnected filters
- GET /admin/invites — invite tokens w/ mesh + createdBy user joins,
  revoked/expired filters
- GET /admin/audit — mesh audit log with eventType/meshId/date filters

Summary endpoint extended: new GET /admin/summary/mesh returns
{meshes, activeMeshes, totalPresences, activePresences, messages24h}.
Messages24h derived from audit_log where event_type='message_sent'
in the past 24h.

Schemas live in packages/api/src/schema/mesh-admin.ts, re-exported from
the schema barrel. All mesh/role/transport enums mirror the DB enums
from packages/db/src/schema/mesh.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-04 22:47:27 +01:00
parent d1ea1a0efa
commit 30928cd71d
11 changed files with 816 additions and 0 deletions

View File

@@ -1,2 +1,3 @@
export * from "./admin";
export * from "./mesh-admin";
export * from "./organization";

View File

@@ -0,0 +1,274 @@
import * as z from "zod";
import {
offsetPaginationSchema,
sortSchema,
} from "@turbostarter/shared/schema";
// ---------------------------------------------------------------------
// Meshes
// ---------------------------------------------------------------------
export const meshTierEnum = z.enum(["free", "pro", "team", "enterprise"]);
export const meshTransportEnum = z.enum([
"managed",
"tailscale",
"self_hosted",
]);
export const meshVisibilityEnum = z.enum(["private", "public"]);
export const getMeshesInputSchema = offsetPaginationSchema.extend({
sort: z
.string()
.transform((val) =>
z.array(sortSchema).parse(JSON.parse(decodeURIComponent(val))),
)
.optional(),
q: z.string().optional(),
tier: z
.union([meshTierEnum.transform((v) => [v]), z.array(meshTierEnum)])
.optional(),
transport: z
.union([
meshTransportEnum.transform((v) => [v]),
z.array(meshTransportEnum),
])
.optional(),
visibility: z
.union([
meshVisibilityEnum.transform((v) => [v]),
z.array(meshVisibilityEnum),
])
.optional(),
archived: z.coerce.boolean().optional(),
createdAt: z.tuple([z.coerce.number(), z.coerce.number()]).optional(),
});
export type GetMeshesInput = z.infer<typeof getMeshesInputSchema>;
export const getMeshesResponseSchema = z.object({
data: z.array(
z.object({
id: z.string(),
name: z.string(),
slug: z.string(),
visibility: meshVisibilityEnum,
transport: meshTransportEnum,
tier: meshTierEnum,
maxPeers: z.number().nullable(),
createdAt: z.coerce.date(),
archivedAt: z.coerce.date().nullable(),
ownerUserId: z.string(),
ownerName: z.string().nullable(),
ownerEmail: z.string().nullable(),
memberCount: z.number(),
}),
),
total: z.number(),
});
export type GetMeshesResponse = z.infer<typeof getMeshesResponseSchema>;
export const getMeshResponseSchema = z.object({
mesh: z
.object({
id: z.string(),
name: z.string(),
slug: z.string(),
visibility: meshVisibilityEnum,
transport: meshTransportEnum,
tier: meshTierEnum,
maxPeers: z.number().nullable(),
createdAt: z.coerce.date(),
archivedAt: z.coerce.date().nullable(),
ownerUserId: z.string(),
ownerName: z.string().nullable(),
ownerEmail: z.string().nullable(),
})
.nullable(),
members: z.array(
z.object({
id: z.string(),
displayName: z.string(),
peerPubkey: z.string(),
role: z.enum(["admin", "member"]),
joinedAt: z.coerce.date(),
lastSeenAt: z.coerce.date().nullable(),
revokedAt: z.coerce.date().nullable(),
userId: z.string().nullable(),
}),
),
presences: z.array(
z.object({
id: z.string(),
memberId: z.string(),
displayName: z.string().nullable(),
sessionId: z.string(),
pid: z.number(),
cwd: z.string(),
status: z.enum(["idle", "working", "dnd"]),
statusSource: z.enum(["hook", "manual", "jsonl"]),
statusUpdatedAt: z.coerce.date(),
connectedAt: z.coerce.date(),
lastPingAt: z.coerce.date(),
disconnectedAt: z.coerce.date().nullable(),
}),
),
invites: z.array(
z.object({
id: z.string(),
token: z.string(),
maxUses: z.number(),
usedCount: z.number(),
role: z.enum(["admin", "member"]),
expiresAt: z.coerce.date(),
createdAt: z.coerce.date(),
revokedAt: z.coerce.date().nullable(),
}),
),
auditEvents: z.array(
z.object({
id: z.string(),
eventType: z.string(),
actorPeerId: z.string().nullable(),
targetPeerId: z.string().nullable(),
metadata: z.record(z.string(), z.any()),
createdAt: z.coerce.date(),
}),
),
});
export type GetMeshResponse = z.infer<typeof getMeshResponseSchema>;
// ---------------------------------------------------------------------
// Sessions (live presences across all meshes)
// ---------------------------------------------------------------------
export const presenceStatusEnum = z.enum(["idle", "working", "dnd"]);
export const getSessionsInputSchema = offsetPaginationSchema.extend({
sort: z
.string()
.transform((val) =>
z.array(sortSchema).parse(JSON.parse(decodeURIComponent(val))),
)
.optional(),
q: z.string().optional(),
status: z
.union([presenceStatusEnum.transform((v) => [v]), z.array(presenceStatusEnum)])
.optional(),
active: z.coerce.boolean().optional(),
});
export type GetSessionsInput = z.infer<typeof getSessionsInputSchema>;
export const getSessionsResponseSchema = z.object({
data: z.array(
z.object({
id: z.string(),
memberId: z.string(),
displayName: z.string().nullable(),
meshId: z.string(),
meshName: z.string().nullable(),
meshSlug: z.string().nullable(),
sessionId: z.string(),
pid: z.number(),
cwd: z.string(),
status: presenceStatusEnum,
statusSource: z.enum(["hook", "manual", "jsonl"]),
statusUpdatedAt: z.coerce.date(),
connectedAt: z.coerce.date(),
lastPingAt: z.coerce.date(),
disconnectedAt: z.coerce.date().nullable(),
}),
),
total: z.number(),
});
export type GetSessionsResponse = z.infer<typeof getSessionsResponseSchema>;
// ---------------------------------------------------------------------
// Invites
// ---------------------------------------------------------------------
export const getInvitesInputSchema = offsetPaginationSchema.extend({
sort: z
.string()
.transform((val) =>
z.array(sortSchema).parse(JSON.parse(decodeURIComponent(val))),
)
.optional(),
q: z.string().optional(),
revoked: z.coerce.boolean().optional(),
expired: z.coerce.boolean().optional(),
});
export type GetInvitesInput = z.infer<typeof getInvitesInputSchema>;
export const getInvitesResponseSchema = z.object({
data: z.array(
z.object({
id: z.string(),
meshId: z.string(),
meshName: z.string().nullable(),
meshSlug: z.string().nullable(),
token: z.string(),
maxUses: z.number(),
usedCount: z.number(),
role: z.enum(["admin", "member"]),
expiresAt: z.coerce.date(),
createdAt: z.coerce.date(),
revokedAt: z.coerce.date().nullable(),
createdByName: z.string().nullable(),
}),
),
total: z.number(),
});
export type GetInvitesResponse = z.infer<typeof getInvitesResponseSchema>;
// ---------------------------------------------------------------------
// Audit log
// ---------------------------------------------------------------------
export const getAuditInputSchema = offsetPaginationSchema.extend({
sort: z
.string()
.transform((val) =>
z.array(sortSchema).parse(JSON.parse(decodeURIComponent(val))),
)
.optional(),
q: z.string().optional(),
eventType: z
.union([z.string().transform((v) => [v]), z.array(z.string())])
.optional(),
meshId: z
.union([z.string().transform((v) => [v]), z.array(z.string())])
.optional(),
createdAt: z.tuple([z.coerce.number(), z.coerce.number()]).optional(),
});
export type GetAuditInput = z.infer<typeof getAuditInputSchema>;
export const getAuditResponseSchema = z.object({
data: z.array(
z.object({
id: z.string(),
meshId: z.string(),
meshName: z.string().nullable(),
meshSlug: z.string().nullable(),
eventType: z.string(),
actorPeerId: z.string().nullable(),
targetPeerId: z.string().nullable(),
metadata: z.record(z.string(), z.any()),
createdAt: z.coerce.date(),
}),
),
total: z.number(),
});
export type GetAuditResponse = z.infer<typeof getAuditResponseSchema>;
// ---------------------------------------------------------------------
// Summary counts
// ---------------------------------------------------------------------
export const getMeshSummaryResponseSchema = z.object({
meshes: z.number(),
activeMeshes: z.number(),
totalPresences: z.number(),
activePresences: z.number(),
messages24h: z.number(),
});
export type GetMeshSummaryResponse = z.infer<typeof getMeshSummaryResponseSchema>;