diff --git a/.github/workflows/bitween-api-cicd-gateway.yml b/.github/workflows/bitween-api-cicd-gateway.yml
index 4caf2eb7..f0e9d2cc 100644
--- a/.github/workflows/bitween-api-cicd-gateway.yml
+++ b/.github/workflows/bitween-api-cicd-gateway.yml
@@ -94,4 +94,10 @@ jobs:
dependabot-alerts-token: ${{ secrets.DEPENDABOT_ALERTS_TOKEN }}
# App configuration passed to helm --set-string during deploy (masked)
- helm-set-secret-values: 'db=${{ secrets.dbConnection }},global.bus.rabbitUrl=${{ secrets.rabbitUrl }},global.cloudFiles.secretAccessKey=${{ secrets.SecretAccessKey }},global.cloudFiles.accessKeyId=${{ secrets.AccessKeyId }},global.cloudFiles.serviceUrl=${{ secrets.ServiceUrl }},global.cloudFiles.bucketName=${{ secrets.BucketName }},environmentVariables.Bitween__RabbitMqManagementUrl=${{ secrets.RabbitMqManagementUrl }},environmentVariables.Bitween__RabbitMqManagementPassword=${{ secrets.RabbitMqManagementPassword }},environmentVariables.Bitween__RabbitMqManagementUsername=${{ secrets.RabbitMqManagementUsername }}'
+ #
+ # global.token.key signs every authentication token. The chart still carries a sample
+ # key as its default, and that value is published in this public repository — anyone
+ # holding it can mint a token for any identity, so the app refuses to start on it.
+ # Set the TokenKey secret to a long random string unique to this environment
+ # (openssl rand -base64 48). Changing it signs out everyone holding an older token.
+ helm-set-secret-values: 'global.token.key=${{ secrets.TokenKey }},db=${{ secrets.dbConnection }},global.bus.rabbitUrl=${{ secrets.rabbitUrl }},global.cloudFiles.secretAccessKey=${{ secrets.SecretAccessKey }},global.cloudFiles.accessKeyId=${{ secrets.AccessKeyId }},global.cloudFiles.serviceUrl=${{ secrets.ServiceUrl }},global.cloudFiles.bucketName=${{ secrets.BucketName }},environmentVariables.Bitween__RabbitMqManagementUrl=${{ secrets.RabbitMqManagementUrl }},environmentVariables.Bitween__RabbitMqManagementPassword=${{ secrets.RabbitMqManagementPassword }},environmentVariables.Bitween__RabbitMqManagementUsername=${{ secrets.RabbitMqManagementUsername }}'
diff --git a/SW.Bitween.Api/Data/BitweenDbContext.cs b/SW.Bitween.Api/Data/BitweenDbContext.cs
index 4efef63e..11f53962 100644
--- a/SW.Bitween.Api/Data/BitweenDbContext.cs
+++ b/SW.Bitween.Api/Data/BitweenDbContext.cs
@@ -479,7 +479,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
Password = defaultPasswordHash,
Deleted = false,
Role = AccountRole.Admin,
- FailedLoginCount = 0
+ FailedLoginCount = 0,
+ // True for a fresh installation, whose password is the published default.
+ // Installations that already exist are handled by the migration, which
+ // flags only those still holding that same value.
+ MustChangePassword = true
});
});
diff --git a/SW.Bitween.Api/Domain/Accounts/Account.cs b/SW.Bitween.Api/Domain/Accounts/Account.cs
index 01e3b2ce..eadb08fe 100644
--- a/SW.Bitween.Api/Domain/Accounts/Account.cs
+++ b/SW.Bitween.Api/Domain/Accounts/Account.cs
@@ -31,6 +31,19 @@ public Account(string displayName, string email, string password, AccountRole ro
public int FailedLoginCount { get; private set; }
public DateTime? LockoutEnd { get; private set; }
+ ///
+ /// Set while the account still has a password nobody chose. Signing in works, but the token
+ /// it returns grants nothing until the password is replaced.
+ ///
+ ///
+ /// This exists for one account: the administrator seeded into every installation, whose
+ /// password ships in our public repository. On installations where it was never changed,
+ /// that published value was full administrative access to anyone who read the repository.
+ /// A migration sets this only where the stored password is still that one, so an
+ /// installation that changed it years ago notices nothing.
+ ///
+ public bool MustChangePassword { get; private set; }
+
public bool IsLockedOut(DateTime nowUtc) => LockoutEnd.HasValue && LockoutEnd.Value > nowUtc;
public void RegisterSuccessfulLogin()
@@ -71,6 +84,8 @@ private bool AddLoginMethod(LoginMethod loginMethod)
public void SetPassword(string password)
{
Password = SecurePasswordHasher.Hash(password);
+ // Whoever set this one chose it, which is the whole requirement.
+ MustChangePassword = false;
}
diff --git a/SW.Bitween.Api/Extensions/AccountExtensions.cs b/SW.Bitween.Api/Extensions/AccountExtensions.cs
index a647579e..e160fd12 100644
--- a/SW.Bitween.Api/Extensions/AccountExtensions.cs
+++ b/SW.Bitween.Api/Extensions/AccountExtensions.cs
@@ -89,6 +89,12 @@ private static ClaimsIdentity CreateClaimsIdentity(this Account account, LoginMe
if (account.Email != null) claims.Add(new Claim(ClaimTypes.Email, account.Email));
+ // Carried on the token rather than read per request like permissions are: it decides
+ // what the token itself is worth, and a token has to keep meaning the same thing for
+ // as long as it is valid.
+ if (account.MustChangePassword)
+ claims.Add(new Claim(RequestContextExtensions.MustChangePasswordClaim, "true"));
+
return new ClaimsIdentity(claims, "Bitween");
}
diff --git a/SW.Bitween.Api/Extensions/RequestContextExtensions.cs b/SW.Bitween.Api/Extensions/RequestContextExtensions.cs
index 93f5bb8f..cb3ccc06 100644
--- a/SW.Bitween.Api/Extensions/RequestContextExtensions.cs
+++ b/SW.Bitween.Api/Extensions/RequestContextExtensions.cs
@@ -11,13 +11,34 @@ namespace SW.Bitween
public static class RequestContextExtensions
{
///
- /// Marks the break-glass token minted by POST /login from configured AdminCredentials. That
- /// token has no account behind it, so its grants can't be resolved from the database. It
- /// used to clear every check only because the old role guard failed open on a missing
- /// claim; this claim makes the same grant deliberate instead of accidental.
+ /// Grants everything to an identity with no account behind it, so there are no roles to
+ /// resolve from the database. Only the integration test fixture mints it.
///
+ ///
+ /// It used to mark the token from POST /login, which signed in against a username
+ /// and password held in configuration. That defaulted to a working pair published in our
+ /// public repository, neither UI ever called it, and a penetration test used it to take
+ /// full control of a deployment. The endpoint is gone.
+ ///
+ /// The claim stays because it is how a caller with no account is granted anything at all.
+ /// Minting one needs the signing key, which is enough to impersonate anybody anyway.
+ ///
+ ///
public const string SuperuserClaim = "bitween_superuser";
+ ///
+ /// Present on a token issued to an account whose password nobody has chosen. Such a token
+ /// authenticates but grants nothing, so the account can reach self-service — changing the
+ /// password — and nothing else.
+ ///
+ ///
+ /// A sign-in has to succeed for the password to be changeable at all: the change requires
+ /// the current password and the caller's own identity, so refusing the sign-in outright
+ /// would leave the account with no way out but an administrator who may not exist. Granting
+ /// nothing is the same thing said in the only place that can act on it.
+ ///
+ public const string MustChangePasswordClaim = "bitween_must_change_password";
+
///
/// Throws unless the caller holds at least one of . This is really a
/// "forbidden" — the caller is signed in and simply isn't allowed — but CqApi renders
@@ -49,6 +70,11 @@ public static async Task HasPermission(this RequestContext requestContext,
public static async Task> GetPermissions(this RequestContext requestContext,
BitweenDbContext dbContext)
{
+ // Checked ahead of everything, superuser included: a password nobody chose is not a
+ // basis for any grant, whatever else the token claims.
+ if (requestContext.User?.FindFirst(MustChangePasswordClaim) is not null)
+ return [];
+
if (requestContext.User?.FindFirst(SuperuserClaim) is not null)
return PermissionCatalog.AllKeys.ToHashSet();
diff --git a/SW.Bitween.Api/Resources/Accounts/ChangePassword.cs b/SW.Bitween.Api/Resources/Accounts/ChangePassword.cs
index dd0b911b..3c13599c 100644
--- a/SW.Bitween.Api/Resources/Accounts/ChangePassword.cs
+++ b/SW.Bitween.Api/Resources/Accounts/ChangePassword.cs
@@ -1,4 +1,5 @@
using System;
+using System.Linq;
using System.Threading.Tasks;
using FluentValidation;
using Microsoft.EntityFrameworkCore;
@@ -28,6 +29,14 @@ public async Task
public string AdapterPath { get; set; }
- public string AdminCredentials { get; set; }
+
+ ///
+ /// Serves the API description and the Swagger interface. Off unless switched on.
+ ///
+ ///
+ /// A penetration test read all 94 endpoints and their request and response shapes off a
+ /// deployed instance without signing in, and used the repository link it found alongside
+ /// them to reach our published source. Useful while building, an inventory for a stranger
+ /// anywhere else.
+ ///
+ /// A setting of its own rather than a Development-only behaviour: the deployment chart
+ /// defaults ASPNETCORE_ENVIRONMENT to Development, so keying this to the environment would
+ /// have left it on for exactly the deployments nobody had configured.
+ ///
+ ///
+ public bool ExposeApiDocs { get; set; }
public string DocumentPrefix { get; set; }
public int ServerlessCommandTimeout { get; set; }
diff --git a/SW.Bitween.IntegrationTests/Fixtures/BitweenFixture.cs b/SW.Bitween.IntegrationTests/Fixtures/BitweenFixture.cs
index ffd74b51..f01a1ff1 100644
--- a/SW.Bitween.IntegrationTests/Fixtures/BitweenFixture.cs
+++ b/SW.Bitween.IntegrationTests/Fixtures/BitweenFixture.cs
@@ -179,7 +179,6 @@ await Task.WhenAll(_postgres.StartAsync(), _rabbitMq.StartAsync(), _mailHog.Star
StorageProvider = "LocalTests",
DatabaseType = "PgSql",
BusDefaultQueuePrefetch = 10,
- AdminCredentials = "configured-admin:configured-password",
JwtExpiryMinutes = 30,
// A passphrase has to exist or secret settings refuse to be stored at
// all, which would make the encryption path untestable.
diff --git a/SW.Bitween.IntegrationTests/Tests/MappingPreviewTests.cs b/SW.Bitween.IntegrationTests/Tests/MappingPreviewTests.cs
index e3f76dca..38c4196e 100644
--- a/SW.Bitween.IntegrationTests/Tests/MappingPreviewTests.cs
+++ b/SW.Bitween.IntegrationTests/Tests/MappingPreviewTests.cs
@@ -133,11 +133,17 @@ public async Task Unreadable_rules_are_a_general_error()
[Fact]
public async Task An_unsupported_format_names_what_is_supported()
{
- var response = await PreviewAsync(new { version = 1, sourceFormat = "csv" }, "{}");
+ // Named rather than a format that merely happens to be unsupported today: this test asked
+ // for "csv" until delimited text landed, at which point it stopped testing anything and
+ // failed on a null error message instead. Asserting the whole supported list is the half
+ // that has to be kept honest — the next format to arrive is meant to break this line, and
+ // it now says so out loud.
+ var response = await PreviewAsync(new { version = 1, sourceFormat = "yaml" }, "{}");
Assert.Contains("not a source format", response.Error);
- Assert.Contains("json", response.Error);
- Assert.Contains("xml", response.Error);
+ Assert.Equal(
+ "'yaml' is not a source format this mapper supports. Supported: csv, json, xml.",
+ response.Error);
}
[Fact]
diff --git a/SW.Bitween.IntegrationTests/Tests/PentestFindingTests.cs b/SW.Bitween.IntegrationTests/Tests/PentestFindingTests.cs
new file mode 100644
index 00000000..25679010
--- /dev/null
+++ b/SW.Bitween.IntegrationTests/Tests/PentestFindingTests.cs
@@ -0,0 +1,251 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using SW.Bitween.Domain;
+using SW.Bitween.Domain.Accounts;
+using SW.Bitween.IntegrationTests.Fixtures;
+using SW.Bitween.Model;
+using SW.Bitween.Services;
+using SW.PrimitiveTypes;
+using Xunit;
+
+namespace SW.Bitween.IntegrationTests.Tests;
+
+///
+/// The findings a penetration test raised against a deployed instance, each pinned by the thing
+/// that would have to stop being true for it to come back.
+///
+///
+/// Grouped by what they have in common rather than split across the handlers they touch: every one
+/// of them is a value leaving the system that should not, and none is visible from reading the
+/// handler alone — a leaked credential looks exactly like a populated field, and a missing rate
+/// limit looks exactly like a fast API. Tests are the only place that distinction is written down.
+///
+[Collection("Bitween")]
+public class PentestFindingTests(BitweenFixture fixture)
+{
+ ///
+ /// GIG.WEB-003. The list endpoint returned every integration's adapter properties in cleartext
+ /// — storage keys, an OAuth secret, partner passwords — to anyone allowed to see the list.
+ ///
+ [Fact]
+ public async Task Subscription_list_withholds_adapter_properties()
+ {
+ await using var scope = fixture.CreateScope();
+ var db = scope.ServiceProvider.GetRequiredService();
+ scope.Superuser();
+
+ var doc = new Document(null, "Pentest Secret Doc", DocumentFormat.Json);
+ db.Set().Add(doc);
+ await db.SaveChangesAsync();
+
+ var sub = new Subscription("Pentest Secret Sub", doc.Id);
+ sub.SetDictionaries(
+ handler: new Dictionary { ["AzureAccessKey"] = "the-storage-key" },
+ mapper: new Dictionary { ["ClientSecret"] = "the-oauth-secret" },
+ receiver: new Dictionary { ["Password"] = "the-partner-password" },
+ document: new Dictionary(),
+ validator: new Dictionary { ["Token"] = "the-validator-token" });
+ db.Set().Add(sub);
+ await db.SaveChangesAsync();
+
+ var handler = ActivatorUtilities.CreateInstance(scope.ServiceProvider);
+ var response = (SearchyResponse)await handler.Handle(
+ new SearchyRequest { PageSize = 500 });
+
+ var row = Assert.Single(response.Result.Where(r => r.Id == sub.Id));
+ Assert.Empty(row.HandlerProperties);
+ Assert.Empty(row.MapperProperties);
+ Assert.Empty(row.ReceiverProperties);
+ Assert.Empty(row.ValidatorProperties);
+ }
+
+ ///
+ /// The filter that searches inside property values still works, which is the whole reason the
+ /// properties are loaded and then dropped rather than left out of the query.
+ ///
+ ///
+ /// Paired with the test above on purpose: the obvious way to stop leaking the properties is to
+ /// stop selecting them, and that would silently break this search instead. Together they say
+ /// "loaded, used, withheld" — remove either and one of them fails.
+ ///
+ [Fact]
+ public async Task Searching_inside_property_values_still_finds_the_subscription()
+ {
+ await using var scope = fixture.CreateScope();
+ var db = scope.ServiceProvider.GetRequiredService();
+ scope.Superuser();
+
+ var doc = new Document(null, "Pentest Searchable Doc", DocumentFormat.Json);
+ db.Set().Add(doc);
+ await db.SaveChangesAsync();
+
+ var sub = new Subscription("Pentest Searchable Sub", doc.Id);
+ sub.SetDictionaries(
+ handler: new Dictionary { ["Url"] = "https://findme.example.test/inbound" },
+ mapper: new Dictionary(),
+ receiver: new Dictionary(),
+ document: new Dictionary(),
+ validator: new Dictionary());
+ db.Set().Add(sub);
+ await db.SaveChangesAsync();
+
+ var handler = ActivatorUtilities.CreateInstance(scope.ServiceProvider);
+ // Rule is immaterial — this field is filtered in memory by Contains, not translated.
+ var response = (SearchyResponse)await handler.Handle(
+ new SearchyRequest("filter=rawsubscriptionproperties:1:findme.example.test") { PageSize = 500 });
+
+ Assert.Contains(response.Result, r => r.Id == sub.Id);
+ }
+
+ ///
+ /// GIG.WEB-002, second half. The administrator seeded into every installation ships with a
+ /// password published in our repository; until it is replaced its token is worth nothing.
+ ///
+ [Fact]
+ public async Task An_unchanged_password_grants_no_permissions()
+ {
+ await using var scope = fixture.CreateScope();
+ var db = scope.ServiceProvider.GetRequiredService();
+
+ var account = new Account("Unchanged", "must-change@test.local", "hash", AccountRole.Admin);
+ db.Set().Add(account);
+ await db.SaveChangesAsync();
+ db.Set().Add(new AccountRoleLink(account.Id, Role.AdministratorId));
+ await db.SaveChangesAsync();
+
+ var ctx = scope.ServiceProvider.GetRequiredService();
+ ctx.Set(new ClaimsPrincipal(new ClaimsIdentity(
+ [
+ new Claim(ClaimTypes.NameIdentifier, account.Id.ToString()),
+ new Claim(Bitween.RequestContextExtensions.MustChangePasswordClaim, "true")
+ ], "integration-test")));
+
+ // Administrator by role, and refused anyway.
+ Assert.Empty(await ctx.GetPermissions(db));
+ await Assert.ThrowsAsync(() =>
+ ctx.EnsurePermission(db, Permissions.Subscriptions.View));
+ }
+
+ ///
+ /// The claim outranks the superuser grant, so no token can carry both and win.
+ ///
+ [Fact]
+ public async Task An_unchanged_password_outranks_even_the_superuser_claim()
+ {
+ await using var scope = fixture.CreateScope();
+ var db = scope.ServiceProvider.GetRequiredService();
+
+ var ctx = scope.ServiceProvider.GetRequiredService();
+ ctx.Set(new ClaimsPrincipal(new ClaimsIdentity(
+ [
+ new Claim(Bitween.RequestContextExtensions.SuperuserClaim, "true"),
+ new Claim(Bitween.RequestContextExtensions.MustChangePasswordClaim, "true")
+ ], "integration-test")));
+
+ Assert.Empty(await ctx.GetPermissions(db));
+ }
+
+ /// Choosing a password is what clears the flag, whoever sets it.
+ [Fact]
+ public void Setting_a_password_clears_the_requirement()
+ {
+ var account = new Account("Seeded", "clears@test.local", "hash", AccountRole.Admin);
+ typeof(Account).GetProperty(nameof(Account.MustChangePassword))!
+ .SetValue(account, true);
+
+ account.SetPassword("Chosen-By-A-Person-9!");
+
+ Assert.False(account.MustChangePassword);
+ }
+
+ ///
+ /// Changing the password ends the sessions opened with the old one.
+ ///
+ ///
+ /// Signing in with a refresh token skips password verification — it looks the account up by id
+ /// and issues a token from whatever state it is now in. Without this, changing a password that
+ /// had leaked would not remove whoever leaked it: their refresh token would still be exchanged
+ /// for a fresh token, and for the seeded administrator that token would come back with the
+ /// restriction lifted. Raised by review on this branch, not by the penetration test.
+ ///
+ [Fact]
+ public async Task Changing_a_password_ends_the_sessions_opened_with_the_old_one()
+ {
+ await using var scope = fixture.CreateScope();
+ var db = scope.ServiceProvider.GetRequiredService();
+
+ var account = new Account("Session Holder", "ends-sessions@test.local",
+ SecurePasswordHasher.Hash("The-Old-Password-9!"), AccountRole.Member);
+ db.Set().Add(account);
+ await db.SaveChangesAsync();
+
+ db.Set().Add(new RefreshToken(account.Id, LoginMethod.EmailAndPassword));
+ db.Set().Add(new RefreshToken(account.Id, LoginMethod.EmailAndPassword));
+ await db.SaveChangesAsync();
+
+ scope.As(account.Id);
+ var handler = ActivatorUtilities.CreateInstance(scope.ServiceProvider);
+ await handler.Handle(new ChangePasswordModel
+ {
+ OldPassword = "The-Old-Password-9!",
+ NewPassword = "The-New-Password-9!"
+ });
+
+ Assert.Empty(db.Set().Where(t => t.AccountId == account.Id));
+ }
+
+ ///
+ /// GIG.WEB-006. The sign-in page reads this endpoint before anyone authenticates, and the
+ /// default GitHub link in it is what led the testers to our public repository — and from there
+ /// to the sample signing key and the default administrator password.
+ ///
+ [Fact]
+ public async Task Pre_auth_config_withholds_the_vendor_link_at_its_default()
+ {
+ await using var scope = fixture.CreateScope();
+ var handler = ActivatorUtilities.CreateInstance(scope.ServiceProvider);
+
+ var theme = ThemeOf(await handler.Handle());
+
+ Assert.False(theme.ContainsKey("githubLink"));
+ // Still the branding endpoint: what the sign-in page paints has to survive.
+ Assert.True(theme.ContainsKey("primaryColor"));
+ Assert.True(theme.ContainsKey("companyName"));
+ Assert.True(theme.ContainsKey("loginLogo"));
+ }
+
+ ///
+ /// A deployment that set its own link is publishing its own address, so it gets it back. Only
+ /// the value we ship is withheld.
+ ///
+ [Fact]
+ public async Task Pre_auth_config_returns_a_vendor_link_someone_chose()
+ {
+ await using var scope = fixture.CreateScope();
+ var themeOptions = scope.ServiceProvider.GetRequiredService();
+ var original = themeOptions.GithubLink;
+ themeOptions.GithubLink = "https://github.com/a-customer-of-ours";
+
+ try
+ {
+ var handler = ActivatorUtilities.CreateInstance(scope.ServiceProvider);
+ var theme = ThemeOf(await handler.Handle());
+
+ Assert.Equal("https://github.com/a-customer-of-ours", theme["githubLink"]);
+ }
+ finally
+ {
+ themeOptions.GithubLink = original;
+ }
+ }
+
+ /// The anonymous payload is an anonymous type, so the theme comes back by reflection.
+ private static Dictionary ThemeOf(object config)
+ {
+ var theme = config.GetType().GetProperty("Theme")!.GetValue(config);
+ return ((Dictionary)theme!);
+ }
+}
diff --git a/SW.Bitween.MsSql/Migrations/20260915090333_AddMustChangePassword.Designer.cs b/SW.Bitween.MsSql/Migrations/20260915090333_AddMustChangePassword.Designer.cs
new file mode 100644
index 00000000..d9fc92e9
--- /dev/null
+++ b/SW.Bitween.MsSql/Migrations/20260915090333_AddMustChangePassword.Designer.cs
@@ -0,0 +1,2395 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using SW.Bitween.MsSql;
+
+#nullable disable
+
+namespace SW.Bitween.MsSql.Migrations
+{
+ [DbContext(typeof(BitweenDbContext))]
+ [Migration("20260915090333_AddMustChangePassword")]
+ partial class AddMustChangePassword
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.19")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.HasSequence("DocumentIds");
+
+ modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreatedOn")
+ .HasColumnType("datetime2");
+
+ b.Property("Deleted")
+ .HasColumnType("bit");
+
+ b.Property("Disabled")
+ .HasColumnType("bit");
+
+ b.Property("DisplayName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("Email")
+ .HasMaxLength(200)
+ .IsUnicode(false)
+ .HasColumnType("varchar(200)");
+
+ b.Property("EmailProvider")
+ .HasColumnType("tinyint");
+
+ b.Property("FailedLoginCount")
+ .HasColumnType("int");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("datetime2");
+
+ b.Property("LoginMethods")
+ .HasColumnType("tinyint");
+
+ b.Property("ModifiedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ModifiedOn")
+ .HasColumnType("datetime2");
+
+ b.Property("MustChangePassword")
+ .HasColumnType("bit");
+
+ b.Property("Password")
+ .HasMaxLength(500)
+ .IsUnicode(false)
+ .HasColumnType("varchar(500)");
+
+ b.Property("Role")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Email")
+ .IsUnique()
+ .HasFilter("[Email] IS NOT NULL");
+
+ b.ToTable("Accounts", (string)null);
+
+ b.HasData(
+ new
+ {
+ Id = 9999,
+ CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc),
+ Deleted = false,
+ Disabled = false,
+ DisplayName = "Admin",
+ Email = "admin@Bitween.systems",
+ EmailProvider = (byte)0,
+ FailedLoginCount = 0,
+ LoginMethods = (byte)2,
+ MustChangePassword = true,
+ Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ",
+ Role = 0
+ });
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.Accounts.AccountRoleLink", b =>
+ {
+ b.Property("AccountId")
+ .HasColumnType("int");
+
+ b.Property("RoleId")
+ .HasColumnType("int");
+
+ b.HasKey("AccountId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AccountRoles", (string)null);
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b =>
+ {
+ b.Property("Id")
+ .HasMaxLength(50)
+ .IsUnicode(false)
+ .HasColumnType("varchar(50)");
+
+ b.Property("AccountId")
+ .HasColumnType("int");
+
+ b.Property("CreatedOn")
+ .HasColumnType("datetime2");
+
+ b.Property("LoginMethod")
+ .HasColumnType("tinyint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccountId");
+
+ b.ToTable("RefreshTokens", (string)null);
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.Accounts.Role", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreatedOn")
+ .HasColumnType("datetime2");
+
+ b.Property("Description")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("IsSystem")
+ .HasColumnType("bit");
+
+ b.Property("ModifiedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ModifiedOn")
+ .HasColumnType("datetime2");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Permissions")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.ToTable("Roles", (string)null);
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc),
+ Description = "Full access to everything, including members, roles and settings.",
+ IsSystem = true,
+ Name = "Administrator",
+ Permissions = "[]"
+ },
+ new
+ {
+ Id = 2,
+ CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc),
+ Description = "Runs and configures integrations. Can't manage members, roles or settings.",
+ IsSystem = true,
+ Name = "Member",
+ Permissions = "[]"
+ },
+ new
+ {
+ Id = 3,
+ CreatedOn = new DateTime(2022, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc),
+ Description = "Read-only access to integrations, exchanges and configuration.",
+ IsSystem = true,
+ Name = "Viewer",
+ Permissions = "[]"
+ });
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.AuditEntry", b =>
+ {
+ b.Property("Id")
+ .HasMaxLength(32)
+ .IsUnicode(false)
+ .HasColumnType("varchar(32)");
+
+ b.Property("Changes")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CorrelationId")
+ .IsRequired()
+ .HasMaxLength(36)
+ .IsUnicode(false)
+ .HasColumnType("varchar(36)");
+
+ b.Property("EntityKey")
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("EntityName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("OccurredOn")
+ .HasColumnType("datetime2");
+
+ b.Property("Sequence")
+ .HasColumnType("int");
+
+ b.Property("State")
+ .IsRequired()
+ .HasMaxLength(10)
+ .IsUnicode(false)
+ .HasColumnType("varchar(10)");
+
+ b.Property("UserId")
+ .HasMaxLength(50)
+ .IsUnicode(false)
+ .HasColumnType("varchar(50)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CorrelationId");
+
+ b.HasIndex("OccurredOn");
+
+ b.HasIndex("EntityName", "EntityKey", "OccurredOn");
+
+ b.ToTable("AuditEntries", (string)null);
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.Cluster.ClusterLease", b =>
+ {
+ b.Property("Id")
+ .HasMaxLength(200)
+ .IsUnicode(false)
+ .HasColumnType("varchar(200)");
+
+ b.Property("AcquiredOn")
+ .HasColumnType("datetime2");
+
+ b.Property("OwnerNode")
+ .HasMaxLength(200)
+ .IsUnicode(false)
+ .HasColumnType("varchar(200)");
+
+ b.Property("Term")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.ToTable("ClusterLeases", (string)null);
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.DataSources.AdapterState", b =>
+ {
+ b.Property("AdapterId")
+ .HasMaxLength(200)
+ .IsUnicode(false)
+ .HasColumnType("varchar(200)");
+
+ b.Property("InstanceKey")
+ .HasMaxLength(200)
+ .IsUnicode(false)
+ .HasColumnType("varchar(200)");
+
+ b.Property("Name")
+ .HasMaxLength(200)
+ .IsUnicode(false)
+ .HasColumnType("varchar(200)");
+
+ b.Property("UpdatedOn")
+ .HasColumnType("datetime2");
+
+ b.Property("Value")
+ .HasMaxLength(8000)
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("AdapterId", "InstanceKey", "Name");
+
+ b.ToTable("AdapterStates", (string)null);
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSource", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AdapterId")
+ .IsRequired()
+ .HasMaxLength(200)
+ .IsUnicode(false)
+ .HasColumnType("varchar(200)");
+
+ b.Property("ConsecutiveFailures")
+ .HasColumnType("int");
+
+ b.Property("CpuLimitSamples")
+ .HasColumnType("int");
+
+ b.Property("CpuPercentLimit")
+ .HasColumnType("float");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreatedOn")
+ .HasColumnType("datetime2");
+
+ b.Property("DeduplicationWindowDays")
+ .HasColumnType("int");
+
+ b.Property("HardMemoryLimitMb")
+ .HasColumnType("int");
+
+ b.Property("Inactive")
+ .HasColumnType("bit");
+
+ b.Property("Kind")
+ .HasColumnType("int");
+
+ b.Property("LastException")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("LastHeartbeatOn")
+ .HasColumnType("datetime2");
+
+ b.Property("LastKnownState")
+ .HasMaxLength(100)
+ .IsUnicode(false)
+ .HasColumnType("varchar(100)");
+
+ b.Property("ModifiedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ModifiedOn")
+ .HasColumnType("datetime2");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("OwnedByNode")
+ .HasMaxLength(200)
+ .IsUnicode(false)
+ .HasColumnType("varchar(200)");
+
+ b.Property("Placement")
+ .HasColumnType("int");
+
+ b.Property("Properties")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("SecretProperties")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("SoftMemoryLimitMb")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.ToTable("DataSources", (string)null);
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.DataSources.DataSourceStatement", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreatedOn")
+ .HasColumnType("datetime2");
+
+ b.Property("CursorColumn")
+ .HasMaxLength(128)
+ .IsUnicode(false)
+ .HasColumnType("varchar(128)");
+
+ b.Property("DataSourceId")
+ .HasColumnType("int");
+
+ b.Property("Description")
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("Inactive")
+ .HasColumnType("bit");
+
+ b.Property("KeyColumn")
+ .HasMaxLength(128)
+ .IsUnicode(false)
+ .HasColumnType("varchar(128)");
+
+ b.Property("ModifiedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ModifiedOn")
+ .HasColumnType("datetime2");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .IsUnicode(false)
+ .HasColumnType("varchar(200)");
+
+ b.Property("Sql")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("WorkGroupId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("WorkGroupId");
+
+ b.HasIndex("DataSourceId", "Name")
+ .IsUnique();
+
+ b.ToTable("DataSourceStatements", (string)null);
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.DataSources.InboundMessage", b =>
+ {
+ b.Property("Id")
+ .HasMaxLength(400)
+ .IsUnicode(false)
+ .HasColumnType("varchar(400)");
+
+ b.Property("DataSourceId")
+ .HasColumnType("int");
+
+ b.Property("SeenOn")
+ .HasColumnType("datetime2");
+
+ b.Property("XchangeId")
+ .HasMaxLength(50)
+ .IsUnicode(false)
+ .HasColumnType("varchar(50)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DataSourceId");
+
+ b.HasIndex("SeenOn");
+
+ b.ToTable("InboundMessages", (string)null);
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b =>
+ {
+ b.Property("Id")
+ .HasMaxLength(50)
+ .IsUnicode(false)
+ .HasColumnType("varchar(50)");
+
+ b.Property("On")
+ .HasColumnType("datetime2");
+
+ b.HasKey("Id");
+
+ b.HasIndex("On");
+
+ b.ToTable("DelayedRetries", (string)null);
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.Document", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasDefaultValueSql("NEXT VALUE FOR [DocumentIds]");
+
+ SqlServerPropertyBuilderExtensions.UseSequence(b.Property("Id"), "DocumentIds");
+
+ b.Property("BusEnabled")
+ .HasColumnType("bit");
+
+ b.Property("BusMessageTypeName")
+ .HasMaxLength(500)
+ .IsUnicode(false)
+ .HasColumnType("varchar(500)");
+
+ b.Property("Code")
+ .HasMaxLength(50)
+ .IsUnicode(false)
+ .HasColumnType("varchar(50)");
+
+ b.Property("DisregardsUnfilteredMessages")
+ .HasColumnType("bit");
+
+ b.Property("DocumentFormat")
+ .HasColumnType("int");
+
+ b.Property("DuplicateInterval")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .IsUnicode(false)
+ .HasColumnType("varchar(100)");
+
+ b.Property("PromotedProperties")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RetiredOn")
+ .HasColumnType("datetime2");
+
+ b.HasKey("Id");
+
+ b.HasIndex("BusMessageTypeName")
+ .IsUnique()
+ .HasFilter("[BusMessageTypeName] IS NOT NULL");
+
+ b.HasIndex("Code")
+ .IsUnique()
+ .HasFilter("[Code] IS NOT NULL");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.ToTable("Documents", (string)null);
+
+ b.HasData(
+ new
+ {
+ Id = 10001,
+ BusEnabled = false,
+ DocumentFormat = 0,
+ DuplicateInterval = 0,
+ Name = "Aggregation Document",
+ PromotedProperties = "{}"
+ });
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreatedOn")
+ .HasColumnType("datetime2");
+
+ b.Property("Inactive")
+ .HasColumnType("bit");
+
+ b.Property("ModifiedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ModifiedOn")
+ .HasColumnType("datetime2");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("UrlName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UrlName")
+ .IsUnique();
+
+ b.ToTable("ApiGateways", (string)null);
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b =>
+ {
+ b.Property("ApiGatewayId")
+ .HasColumnType("int");
+
+ b.Property("PartnerId")
+ .HasColumnType("int");
+
+ b.Property("SubscriptionId")
+ .HasColumnType("int");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreatedOn")
+ .HasColumnType("datetime2");
+
+ b.Property("ModifiedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ModifiedOn")
+ .HasColumnType("datetime2");
+
+ b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId");
+
+ b.HasIndex("PartnerId");
+
+ b.HasIndex("SubscriptionId");
+
+ b.ToTable("ApiGatewayPartners", (string)null);
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreatedOn")
+ .HasColumnType("datetime2");
+
+ b.Property("DataSourceId")
+ .HasColumnType("int");
+
+ b.Property("DocumentId")
+ .HasColumnType("int");
+
+ b.Property("Endpoint")
+ .HasMaxLength(500)
+ .IsUnicode(false)
+ .HasColumnType("varchar(500)");
+
+ b.Property("EndpointProperties")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Inactive")
+ .HasColumnType("bit");
+
+ b.Property("ModifiedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ModifiedOn")
+ .HasColumnType("datetime2");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DataSourceId");
+
+ b.HasIndex("DocumentId");
+
+ b.ToTable("BusGateways", (string)null);
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("BusGatewayId")
+ .HasColumnType("int");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreatedOn")
+ .HasColumnType("datetime2");
+
+ b.Property("MatchExpression")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ModifiedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ModifiedOn")
+ .HasColumnType("datetime2");
+
+ b.Property("PartnerId")
+ .HasColumnType("int");
+
+ b.Property("SubscriptionId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("BusGatewayId");
+
+ b.HasIndex("PartnerId");
+
+ b.HasIndex("SubscriptionId");
+
+ b.ToTable("BusGatewayRoutes", (string)null);
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b =>
+ {
+ b.Property("Id")
+ .HasMaxLength(200)
+ .IsUnicode(false)
+ .HasColumnType("varchar(200)");
+
+ b.Property("Name")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Values")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("GlobalAdapterValuesSets", (string)null);
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.Notifier", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("HandlerId")
+ .HasMaxLength(200)
+ .IsUnicode(false)
+ .HasColumnType("varchar(200)");
+
+ b.Property("HandlerProperties")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Inactive")
+ .HasColumnType("bit");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("RunOnBadResult")
+ .HasColumnType("bit");
+
+ b.Property("RunOnFailedResult")
+ .HasColumnType("bit");
+
+ b.Property("RunOnSubscriptions")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RunOnSuccessfulResult")
+ .HasColumnType("bit");
+
+ b.HasKey("Id");
+
+ b.ToTable("Notifiers", (string)null);
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("BadData")
+ .HasColumnType("bit");
+
+ b.Property("Data")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("FileName")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("References")
+ .HasMaxLength(1024)
+ .HasColumnType("nvarchar(1024)");
+
+ b.Property("SubscriptionId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SubscriptionId");
+
+ b.ToTable("OnHoldXchanges", (string)null);
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.Partner", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AdapterProperties")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .IsUnicode(false)
+ .HasColumnType("varchar(200)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Partners", (string)null);
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ Name = "SYSTEM"
+ });
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.ReceiveAttempt", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ErrorMessage")
+ .HasMaxLength(4000)
+ .HasColumnType("nvarchar(4000)");
+
+ b.Property("ExchangeIds")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("FinishedOn")
+ .HasColumnType("datetime2");
+
+ b.Property("Outcome")
+ .HasColumnType("int");
+
+ b.Property("StartedOn")
+ .HasColumnType("datetime2");
+
+ b.Property("SubscriptionId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SubscriptionId", "StartedOn");
+
+ b.ToTable("ReceiveAttempts", (string)null);
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.RetryAlertOverride", b =>
+ {
+ b.Property("SubscriptionId")
+ .HasColumnType("int");
+
+ b.Property("GroupId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("AlertHandlerId")
+ .HasMaxLength(200)
+ .IsUnicode(false)
+ .HasColumnType("varchar(200)");
+
+ b.Property("AlertHandlerProperties")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("AlertMode")
+ .HasColumnType("tinyint");
+
+ b.HasKey("SubscriptionId", "GroupId");
+
+ b.ToTable("RetryAlertOverrides", (string)null);
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.RetryGroupUsage", b =>
+ {
+ b.Property("SubscriptionId")
+ .HasColumnType("int");
+
+ b.Property("GroupId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("AttemptsUsed")
+ .HasColumnType("int");
+
+ b.Property("ExhaustedNotifiedOn")
+ .HasColumnType("datetime2");
+
+ b.Property("LastAttemptOn")
+ .HasColumnType("datetime2");
+
+ b.HasKey("SubscriptionId", "GroupId");
+
+ b.ToTable("RetryGroupUsages", (string)null);
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AlertHandlerId")
+ .HasMaxLength(200)
+ .IsUnicode(false)
+ .HasColumnType("varchar(200)");
+
+ b.Property("AlertHandlerProperties")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreatedOn")
+ .HasColumnType("datetime2");
+
+ b.Property("Groups")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ModifiedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ModifiedOn")
+ .HasColumnType("datetime2");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.HasKey("Id");
+
+ b.ToTable("RetryPolicies", (string)null);
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.Setting", b =>
+ {
+ b.Property("Id")
+ .HasMaxLength(200)
+ .IsUnicode(false)
+ .HasColumnType("varchar(200)");
+
+ b.Property("CreatedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreatedOn")
+ .HasColumnType("datetime2");
+
+ b.Property("ModifiedBy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ModifiedOn")
+ .HasColumnType("datetime2");
+
+ b.Property("Value")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Settings", (string)null);
+ });
+
+ modelBuilder.Entity("SW.Bitween.Domain.Subscription", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AggregateOn")
+ .HasColumnType("datetime2");
+
+ b.Property("AggregationForId")
+ .HasColumnType("int");
+
+ b.Property("AggregationTarget")
+ .HasColumnType("tinyint");
+
+ b.Property("CategoryId")
+ .HasColumnType("int");
+
+ b.Property("ConsecutiveFailures")
+ .HasColumnType("int");
+
+ b.Property("CustomRetryPolicy")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DataSourceId")
+ .HasColumnType("int");
+
+ b.Property("DocumentFilter")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DocumentId")
+ .HasColumnType("int");
+
+ b.Property("HandlerId")
+ .HasMaxLength(200)
+ .IsUnicode(false)
+ .HasColumnType("varchar(200)");
+
+ b.Property("HandlerProperties")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Inactive")
+ .HasColumnType("bit");
+
+ b.Property("IsRunning")
+ .HasColumnType("bit");
+
+ b.Property