Phase 5: Global Admin System
Date: December 30, 2025
Session Type: Feature Development
Scope: Global admin capabilities, audit trail, user bans
Branch: feature/phase5-global-admin
Stats: ~25 files, full Android + iOS parity
Review: Pending
Overview
Phase 5 implements a global admin system for moderating the leaderboard and tournament ecosystem. Key design decisions:
- Firebase Custom Claims for role assignment (no Cloud Functions required)
- Online-Only Operations for audit trail integrity
- Immutable Audit Trail for accountability
- Platform Parity between Android and iOS
Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ UI Layer │
│ ┌─────────────────────────┐ ┌─────────────────────────────┐ │
│ │ AdminPanelScreen │ │ AdminPanelView │ │
│ │ (Android Compose) │ │ (iOS SwiftUI) │ │
│ └───────────┬─────────────┘ └─────────────┬───────────────┘ │
│ │ │ │
│ ┌───────────▼─────────────┐ ┌─────────────▼───────────────┐ │
│ │ AdminPanelViewModel.kt │ │ AdminPanelViewModel.swift │ │
│ └───────────┬─────────────┘ └─────────────┬───────────────┘ │
└──────────────│──────────────────────────────────│───────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ Service Layer (Android) │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ AdminService.kt │ │
│ │ - getAdminCapabilities() - getAdminStats() │ │
│ │ - deleteTournament() - banUser() │ │
│ └───────────────────────────────┬───────────────────────────────┘ │
└──────────────────────────────────│──────────────────────────────────┘
│
┌──────────────────────────────────▼──────────────────────────────────┐
│ Repository Layer │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ GlobalAdminRepositoryImpl.kt │ │
│ │ - Network check - Admin verification │ │
│ │ - Error handling - Result wrapping │ │
│ └───────────────────────────────┬───────────────────────────────┘ │
└──────────────────────────────────│──────────────────────────────────┘
│
┌──────────────────────────────────▼──────────────────────────────────┐
│ Data Source Layer │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ FirebaseAdminDataSource.kt │ │
│ │ - Firestore CRUD - Audit logging │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Firebase │
│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────────────┐ │
│ │ tournaments/ │ │ leaderboards/ │ │ admin_audit_logs/ │ │
│ └─────────────────┘ └─────────────────┘ └──────────────────────┘ │
│ ┌──────────────────────────┐ │
│ │ global_banned_users/ │ │
│ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Admin Role System
Firebase Custom Claims
Admin status is stored as a Firebase Auth custom claim:
{
"admin": true
}Assignment: Via Firebase Console → Authentication → Users → Edit user → Custom claims
App-Side Check (Android):
val tokenResult = auth.currentUser?.getIdToken(true)?.await()
val isAdmin = tokenResult?.claims?.get("admin") == trueApp-Side Check (iOS):
let tokenResult = try await user.getIDTokenResult(forcingRefresh: true)
let isAdmin = tokenResult.claims["admin"] as? Bool ?? falseWhy Custom Claims?
| Approach | Pros | Cons |
|---|---|---|
| Firestore Document | Simple to implement | Can be modified by user if rules weak |
| Custom Claims | Secure, server-side | Requires Firebase Console or Functions |
| Cloud Functions | Automated assignment | Infrastructure cost |
Decision: Custom Claims via Firebase Console - secure and zero-cost.
Admin Capabilities
Actions Available
| Action | Description | Target |
|---|---|---|
DELETE_TOURNAMENT | Delete any tournament | Tournament |
DELETE_SCORE | Delete any leaderboard entry | Score |
VERIFY_SCORE | Set verification to ADMIN_VERIFIED | Score |
UNVERIFY_SCORE | Reset verification to SELF_REPORTED | Score |
GLOBAL_BAN | Ban user from all features | User |
GLOBAL_UNBAN | Remove global ban | User |
AdminCapabilities Data Class
data class AdminCapabilities(
val canDeleteTournaments: Boolean,
val canDeleteScores: Boolean,
val canVerifyScores: Boolean,
val canGlobalBan: Boolean,
val canViewAuditLogs: Boolean
) {
companion object {
val NONE = AdminCapabilities(false, false, false, false, false)
val ALL = AdminCapabilities(true, true, true, true, true)
}
}Firestore Collections
admin_audit_logs/{logId}
Immutable audit trail (create-only, no update/delete):
{
"logId": "auto-generated-uuid",
"adminId": "firebase-uid-of-admin",
"action": "DELETE_TOURNAMENT",
"targetType": "TOURNAMENT",
"targetId": "tournament-123",
"metadata": {
"tournamentName": "Weekend Shoot"
},
"timestamp": 1735567200000,
"reason": "Duplicate entry"
}global_banned_users/{userId}
Banned user records:
{
"userId": "banned-user-uid",
"bannedAt": 1735567200000,
"bannedBy": "admin-uid",
"reason": "Fraudulent scores"
}Security Rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Admin audit logs - immutable, create-only
match /admin_audit_logs/{logId} {
allow read: if isAdmin();
allow create: if isAdmin();
// No update or delete allowed
}
// Banned users - admins only
match /global_banned_users/{userId} {
allow read, write: if isAdmin();
}
// Helper function
function isAdmin() {
return request.auth != null
&& request.auth.token.admin == true;
}
}
}Error Handling
AdminException Hierarchy
sealed class AdminException(message: String, cause: Throwable?) : Exception {
class NotAuthenticated : AdminException("User is not authenticated")
class NotAuthorized : AdminException("User does not have admin privileges")
class OfflineNotAllowed : AdminException("Admin operations require network connectivity")
class CannotBanAdmin : AdminException("Cannot ban another admin")
class OperationFailed(message: String, cause: Throwable?) : AdminException
}Online-Only Enforcement
All admin operations check network connectivity first:
Android:
if (!networkMonitor.isConnected) {
return Result.failure(AdminException.OfflineNotAllowed())
}iOS:
guard networkMonitor.isConnected else {
operationMessage = "Admin operations require network connectivity"
return
}Key Implementation Files
KMP Shared (Domain)
| File | Purpose |
|---|---|
AdminModels.kt | AdminAuditLog, GlobalBannedUser, enums |
AdminException.kt | Sealed exception hierarchy |
GlobalAdminRepository.kt | Repository interface |
Android
| File | Purpose |
|---|---|
FirebaseAdminDataSource.kt | Firestore operations |
GlobalAdminRepositoryImpl.kt | Repository implementation |
AdminService.kt | Service facade |
AdminPanelViewModel.kt | UI state management |
AdminPanelScreen.kt | Compose UI |
iOS
| File | Purpose |
|---|---|
AdminPanelViewModel.swift | ViewModel + data source (combined) |
AdminPanelView.swift | SwiftUI UI |
Platform Parity
Feature Comparison
| Feature | Android | iOS |
|---|---|---|
| Admin status check | ✅ | ✅ |
| Delete tournament | ✅ | ✅ |
| Delete leaderboard entry | ✅ | ✅ |
| Verify/unverify scores | ✅ | ✅ |
| Global ban/unban | ✅ | ✅ |
| View audit logs | ✅ | ✅ |
| View banned users | ✅ | ✅ |
| Network check | ✅ | ✅ |
Architecture Differences
| Aspect | Android | iOS |
|---|---|---|
| Layer separation | Repository + DataSource | ViewModel handles all |
| Dependency injection | Hilt | Manual init |
| State management | StateFlow | @Published |
Testing
Test Coverage
| Component | Tests | Coverage |
|---|---|---|
| AdminModels | 8 | 100% |
| GlobalAdminRepositoryImpl | 12 | 95% |
| AdminService | 10 | 92% |
| AdminPanelViewModel | 8 | 88% |
| FirebaseAdminDataSource | 15 | 90% |
Key Test Scenarios
- Non-admin user denied access
- Network offline blocks operations
- Audit logging on every action
- Cannot ban another admin
- Error recovery and UI feedback
Design Decisions
1. Online-Only Operations
Rationale:
- Ensures audit trail is immediately recorded
- Prevents conflicting offline actions
- Maintains authorization freshness
Trade-off: Admins cannot work offline, but moderation should be deliberate anyway.
2. Immutable Audit Trail
Rationale:
- Full accountability for admin actions
- Prevents tampering with records
- Enables investigation of disputes
Implementation: Firestore rules prevent update/delete on audit_logs.
3. No Cloud Functions for Role Assignment
Rationale:
- Zero infrastructure cost
- Firebase Console is sufficient for small admin team
- Custom claims are secure
Trade-off: Manual role assignment, but admin count is expected to be small.
4. Creator-Like Access for Admins
Rationale:
- Admins need to manage any tournament
- Reuses existing
CreatorAuthorizationService - Single permission model
Commits
feat(admin): Add AdminModels and GlobalAdminRepository interfacefeat(admin): Add FirebaseAdminDataSourcefeat(admin): Add GlobalAdminRepositoryImplfeat(admin): Add AdminService facadefeat(admin): Add AdminPanelViewModel and Screen (Android)feat(ios): Add iOS Admin Panel (Phase 5 Step 6)fix(ios): Add network connectivity check to admin operationsfeat(admin): Grant admin creator-like access to all tournamentstest: Add coverage tests for Phase 5 admin and maintenance services
Related Documentation
- global-admin-system - Architecture reference
- admin-audit-trail - Audit trail details
- admin-role-assignment - Role assignment guide
- verification-system - Score verification (Phase 3)