go-auth
Routes

Admin

Platform administration: users, sessions, audit, stats, orgs, invites.

Admin

Every route here needs an admin session. middleware.RequireRole(domain.RoleAdmin, ...) wraps the handlers: no session is 401 unauthorized, a non-admin session is 403 forbidden. See the Admin guide for flows and examples.

Login

Admin login

POST /auth/admin/login

Same body as Login ({ "email": "...", "password": "..." }), but only role: "admin" accounts pass (a correct non-admin password fails exactly like a wrong one). Always returns the 2FA challenge below unless TwoFactorConfig.DisableAdminTwoFactor is set. See Admin login.

Response (200 OK):

{
  "user": { "...": "role: admin" },
  "requiresTwoFactor": true,
  "codeSent": true,
  "challengeId": "c4a1...",
  "expiresAt": "2026-08-09T12:05:00Z",
  "message": "Two-factor code sent to your email"
}

Complete with POST /auth/2fa/verify ({challengeId, code}).

Errors: invalid_credentials (401).

User management

List users

GET /admin/users

Paginated user list with filters. Invalid role values and unparsable dates are ignored, not errors.

Query:

ParamTypeDefaultRequiredNotes
offsetint0Optional
limitint20OptionalCapped at 100
emailstringnoneOptionalExact match
searchstringnoneOptionalMatches name or email
rolestringnoneOptionaladmin or user
isBannedboolnoneOptionaltrue or false
isVerifiedboolnoneOptionaltrue or false
twoFactorEnabledboolnoneOptionaltrue or false
neverLoggedInboolnoneOptionaltrue only (last_login_at IS NULL)
lastLoginBeforeRFC3339noneOptionallast_login_at < X
orderBystringcreated_atOptionalOr updated_at
orderDirectionstringdescOptionalOr asc

Response (200 OK):

{ "users": [...], "total": 150, "limit": 20, "offset": 0 }

Count users

GET /admin/users/count

Same filters as list; pagination and ordering are ignored.

Response (200 OK):

{ "count": 150 }

Get user detail

GET /admin/users/{id}

The user plus activeSessionCount, hasPassword (false for OAuth-only accounts; the hash is never returned), and linked providers.

Path params:

ParamTypeRequiredNotes
idstringRequiredThe user UUID

Response (200 OK):

{
  "user": { "...": "same shape as list" },
  "activeSessionCount": 2,
  "hasPassword": true,
  "providers": [{ "provider": "github", "providerUserID": "12345" }]
}

Errors: user_not_found (404).


Create a user

POST /admin/users

Creates the user pre-verified. No session is issued.

Request body

FieldTypeRequiredNotes
emailstringRequired
passwordstringRequiredMust pass PasswordPolicy
namestringRequiredMust not be blank
rolestringOptional"user" (default) or "admin"

Response (201 Created): the user object.

Errors: name_required, weak_password, email_already_exists (409).


Change role

PATCH /admin/users/{id}/role

Path params:

ParamTypeRequiredNotes
idstringRequiredThe user UUID

Request body

FieldTypeRequiredNotes
rolestringRequiredMust be "user" or "admin"

Response (200 OK):

{ "message": "Role updated" }

Errors: invalid_role, user_not_found, last_admin (cannot demote the last admin).


Ban

PATCH /admin/users/{id}/ban

Bans the user and revokes all their sessions. isBanned is checked on every request. No body.

Path params:

ParamTypeRequiredNotes
idstringRequiredThe user UUID

Response (200 OK):

{ "message": "User banned successfully" }

Errors: user_not_found, already_banned, last_admin (cannot ban the last admin).


Unban

PATCH /admin/users/{id}/unban

No body.

Path params:

ParamTypeRequiredNotes
idstringRequiredThe user UUID

Response (200 OK):

{ "message": "User unbanned successfully" }

Errors: user_not_found, not_banned


Delete

DELETE /admin/users/{id}

Revokes all sessions, then deletes the row. Irreversible. No body.

Path params:

ParamTypeRequiredNotes
idstringRequiredThe user UUID

Response (200 OK):

{ "message": "User deleted successfully" }

Errors: user_not_found, last_admin

User sessions

List a user's sessions

GET /admin/users/{id}/sessions

Path params:

ParamTypeRequiredNotes
idstringRequiredThe user UUID

Query:

ParamTypeDefaultRequiredNotes
offsetint0Optional
limitint20OptionalCapped at 100

Response (200 OK):

{ "sessions": [...], "total": 5 }

Errors: user_not_found.


Revoke one session

DELETE /admin/users/{id}/sessions/{sessionId}

No body.

Path params:

ParamTypeRequiredNotes
idstringRequiredThe user UUID
sessionIdstringRequiredThe session UUID

Response (200 OK):

{ "message": "Session revoked" }

Errors: user_not_found, session_not_found


Revoke all sessions

DELETE /admin/users/{id}/sessions

No body.

Path params:

ParamTypeRequiredNotes
idstringRequiredThe user UUID

Response (200 OK):

{ "message": "Sessions revoked" }

Errors: user_not_found.

Sessions across every user

List sessions

GET /admin/sessions

Active sessions platform-wide. Same base condition as user sessions (not revoked, not expired), plus filters.

Query:

ParamTypeDefaultRequiredNotes
offsetint0Optional
limitint20OptionalCapped at 100
userIdstringnoneOptionalOne user's sessions
ipstringnoneOptionalExact IP match
searchstringnoneOptionalMatches user email/name
createdAfterRFC3339noneOptionalUnparsable values ignored
createdBeforeRFC3339noneOptionalUnparsable values ignored
expiresAfterRFC3339noneOptionalUnparsable values ignored
expiresBeforeRFC3339noneOptionalUnparsable values ignored
lastActiveAfterRFC3339noneOptionalUnparsable values ignored
lastActiveBeforeRFC3339noneOptionalUnparsable values ignored
orderBystringcreated_atOptionalOr expires_at, last_active_at
orderDirectionstringdescOptionalOr asc

Response (200 OK):

{ "sessions": [...], "limit": 20, "offset": 0 }

No total; use the count route.


Count sessions

GET /admin/sessions/count

Same params as list.

Response (200 OK):

{ "count": 5 }

Bulk user actions

Bulk ban, unban, delete, revoke-sessions:

POST /admin/users/bulk/ban, POST /admin/users/bulk/unban, POST /admin/users/bulk/delete, POST /admin/users/bulk/revoke-sessions

Applies one action to many users, sequentially, not atomically. One user's failure does not stop or roll back the rest.

Request body

FieldTypeRequiredNotes
userIdsstring[]Required1 to 100 IDs (invalid_input otherwise)

Response (200 OK):

{
  "succeeded": ["id1"],
  "failed": [{ "userId": "id2", "code": "last_admin", "message": "Cannot ban the last admin" }]
}

Per-item codes are the single-user action's codes (already_banned, last_admin, user_not_found, ...).

Audit logs

User audit rows with resolved actor/target emails. The per-user route overrides any target_user_id/targetEmail query params with the path ID.

Query:

ParamTypeDefaultRequiredNotes
offsetintnoneOptionalPassed to the service as given
limitintnoneOptionalPassed to the service as given
event_typestringnoneOptionalComma-separated list
actor_idstringnoneOptionalActor user UUID
actorEmailstringnoneOptionalActor email match
target_user_idstringnoneOptionalTarget user UUID
targetEmailstringnoneOptionalTarget email match
session_idstringnoneOptionalSession UUID
org_idstringnoneOptionalOrg UUID
deviceTypestringnoneOptionalmobile, desktop, tablet, bot (from the parsed user agent)
ipstringnoneOptionalExact IP match
searchstringnoneOptionalText search
fromRFC3339noneOptionalUnparsable values ignored
toRFC3339noneOptionalUnparsable values ignored
successboolnoneOptionaltrue or false

List audit logs

GET /admin/audit-logs

Response (200 OK):

{ "events": [...] }

Count audit logs

GET /admin/audit-logs/count

Response (200 OK):

{ "count": 42 }

List a user's audit logs

GET /admin/users/{id}/audit-logs

Scoped to one user; same query params.

Path params:

ParamTypeRequiredNotes
idstringRequiredThe user UUID (overrides target_user_id/targetEmail)

Response (200 OK):

{ "events": [...] }

Count a user's audit logs

GET /admin/users/{id}/audit-logs/count

Path params:

ParamTypeRequiredNotes
idstringRequiredThe user UUID

Response (200 OK):

{ "count": 3 }

Stats and activity

Overview

GET /admin/stats

Platform-wide counts. No params.

Response (200 OK):

{
  "totalUsers": 1204,
  "verifiedUsers": 1150,
  "bannedUsers": 3,
  "twoFactorEnabledUsers": 812,
  "neverLoggedInUsers": 47,
  "activeSessions": 389
}

Registration trend

GET /admin/stats/registrations

Query

ParamTypeRequiredNotes
fromRFC3339RequiredRange start
toRFC3339RequiredRange end; range capped at 400 days

Response (200 OK): one bucket per day with at least one registration:

{ "registrations": [{ "date": "2026-08-01T00:00:00Z", "count": 4 }] }

Errors: invalid_input (missing or unparsable from/to).


Login activity

GET /admin/stats/logins

Query

ParamTypeRequiredNotes
fromRFC3339RequiredRange start
toRFC3339RequiredRange end
userIdstringOptionalOmit for the global heatmap

Counts login.success audit events, so it only returns data when audit logging is enabled.

Response (200 OK):

{ "logins": [{ "date": "2026-08-01T00:00:00Z", "count": 12 }] }

Errors: invalid_input (missing or unparsable from/to).

Organizations: requires organizations enabled

Platform-wide org oversight regardless of the caller's own membership. Disabled feature: 404 not_found. List routes share the pointer-limit semantics from Organizations (omit for the default 20; 0 = unlimited; else capped at 100).

List organizations

GET /admin/orgs

Query:

ParamTypeDefaultRequiredNotes
offsetint0Optional
limitint20OptionalOmit for the default page; 0 = unlimited; else capped at 100
searchstringnoneOptionalMatches name or slug
createdAfterRFC3339noneOptionalUnparsable values ignored
createdBeforeRFC3339noneOptionalUnparsable values ignored
orderBystringnameOptionalOr created_at, member_count
orderDirectionstringascOptionalOr desc

Response (200 OK):

{ "orgs": [...], "limit": 20, "offset": 0 }

(no total; use the count route).


Count organizations

GET /admin/orgs/count

Response (200 OK):

{ "count": 42 }

Get organization

GET /admin/orgs/{orgID}

Returns the org regardless of the caller's membership. Publishes admin.org.viewed.

Path params:

ParamTypeRequiredNotes
orgIDstringRequiredThe org UUID

Response (200 OK): the org object.

Errors: org_not_found (404).


List org members

GET /admin/orgs/{orgID}/members

Same params as the self-service members list, without the membership check. Publishes admin.org.viewed.

Path params:

ParamTypeRequiredNotes
orgIDstringRequiredThe org UUID

Query:

ParamTypeDefaultRequiredNotes
offsetint0Optional
limitint20OptionalOmit for the default page; 0 = unlimited; else capped at 100
searchstringnoneOptionalMatches name or email
rolestringnoneOptionalowner, admin, or member; anything else is 400 invalid_input
orderBystringjoined_atOptionalOr role, name, email
orderDirectionstringascOptionalOr desc

Response (200 OK):

{ "members": [...], "limit": 20, "offset": 0 }

(no total; use the count route).


Count org members

GET /admin/orgs/{orgID}/members/count

Path params:

ParamTypeRequiredNotes
orgIDstringRequiredThe org UUID

Response (200 OK):

{ "count": 12 }

Add org member

POST /admin/orgs/{orgID}/members

Path params:

ParamTypeRequiredNotes
orgIDstringRequiredThe org UUID

Request body

FieldTypeRequiredNotes
userIdstringRequiredMust belong to an existing user
rolestringRequired"owner", "admin", or "member"

Response (201 Created):

{ "message": "Member added" }

Errors: user_not_found, invalid_role, org_member_exists.


Delete organization

DELETE /admin/orgs/{orgID}

No body.

Path params:

ParamTypeRequiredNotes
orgIDstringRequiredThe org UUID

Response (200 OK):

{ "message": "Organization deleted" }

Errors: org_not_found.


Remove org member

DELETE /admin/orgs/{orgID}/members/{userID}

Refuses to remove the last owner. No body.

Path params:

ParamTypeRequiredNotes
orgIDstringRequiredThe org UUID
userIDstringRequiredThe member's user UUID

Response (200 OK):

{ "message": "Member removed" }

Errors: org_member_not_found, cannot_remove_last_owner.


Change member role

PATCH /admin/orgs/{orgID}/members/{userID}/role

Path params:

ParamTypeRequiredNotes
orgIDstringRequiredThe org UUID
userIDstringRequiredThe member's user UUID

Request body

FieldTypeRequiredNotes
rolestringRequired"owner", "admin", or "member"

Response (200 OK):

{ "message": "Role updated" }

Errors: org_member_not_found, invalid_role, org_forbidden (granting owner as a non-owner).


List a user's organizations

GET /admin/users/{id}/orgs

Path params:

ParamTypeRequiredNotes
idstringRequiredThe user UUID

Query:

ParamTypeDefaultRequiredNotes
offsetint0Optional
limitint20OptionalOmit for the default page; 0 = unlimited; else capped at 100
searchstringnoneOptionalMatches name or slug
rolestringnoneOptionalowner, admin, or member
orderBystringnameOptionalOr created_at, member_count
orderDirectionstringascOptionalOr desc

Response (200 OK):

{ "orgs": [...], "limit": 20, "offset": 0 }

(no total; use the count route).


Count a user's organizations

GET /admin/users/{id}/orgs/count

Path params:

ParamTypeRequiredNotes
idstringRequiredThe user UUID

Response (200 OK):

{ "count": 3 }

Platform invites: requires EnableInvite

Create invite

POST /admin/invites

Request body

FieldTypeRequiredNotes
emailstringRequiredThe code is emailed directly and returned once as rawCode; lists never include it

Response (201 Created): the invite object.

Errors: method_disabled, email_failed, invite_already_exists.


List invites

GET /admin/invites

Query:

ParamTypeDefaultRequiredNotes
offsetint0Optional
limitint20OptionalCapped at 100
searchstringnoneOptionalMatches email
statusstringnoneOptionalpending, accepted, revoked, or expired
orderBystringcreated_atOptionalOr expires_at, email, status
orderDirectionstringdescOptionalOr asc

Response (200 OK):

{ "invites": [...] }

Count invites

GET /admin/invites/count

Response (200 OK):

{ "count": 42 }

Revoke invite

DELETE /admin/invites/{id}

Soft-cancels (status: "revoked"). The row stays visible in lists; redeeming it fails with invite_already_used. No body.

Path params:

ParamTypeRequiredNotes
idstringRequiredThe invite UUID

Response (200 OK):

{ "message": "Invite revoked" }

Errors: invite_not_found.


Resend invite

POST /admin/invites/{id}/resend

Mails a fresh code; the old one stops working. No body.

Path params:

ParamTypeRequiredNotes
idstringRequiredThe invite UUID

Response (200 OK):

{ "message": "Invite resent" }

Errors: invite_not_found, email_failed.


Hard-delete invite

DELETE /admin/invites/{id}/hard

Removes the row outright. No body.

Path params:

ParamTypeRequiredNotes
idstringRequiredThe invite UUID

Response (200 OK):

{ "message": "Invite deleted" }

Errors: invite_not_found.


Bulk invite actions

POST /admin/invites/bulk/send, POST /admin/invites/bulk/resend, POST /admin/invites/bulk/revoke, POST /admin/invites/bulk/delete

Send takes emails; the rest take invite IDs. Per-item results, not atomic.

Request body

RouteFieldTypeRequiredNotes
sendemailsstring[]RequiredMax 25 (invalid_input otherwise)
resendinviteIdsstring[]RequiredMax 25
revoke, deleteinviteIdsstring[]RequiredMax 100

Response (200 OK):

{ "succeeded": [...], "failed": [{ "inviteId": "...", "email": "...", "code": "...", "message": "..." }] }

(email only on send failures, inviteId otherwise).

On this page