go-auth
GuidesClient

Admin

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

Admin client

Use the admin fetch wrapper, including for 2FA, refresh, and logout. It selects the independent admin context. Keep delegated operators who sign in through ordinary app login on the app client wrapper.

adminApi covers common dashboard operations from the Admin and Audit Logs guides. The examples below use disabled-mode fixed admin/user roles, which remain supported when app permissions are disabled. When app permissions are enabled, use GET /auth/access to render the current isFullAccess and permissionKeys, and use the enabled-mode appRoleId payloads/filters described in the app permission guide. Delegated operators sign in through ordinary login. Every backend action still checks current access; frontend flags are informational.

For enabled-mode authorization management, follow the app roles and permissions client. It provides typed wrappers and shadcn examples for every action, including role assignment and permission-aware controls for delegated operators.

// Use the admin-specific wrapper from Admin Client Integration.
// It adds X-GoAuth-Session: admin to bootstrap and every API call.
import { apiRequest } from "./http";

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

  listUsers: (params: Record<string, string | number> = {}) =>
    apiRequest("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("GET", `/admin/users/count?${new URLSearchParams(params as Record<string, string>)}`),

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

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

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

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

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

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

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

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

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

  // --- Stats and activity ---

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

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

  getLoginActivity: (from: string, to: string, userId?: string) =>
    apiRequest(
      "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("GET", `/admin/audit-logs?${new URLSearchParams(params as Record<string, string>)}`),

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

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

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

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

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

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

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

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

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

  deleteInvite: (inviteId: string) => apiRequest("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,admin.login.failed" });

// 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={severityVariant(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

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

On this page