Add multi-user MAX authentication and tenant isolation
This commit is contained in:
@@ -90,6 +90,7 @@ builder.Services.AddControllers().AddJsonOptions(options =>
|
||||
});
|
||||
builder.Services.AddSignalR();
|
||||
builder.Services.AddHttpClient();
|
||||
builder.Services.AddSingleton<ICurrentUserAccessor, CurrentUserAccessor>();
|
||||
builder.Services.AddSingleton<ITokenService, TokenService>();
|
||||
builder.Services.AddScoped<ChatProjectionService>();
|
||||
builder.Services.AddScoped<IPushNotificationService, FirebasePushNotificationService>();
|
||||
@@ -117,6 +118,15 @@ app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
|
||||
app.UseCors();
|
||||
app.UseAuthentication();
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
var currentUser = context.RequestServices.GetRequiredService<ICurrentUserAccessor>();
|
||||
var userId = context.User.Identity?.IsAuthenticated == true ? context.User.GetUserId() : (Guid?)null;
|
||||
using (currentUser.Push(userId))
|
||||
{
|
||||
await next(context);
|
||||
}
|
||||
});
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
app.MapHub<QMaxHub>("/hubs/qmax");
|
||||
@@ -127,8 +137,12 @@ using (var scope = app.Services.CreateScope())
|
||||
Directory.CreateDirectory(options.StoragePath);
|
||||
Directory.CreateDirectory(options.ReleasesPath);
|
||||
var db = scope.ServiceProvider.GetRequiredService<QMaxDbContext>();
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
await EnsureCompatibilitySchemaAsync(db);
|
||||
var currentUser = scope.ServiceProvider.GetRequiredService<ICurrentUserAccessor>();
|
||||
using (currentUser.Push(null, bypassTenantFilter: true))
|
||||
{
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
await EnsureCompatibilitySchemaAsync(db);
|
||||
}
|
||||
}
|
||||
|
||||
app.Run();
|
||||
@@ -152,6 +166,34 @@ static async Task EnsureCompatibilitySchemaAsync(QMaxDbContext db)
|
||||
}
|
||||
}
|
||||
|
||||
if (!chatColumns.Contains("UserId"))
|
||||
{
|
||||
await db.Database.ExecuteSqlRawAsync("ALTER TABLE Chats ADD COLUMN UserId TEXT NULL;");
|
||||
var legacyUserId = await db.Users.Select(x => x.Id).FirstOrDefaultAsync();
|
||||
if (legacyUserId != Guid.Empty)
|
||||
{
|
||||
await db.Database.ExecuteSqlInterpolatedAsync($"UPDATE Chats SET UserId = {legacyUserId} WHERE UserId IS NULL;");
|
||||
}
|
||||
}
|
||||
|
||||
var maxStateColumns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
await using (var command = connection.CreateCommand())
|
||||
{
|
||||
command.CommandText = "PRAGMA table_info(MaxAccountStates);";
|
||||
await using var reader = await command.ExecuteReaderAsync();
|
||||
while (await reader.ReadAsync()) maxStateColumns.Add(reader.GetString(1));
|
||||
}
|
||||
if (maxStateColumns.Count > 0 && !maxStateColumns.Contains("UserId"))
|
||||
{
|
||||
await db.Database.ExecuteSqlRawAsync("ALTER TABLE MaxAccountStates ADD COLUMN UserId TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000';");
|
||||
var legacyUserId = await db.Users.Select(x => x.Id).FirstOrDefaultAsync();
|
||||
if (legacyUserId != Guid.Empty)
|
||||
{
|
||||
await db.Database.ExecuteSqlInterpolatedAsync($"UPDATE MaxAccountStates SET UserId = {legacyUserId} WHERE UserId = '00000000-0000-0000-0000-000000000000';");
|
||||
}
|
||||
await db.Database.ExecuteSqlRawAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_MaxAccountStates_UserId ON MaxAccountStates (UserId);");
|
||||
}
|
||||
|
||||
if (!chatColumns.Contains("WebUrl"))
|
||||
{
|
||||
await db.Database.ExecuteSqlRawAsync("ALTER TABLE Chats ADD COLUMN WebUrl TEXT;");
|
||||
@@ -218,6 +260,10 @@ static async Task EnsureCompatibilitySchemaAsync(QMaxDbContext db)
|
||||
await QMaxDatabaseCleanup.MergeOutgoingRemoteAttachmentEchoesAsync(db);
|
||||
await QMaxDatabaseCleanup.ClearUnreadCountsForLatestOutgoingChatsAsync(db);
|
||||
await db.Database.ExecuteSqlRawAsync("DROP INDEX IF EXISTS IX_MessageAttachments_MessageId_ExternalId;");
|
||||
await db.Database.ExecuteSqlRawAsync("DROP INDEX IF EXISTS IX_Chats_ExternalId;");
|
||||
await db.Database.ExecuteSqlRawAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_Chats_UserId_ExternalId ON Chats (UserId, ExternalId);");
|
||||
await db.Database.ExecuteSqlRawAsync("DROP INDEX IF EXISTS IX_Users_PhoneNumber;");
|
||||
await db.Database.ExecuteSqlRawAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_Users_PhoneNumber ON Users (PhoneNumber) WHERE PhoneNumber IS NOT NULL AND PhoneNumber <> '';");
|
||||
await db.Database.ExecuteSqlRawAsync("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS IX_MessageAttachments_MessageId_ExternalId
|
||||
ON MessageAttachments (MessageId, ExternalId)
|
||||
@@ -255,6 +301,22 @@ static async Task EnsureCompatibilitySchemaAsync(QMaxDbContext db)
|
||||
CONSTRAINT FK_MessageReactions_Messages_MessageId FOREIGN KEY (MessageId) REFERENCES Messages (Id) ON DELETE CASCADE
|
||||
);
|
||||
""");
|
||||
|
||||
await db.Database.ExecuteSqlRawAsync("""
|
||||
CREATE TABLE IF NOT EXISTS MaxLoginChallenges (
|
||||
Id TEXT NOT NULL CONSTRAINT PK_MaxLoginChallenges PRIMARY KEY,
|
||||
UserId TEXT NOT NULL,
|
||||
SecretHash TEXT NOT NULL,
|
||||
DeviceName TEXT NOT NULL,
|
||||
CreatedAt TEXT NOT NULL,
|
||||
ExpiresAt TEXT NOT NULL,
|
||||
CompletedAt TEXT NULL,
|
||||
FailedAttempts INTEGER NOT NULL DEFAULT 0,
|
||||
CONSTRAINT FK_MaxLoginChallenges_Users_UserId FOREIGN KEY (UserId) REFERENCES Users (Id) ON DELETE CASCADE
|
||||
);
|
||||
""");
|
||||
await db.Database.ExecuteSqlRawAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_MaxLoginChallenges_SecretHash ON MaxLoginChallenges (SecretHash);");
|
||||
await db.Database.ExecuteSqlRawAsync("CREATE INDEX IF NOT EXISTS IX_MaxLoginChallenges_UserId ON MaxLoginChallenges (UserId);");
|
||||
await db.Database.ExecuteSqlRawAsync("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS IX_MessageReactions_MessageId_ActorKey
|
||||
ON MessageReactions (MessageId, ActorKey);
|
||||
|
||||
Reference in New Issue
Block a user