using LiteDB; using Campus_live_rewrite.api.server; namespace Campus_live_rewrite.api.dbs { /// /// Device and IP bans. /// /// An account ban (player_extra.ModerationBlockDetails) only stops that one account - /// the player makes a new one and is straight back in. A device or IP ban is checked /// at LOGIN, before a token is issued, so every account from that machine or address /// is refused. /// /// Lives in the existing Players.db so it is covered by the WAL checkpointing in /// DbMaintenance rather than adding another file that never checkpoints. /// public static class BanDB { public const string Device = "device"; public const string Ip = "ip"; public class BanEntry { [BsonId] public string Key { get; set; } = ""; // "device:" or "ip:" public string Type { get; set; } = ""; // Device | Ip public string Value { get; set; } = ""; // device id, or AES-encrypted IP public ulong PlayerId { get; set; } // account the ban originated from public ulong BannedBy { get; set; } public string? Reason { get; set; } public long CreatedAt { get; set; } /// Unix seconds when this lapses. 0 = permanent. public long ExpiresAt { get; set; } } private static ILiteCollection Col { get { var col = PlayerDB.PlayerDBFile.GetCollection("bans"); col.EnsureIndex(x => x.Type); col.EnsureIndex(x => x.PlayerId); return col; } } private static string KeyFor(string type, string value) => $"{type}:{value}"; private static long Now => DateTimeOffset.UtcNow.ToUnixTimeSeconds(); /// /// IPs are stored AES-encrypted, matching player_extra.IpAddresses. Utils.EncryptAes /// uses a fixed IV, so the same address always produces the same ciphertext - which /// is what makes a stored IP comparable at all. /// private static string NormalizeIp(string ip) => Utils.EncryptAes((ip ?? "").Trim()); // ---- writing ----------------------------------------------------------- /// Bans a device id. durationSeconds <= 0 (or int.MaxValue) = permanent. public static bool BanDevice(string deviceId, ulong playerId, ulong bannedBy, string? reason, int durationSeconds) { if (string.IsNullOrWhiteSpace(deviceId)) return false; return Upsert(Device, deviceId.Trim(), playerId, bannedBy, reason, durationSeconds); } /// Bans an IP address (plaintext in; stored encrypted). public static bool BanIp(string ip, ulong playerId, ulong bannedBy, string? reason, int durationSeconds) { if (string.IsNullOrWhiteSpace(ip)) return false; return Upsert(Ip, NormalizeIp(ip), playerId, bannedBy, reason, durationSeconds); } private static bool Upsert(string type, string value, ulong playerId, ulong bannedBy, string? reason, int durationSeconds) { long expires = (durationSeconds <= 0 || durationSeconds == int.MaxValue) ? 0 : Now + durationSeconds; // Drop any cached "not banned" decision so the 502 applies immediately. if (type == Ip) BannedIpMiddleware.Forget(value == null ? null : Utils.DecryptAes(value)); Col.Upsert(new BanEntry { Key = KeyFor(type, value), Type = type, Value = value, PlayerId = playerId, BannedBy = bannedBy, Reason = reason, CreatedAt = Now, ExpiresAt = expires }); return true; } // ---- checking ---------------------------------------------------------- public static BanEntry? GetDeviceBan(string? deviceId) => string.IsNullOrWhiteSpace(deviceId) ? null : GetActive(Device, deviceId.Trim()); public static BanEntry? GetIpBan(string? ip) => string.IsNullOrWhiteSpace(ip) ? null : GetActive(Ip, NormalizeIp(ip)); public static bool IsDeviceBanned(string? deviceId) => GetDeviceBan(deviceId) != null; public static bool IsIpBanned(string? ip) => GetIpBan(ip) != null; /// Returns the entry only if it is still in force; expired entries are removed. private static BanEntry? GetActive(string type, string value) { var entry = Col.FindById(KeyFor(type, value)); if (entry == null) return null; if (entry.ExpiresAt > 0 && entry.ExpiresAt <= Now) { Col.Delete(entry.Key); // self-cleaning: a lapsed ban stops applying return null; } return entry; } // ---- lifting ----------------------------------------------------------- public static bool UnbanDevice(string? deviceId) => !string.IsNullOrWhiteSpace(deviceId) && Col.Delete(KeyFor(Device, deviceId!.Trim())); public static bool UnbanIp(string? ip) { if (string.IsNullOrWhiteSpace(ip)) return false; BannedIpMiddleware.Forget(ip); // release immediately, don't wait for the cache return Col.Delete(KeyFor(Ip, NormalizeIp(ip!))); } /// /// Lifts every device/IP ban that originated from this account. Used by the admin /// panel's Unban so lifting a ban actually lets the player back in, instead of /// clearing the account block while their device stays blacklisted. /// public static int UnbanAllForPlayer(ulong playerId) { var entries = Col.Find(x => x.PlayerId == playerId).ToList(); int n = 0; foreach (var e in entries) if (Col.Delete(e.Key)) n++; // An unban must let them straight back in, not after the cache expires. if (n > 0) BannedIpMiddleware.ForgetAll(); return n; } // ---- listing ----------------------------------------------------------- public static List ForPlayer(ulong playerId) => Col.Find(x => x.PlayerId == playerId).ToList(); public static List All() => Col.FindAll().OrderByDescending(x => x.CreatedAt).ToList(); } }