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
| Scenario | Expected Behavior |
|---|---|
| Non-admin access | AccessDenied state, no operations allowed |
| Offline operation | OfflineNotAllowed error |
| Delete tournament | Tournament deleted, audit logged |
| Ban user | User added to banned list, audit logged |
| Ban another admin | CannotBanAdmin error |
| View audit logs | Logs 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)
}Related Documentation
- 2025-12-30-phase5-admin-system - Phase 5 session notes
- admin-audit-trail - Audit trail details
- admin-role-assignment - Role assignment guide
- verification-system - Score verification system