Changelog
v0.2.1
Row counts split into dedicated /count endpoints, admin-console read-path indexes, trigram search for users and invites, derived invite expiry, admin-only invite service, and bulk invite actions.
- List endpoints no longer return a
total. Every paginated admin/org listing (/admin/users,/admin/sessions,/admin/audit-logs,/admin/orgs,/admin/orgs/{orgID}/members,/admin/invites,/auth/orgs,/auth/orgs/{orgID}/members,/auth/orgs/{orgID}/invites,/admin/users/{id}/audit-logs) dropped thetotalfield and gained a siblingGET .../countthat takes the same filter params (pagination/ordering ignored) and returns{ "count": N }. A paginated table fetches the count once per filter change instead of paying for aCOUNT(*)on every page. The repository interfaces split the same way —List(...) ([]T, error)+Count(...) (int, error)— and services exposeCount*methods alongside theirList*counterparts. GetStatsnow issues oneCOUNT(*)per field instead of aList(Limit: 1)that ran a count as a side effect.isBannedandisVerifiedquery params onGET /admin/users(and/admin/users/count), wired end to end.- Admin-console indexes across the Postgres/MySQL/SQLite schemas: role-scoped user listing and sort, partial indexes on the low-cardinality
is_banned/is_verified/two_factor_enabledflags, sessionip_address/last_active_at/expires_at, invitestatus, and per-user audit trails. Composite indexes are ordered(col DESC, id DESC)so keyset pagination is a later code-only change. - Trigram user search on Postgres —
pg_trgmGIN indexes onusers(name)/users(email)turn the substringname/email ILIKE '%term%'lookup into an index scan. MySQL and SQLite, which have no portable substring index, fall back to a prefix match (term%) a plain btree can serve.
Invites
- Invite list/count/search worked on Postgres only.
InviteRepositoryemitted$Nplaceholders directly instead of going throughDB.Rebind, so the queries were malformed on MySQL and SQLite. Now rebound like every other repository. - Invite search is indexed. The email filter was
LOWER(email) LIKE LOWER($1), which no index can serve. It now uses the same driver-aware split as user search — substringILIKEagainst apg_trgmGIN index on Postgres, prefixLIKEon MySQL/SQLite. expiredis derived rather than stored. The terminal status is only written when someone opens an invite link, so lapsed invites lingered aspending— inflating the pending count and makingstatus=expiredreturn almost nothing.GET /admin/invitesnow treats pending-and-past-due as expired in both the filter and the returned rows. Redemption endpoints are unchanged.- Invite indexes across all three schemas: default list order,
expires_at, a partial index on pending invites (Postgres/SQLite), and the trigram email index on Postgres. - App-wide invites are admin-only at the service layer. Every method on
InviteServicenow takes an actor and checks it, closing theAuth.Services.Invite.*direct-call path that has no middleware in front of it.RevokeInvite,ResendInviteEmailandHardDeleteInvitetake an actor ID;ListInvitesInputgainedActorID. Organization invites already enforced org admin/owner and are unchanged. CreateInviterejects duplicates. Inviting an address that already has an account returnsemail_already_exists; a second live invite for the same address returns the newinvite_already_exists(409). Previously both silently created a row — which a bulk send over a pasted list would have multiplied.- Bulk invite actions:
POST /admin/invites/bulk/{send,resend,revoke,delete}, same partial-failure contract as the bulk user actions.send/resendcap at 25 per request and fan out across a worker pool with a per-send timeout, because each item is an SMTP round-trip;revoke/deleteare pure DB writes and take 100.deleteremoves the row outright rather than revoking first — the row is gone either way, and the trail lives in the audit log.
v0.2.2
RemoteAuth — authenticate a second Go service against a go-auth server it shares no database with — plus an admin view of any user's organizations, and role filters that reject a bad value instead of silently returning everything.
v0.2.0
Platform-admin org oversight, cross-user session admin, stats endpoints, and reworked audit-log filtering.