using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Cors; using Microsoft.EntityFrameworkCore; using System.Text.Json; using RadicadeServer.API.Notifications; using RadicadeServer.Auth; using RadicadeServer.DB; using RadicadeServer.Extensions; using RadicadeServer.Radicade.Discord; using RadicadeServer.RecNet.Accounts; using RadicadeServer.RecNet.Auth; using RadicadeServer.RecNet.Enums.Messages; using RadicadeServer.RecNet.Enums.Moderation; using RadicadeServer.RecNet.Enums.Players; using RadicadeServer.RecNet.Messages; using RadicadeServer.RecNet.Moderation; using RadicadeServer.RecNet.Notifications; using RadicadeServer.RecNet.Rooms; using RadicadeServer.Utils; namespace RadicadeServer.Admin { [Route("admin/profiles")] [EnableCors("AdminPanel")] public class AdminProfileAPI(RecDBContext db) : Controller { private Task RejectInvalidKey(string key) { return Task.FromResult(key.IsAdminKey() ? null : Unauthorized("Invalid secret")); } private async Task NotifyUser(long playerId) { var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId); if (profile == null) return; var plrData = await db.Players.FirstOrDefaultAsync(x => x.AccountId == playerId); if (plrData == null) return; await Notify.SendNotify(playerId, new Notification { Id = "AccountUpdate", Msg = profile }); await Notify.UpdateSubscriptions(playerId, Notifs.SubscriptionUpdateProfile, profile); await Notify.SendNotify(playerId, new Notification { Id = "SelfAccountUpdate", Msg = new SelfAccount { juniorState = plrData.Birthday != null && plrData.Birthday.Value.Year > 1900 ? JuniorState.BasedOnBirthday : JuniorState.PendingOverride, accountId = playerId, parentAccountId = plrData.Parent, availableUsernameChanges = 99, bio = profile.bio, displayName = profile.displayName, email = plrData.Email, createdAt = profile.createdAt, isJunior = profile.isJunior, phone = plrData.Phone, platforms = profile.platforms, profileImage = profile.profileImage, username = profile.username } }); } [HttpGet("whitelist")] public async Task WhitelistUser([FromQuery] string key, [FromQuery] long playerId) { var rejection = await RejectInvalidKey(key); if (rejection != null) return rejection; var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId); if (profile == null) return NotFound(); var cachedLogins = await db.CachedLogins.Where(x => x.accountId == playerId).ToListAsync(); string addedIds = ""; List ids = new(); foreach (var cached in cachedLogins) { if (cached.platform != Platforms.Steam) continue; if (!SteamWhitelist.Contains(cached.platformId)) { SteamWhitelist.Add(cached.platformId); addedIds += cached.platformId + ","; ids.Add(cached.platformId); } } addedIds = addedIds.Trim().TrimEnd(','); return Ok($"Whitelisted @{profile.username}! Added Steam IDs: {addedIds}"); } [HttpGet("whitelist/toggle")] public async Task ToggleWhitelist([FromQuery] string key, [FromQuery] bool enabled) { var rejection = await RejectInvalidKey(key); if (rejection != null) return rejection; SteamWhitelist.Enabled = enabled; return Ok(enabled ? "Steam whitelist is now ENABLED. Only approved Steam IDs can sign in.\nConfig: " + SteamWhitelist.FilePath : "Steam whitelist is now DISABLED. Anyone can sign in."); } [HttpGet("whitelist/list")] public async Task GetWhitelist([FromQuery] string key) { var rejection = await RejectInvalidKey(key); if (rejection != null) return rejection; return Ok($"Whitelist {(SteamWhitelist.Enabled ? "ENABLED" : "disabled")} ({SteamWhitelist.SteamIds.Count} IDs)\nFilename: {SteamWhitelist.FilePath}\n```json\n{new SteamWhitelist.WhitelistFile { Enabled = SteamWhitelist.Enabled, SteamIds = SteamWhitelist.SteamIds.ToList() }.ToJson()}\n```"); } [HttpGet("whitelist/add")] public async Task AddWhitelistId([FromQuery] string key, [FromQuery] string steamId) { var rejection = await RejectInvalidKey(key); if (rejection != null) return rejection; if (string.IsNullOrWhiteSpace(steamId)) return BadRequest("No steamId given."); var added = SteamWhitelist.Add(steamId); return Ok(added ? $"Steam ID {steamId} was added to the whitelist." : $"Steam ID {steamId} is already on the whitelist."); } [HttpGet("whitelist/remove")] public async Task RemoveWhitelistId([FromQuery] string key, [FromQuery] string steamId) { var rejection = await RejectInvalidKey(key); if (rejection != null) return rejection; if (string.IsNullOrWhiteSpace(steamId)) return BadRequest("No steamId given."); var removed = SteamWhitelist.Remove(steamId); return Ok(removed ? $"Steam ID {steamId} was removed from the whitelist." : $"Steam ID {steamId} is not on the whitelist."); } [HttpGet("set/uname")] public async Task SetUsername([FromQuery] string key, [FromQuery] long playerId, [FromQuery] string newName) { var rejection = await RejectInvalidKey(key); if (rejection != null) return rejection; var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId); if (profile == null) return NotFound(); profile.username = newName; await db.SaveChangesAsync(); await NotifyUser(profile.accountId); return Ok("Updated profile!\n```json" + profile.ToJson() + "```"); } [HttpGet("set/dname")] public async Task SetDisplayName([FromQuery] string key, [FromQuery] long playerId, [FromQuery] string newName) { var rejection = await RejectInvalidKey(key); if (rejection != null) return rejection; var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId); if (profile == null) return NotFound(); profile.displayName = newName; await db.SaveChangesAsync(); await NotifyUser(profile.accountId); return Ok("Updated profile!\n```json" + profile.ToJson() + "```"); } [HttpGet("set/image")] public async Task SetProfileImage([FromQuery] string key, [FromQuery] long playerId, [FromQuery] string imgName) { var rejection = await RejectInvalidKey(key); if (rejection != null) return rejection; var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId); if (profile == null) return NotFound(); profile.profileImage = imgName; await db.SaveChangesAsync(); await NotifyUser(profile.accountId); return Ok("Updated profile!\n```json" + profile.ToJson() + "```"); } [HttpGet("profiles/{profileId}")] public async Task GetRoom([FromQuery] string key, long profileId) { var rejection = await RejectInvalidKey(key); if (rejection != null) return rejection; var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == profileId); if (profile == null) return NotFound(); return Ok(profile); } [HttpGet("ban")] public async Task BanUser([FromQuery] string key, [FromQuery] long userId, [FromQuery] ReportCategory category, [FromQuery] int duration, [FromQuery] string reason) { var rejection = await RejectInvalidKey(key); if (rejection != null) return rejection; var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == userId); if (profile == null) return NotFound("User does not exist."); bool didBan = false; DateTime banExpiry = DateTime.MaxValue; string appealCode = string.Empty; var existingModeration = await db.Moderation.FirstOrDefaultAsync(x => x.PlayerId == userId && x.IsBan); if (existingModeration != null) { if (duration > 0) { var expireTime = DateTime.UtcNow.AddMinutes(duration); existingModeration.ExpireDuration = expireTime; existingModeration.Message = reason; existingModeration.Category = category; existingModeration.IsBan = true; didBan = true; banExpiry = expireTime; } else if (duration == -1) { existingModeration.ExpireDuration = DateTime.MaxValue; existingModeration.Message = reason; existingModeration.Category = category; existingModeration.IsBan = true; didBan = true; banExpiry = DateTime.MaxValue; } else { db.Moderation.Remove(existingModeration); didBan = false; await Notify.SendNotify(userId, new Notification { Id = NotificationType.RefreshLogin, Msg = profile }); await PlayerUtils.ReleaseModerationHoldAsync(userId); } } else { if (duration > 0) { var expireTime = DateTime.UtcNow.AddMinutes(duration); var (newCode, _) = await PlayerUtils.GetAppealForNewBanAsync(db, userId); var newModeration = new ModerationDB { Category = category, ExpireDuration = expireTime, IsBan = true, Message = reason, PlayerId = userId, AppealCode = newCode }; await db.Moderation.AddAsync(newModeration); appealCode = newCode ?? string.Empty; didBan = true; banExpiry = expireTime; } else if (duration == -1) { var expireTime = DateTime.MaxValue; var (newCode, _) = await PlayerUtils.GetAppealForNewBanAsync(db, userId); var newModeration = new ModerationDB { Category = category, ExpireDuration = expireTime, IsBan = true, Message = reason, PlayerId = userId, AppealCode = newCode }; await db.Moderation.AddAsync(newModeration); appealCode = newCode ?? string.Empty; didBan = true; banExpiry = expireTime; } } if (didBan) { var activeBan = existingModeration ?? await db.Moderation.FirstOrDefaultAsync(x => x.PlayerId == userId && x.IsBan); if (activeBan != null && string.IsNullOrWhiteSpace(activeBan.AppealCode)) { var (newCode, _) = await PlayerUtils.GetAppealForNewBanAsync(db, userId); activeBan.AppealCode = newCode; appealCode = newCode ?? string.Empty; } else if (string.IsNullOrWhiteSpace(appealCode) && activeBan != null) { appealCode = activeBan.AppealCode ?? string.Empty; } await db.VoteKicks.AddAsync(new VoteKickLog { PlayerId = userId, Reason = reason, Category = category, IsForce = true, CreatedAt = DateTime.UtcNow, ExpiresAt = banExpiry }); var rep = await db.Reputations.FirstOrDefaultAsync(x => x.AccountId == userId); if (rep != null) rep.ApplyBanPenalty(permanent: duration == -1, cheating: false); } await db.SaveChangesAsync(); if (didBan) { await AdminActionDiscord.LogBanAsync(db, userId, bannedBy: "Admin (Profile API)", causedByReports: false, reason: reason, category: category, expiry: banExpiry, isModerator: false); } if (didBan) { var banStart = (await db.Moderation.AsNoTracking().FirstOrDefaultAsync(x => x.PlayerId == userId && x.IsBan))?.CreatedAt; await PlayerUtils.KickOrPromptAsync(db, userId, PlayerUtils.BuildBlockDetails(true, category, reason, banExpiry, appealCode: appealCode, startedAtUtc: banStart ?? DateTime.UtcNow)); } if (didBan) { return Ok(duration == -1 ? $"Successfully banned @{profile.username} permanently!" : $"Successfully banned @{profile.username} for {duration} minutes!"); } else return Ok($"Successfully unbanned @{profile.username}!"); } [HttpGet("coach")] public async Task SendCoachMessage([FromQuery] string key, [FromQuery] long userId, [FromQuery] string message) { var rejection = await RejectInvalidKey(key); if (rejection != null) return rejection; var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == userId); if (profile == null) return NotFound("User does not exist."); await Notify.SendMessage(userId, new Message { Type = MessageType.CoachMessage, Data = message, FromPlayerId = 1 }); return Ok($"Successfully sent coach message to @{profile.username}!"); } [HttpGet("coach/all")] public async Task SendCoachMessageToAll([FromQuery] string key, [FromQuery] string message) { var rejection = await RejectInvalidKey(key); if (rejection != null) return rejection; var profiles = await db.Profiles.ToListAsync(); long playerCount = 0; foreach (var profile in profiles) { if (profile.accountId == 1) continue; await Notify.SendMessage(profile.accountId, new Message { Type = MessageType.CoachMessage, Data = message, FromPlayerId = 1 }); playerCount++; } return Ok($"Successfully sent coach message to {playerCount} players!"); } [HttpGet("forcequit")] public async Task ForceQuitPlayer([FromQuery] string key, [FromQuery] long userId) { var rejection = await RejectInvalidKey(key); if (rejection != null) return rejection; var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == userId); if (profile == null) return NotFound("User does not exist."); Notify.SendReliably(userId, new Notification { Id = NotificationType.ModerationQuitGame, Msg = "" }, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(1), Notify.EmptyObjectPayload(NotificationType.ModerationQuitGame)); var presence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == userId); if (presence != null) { presence.isOnline = false; presence.roomInstance = null; presence.LoginLock = null; await db.SaveChangesAsync(); } return Ok($"Successfully force quit @{profile.username}!"); } } }