Try to fix database sync with Antigravity.

This commit is contained in:
Creeper Lv
2026-06-01 06:38:20 +10:00
parent b263291dc2
commit c60540fb2b
4 changed files with 261 additions and 24 deletions
-1
View File
@@ -14,7 +14,6 @@ public class ServerDbContext : DbContext
public DbSet<NodeKey> NodeKeys => Set<NodeKey>(); public DbSet<NodeKey> NodeKeys => Set<NodeKey>();
public DbSet<PendingShare> PendingShares => Set<PendingShare>(); public DbSet<PendingShare> PendingShares => Set<PendingShare>();
public DbSet<AclEntry> AclEntries => Set<AclEntry>(); public DbSet<AclEntry> AclEntries => Set<AclEntry>();
public DbSet<PeerServer> PeerServers => Set<PeerServer>();
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
+59 -1
View File
@@ -18,7 +18,7 @@ public static class AuthEndpoints
{ {
var group = routes.MapGroup("/api/auth"); var group = routes.MapGroup("/api/auth");
group.MapPost("/register", async (RegisterRequest req, ServerDbContext db) => group.MapPost("/register", async (RegisterRequest req, HttpContext context, ServerDbContext db, PeerCache peerCache, RsaKeyManager rsaKeyManager, IConfiguration configuration, HttpClient httpClient) =>
{ {
if (string.IsNullOrWhiteSpace(req.Username) || string.IsNullOrWhiteSpace(req.Password) || string.IsNullOrWhiteSpace(req.EncryptedKey)) if (string.IsNullOrWhiteSpace(req.Username) || string.IsNullOrWhiteSpace(req.Password) || string.IsNullOrWhiteSpace(req.EncryptedKey))
{ {
@@ -41,9 +41,66 @@ public static class AuthEndpoints
db.Users.Add(user); db.Users.Add(user);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
_ = Task.Run(async () =>
{
try
{
using var scope = context.RequestServices.CreateScope();
var scopeDb = scope.ServiceProvider.GetRequiredService<ServerDbContext>();
await SyncEndpoints.BroadcastUserChangeAsync(user.Username, scopeDb, peerCache, rsaKeyManager, configuration, httpClient);
}
catch (Exception ex)
{
Console.WriteLine($"Error broadcasting user registration: {ex.Message}");
}
});
return Results.Ok(new { message = "Registration successful." }); return Results.Ok(new { message = "Registration successful." });
}); });
group.MapPost("/change-password", async (ChangePasswordRequest req, HttpContext context, ServerDbContext db, CertificateManager certManager, PeerCache peerCache, RsaKeyManager rsaKeyManager, IConfiguration configuration, HttpClient httpClient) =>
{
var username = AuthHelper.GetAuthenticatedUser(context, certManager);
if (string.IsNullOrEmpty(username)) return Results.Unauthorized();
if (string.IsNullOrWhiteSpace(req.OldPassword) || string.IsNullOrWhiteSpace(req.NewPassword) || string.IsNullOrWhiteSpace(req.NewEncryptedKey))
{
return Results.BadRequest("Old password, new password, and new encrypted key are required.");
}
var user = await db.Users.FirstOrDefaultAsync(u => u.Username.ToLower() == username.ToLower());
if (user == null) return Results.NotFound("User not found.");
// Verify old password
if (!PasswordHasher.VerifyPassword(user.PasswordHash, req.OldPassword))
{
return Results.BadRequest("Incorrect old password.");
}
// Update user properties
user.PasswordHash = PasswordHasher.HashPassword(req.NewPassword);
user.EncryptedKey = req.NewEncryptedKey;
await db.SaveChangesAsync();
// Broadcast the user password change!
_ = Task.Run(async () =>
{
try
{
using var scope = context.RequestServices.CreateScope();
var scopeDb = scope.ServiceProvider.GetRequiredService<ServerDbContext>();
await SyncEndpoints.BroadcastUserChangeAsync(user.Username, scopeDb, peerCache, rsaKeyManager, configuration, httpClient);
}
catch (Exception ex)
{
Console.WriteLine($"Error broadcasting password change: {ex.Message}");
}
});
return Results.Ok(new { message = "Password changed successfully." });
});
group.MapPost("/login", async (LoginRequest req, ServerDbContext db, CertificateManager certManager) => group.MapPost("/login", async (LoginRequest req, ServerDbContext db, CertificateManager certManager) =>
{ {
if (string.IsNullOrWhiteSpace(req.Username) || string.IsNullOrWhiteSpace(req.Password)) if (string.IsNullOrWhiteSpace(req.Username) || string.IsNullOrWhiteSpace(req.Password))
@@ -97,3 +154,4 @@ public static class AuthEndpoints
public record RegisterRequest(string Username, string Password, string EncryptedKey); public record RegisterRequest(string Username, string Password, string EncryptedKey);
public record LoginRequest(string Username, string Password); public record LoginRequest(string Username, string Password);
public record LoginResponse(string Token, string EncryptedKey); public record LoginResponse(string Token, string EncryptedKey);
public record ChangePasswordRequest(string OldPassword, string NewPassword, string NewEncryptedKey);
+59 -3
View File
@@ -338,7 +338,7 @@ public static class NodeEndpoints
}); });
// PUT update ACL entries // PUT update ACL entries
group.MapPut("/{uuid}/acl", async (string uuid, List<AclEntryRequest> aclReqs, HttpContext context, ServerDbContext db, CertificateManager certManager) => group.MapPut("/{uuid}/acl", async (string uuid, List<AclEntryRequest> aclReqs, HttpContext context, ServerDbContext db, CertificateManager certManager, PeerCache peerCache, HttpClient httpClient) =>
{ {
var username = AuthHelper.GetAuthenticatedUser(context, certManager); var username = AuthHelper.GetAuthenticatedUser(context, certManager);
if (string.IsNullOrEmpty(username)) return Results.Unauthorized(); if (string.IsNullOrEmpty(username)) return Results.Unauthorized();
@@ -431,11 +431,27 @@ public static class NodeEndpoints
node.UpdatedAt = DateTime.UtcNow; node.UpdatedAt = DateTime.UtcNow;
await db.SaveChangesAsync(); await db.SaveChangesAsync();
_ = Task.Run(async () =>
{
try
{
using var scope = context.RequestServices.CreateScope();
var scopeDb = scope.ServiceProvider.GetRequiredService<ServerDbContext>();
var rsaKeyManager = scope.ServiceProvider.GetRequiredService<RsaKeyManager>();
var configuration = scope.ServiceProvider.GetRequiredService<IConfiguration>();
await SyncEndpoints.BroadcastNodeChangeAsync(uuid, scopeDb, peerCache, rsaKeyManager, configuration, httpClient);
}
catch (Exception ex)
{
Console.WriteLine($"Error broadcasting ACL change: {ex.Message}");
}
});
return Results.Ok(new { message = "ACLs updated successfully." }); return Results.Ok(new { message = "ACLs updated successfully." });
}); });
// POST Share a Node (generates k0 encrypted by k1 and saves in PendingShare) // POST Share a Node (generates k0 encrypted by k1 and saves in PendingShare)
group.MapPost("/share", async (ShareNodeRequest req, HttpContext context, ServerDbContext db, CertificateManager certManager) => group.MapPost("/share", async (ShareNodeRequest req, HttpContext context, ServerDbContext db, CertificateManager certManager, PeerCache peerCache, HttpClient httpClient) =>
{ {
var username = AuthHelper.GetAuthenticatedUser(context, certManager); var username = AuthHelper.GetAuthenticatedUser(context, certManager);
if (string.IsNullOrEmpty(username)) return Results.Unauthorized(); if (string.IsNullOrEmpty(username)) return Results.Unauthorized();
@@ -499,13 +515,30 @@ public static class NodeEndpoints
}); });
} }
node.UpdatedAt = DateTime.UtcNow;
await db.SaveChangesAsync(); await db.SaveChangesAsync();
_ = Task.Run(async () =>
{
try
{
using var scope = context.RequestServices.CreateScope();
var scopeDb = scope.ServiceProvider.GetRequiredService<ServerDbContext>();
var rsaKeyManager = scope.ServiceProvider.GetRequiredService<RsaKeyManager>();
var configuration = scope.ServiceProvider.GetRequiredService<IConfiguration>();
await SyncEndpoints.BroadcastNodeChangeAsync(req.NodeUuid, scopeDb, peerCache, rsaKeyManager, configuration, httpClient);
}
catch (Exception ex)
{
Console.WriteLine($"Error broadcasting share change: {ex.Message}");
}
});
return Results.Ok(new { message = "Share initiated successfully.", guid = req.NodeUuid }); return Results.Ok(new { message = "Share initiated successfully.", guid = req.NodeUuid });
}); });
// POST Accept a Share (decrypts E_k1(k0) using k1 and re-encrypts using recipient's masterKey) // POST Accept a Share (decrypts E_k1(k0) using k1 and re-encrypts using recipient's masterKey)
group.MapPost("/accept-share", async (AcceptShareRequest req, HttpContext context, ServerDbContext db, CertificateManager certManager) => group.MapPost("/accept-share", async (AcceptShareRequest req, HttpContext context, ServerDbContext db, CertificateManager certManager, PeerCache peerCache, HttpClient httpClient) =>
{ {
var username = AuthHelper.GetAuthenticatedUser(context, certManager); var username = AuthHelper.GetAuthenticatedUser(context, certManager);
if (string.IsNullOrEmpty(username)) return Results.Unauthorized(); if (string.IsNullOrEmpty(username)) return Results.Unauthorized();
@@ -563,8 +596,31 @@ public static class NodeEndpoints
// Delete pending share // Delete pending share
db.PendingShares.Remove(pendingShare); db.PendingShares.Remove(pendingShare);
// Fetch the corresponding node and update UpdatedAt timestamp
var node = await db.Nodes.FirstOrDefaultAsync(n => n.Uuid == req.NodeUuid && !n.IsDeleted);
if (node != null)
{
node.UpdatedAt = DateTime.UtcNow;
}
await db.SaveChangesAsync(); await db.SaveChangesAsync();
_ = Task.Run(async () =>
{
try
{
using var scope = context.RequestServices.CreateScope();
var scopeDb = scope.ServiceProvider.GetRequiredService<ServerDbContext>();
var rsaKeyManager = scope.ServiceProvider.GetRequiredService<RsaKeyManager>();
var configuration = scope.ServiceProvider.GetRequiredService<IConfiguration>();
await SyncEndpoints.BroadcastNodeChangeAsync(req.NodeUuid, scopeDb, peerCache, rsaKeyManager, configuration, httpClient);
}
catch (Exception ex)
{
Console.WriteLine($"Error broadcasting accept-share change: {ex.Message}");
}
});
return Results.Ok(new { message = "Share accepted and linked successfully." }); return Results.Ok(new { message = "Share accepted and linked successfully." });
}); });
} }
+143 -19
View File
@@ -112,9 +112,8 @@ public static class SyncEndpoints
var nodeKeys = await db.NodeKeys.ToListAsync(); var nodeKeys = await db.NodeKeys.ToListAsync();
var pendingShares = await db.PendingShares.ToListAsync(); var pendingShares = await db.PendingShares.ToListAsync();
var aclEntries = await db.AclEntries.ToListAsync(); var aclEntries = await db.AclEntries.ToListAsync();
var peerServers = await db.PeerServers.ToListAsync();
var dump = new DatabaseDumpPayload(users, nodes, nodeKeys, pendingShares, aclEntries, peerServers); var dump = new DatabaseDumpPayload(users, nodes, nodeKeys, pendingShares, aclEntries);
return Results.Ok(dump); return Results.Ok(dump);
}); });
@@ -234,6 +233,10 @@ public static class SyncEndpoints
db.Nodes.Add(item.Node); db.Nodes.Add(item.Node);
db.AclEntries.AddRange(item.AclEntries); db.AclEntries.AddRange(item.AclEntries);
db.NodeKeys.AddRange(item.NodeKeys); db.NodeKeys.AddRange(item.NodeKeys);
if (item.PendingShares != null)
{
db.PendingShares.AddRange(item.PendingShares);
}
updated = true; updated = true;
} }
else if (localNode.UpdatedAt < item.Node.UpdatedAt) else if (localNode.UpdatedAt < item.Node.UpdatedAt)
@@ -253,6 +256,13 @@ public static class SyncEndpoints
db.NodeKeys.RemoveRange(existingKeys); db.NodeKeys.RemoveRange(existingKeys);
db.NodeKeys.AddRange(item.NodeKeys); db.NodeKeys.AddRange(item.NodeKeys);
var existingShares = await db.PendingShares.Where(s => s.NodeUuid == item.Node.Uuid).ToListAsync();
db.PendingShares.RemoveRange(existingShares);
if (item.PendingShares != null)
{
db.PendingShares.AddRange(item.PendingShares);
}
updated = true; updated = true;
} }
@@ -273,6 +283,59 @@ public static class SyncEndpoints
return Results.Ok(new { message = "Broadcast processed.", updated }); return Results.Ok(new { message = "Broadcast processed.", updated });
}); });
// Receive broadcast of user registration or password updates
routes.MapPost("/api/sync/broadcast-user", async (UserBroadcastPayload payload, HttpContext context, ServerDbContext db, PeerCache peerCache, RsaKeyManager rsaKeyManager, HttpClient httpClient) =>
{
if (!AuthHelper.IsServerTokenValid(context, peerCache))
{
return Results.Unauthorized();
}
var broadcastId = payload.BroadcastId;
if (!string.IsNullOrEmpty(broadcastId))
{
if (!peerCache.TryProcessBroadcast(broadcastId))
{
return Results.Ok(new { message = "User broadcast already processed (loop detected).", updated = false });
}
}
var existingUser = await db.Users.FirstOrDefaultAsync(u => u.Username.ToLower() == payload.User.Username.ToLower());
bool updated = false;
if (existingUser == null)
{
db.Users.Add(payload.User);
updated = true;
}
else
{
if (existingUser.PasswordHash != payload.User.PasswordHash || existingUser.EncryptedKey != payload.User.EncryptedKey)
{
existingUser.PasswordHash = payload.User.PasswordHash;
existingUser.EncryptedKey = payload.User.EncryptedKey;
updated = true;
}
}
if (updated)
{
await db.SaveChangesAsync();
// Relay the user broadcast to other peers
if (!string.IsNullOrEmpty(broadcastId))
{
_ = Task.Run(async () =>
{
var configuration = context.RequestServices.GetRequiredService<IConfiguration>();
await BroadcastUserChangeAsync(payload.User.Username, db, peerCache, rsaKeyManager, configuration, httpClient, broadcastId);
});
}
}
return Results.Ok(new { message = "User broadcast processed.", updated });
});
} }
// Handshake: Tells the destination server who we are and registers us using RSA asymmetric verification // Handshake: Tells the destination server who we are and registers us using RSA asymmetric verification
@@ -353,32 +416,33 @@ public static class SyncEndpoints
var acls = await db.AclEntries.Where(a => a.NodeUuid == nodeUuid).ToListAsync(); var acls = await db.AclEntries.Where(a => a.NodeUuid == nodeUuid).ToListAsync();
var keys = await db.NodeKeys.Where(k => k.NodeUuid == nodeUuid).ToListAsync(); var keys = await db.NodeKeys.Where(k => k.NodeUuid == nodeUuid).ToListAsync();
var pendingShares = await db.PendingShares.Where(s => s.NodeUuid == nodeUuid).ToListAsync();
var bId = string.IsNullOrEmpty(broadcastId) ? Guid.NewGuid().ToString() : broadcastId; var bId = string.IsNullOrEmpty(broadcastId) ? Guid.NewGuid().ToString() : broadcastId;
// Register it locally in PeerCache so we don't process it ourselves if it loops back // Register it locally in PeerCache so we don't process it ourselves if it loops back
peerCache.TryProcessBroadcast(bId); peerCache.TryProcessBroadcast(bId);
var payload = new SyncPushItem(node, acls, keys, bId); var payload = new SyncPushItem(node, acls, keys, bId, pendingShares);
var json = JsonSerializer.Serialize(payload); var json = JsonSerializer.Serialize(payload);
// Fetch database configured peers (upstream destination)
var dbPeers = await db.PeerServers.Select(p => p.Url).ToListAsync();
// Fetch dynamically handshake-registered in-memory peers (downstream child nodes) // Fetch dynamically handshake-registered in-memory peers (downstream child nodes)
var dynamicPeers = peerCache.GetPeers(); var allPeers = peerCache.GetPeers();
// Combine peers var localUrl = (configuration["Sync:LocalServerUrl"] ?? "").Trim().TrimEnd('/');
var allPeers = dbPeers.Union(dynamicPeers, StringComparer.OrdinalIgnoreCase).ToList(); var destUrl = (configuration["Sync:DestinationServerUrl"] ?? "").Trim().TrimEnd('/');
var localUrl = configuration["Sync:LocalServerUrl"] ?? ""; if (!string.IsNullOrEmpty(destUrl) && !allPeers.Contains(destUrl, StringComparer.OrdinalIgnoreCase))
var destUrl = configuration["Sync:DestinationServerUrl"] ?? "";
foreach (var peerUrl in allPeers)
{ {
allPeers.Add(destUrl);
}
foreach (var rawPeerUrl in allPeers)
{
var peerUrl = rawPeerUrl.Trim().TrimEnd('/');
try try
{ {
var request = new HttpRequestMessage(HttpMethod.Post, $"{peerUrl.TrimEnd('/')}/api/sync/broadcast"); var request = new HttpRequestMessage(HttpMethod.Post, $"{peerUrl}/api/sync/broadcast");
request.Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); request.Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
// Attach custom headers for secure authentication // Attach custom headers for secure authentication
@@ -412,6 +476,67 @@ public static class SyncEndpoints
} }
} }
public static async Task BroadcastUserChangeAsync(string username, ServerDbContext db, PeerCache peerCache, RsaKeyManager rsaKeyManager, IConfiguration configuration, HttpClient httpClient, string? broadcastId = null)
{
var user = await db.Users.FirstOrDefaultAsync(u => u.Username.ToLower() == username.ToLower());
if (user == null) return;
var bId = string.IsNullOrEmpty(broadcastId) ? Guid.NewGuid().ToString() : broadcastId;
// Register it locally in PeerCache so we don't process it ourselves if it loops back
peerCache.TryProcessBroadcast(bId);
var payload = new UserBroadcastPayload(user, bId);
var json = JsonSerializer.Serialize(payload);
// Fetch dynamically handshake-registered in-memory peers (downstream child nodes)
var allPeers = peerCache.GetPeers();
var localUrl = (configuration["Sync:LocalServerUrl"] ?? "").Trim().TrimEnd('/');
var destUrl = (configuration["Sync:DestinationServerUrl"] ?? "").Trim().TrimEnd('/');
if (!string.IsNullOrEmpty(destUrl) && !allPeers.Contains(destUrl, StringComparer.OrdinalIgnoreCase))
{
allPeers.Add(destUrl);
}
foreach (var rawPeerUrl in allPeers)
{
var peerUrl = rawPeerUrl.Trim().TrimEnd('/');
try
{
var request = new HttpRequestMessage(HttpMethod.Post, $"{peerUrl}/api/sync/broadcast-user");
request.Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
// Attach custom headers for secure authentication
request.Headers.Add("X-Server-Url", localUrl);
if (string.Equals(peerUrl, destUrl, StringComparison.OrdinalIgnoreCase))
{
if (!string.IsNullOrEmpty(UpstreamSessionToken))
{
request.Headers.Add("X-Server-Token", UpstreamSessionToken);
}
}
else
{
var token = peerCache.GetToken(peerUrl);
if (!string.IsNullOrEmpty(token))
{
request.Headers.Add("X-Server-Token", token);
}
}
var response = await httpClient.SendAsync(request);
Console.WriteLine($"User broadcast to {peerUrl}: {response.StatusCode}");
}
catch (Exception ex)
{
Console.WriteLine($"Failed to broadcast user change to {peerUrl}: {ex.Message}");
}
}
}
// Periodically pings downstream peer servers in PeerCache to ensure they are online and authentic // Periodically pings downstream peer servers in PeerCache to ensure they are online and authentic
public static async Task StartHeartbeatPingerAsync(PeerCache peerCache, RsaKeyManager rsaKeyManager, HttpClient httpClient, System.Threading.CancellationToken token) public static async Task StartHeartbeatPingerAsync(PeerCache peerCache, RsaKeyManager rsaKeyManager, HttpClient httpClient, System.Threading.CancellationToken token)
{ {
@@ -607,7 +732,6 @@ public static class SyncEndpoints
db.NodeKeys.RemoveRange(await db.NodeKeys.ToListAsync()); db.NodeKeys.RemoveRange(await db.NodeKeys.ToListAsync());
db.PendingShares.RemoveRange(await db.PendingShares.ToListAsync()); db.PendingShares.RemoveRange(await db.PendingShares.ToListAsync());
db.AclEntries.RemoveRange(await db.AclEntries.ToListAsync()); db.AclEntries.RemoveRange(await db.AclEntries.ToListAsync());
db.PeerServers.RemoveRange(await db.PeerServers.ToListAsync());
await db.SaveChangesAsync(); await db.SaveChangesAsync();
db.Users.AddRange(dump.Users); db.Users.AddRange(dump.Users);
@@ -615,7 +739,6 @@ public static class SyncEndpoints
db.NodeKeys.AddRange(dump.NodeKeys); db.NodeKeys.AddRange(dump.NodeKeys);
db.PendingShares.AddRange(dump.PendingShares); db.PendingShares.AddRange(dump.PendingShares);
db.AclEntries.AddRange(dump.AclEntries); db.AclEntries.AddRange(dump.AclEntries);
db.PeerServers.AddRange(dump.PeerServers);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
Console.WriteLine("Full database pull bootstrap completed successfully!"); Console.WriteLine("Full database pull bootstrap completed successfully!");
@@ -641,13 +764,14 @@ public record DatabaseDumpPayload(
List<Node> Nodes, List<Node> Nodes,
List<NodeKey> NodeKeys, List<NodeKey> NodeKeys,
List<PendingShare> PendingShares, List<PendingShare> PendingShares,
List<AclEntry> AclEntries, List<AclEntry> AclEntries
List<PeerServer> PeerServers
); );
public record SyncNodeState(string Uuid, DateTime UpdatedAt, bool IsDeleted); public record SyncNodeState(string Uuid, DateTime UpdatedAt, bool IsDeleted);
public record SyncExchangeRequest(List<SyncNodeState> Items); public record SyncExchangeRequest(List<SyncNodeState> Items);
public record SyncPushItem(Node Node, List<AclEntry> AclEntries, List<NodeKey> NodeKeys, string? BroadcastId = null); public record SyncPushItem(Node Node, List<AclEntry> AclEntries, List<NodeKey> NodeKeys, string? BroadcastId = null, List<PendingShare>? PendingShares = null);
public record SyncExchangeResponse(List<SyncPushItem> ToPush, List<string> ToRequest); public record SyncExchangeResponse(List<SyncPushItem> ToPush, List<string> ToRequest);
public record PingResponse(string PublicKey, string Timestamp, string Token); public record PingResponse(string PublicKey, string Timestamp, string Token);
public record UserBroadcastPayload(User User, string? BroadcastId = null);