go-auth
GuidesClient

Admin

adminApi — user management, audit-log queries, and platform invites for an admin dashboard, built on the apiRequest helper.

Admin client

adminApi covers common dashboard operations from the Admin and Audit Logs guides. Add any remaining admin route with the same apiRequest helper. The login method is public; every /admin/* method requires a logged-in user with role: "admin".

import { apiRequest } from "./client";

const API_BASE = "https://api.myapp.com";

export const adminApi = {
  login: (email: string, password: string) =>
    apiRequest(API_BASE, "POST", "/auth/admin/login", { email, password }),

  listUsers: (params: Record<string, string | number> = {}) =>
    apiRequest(API_BASE, "GET", `/admin/users?${new URLSearchParams(params as Record<string, string>)}`),

  // The list response has no `total`. Fetch the count separately, keyed on
  // the filter params only (drop offset/limit) so paging doesn't re-run it.
  countUsers: (params: Record<string, string | number> = {}) =>
    apiRequest(API_BASE, "GET", `/admin/users/count?${new URLSearchParams(params as Record<string, string>)}`),

  getUser: (userId: string) => apiRequest(API_BASE, "GET", `/admin/users/${userId}`),

  createUser: (input: { email: string; password: string; name: string; role?: "user" | "admin" }) =>
    apiRequest(API_BASE, "POST", "/admin/users", input),

  updateRole: (userId: string, role: "user" | "admin") =>
    apiRequest(API_BASE, "PATCH", `/admin/users/${userId}/role`, { role }),

  ban: (userId: string) => apiRequest(API_BASE, "PATCH", `/admin/users/${userId}/ban`),

  unban: (userId: string) => apiRequest(API_BASE, "PATCH", `/admin/users/${userId}/unban`),

  deleteUser: (userId: string) => apiRequest(API_BASE, "DELETE", `/admin/users/${userId}`),

  listUserSessions: (userId: string, offset = 0, limit = 20) =>
    apiRequest(API_BASE, "GET", `/admin/users/${userId}/sessions?offset=${offset}&limit=${limit}`),

  revokeUserSession: (userId: string, sessionId: string) =>
    apiRequest(API_BASE, "DELETE", `/admin/users/${userId}/sessions/${sessionId}`),

  revokeUserSessions: (userId: string) =>
    apiRequest(API_BASE, "DELETE", `/admin/users/${userId}/sessions`),

  // --- Stats and activity ---

  getStats: () => apiRequest(API_BASE, "GET", "/admin/stats"),

  getRegistrationTrend: (from: string, to: string) =>
    apiRequest(API_BASE, "GET", `/admin/stats/registrations?from=${from}&to=${to}`),

  getLoginActivity: (from: string, to: string, userId?: string) =>
    apiRequest(
      API_BASE, "GET",
      `/admin/stats/logins?from=${from}&to=${to}${userId ? `&userId=${userId}` : ""}`
    ),

  // --- Audit logs — requires WithAudit(AuditConfig{Enabled: true}) server-side ---

  listAuditLogs: (params: Record<string, string | number> = {}) =>
    apiRequest(API_BASE, "GET", `/admin/audit-logs?${new URLSearchParams(params as Record<string, string>)}`),

  countAuditLogs: (params: Record<string, string | number> = {}) =>
    apiRequest(API_BASE, "GET", `/admin/audit-logs/count?${new URLSearchParams(params as Record<string, string>)}`),

  listUserAuditLogs: (userId: string, params: Record<string, string | number> = {}) =>
    apiRequest(API_BASE, "GET", `/admin/users/${userId}/audit-logs?${new URLSearchParams(params as Record<string, string>)}`),

  countUserAuditLogs: (userId: string, params: Record<string, string | number> = {}) =>
    apiRequest(API_BASE, "GET", `/admin/users/${userId}/audit-logs/count?${new URLSearchParams(params as Record<string, string>)}`),

  // --- Platform invites — requires RegistrationConfig.EnableInvite ---

  createInvite: (email: string) => apiRequest(API_BASE, "POST", "/admin/invites", { email }),

  listInvites: (params: Record<string, string | number> = {}) =>
    apiRequest(API_BASE, "GET", `/admin/invites?${new URLSearchParams(params as Record<string, string>)}`),

  countInvites: (params: Record<string, string | number> = {}) =>
    apiRequest(API_BASE, "GET", `/admin/invites/count?${new URLSearchParams(params as Record<string, string>)}`),

  revokeInvite: (inviteId: string) => apiRequest(API_BASE, "DELETE", `/admin/invites/${inviteId}`),

  resendInvite: (inviteId: string) => apiRequest(API_BASE, "POST", `/admin/invites/${inviteId}/resend`),

  deleteInvite: (inviteId: string) => apiRequest(API_BASE, "DELETE", `/admin/invites/${inviteId}/hard`),
};

Using it

const { users } = await adminApi.listUsers({ limit: 20, search: "ada" });
const { count } = await adminApi.countUsers({ search: "ada" }); // total for the pager
try {
  await adminApi.updateRole(userId, "user");
} catch (err) {
  // last_admin — see the Admin guide
  showError(err.message ?? err.error);
}

Stats and activity

const stats = await adminApi.getStats();
const { registrations } = await adminApi.getRegistrationTrend("2026-07-01T00:00:00Z", "2026-08-01T00:00:00Z");
const { logins } = await adminApi.getLoginActivity("2026-07-01T00:00:00Z", "2026-08-01T00:00:00Z"); // global
const { logins: userLogins } = await adminApi.getLoginActivity(from, to, userId); // one user's heatmap

Audit logs

listAuditLogs() with no filters returns everything in the audit log, paginated — it's never a 404, even if audit logging was never enabled server-side (that just means the table is empty).

const { events } = await adminApi.listAuditLogs({ event_type: "login.failed", limit: 50 });
const { count } = await adminApi.countAuditLogs({ event_type: "login.failed" });

// event_type takes a comma-separated list — matches any of them
const failures = await adminApi.listAuditLogs({ event_type: "login.failed,login.locked" });

// actorEmail/targetEmail resolve to a user ID server-side — no need to
// already know the UUID. 404 user_not_found if no user has that email.
const byActor = await adminApi.listAuditLogs({ actorEmail: "ada@example.com" });

// deviceType filters on the parsed user agent: mobile, desktop, tablet, bot
const mobileLogins = await adminApi.listAuditLogs({ event_type: "login.success", deviceType: "mobile" });

Audit logs data table

// components/audit-logs-table.tsx
"use client";

import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Input } from "@/components/ui/input";
import { adminApi } from "../lib/api/admin";

interface AuditEvent {
  id: string;
  type: string;
  severity: "info" | "warning" | "error" | "critical";
  success: boolean;
  actorId: string | null;
  actorEmail: string | null;
  targetUserId: string | null;
  targetEmail: string | null;
  ip: string | null;
  userAgent: string | null;
  metadata: Record<string, unknown>;
  createdAt: string;
}

export function AuditLogsTable() {
  const [events, setEvents] = useState<AuditEvent[]>([]);
  const [total, setTotal] = useState(0);
  const [offset, setOffset] = useState(0);
  const [filters, setFilters] = useState<Record<string, string>>({});
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);
  const limit = 25;

  useEffect(() => { loadEvents(); }, [offset, filters]);

  async function loadEvents() {
    setLoading(true);
    setError(null);
    try {
      const params = { ...filters, limit, offset };
      const result = await adminApi.listAuditLogs(params);
      setEvents(result.events);
      const countResult = await adminApi.countAuditLogs(filters);
      setTotal(countResult.count);
    } catch (err: any) {
      setError(err.message ?? err.error);
    } finally {
      setLoading(false);
    }
  }

  function severityVariant(severity: string) {
    if (severity === "critical") return "destructive";
    if (severity === "error") return "destructive";
    if (severity === "warning") return "secondary";
    return "outline";
  }

  return (
    <Card>
      <CardHeader>
        <CardTitle>Audit log ({total})</CardTitle>
      </CardHeader>
      <CardContent className="space-y-4">
        {error && (
          <Alert variant="destructive">
            <AlertDescription>{error}</AlertDescription>
          </Alert>
        )}
        <div className="flex flex-wrap gap-2">
          <Input
            placeholder="Search events, IP, user agent..."
            className="flex-1 min-w-[200px]"
            onChange={(e) => { setFilters({ ...filters, search: e.target.value }); setOffset(0); }}
          />
          <Input
            placeholder="Event type (e.g. login.failed)"
            className="w-48"
            onChange={(e) => { setFilters({ ...filters, event_type: e.target.value }); setOffset(0); }}
          />
          <Input
            placeholder="Actor email"
            className="w-48"
            onChange={(e) => { setFilters({ ...filters, actorEmail: e.target.value }); setOffset(0); }}
          />
        </div>
        {loading ? (
          <p className="text-sm text-muted-foreground">Loading...</p>
        ) : events.length === 0 ? (
          <p className="text-sm text-muted-foreground">No audit events match these filters.</p>
        ) : (
          <div className="space-y-2">
            {events.map((event) => (
              <div key={event.id} className="flex items-center justify-between rounded-md border p-3">
                <div className="min-w-0 flex-1">
                  <div className="flex items-center gap-2">
                    <p className="font-medium truncate">{event.type}</p>
                    <Badge variant={event.success ? "outline" : "destructive"}>
                      {event.success ? "Success" : "Failed"}
                    </Badge>
                    {event.severity !== "info" && (
                      <Badge variant={severity variant(event.severity)}>
                        {event.severity}
                      </Badge>
                    )}
                  </div>
                  <p className="text-sm text-muted-foreground truncate">
                    {event.actorEmail ?? event.actorId ?? "—"}
                    {event.targetEmail ? ` → ${event.targetEmail}` : ""}
                    {event.ip ? ` · ${event.ip}` : ""}
                    {` · ${new Date(event.createdAt).toLocaleString()}`}
                  </p>
                </div>
              </div>
            ))}
          </div>
        )}
        <div className="flex justify-between">
          <Button variant="outline" size="sm" disabled={offset === 0} onClick={() => setOffset(offset - limit)}>
            Previous
          </Button>
          <span className="text-sm text-muted-foreground">
            {offset + 1}–{Math.min(offset + limit, total)} of {total}
          </span>
          <Button variant="outline" size="sm" disabled={offset + limit >= total} onClick={() => setOffset(offset + limit)}>
            Next
          </Button>
        </div>
      </CardContent>
    </Card>
  );
}

Per-user audit logs

For a user detail page, scope the query to one user:

const { events } = await adminApi.listUserAuditLogs(userId, { event_type: "login.failed" });
const { count } = await adminApi.countUserAuditLogs(userId);

Platform invites

const invite = await adminApi.createInvite("ada@example.com");
const { invites } = await adminApi.listInvites({ status: "pending" });
const { count } = await adminApi.countInvites({ status: "pending" });
await adminApi.resendInvite(invite.id); // issues a fresh code, invalidates the old one

Next

  • Setup — the apiRequest helper this is built on
  • Admin — full request/response/error reference
  • Audit Logs — event types and filter params

On this page