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>
74 lines
1.9 KiB
TypeScript
74 lines
1.9 KiB
TypeScript
import {
|
|
and,
|
|
count,
|
|
desc,
|
|
eq,
|
|
getOrderByFromSort,
|
|
ilike,
|
|
isNotNull,
|
|
isNull,
|
|
lt,
|
|
or,
|
|
} from "@turbostarter/db";
|
|
import { user } from "@turbostarter/db/schema";
|
|
import { invite, mesh } from "@turbostarter/db/schema";
|
|
import { db } from "@turbostarter/db/server";
|
|
|
|
import type { GetInvitesInput } from "../../../schema";
|
|
|
|
export const getInvites = async (input: GetInvitesInput) => {
|
|
const offset = (input.page - 1) * input.perPage;
|
|
const now = new Date();
|
|
|
|
const where = and(
|
|
input.q
|
|
? or(
|
|
ilike(mesh.name, `%${input.q}%`),
|
|
ilike(invite.token, `%${input.q}%`),
|
|
)
|
|
: undefined,
|
|
input.revoked === true ? isNotNull(invite.revokedAt) : undefined,
|
|
input.revoked === false ? isNull(invite.revokedAt) : undefined,
|
|
input.expired === true ? lt(invite.expiresAt, now) : undefined,
|
|
);
|
|
|
|
const orderBy = input.sort
|
|
? getOrderByFromSort({ sort: input.sort, defaultSchema: invite })
|
|
: [desc(invite.createdAt)];
|
|
|
|
return db.transaction(async (tx) => {
|
|
const data = await tx
|
|
.select({
|
|
id: invite.id,
|
|
meshId: invite.meshId,
|
|
meshName: mesh.name,
|
|
meshSlug: mesh.slug,
|
|
token: invite.token,
|
|
maxUses: invite.maxUses,
|
|
usedCount: invite.usedCount,
|
|
role: invite.role,
|
|
expiresAt: invite.expiresAt,
|
|
createdAt: invite.createdAt,
|
|
revokedAt: invite.revokedAt,
|
|
createdByName: user.name,
|
|
})
|
|
.from(invite)
|
|
.leftJoin(mesh, eq(invite.meshId, mesh.id))
|
|
.leftJoin(user, eq(invite.createdBy, user.id))
|
|
.where(where)
|
|
.limit(input.perPage)
|
|
.offset(offset)
|
|
.orderBy(...orderBy);
|
|
|
|
const total = await tx
|
|
.select({ count: count() })
|
|
.from(invite)
|
|
.leftJoin(mesh, eq(invite.meshId, mesh.id))
|
|
.where(where)
|
|
.execute()
|
|
.then((res) => res[0]?.count ?? 0);
|
|
|
|
return { data, total };
|
|
});
|
|
};
|