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);
+ }
}