Skip to content

Admin

Purpose

The admin layer surfaces dashboard-grade statistics about the platform (user counts, event counts, daily active users by platform, signup-method breakdowns) and lets admins moderate individual users (toggle active, change subscription plan). It also gates the category-creation endpoint, since category trees are seeded from JSON on startup but can be extended live by admins.

There is no AdminService — admin handlers go straight to AdminRepository for the read-heavy stats queries. The closest service-layer companion is AuditService, which records every admin mutation (UpdateUserStatus, UpdateUserSubscription) for later review.

Responsibilities

  • Aggregate platform-wide stats (totals, DAU, signup-method breakdown, platform breakdown, 7-day platform DAU)
  • Paginated user list + per-user activity stats
  • Daily activity stats over N days
  • Update a user's is_active flag (suspend / unsuspend)
  • Update a user's subscription_status manually (used to grant comps / debug billing)
  • Create a category at runtime (parent or child)
  • Record every admin action to audit_logs

HTTP endpoints

All routes are JWT-gated and admin-role-gated by middleware.AdminAuth (see backend/internal/middleware/admin.go).

Method Path Auth Description
GET /api/v1/admin/stats/system JWT + admin Totals, DAU, event counts, version + commit SHA, platform / signup-method / daily-platform stats
GET /api/v1/admin/stats/users JWT + admin Paginated per-user stats (limit, offset)
GET /api/v1/admin/stats/daily JWT + admin Activity per day (days query, default 30)
GET /api/v1/admin/users JWT + admin Paginated user list + total count (limit, offset)
PATCH /api/v1/admin/users/:id/status JWT + admin Body { "is_active": bool }, audit-logged
PATCH /api/v1/admin/users/:id/subscription JWT + admin Body { "status": string }, audit-logged
POST /api/v1/admin/categories JWT + admin Create a category (parent or child)

Key types

// AdminRepository
type AdminRepository interface {
    GetSystemStats() (*SystemStats, error)
    GetUserStats(limit, offset int) ([]UserStats, error)
    GetDailyStats(days int) ([]DailyStats, error)
    GetAllUsers(limit, offset int) ([]models.User, int64, error)
    UpdateUserStatus(userID string, isActive bool) error
    UpdateUserSubscription(userID, status string) error
    GetPlatformStats() ([]PlatformStats, error)
    GetSignupMethodStats() ([]SignupMethodStats, error)
    GetDailyActiveUsersByPlatform(days int) ([]DailyPlatformStats, error)
}

// AuditService
func (s *AuditService) Log(
    userID uuid.UUID, action models.AuditAction,
    resourceType, resourceID, details, ip, userAgent string,
) error

Data model

  • usersis_active, subscription_status, role (user / admin)
  • audit_logsuser_id, action, resource_type, resource_id, details, ip_address, user_agent, created_at
  • categoriesname, parent_id (nullable for roots), type (event / location). Seeded from backend/data/categories_*.json on startup via CategoryHandler.SeedCategories

Dependencies

  • repository.AdminRepository — direct stats aggregation (raw SQL with Postgres date functions)
  • AuditService — writes to audit_logs after every state-changing admin action
  • middleware.AdminAuth(userRepo) — verifies users.role = 'admin' before any handler runs
  • internal/version — exposes Version + CommitSHA so /admin/stats/system echoes the running build

Notable behavior

Mutations always go through Audit

Both UpdateUserStatus and UpdateUserSubscription call auditService.Log(adminID, AuditActionUpdate, "user", userID, ...) after a successful write. Don't add a new admin mutation without that call — it's the only forensic trail.

No AdminService

Unlike most other domains, the admin layer is intentionally thin. The repository performs the heavy SQL (cohorts, platform breakdowns, daily aggregates) and the handler glues it together. If a future admin feature needs cross-domain logic, introduce an AdminService; until then the direct handler-to-repo path is fine.

Categories are dual-source

On every startup CategoryHandler.SeedCategories reads backend/data/categories_events.json and categories_locations.json and upserts the rows. Admins can also add categories at runtime via POST /admin/categories. The seed files do not delete: removing a category from JSON does not remove it from the DB. Manually clean up via SQL if you need to retire a category.

ActivityLog vs. AuditLog

The codebase distinguishes two append-only logs and they are not interchangeable:

audit_logs (AuditService) user_activity_logs (ActivityLogService)
Purpose Forensic record of admin / sensitive actions Product analytics for user behaviour (joined event, sent friend request, location check-in)
Path Direct synchronous write Enqueued to the low Asynq queue (worker.TaskLogUserActivity)
Read access Admin tooling Internal analytics / recommendation features
Touched by AdminHandler.UpdateUserStatus / UpdateUserSubscription, also called from various sensitive flows EventService, FriendService, SessionHandler.RefreshToken

Where to look