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_activeflag (suspend / unsuspend) - Update a user's
subscription_statusmanually (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¶
users—is_active,subscription_status,role(user/admin)audit_logs—user_id,action,resource_type,resource_id,details,ip_address,user_agent,created_atcategories—name,parent_id(nullable for roots),type(event/location). Seeded frombackend/data/categories_*.jsonon startup viaCategoryHandler.SeedCategories
Dependencies¶
repository.AdminRepository— direct stats aggregation (raw SQL with Postgres date functions)AuditService— writes toaudit_logsafter every state-changing admin actionmiddleware.AdminAuth(userRepo)— verifiesusers.role = 'admin'before any handler runsinternal/version— exposesVersion+CommitSHAso/admin/stats/systemechoes 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¶
backend/internal/handlers/admin_handler.gobackend/internal/repository/admin_repository.gobackend/internal/handlers/category_handler.go— seed + createbackend/internal/middleware/admin.go— admin role gatingbackend/internal/services/audit_service.go—AuditService.Logbackend/internal/services/activity_log_service.go—ActivityLogService.LogActivity(async)backend/internal/models/audit_log.go,activity_log.gobackend/data/categories_events.json,categories_locations.json— startup seed data