diff --git a/apps/server/AliasVault.Api/Controllers/AuthController.cs b/apps/server/AliasVault.Api/Controllers/AuthController.cs index 4a660acdb..48d2880ba 100644 --- a/apps/server/AliasVault.Api/Controllers/AuthController.cs +++ b/apps/server/AliasVault.Api/Controllers/AuthController.cs @@ -110,10 +110,11 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM if (AppInfo.MinimumClientVersions.TryGetValue(platform, out var minimumVersion)) { - if (VersionHelper.IsVersionEqualOrNewer(clientVersion, minimumVersion)) - { - clientSupported = true; - } + // Check if version meets minimum requirement AND is not in blocked list + var meetsMinimum = VersionHelper.IsVersionEqualOrNewer(clientVersion, minimumVersion); + var isBlocked = VersionHelper.IsVersionBlocked(platform, clientVersion, AppInfo.UnsupportedClientVersions); + + clientSupported = meetsMinimum && !isBlocked; } else { diff --git a/apps/server/AliasVault.Api/Helpers/VersionHelper.cs b/apps/server/AliasVault.Api/Helpers/VersionHelper.cs index 5f2f6d4b6..34bcd8c93 100644 --- a/apps/server/AliasVault.Api/Helpers/VersionHelper.cs +++ b/apps/server/AliasVault.Api/Helpers/VersionHelper.cs @@ -59,4 +59,36 @@ public static class VersionHelper // Compare the versions return v1 >= v2; } + + /// + /// Checks if a version is blocked for a specific platform. + /// Checks both platform-specific blocks and global blocks (using "*" key). + /// + /// The platform to check (e.g., "chrome", "ios"). + /// The version to check. + /// Dictionary of platform to blocked versions. Use "*" for global blocks. + /// True if the version is blocked for this platform, false otherwise. + public static bool IsVersionBlocked(string platform, string version, IReadOnlyDictionary> blockedVersions) + { + if (string.IsNullOrEmpty(version) || blockedVersions == null || blockedVersions.Count == 0) + { + return false; + } + + // Check global blocks (applies to all platforms) + if (blockedVersions.TryGetValue("*", out var globalBlocked) && globalBlocked.Contains(version)) + { + return true; + } + + // Check platform-specific blocks + if (!string.IsNullOrEmpty(platform) && + blockedVersions.TryGetValue(platform, out var platformBlocked) && + platformBlocked.Contains(version)) + { + return true; + } + + return false; + } } diff --git a/apps/server/Shared/AliasVault.Shared.Core/AppInfo.cs b/apps/server/Shared/AliasVault.Shared.Core/AppInfo.cs index c78e9cb95..d79fc7b41 100644 --- a/apps/server/Shared/AliasVault.Shared.Core/AppInfo.cs +++ b/apps/server/Shared/AliasVault.Shared.Core/AppInfo.cs @@ -42,7 +42,7 @@ public static class AppInfo /// for all clients as we are using a monorepo to build all clients from the same source code. But it's /// possible to override the minimum client version for a specific client if needed. /// - public const string MinimumClientVersion = "0.26.1"; + public const string MinimumClientVersion = "0.12.0"; /// /// Gets a dictionary of minimum supported client versions that the WebApi supports. @@ -68,6 +68,24 @@ public static class AppInfo { "android", MinimumClientVersion }, }.AsReadOnly(); + /// + /// Gets a dictionary of specific client versions that are explicitly unsupported (blocked) per platform. + /// This is useful for blocking specific versions with (critical) bugs while still allowing + /// older versions that predate the bug to continue working. + /// For example: if 0.26.0 has a critical bug fixed in 0.26.1, we can block only 0.26.0 + /// without affecting users on 0.25.x who may still have compatible vaults. + /// Use "*" as the platform key to block a version across all platforms. + /// + public static IReadOnlyDictionary> UnsupportedClientVersions { get; } = new Dictionary> + { + // Block version across all platforms: "*" applies to all clients. + { "*", ["0.26.0"] }, // Version with vault migration bug, fixed in 0.26.1 + + // Platform-specific blocks (examples): + // { "chrome", ["0.25.0"] }, + // { "ios", ["0.24.0", "0.24.1"] }, + }; + /// /// Gets the build number, typically used in CI/CD pipelines. /// Can be overridden at build time. diff --git a/apps/server/Tests/AliasVault.UnitTests/Vault/VersionTests.cs b/apps/server/Tests/AliasVault.UnitTests/Vault/VersionTests.cs index 0b3685858..fb4fef0cc 100644 --- a/apps/server/Tests/AliasVault.UnitTests/Vault/VersionTests.cs +++ b/apps/server/Tests/AliasVault.UnitTests/Vault/VersionTests.cs @@ -68,4 +68,96 @@ public class VersionTests var version2 = "1.1.0"; Assert.That(VersionHelper.IsVersionEqualOrNewer(version1, version2), Is.True); } + + /// + /// Test that a version in the global blocked list is correctly identified as blocked. + /// + [Test] + public void VersionBlockedReturnsTrueForGloballyBlockedVersion() + { + var blockedVersions = new Dictionary> + { + { "*", ["0.26.0", "0.27.0"] }, + }; + + Assert.That(VersionHelper.IsVersionBlocked("chrome", "0.26.0", blockedVersions), Is.True); + Assert.That(VersionHelper.IsVersionBlocked("ios", "0.27.0", blockedVersions), Is.True); + Assert.That(VersionHelper.IsVersionBlocked("android", "0.26.0", blockedVersions), Is.True); + } + + /// + /// Test that a version in the platform-specific blocked list is correctly identified as blocked. + /// + [Test] + public void VersionBlockedReturnsTrueForPlatformSpecificBlockedVersion() + { + var blockedVersions = new Dictionary> + { + { "chrome", ["0.25.0"] }, + { "ios", ["0.24.0"] }, + }; + + // Platform-specific blocks should work + Assert.That(VersionHelper.IsVersionBlocked("chrome", "0.25.0", blockedVersions), Is.True); + Assert.That(VersionHelper.IsVersionBlocked("ios", "0.24.0", blockedVersions), Is.True); + + // Other platforms should not be blocked + Assert.That(VersionHelper.IsVersionBlocked("firefox", "0.25.0", blockedVersions), Is.False); + Assert.That(VersionHelper.IsVersionBlocked("android", "0.24.0", blockedVersions), Is.False); + } + + /// + /// Test that global and platform-specific blocks work together. + /// + [Test] + public void VersionBlockedCombinesGlobalAndPlatformSpecific() + { + var blockedVersions = new Dictionary> + { + { "*", ["0.26.0"] }, + { "chrome", ["0.25.0"] }, + }; + + // Global block applies to all platforms for exact version + Assert.That(VersionHelper.IsVersionBlocked("chrome", "0.26.0", blockedVersions), Is.True); + Assert.That(VersionHelper.IsVersionBlocked("ios", "0.26.0", blockedVersions), Is.True); + Assert.That(VersionHelper.IsVersionBlocked("ios", "0.26.1", blockedVersions), Is.False); + + // Platform-specific block only applies to that platform + Assert.That(VersionHelper.IsVersionBlocked("chrome", "0.25.0", blockedVersions), Is.True); + Assert.That(VersionHelper.IsVersionBlocked("firefox", "0.25.0", blockedVersions), Is.False); + } + + /// + /// Test that a version not in the blocked list is correctly identified as not blocked. + /// + [Test] + public void VersionBlockedReturnsFalseForNonBlockedVersion() + { + var blockedVersions = new Dictionary> + { + { "*", ["0.26.0"] }, + }; + + Assert.That(VersionHelper.IsVersionBlocked("chrome", "0.25.3", blockedVersions), Is.False); + Assert.That(VersionHelper.IsVersionBlocked("chrome", "0.26.1", blockedVersions), Is.False); + Assert.That(VersionHelper.IsVersionBlocked("chrome", "0.27.0", blockedVersions), Is.False); + } + + /// + /// Test that empty or null inputs are handled correctly. + /// + [Test] + public void VersionBlockedHandlesEmptyInputs() + { + var blockedVersions = new Dictionary> + { + { "*", ["0.26.0"] }, + }; + + var emptyBlockedVersions = new Dictionary>(); + + Assert.That(VersionHelper.IsVersionBlocked("chrome", string.Empty, blockedVersions), Is.False); + Assert.That(VersionHelper.IsVersionBlocked("chrome", "0.26.0", emptyBlockedVersions), Is.False); + } }