Global Admin System Architecture

The Global Admin System provides moderation capabilities for the leaderboard and tournament ecosystem. Admins can delete content, verify scores, and ban users, with all actions logged to an immutable audit trail.

Overview

┌─────────────────────────────────────────────────────────────────────┐
│                         Firebase Auth                                │
│                    Custom Claims: { "admin": true }                  │
└─────────────────────────────────────┬───────────────────────────────┘
                                      │
                                      ▼
┌─────────────────────────────────────────────────────────────────────┐
│                      GlobalAdminRepository                           │
│  ┌─────────────────┐  ┌─────────────────┐  ┌──────────────────────┐ │
│  │ isCurrentUser   │  │ deleteTournament│  │ verifyScore          │ │
│  │ Admin()         │  │ ()              │  │ ()                   │ │
│  └─────────────────┘  └─────────────────┘  └──────────────────────┘ │
│  ┌─────────────────┐  ┌─────────────────┐  ┌──────────────────────┐ │
│  │ globalBanUser() │  │ getAuditLogs()  │  │ getBannedUsers()     │ │
│  └─────────────────┘  └─────────────────┘  └──────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
                                      │
                                      ▼
┌─────────────────────────────────────────────────────────────────────┐
│                      Firestore Collections                           │
│  ┌─────────────────┐  ┌─────────────────────────────────────────┐   │
│  │ global_banned_  │  │ admin_audit_logs/                       │   │
│  │ users/{userId}  │  │ {logId}                                 │   │
│  │                 │  │ - Immutable (no update/delete)          │   │
│  └─────────────────┘  └─────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘

Domain Models (KMP)

AdminAuditLog

Location: shared/domain/src/commonMain/kotlin/.../admin/AdminModels.kt

@Serializable
data class AdminAuditLog(
    val logId: String = "",
    val adminId: String = "",
    val action: AdminAction = AdminAction.DELETE_TOURNAMENT,
    val targetType: AdminTargetType = AdminTargetType.TOURNAMENT,
    val targetId: String = "",
    val metadata: Map<String, String> = emptyMap(),
    val timestamp: Long = 0L,
    val reason: String = ""
) {
    fun toFirestoreMap(): Map<String, Any>
 
    companion object {
        fun fromFirestoreMap(map: Map<String, Any?>): AdminAuditLog
        fun create(adminId, action, targetType, targetId, reason, metadata): AdminAuditLog
    }
}

AdminAction Enum

enum class AdminAction {
    DELETE_TOURNAMENT,  // Delete any tournament
    DELETE_SCORE,       // Delete leaderboard entry
    VERIFY_SCORE,       // Upgrade to ADMIN_VERIFIED
    UNVERIFY_SCORE,     // Reset to SELF_REPORTED
    GLOBAL_BAN,         // Ban user globally
    GLOBAL_UNBAN        // Remove global ban
}

AdminTargetType Enum

enum class AdminTargetType {
    TOURNAMENT,  // Tournament entity
    SCORE,       // Leaderboard entry
    USER         // User account
}

GlobalBannedUser

@Serializable
data class GlobalBannedUser(
    val userId: String = "",
    val bannedAt: Long = 0L,
    val bannedBy: String = "",
    val reason: String = ""
) {
    fun toFirestoreMap(): Map<String, Any>
 
    companion object {
        fun fromFirestoreMap(map: Map<String, Any?>): GlobalBannedUser
        fun create(userId, bannedBy, reason): GlobalBannedUser
    }
}

Exception Handling

AdminException Hierarchy

Location: shared/domain/src/commonMain/kotlin/.../admin/AdminException.kt

sealed class AdminException(message: String, cause: Throwable? = null) : Exception {
 
    // User not signed in
    class NotAuthenticated : AdminException("User is not authenticated")
 
    // User lacks admin claim
    class NotAuthorized : AdminException("User does not have admin privileges")
 
    // Network required for admin ops
    class OfflineNotAllowed : AdminException("Admin operations require network connectivity")
 
    // Prevent admin-on-admin bans
    class CannotBanAdmin : AdminException("Cannot ban another admin")
 
    // General operation failure
    class OperationFailed(message: String, cause: Throwable?) : AdminException
}

Repository Interface

Location: shared/domain/src/commonMain/kotlin/.../repositories/GlobalAdminRepository.kt

interface GlobalAdminRepository {
    // Authorization
    suspend fun isCurrentUserAdmin(): Boolean
 
    // Tournament operations
    suspend fun deleteTournament(tournamentId: String, reason: String): Result<Unit>
 
    // Leaderboard operations
    suspend fun deleteLeaderboardEntry(entryId: String, reason: String): Result<Unit>
    suspend fun verifyScore(entryId: String, reason: String): Result<Unit>
    suspend fun unverifyScore(entryId: String, reason: String): Result<Unit>
 
    // Ban operations
    suspend fun globalBanUser(userId: String, reason: String): Result<Unit>
    suspend fun globalUnbanUser(userId: String, reason: String): Result<Unit>
    suspend fun isUserGloballyBanned(userId: String): Boolean
 
    // Audit and reporting
    suspend fun getAuditLogs(limit: Int = 50, action: AdminAction? = null): Result<List<AdminAuditLog>>
    suspend fun getBannedUsers(): Result<List<GlobalBannedUser>>
}

Service Layer

AdminService

Location: app/src/main/java/.../services/AdminService.kt

Provides a facade over the repository with additional features:

class AdminService(
    private val adminRepository: GlobalAdminRepository
) {
    // Capability check
    suspend fun getAdminCapabilities(): AdminCapabilities
 
    // Dashboard stats
    suspend fun getAdminStats(): Result<AdminStats>
 
    // Ban status (non-admin can check self)
    suspend fun isUserBanned(userId: String): Boolean
 
    // Delegated operations
    suspend fun deleteTournament(tournamentId: String, reason: String): Result<Unit>
    suspend fun deleteLeaderboardEntry(entryId: String, reason: String): Result<Unit>
    suspend fun verifyScore(entryId: String, reason: String): Result<Unit>
    suspend fun unverifyScore(entryId: String, reason: String): Result<Unit>
    suspend fun banUser(userId: String, reason: String): Result<Unit>
    suspend fun unbanUser(userId: String, reason: String): Result<Unit>
    suspend fun getAuditLogs(limit: Int, action: AdminAction?): Result<List<AdminAuditLog>>
    suspend fun getBannedUsers(): Result<List<GlobalBannedUser>>
}
 
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)
    }
}
 
data class AdminStats(
    val totalBannedUsers: Int,
    val recentAuditEvents: Int
)

Data Source

FirebaseAdminDataSource

Location: shared/data/src/androidMain/kotlin/.../remote/FirebaseAdminDataSource.kt

class FirebaseAdminDataSource(
    private val firestore: FirebaseFirestore
) {
    // Tournament operations
    suspend fun deleteTournament(adminId: String, tournamentId: String, reason: String): Result<Unit>
 
    // Leaderboard operations
    suspend fun deleteLeaderboardEntry(adminId: String, entryId: String, reason: String): Result<Unit>
    suspend fun verifyScore(adminId: String, entryId: String, level: String, reason: String): Result<Unit>
    suspend fun unverifyScore(adminId: String, entryId: String, reason: String): Result<Unit>
 
    // Ban operations
    suspend fun globalBanUser(adminId: String, userId: String, reason: String): Result<Unit>
    suspend fun globalUnbanUser(adminId: String, userId: String, reason: String): Result<Unit>
    suspend fun isUserGloballyBanned(userId: String): Boolean
    suspend fun getBannedUsers(): List<GlobalBannedUser>
 
    // Audit logs
    suspend fun getAuditLogsList(limit: Int, action: AdminAction?): List<AdminAuditLog>
 
    // Internal - always called after operations
    private suspend fun logAuditEvent(
        adminId: String,
        action: AdminAction,
        targetType: AdminTargetType,
        targetId: String,
        reason: String,
        metadata: Map<String, String>
    )
}

Firestore Schema

admin_audit_logs/{logId}

{
  "logId": "abc123",
  "adminId": "admin-firebase-uid",
  "action": "DELETE_TOURNAMENT",
  "targetType": "TOURNAMENT",
  "targetId": "tournament-456",
  "metadata": {
    "tournamentName": "Weekend Shoot",
    "previousState": "ACTIVE"
  },
  "timestamp": 1735567200000,
  "reason": "Duplicate entry reported by multiple users"
}

global_banned_users/{userId}

{
  "userId": "banned-user-uid",
  "bannedAt": 1735567200000,
  "bannedBy": "admin-uid",
  "reason": "Repeated fraudulent score submissions"
}

Security Rules

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
 
    // Check admin claim
    function isAdmin() {
      return request.auth != null
             && request.auth.token.admin == true;
    }
 
    // Admin audit logs - IMMUTABLE
    match /admin_audit_logs/{logId} {
      allow read: if isAdmin();
      allow create: if isAdmin();
      // No update or delete - immutable audit trail
    }
 
    // Banned users - admin CRUD
    match /global_banned_users/{userId} {
      allow read: if isAdmin();
      allow create, delete: if isAdmin();
      // No update - ban/unban, not modify
    }
 
    // Tournament deletion (admin override)
    match /tournaments/{tournamentId} {
      allow delete: if isAdmin() || isCreator();
    }
 
    // Leaderboard modification (admin override)
    match /leaderboards/{entryId} {
      allow delete: if isAdmin() || isOwner();
      allow update: if isAdmin() || isOwner();
    }
  }
}

Online-Only Enforcement

Admin operations require network connectivity to ensure:

  • Immediate audit trail recording
  • Consistent authorization verification
  • No conflicting offline actions
// Repository implementation
override suspend fun deleteTournament(
    tournamentId: String,
    reason: String
): Result<Unit> {
    // Check network first
    if (!networkMonitor.isConnected) {
        return Result.failure(AdminException.OfflineNotAllowed())
    }
 
    // Check admin status
    if (!isCurrentUserAdmin()) {
        return Result.failure(AdminException.NotAuthorized())
    }
 
    // Execute operation
    return dataSource.deleteTournament(currentUserId, tournamentId, reason)
}

UI Integration

Android (Compose)

Location: app/src/main/java/.../ui/admin/AdminPanelScreen.kt

@Composable
fun AdminPanelScreen(
    viewModel: AdminPanelViewModel = hiltViewModel(),
    onNavigateBack: () -> Unit
) {
    val state by viewModel.state.collectAsState()
    val stats by viewModel.stats.collectAsState()
    val auditLogs by viewModel.auditLogs.collectAsState()
 
    when (state) {
        AdminPanelState.Loading -> LoadingIndicator()
        AdminPanelState.AccessDenied -> AccessDeniedMessage()
        is AdminPanelState.Error -> ErrorMessage((state as AdminPanelState.Error).message)
        AdminPanelState.Ready -> AdminDashboard(stats, auditLogs, viewModel::onAction)
    }
}

iOS (SwiftUI)

Location: iosApp/.../Views/Admin/AdminPanelView.swift

struct AdminPanelView: View {
    @StateObject var viewModel = AdminPanelViewModel()
 
    var body: some View {
        Group {
            switch viewModel.state {
            case .loading:
                ProgressView()
            case .accessDenied:
                AccessDeniedView()
            case .error(let message):
                ErrorView(message: message)
            case .ready:
                AdminDashboardView(viewModel: viewModel)
            }
        }
        .navigationTitle("Admin Panel")
    }
}

Testing

Test Scenarios

ScenarioExpected Behavior
Non-admin accessAccessDenied state, no operations allowed
Offline operationOfflineNotAllowed error
Delete tournamentTournament deleted, audit logged
Ban userUser added to banned list, audit logged
Ban another adminCannotBanAdmin error
View audit logsLogs displayed in descending order

Mock Injection

@Test
fun `delete tournament fails when offline`() = runTest {
    val mockNetwork = FakeNetworkMonitor(isConnected = false)
    val repo = GlobalAdminRepositoryImpl(mockDataSource, mockAuth, mockNetwork)
 
    val result = repo.deleteTournament("tournament-123", "Test reason")
 
    assertTrue(result.isFailure)
    assertTrue(result.exceptionOrNull() is AdminException.OfflineNotAllowed)
}