diff --git a/API/Controller/Admin/DTOs/AddWebhookDto.cs b/API/Controller/Admin/DTOs/AddWebhookDto.cs
index 0a271d27a..bdab02238 100644
--- a/API/Controller/Admin/DTOs/AddWebhookDto.cs
+++ b/API/Controller/Admin/DTOs/AddWebhookDto.cs
@@ -1,7 +1,11 @@
-namespace OpenShock.API.Controller.Admin.DTOs;
+using System.ComponentModel.DataAnnotations;
+using OpenShock.Common.Constants;
+
+namespace OpenShock.API.Controller.Admin.DTOs;
public sealed class AddWebhookDto
{
+ [StringLength(ApiHardLimits.DiscordWebhookNameMaxLength)]
public required string Name { get; init; }
public required Uri Url { get; init; }
}
diff --git a/API/Controller/Admin/DTOs/ConfigurationAddItemRequest.cs b/API/Controller/Admin/DTOs/ConfigurationAddItemRequest.cs
index d934f314d..a88979dec 100644
--- a/API/Controller/Admin/DTOs/ConfigurationAddItemRequest.cs
+++ b/API/Controller/Admin/DTOs/ConfigurationAddItemRequest.cs
@@ -1,5 +1,6 @@
-using OpenShock.Common.OpenShockDb;
-using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations;
+using OpenShock.Common.Constants;
+using OpenShock.Common.OpenShockDb;
namespace OpenShock.API.Controller.Admin.DTOs;
@@ -13,12 +14,14 @@ public sealed class ConfigurationAddItemRequest
/// Must consist of uppercase letters and underscores only.
///
[Required(AllowEmptyStrings = false)]
+ [StringLength(ApiHardLimits.ConfigurationNameMaxLength)]
public required string Name { get; init; }
///
/// A human-readable description of the configuration item’s purpose.
///
[Required(AllowEmptyStrings = true)]
+ [StringLength(ApiHardLimits.ConfigurationDescriptionMaxLength)]
public required string Description { get; init; }
///
@@ -32,5 +35,6 @@ public sealed class ConfigurationAddItemRequest
/// Must conform to the format required by .
///
[Required(AllowEmptyStrings = true)]
+ [StringLength(ApiHardLimits.ConfigurationValueMaxLength)]
public required string Value { get; init; }
}
diff --git a/API/Controller/Admin/DTOs/ConfigurationUpdateItemRequest.cs b/API/Controller/Admin/DTOs/ConfigurationUpdateItemRequest.cs
index bc157ed9b..34a252794 100644
--- a/API/Controller/Admin/DTOs/ConfigurationUpdateItemRequest.cs
+++ b/API/Controller/Admin/DTOs/ConfigurationUpdateItemRequest.cs
@@ -1,5 +1,6 @@
-using OpenShock.Common.OpenShockDb;
-using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations;
+using OpenShock.Common.Constants;
+using OpenShock.Common.OpenShockDb;
namespace OpenShock.API.Controller.Admin.DTOs;
@@ -15,16 +16,19 @@ public sealed class ConfigurationUpdateItemRequest
/// Must consist only of uppercase letters and underscores.
///
[Required(AllowEmptyStrings = false)]
+ [StringLength(ApiHardLimits.ConfigurationNameMaxLength)]
public required string Name { get; init; }
///
/// (Optional) New description for the configuration item.
///
+ [StringLength(ApiHardLimits.ConfigurationDescriptionMaxLength)]
public string? Description { get; init; }
///
/// (Optional) New value for the configuration item.
/// Must match the format expected by its existing .
///
+ [StringLength(ApiHardLimits.ConfigurationValueMaxLength)]
public string? Value { get; init; }
}
diff --git a/API/Services/Account/AccountService.cs b/API/Services/Account/AccountService.cs
index da28b8db3..0453e1a6a 100644
--- a/API/Services/Account/AccountService.cs
+++ b/API/Services/Account/AccountService.cs
@@ -234,7 +234,7 @@ public async Task CreateOAuthOnlyAccountAsync(
UserId = user.Id,
ProviderKey = provider,
ExternalId = providerAccountId,
- DisplayName = providerAccountName,
+ DisplayName = providerAccountName?.Truncate(ApiHardLimits.OAuthDisplayNameMaxLength),
CreatedAt = creationTime,
});
diff --git a/API/Services/OAuthConnection/OAuthConnectionService.cs b/API/Services/OAuthConnection/OAuthConnectionService.cs
index e01f2d601..0d0b779b2 100644
--- a/API/Services/OAuthConnection/OAuthConnectionService.cs
+++ b/API/Services/OAuthConnection/OAuthConnectionService.cs
@@ -1,8 +1,10 @@
using Microsoft.EntityFrameworkCore;
using Npgsql;
+using OpenShock.Common.Constants;
using OpenShock.Common.Models;
using OpenShock.Common.OpenShockDb;
using OpenShock.Common.Services.Audit;
+using OpenShock.Common.Utils;
namespace OpenShock.API.Services.OAuthConnection;
@@ -58,7 +60,7 @@ public async Task TryAddConnectionAsync(Guid userId, string provider, stri
UserId = userId,
ProviderKey = provider.ToLowerInvariant(),
ExternalId = providerAccountId,
- DisplayName = providerAccountName
+ DisplayName = providerAccountName?.Truncate(ApiHardLimits.OAuthDisplayNameMaxLength)
});
await _db.SaveChangesAsync();
}
diff --git a/Common.Tests/Utils/StringUtilsTests.cs b/Common.Tests/Utils/StringUtilsTests.cs
index 8c4caa6c8..a3e12dd75 100644
--- a/Common.Tests/Utils/StringUtilsTests.cs
+++ b/Common.Tests/Utils/StringUtilsTests.cs
@@ -45,4 +45,18 @@ public async Task Truncate_MaxOne_ReturnsSingleChar()
var result = "hello".Truncate(1);
await Assert.That(result).IsEqualTo("h");
}
+
+ [Test]
+ public async Task Truncate_CutInsideSurrogatePair_DropsWholePair()
+ {
+ var result = "abc😀".Truncate(4);
+ await Assert.That(result).IsEqualTo("abc");
+ }
+
+ [Test]
+ public async Task Truncate_CutAfterSurrogatePair_KeepsPair()
+ {
+ var result = "abc😀def".Truncate(5);
+ await Assert.That(result).IsEqualTo("abc😀");
+ }
}
diff --git a/Common/Constants/ApiHardLimits.cs b/Common/Constants/ApiHardLimits.cs
index 23e01a901..d3bd51a45 100644
--- a/Common/Constants/ApiHardLimits.cs
+++ b/Common/Constants/ApiHardLimits.cs
@@ -1,5 +1,3 @@
-using OpenShock.Internal.Common.Constants;
-
namespace OpenShock.Common.Constants;
///
@@ -27,6 +25,17 @@ public static class ApiHardLimits
public const int PasswordResetSecretMaxLength = 100;
public const int AuditReasonMaxLength = 512;
+ public const int ConfigurationNameMaxLength = 64;
+ public const int ConfigurationDescriptionMaxLength = 255;
+ public const int ConfigurationValueMaxLength = 4096;
+
+ public const int OAuthProviderKeyMaxLength = 16;
+ public const int OAuthExternalIdMaxLength = 255;
+ public const int OAuthDisplayNameMaxLength = 64;
+
+ public const int DiscordWebhookNameMaxLength = 64;
+ public const int DiscordWebhookTokenMaxLength = 255;
+
public const int MaxTurnstileResponseTokenLength = 2048; // https://developers.cloudflare.com/turnstile/get-started/server-side-validation/
// Don't allow any firmware prior to 2024.
diff --git a/Common/Migrations/20261113184758_AddStringLengthLimits.Designer.cs b/Common/Migrations/20261113184758_AddStringLengthLimits.Designer.cs
new file mode 100644
index 000000000..49d4b587d
--- /dev/null
+++ b/Common/Migrations/20261113184758_AddStringLengthLimits.Designer.cs
@@ -0,0 +1,1923 @@
+//
+using System;
+using System.Collections.Generic;
+using System.Net;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using OpenShock.Common.OpenShockDb;
+
+#nullable disable
+
+namespace OpenShock.Common.Migrations;
+
+[DbContext(typeof(MigrationOpenShockContext))]
+[Migration("20261113184758_AddStringLengthLimits")]
+partial class _20261113184758_AddStringLengthLimits
+{
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("Npgsql:CollationDefinition:public.ndcoll", "und-u-ks-level2,und-u-ks-level2,icu,False")
+ .HasAnnotation("ProductVersion", "11.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "audit_action", new[] { "login", "logout", "password_changed", "email_change_requested", "email_changed", "username_changed", "api_token_created", "api_token_deleted", "oauth_connected", "oauth_disconnected", "account_deactivated", "account_reactivated", "account_deleted", "automation_token_created", "automation_token_updated", "automation_token_rotated", "automation_token_deleted", "automation_token_used" });
+ NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "automation_token_type", new[] { "turnstile", "rate_limit", "control_stats_read" });
+ NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "configuration_value_type", new[] { "string", "bool", "int", "float", "json" });
+ NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "control_limit_mode", new[] { "clamp", "lerp" });
+ NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "control_type", new[] { "stop", "shock", "vibrate", "sound" });
+ NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "email_status", new[] { "pending", "sending", "sent", "failed", "skipped" });
+ NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "email_type", new[] { "account_activation", "password_reset", "email_verification", "email_change_notice" });
+ NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "match_type_enum", new[] { "exact", "contains" });
+ NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "ota_update_status", new[] { "started", "running", "finished", "error", "timeout" });
+ NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "password_encryption_type", new[] { "bcrypt_enhanced", "pbkdf2" });
+ NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "permission_type", new[] { "shockers.use", "shockers.edit", "shockers.pause", "devices.edit", "devices.auth", "usershares.edit", "usershares.pause", "publicshares.pause", "publicshares.edit" });
+ NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "role_type", new[] { "support", "staff", "admin", "system" });
+ NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "shocker_model_type", new[] { "cai_xianlin", "petrainer", "petrainer_998dr", "wellturn_t330" });
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ var id = b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(id);
+
+ b.Property("FriendlyName")
+ .HasColumnType("text");
+
+ b.Property("Xml")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("DataProtectionKeys");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.AdminUsersView", b =>
+ {
+ b.Property("ActivatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("activated_at");
+
+ b.Property("ApiTokenCount")
+ .HasColumnType("integer")
+ .HasColumnName("api_token_count");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("CreatedByAutomationTokenId")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_automation_token_id");
+
+ b.Property("DeactivatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deactivated_at");
+
+ b.Property("DeactivatedByUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("deactivated_by_user_id");
+
+ b.Property("DeviceCount")
+ .HasColumnType("integer")
+ .HasColumnName("device_count");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasColumnType("character varying")
+ .HasColumnName("email");
+
+ b.Property("EmailChangeRequestCount")
+ .HasColumnType("integer")
+ .HasColumnName("email_change_request_count");
+
+ b.Property("Id")
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("character varying")
+ .HasColumnName("name");
+
+ b.Property("NameChangeRequestCount")
+ .HasColumnType("integer")
+ .HasColumnName("name_change_request_count");
+
+ b.Property("PasswordHashType")
+ .HasColumnType("character varying")
+ .HasColumnName("password_hash_type");
+
+ b.Property("PasswordResetCount")
+ .HasColumnType("integer")
+ .HasColumnName("password_reset_count");
+
+ b.Property("Roles")
+ .IsRequired()
+ .HasColumnType("role_type[]")
+ .HasColumnName("roles");
+
+ b.Property("ShockerControlLogCount")
+ .HasColumnType("integer")
+ .HasColumnName("shocker_control_log_count");
+
+ b.Property("ShockerCount")
+ .HasColumnType("integer")
+ .HasColumnName("shocker_count");
+
+ b.Property("ShockerPublicShareCount")
+ .HasColumnType("integer")
+ .HasColumnName("shocker_public_share_count");
+
+ b.Property("ShockerUserShareCount")
+ .HasColumnType("integer")
+ .HasColumnName("shocker_user_share_count");
+
+ b.ToTable((string)null);
+
+ b.ToView("admin_users_view", (string)null);
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.ApiToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("CreatedByIp")
+ .IsRequired()
+ .HasColumnType("inet")
+ .HasColumnName("created_by_ip");
+
+ b.Property("LastUsed")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_used");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("name");
+
+ b.Property>("Permissions")
+ .IsRequired()
+ .HasColumnType("permission_type[]")
+ .HasColumnName("permissions");
+
+ b.Property("ShockerControlDurationMax")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(65535)
+ .HasColumnName("shocker_control_duration_max");
+
+ b.Property("ShockerControlDurationMin")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(300)
+ .HasColumnName("shocker_control_duration_min");
+
+ b.Property("ShockerControlDurationMode")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("control_limit_mode")
+ .HasDefaultValue(ControlLimitMode.Clamp)
+ .HasColumnName("shocker_control_duration_mode");
+
+ b.Property("ShockerControlIntensityMax")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("smallint")
+ .HasDefaultValue((byte)100)
+ .HasColumnName("shocker_control_intensity_max");
+
+ b.Property("ShockerControlIntensityMin")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("smallint")
+ .HasDefaultValue((byte)0)
+ .HasColumnName("shocker_control_intensity_min");
+
+ b.Property("ShockerControlIntensityMode")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("control_limit_mode")
+ .HasDefaultValue(ControlLimitMode.Clamp)
+ .HasColumnName("shocker_control_intensity_mode");
+
+ b.Property("ShockerControlPaused")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(false)
+ .HasColumnName("shocker_control_paused");
+
+ b.Property("TokenHash")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("token_hash")
+ .UseCollation("C");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("ValidUntil")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("valid_until");
+
+ b.HasKey("Id")
+ .HasName("api_tokens_pkey");
+
+ b.HasIndex("TokenHash")
+ .IsUnique();
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("ValidUntil");
+
+ b.ToTable("api_tokens");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.ApiTokenReport", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("AffectedCount")
+ .HasColumnType("integer")
+ .HasColumnName("affected_count");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("IpAddress")
+ .IsRequired()
+ .HasColumnType("inet")
+ .HasColumnName("ip_address");
+
+ b.Property("IpCountry")
+ .HasMaxLength(4)
+ .HasColumnType("character varying(4)")
+ .HasColumnName("ip_country");
+
+ b.Property("SubmittedCount")
+ .HasColumnType("integer")
+ .HasColumnName("submitted_count");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("api_token_reports_pkey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("api_token_reports");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.AutomationToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("AutoCleanupAfter")
+ .HasColumnType("interval")
+ .HasColumnName("auto_cleanup_after");
+
+ b.Property("AutoCleanupUsers")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(false)
+ .HasColumnName("auto_cleanup_users");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("LastRotatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_rotated_at");
+
+ b.Property("LastUsedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_used_at");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("name");
+
+ b.Property("TokenHash")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("token_hash")
+ .UseCollation("C");
+
+ b.Property>("Types")
+ .IsRequired()
+ .HasColumnType("automation_token_type[]")
+ .HasColumnName("types");
+
+ b.Property("UseCount")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasDefaultValue(0L)
+ .HasColumnName("use_count");
+
+ b.HasKey("Id")
+ .HasName("automation_tokens_pkey");
+
+ b.HasIndex("TokenHash")
+ .IsUnique();
+
+ b.ToTable("automation_tokens");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.ConfigurationItem", b =>
+ {
+ b.Property("Name")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("name")
+ .UseCollation("C");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)")
+ .HasColumnName("description");
+
+ b.Property("Type")
+ .HasColumnType("configuration_value_type")
+ .HasColumnName("type");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated_at");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("value");
+
+ b.HasKey("Name")
+ .HasName("configuration_pkey");
+
+ b.ToTable("configuration");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.ControlStat", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("Bucket")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("bucket");
+
+ b.Property("ControllerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("controller_user_id");
+
+ b.Property("Count")
+ .HasColumnType("bigint")
+ .HasColumnName("count");
+
+ b.Property("DurationMs")
+ .HasColumnType("bigint")
+ .HasColumnName("duration_ms");
+
+ b.Property("IntensityMs")
+ .HasColumnType("bigint")
+ .HasColumnName("intensity_ms");
+
+ b.Property("Live")
+ .HasColumnType("boolean")
+ .HasColumnName("live");
+
+ b.Property("MaxIntensity")
+ .HasColumnType("smallint")
+ .HasColumnName("max_intensity");
+
+ b.Property("OwnerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("owner_user_id");
+
+ b.Property("ShockerId")
+ .HasColumnType("uuid")
+ .HasColumnName("shocker_id");
+
+ b.Property("Type")
+ .HasColumnType("control_type")
+ .HasColumnName("type");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated_at");
+
+ b.HasKey("Id")
+ .HasName("control_stats_pkey");
+
+ b.HasIndex("ShockerId");
+
+ b.HasIndex("UpdatedAt");
+
+ b.HasIndex("ControllerUserId", "Bucket");
+
+ b.HasIndex("OwnerUserId", "Bucket");
+
+ var index = b.HasIndex("OwnerUserId", "ShockerId", "ControllerUserId", "Bucket", "Type", "Live")
+ .IsUnique()
+ .HasDatabaseName("ix_control_stats_upsert_key")
+ .HasFilter("shocker_id IS NOT NULL");
+
+ NpgsqlIndexBuilderExtensions.AreNullsDistinct(index, false);
+
+ b.ToTable("control_stats");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.ControlStatBatch", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("AppliedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("applied_at");
+
+ b.HasKey("Id")
+ .HasName("control_stat_batches_pkey");
+
+ b.HasIndex("AppliedAt");
+
+ b.ToTable("control_stat_batches");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.ControlStatRemoval", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("RemovedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("removed_at");
+
+ b.HasKey("UserId")
+ .HasName("control_stat_removals_pkey");
+
+ b.HasIndex("RemovedAt");
+
+ b.ToTable("control_stat_removals");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.Device", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("name");
+
+ b.Property("OwnerId")
+ .HasColumnType("uuid")
+ .HasColumnName("owner_id");
+
+ b.Property("Token")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)")
+ .HasColumnName("token")
+ .UseCollation("C");
+
+ b.HasKey("Id")
+ .HasName("devices_pkey");
+
+ b.HasIndex("OwnerId");
+
+ b.HasIndex("Token")
+ .IsUnique();
+
+ b.ToTable("devices");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.DeviceOtaUpdate", b =>
+ {
+ b.Property("DeviceId")
+ .HasColumnType("uuid")
+ .HasColumnName("device_id");
+
+ b.Property("UpdateId")
+ .HasColumnType("integer")
+ .HasColumnName("update_id");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("Message")
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)")
+ .HasColumnName("message");
+
+ b.Property("Status")
+ .HasColumnType("ota_update_status")
+ .HasColumnName("status");
+
+ b.Property("Version")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("version");
+
+ b.HasKey("DeviceId", "UpdateId")
+ .HasName("device_ota_updates_pkey");
+
+ b.HasIndex(new[] { "CreatedAt" }, "device_ota_updates_created_at_idx");
+
+ b.ToTable("device_ota_updates");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.DiscordWebhook", b =>
+ {
+ b.Property("Name")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("name");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("Id")
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("WebhookId")
+ .HasColumnType("bigint")
+ .HasColumnName("webhook_id");
+
+ b.Property("WebhookToken")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)")
+ .HasColumnName("webhook_token");
+
+ b.HasKey("Name")
+ .HasName("discord_webhooks_pkey");
+
+ b.ToTable("discord_webhooks");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.EmailOutboxMessage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("AttemptCount")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0)
+ .HasColumnName("attempt_count");
+
+ b.Property("CoalesceKey")
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)")
+ .HasColumnName("coalesce_key");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("FailedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("failed_at");
+
+ b.Property("LastError")
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)")
+ .HasColumnName("last_error");
+
+ b.Property("NextAttemptAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("next_attempt_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property>("Payload")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("payload");
+
+ b.Property("Recipient")
+ .IsRequired()
+ .HasMaxLength(320)
+ .HasColumnType("character varying(320)")
+ .HasColumnName("recipient");
+
+ b.Property("RecipientName")
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)")
+ .HasColumnName("recipient_name");
+
+ b.Property("SentAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("sent_at");
+
+ b.Property("Status")
+ .HasColumnType("email_status")
+ .HasColumnName("status");
+
+ b.Property("Type")
+ .HasColumnType("email_type")
+ .HasColumnName("type");
+
+ b.HasKey("Id")
+ .HasName("email_outbox_pkey");
+
+ b.HasIndex("CoalesceKey");
+
+ b.HasIndex("Recipient");
+
+ b.HasIndex("Status", "NextAttemptAt");
+
+ b.ToTable("email_outbox");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.EmailProviderBlacklist", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("Domain")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)")
+ .HasColumnName("domain")
+ .UseCollation("ndcoll");
+
+ b.HasKey("Id")
+ .HasName("email_provider_blacklist_pkey");
+
+ var index = b.HasIndex("Domain")
+ .IsUnique();
+
+ NpgsqlIndexBuilderExtensions.UseCollation(index, new[] { "ndcoll" });
+
+ b.ToTable("email_provider_blacklist");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.PublicShare", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expires_at");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("name");
+
+ b.Property("OwnerId")
+ .HasColumnType("uuid")
+ .HasColumnName("owner_id");
+
+ b.HasKey("Id")
+ .HasName("public_shares_pkey");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("public_shares");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.PublicShareShocker", b =>
+ {
+ b.Property("PublicShareId")
+ .HasColumnType("uuid")
+ .HasColumnName("public_share_id");
+
+ b.Property("ShockerId")
+ .HasColumnType("uuid")
+ .HasColumnName("shocker_id");
+
+ b.Property("AllowLiveControl")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(false)
+ .HasColumnName("allow_livecontrol");
+
+ b.Property("AllowShock")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true)
+ .HasColumnName("allow_shock");
+
+ b.Property("AllowSound")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true)
+ .HasColumnName("allow_sound");
+
+ b.Property("AllowVibrate")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true)
+ .HasColumnName("allow_vibrate");
+
+ b.Property("Cooldown")
+ .HasColumnType("integer")
+ .HasColumnName("cooldown");
+
+ b.Property("IsPaused")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(false)
+ .HasColumnName("is_paused");
+
+ b.Property("MaxDuration")
+ .HasColumnType("integer")
+ .HasColumnName("max_duration");
+
+ b.Property("MaxIntensity")
+ .HasColumnType("smallint")
+ .HasColumnName("max_intensity");
+
+ b.HasKey("PublicShareId", "ShockerId")
+ .HasName("public_share_shockers_pkey");
+
+ b.HasIndex("ShockerId");
+
+ b.ToTable("public_share_shockers");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.Shocker", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("DeviceId")
+ .HasColumnType("uuid")
+ .HasColumnName("device_id");
+
+ b.Property("IsPaused")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(false)
+ .HasColumnName("is_paused");
+
+ b.Property("Model")
+ .HasColumnType("shocker_model_type")
+ .HasColumnName("model");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("name");
+
+ b.Property("RfId")
+ .HasColumnType("integer")
+ .HasColumnName("rf_id");
+
+ b.HasKey("Id")
+ .HasName("shockers_pkey");
+
+ b.HasIndex("DeviceId");
+
+ b.ToTable("shockers");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerControlLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("ControlledByUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("controlled_by_user_id");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("CustomName")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("custom_name");
+
+ b.Property("Duration")
+ .HasColumnType("bigint")
+ .HasColumnName("duration");
+
+ b.Property("Intensity")
+ .HasColumnType("smallint")
+ .HasColumnName("intensity");
+
+ b.Property("LiveControl")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(false)
+ .HasColumnName("live_control");
+
+ b.Property("ShockerId")
+ .HasColumnType("uuid")
+ .HasColumnName("shocker_id");
+
+ b.Property("Type")
+ .HasColumnType("control_type")
+ .HasColumnName("type");
+
+ b.HasKey("Id")
+ .HasName("shocker_control_logs_pkey");
+
+ b.HasIndex("ControlledByUserId");
+
+ b.HasIndex("ShockerId");
+
+ b.ToTable("shocker_control_logs");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerShareCode", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("AllowLiveControl")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true)
+ .HasColumnName("allow_livecontrol");
+
+ b.Property("AllowShock")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true)
+ .HasColumnName("allow_shock");
+
+ b.Property("AllowSound")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true)
+ .HasColumnName("allow_sound");
+
+ b.Property("AllowVibrate")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true)
+ .HasColumnName("allow_vibrate");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("IsPaused")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(false)
+ .HasColumnName("is_paused");
+
+ b.Property("MaxDuration")
+ .HasColumnType("integer")
+ .HasColumnName("max_duration");
+
+ b.Property("MaxIntensity")
+ .HasColumnType("smallint")
+ .HasColumnName("max_intensity");
+
+ b.Property("ShockerId")
+ .HasColumnType("uuid")
+ .HasColumnName("shocker_id");
+
+ b.HasKey("Id")
+ .HasName("shocker_share_codes_pkey");
+
+ b.HasIndex("ShockerId");
+
+ b.ToTable("shocker_share_codes");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("ActivatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("activated_at");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("CreatedByAutomationTokenId")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_automation_token_id");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(320)
+ .HasColumnType("character varying(320)")
+ .HasColumnName("email");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)")
+ .HasColumnName("name")
+ .UseCollation("ndcoll");
+
+ b.Property("PasswordHash")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("password_hash")
+ .UseCollation("C");
+
+ b.Property>("Roles")
+ .IsRequired()
+ .HasColumnType("role_type[]")
+ .HasColumnName("roles");
+
+ b.Property("SecurityStamp")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("security_stamp")
+ .HasDefaultValueSql("gen_random_uuid()");
+
+ b.HasKey("Id")
+ .HasName("users_pkey");
+
+ b.HasIndex("CreatedByAutomationTokenId");
+
+ b.HasIndex("Email")
+ .IsUnique();
+
+ var index = b.HasIndex("Name")
+ .IsUnique();
+
+ NpgsqlIndexBuilderExtensions.UseCollation(index, new[] { "ndcoll" });
+
+ b.ToTable("users");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserActivationRequest", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("EmailSendAttempts")
+ .HasColumnType("integer")
+ .HasColumnName("email_send_attempts");
+
+ b.Property("TokenHash")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)")
+ .HasColumnName("token_hash")
+ .UseCollation("C");
+
+ b.HasKey("UserId")
+ .HasName("user_activation_requests_pkey");
+
+ b.HasIndex("TokenHash")
+ .IsUnique();
+
+ b.ToTable("user_activation_requests");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserAuditLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("Action")
+ .HasColumnType("audit_action")
+ .HasColumnName("action");
+
+ b.Property("ActorId")
+ .HasColumnType("uuid")
+ .HasColumnName("actor_id");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("IpAddress")
+ .HasMaxLength(40)
+ .HasColumnType("character varying(40)")
+ .HasColumnName("ip_address");
+
+ b.Property("Metadata")
+ .HasColumnType("jsonb")
+ .HasColumnName("metadata");
+
+ b.Property("Reason")
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)")
+ .HasColumnName("reason");
+
+ b.Property("UserAgent")
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)")
+ .HasColumnName("user_agent");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("user_audit_logs_pkey");
+
+ b.HasIndex("ActorId");
+
+ b.HasIndex("CreatedAt");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("user_audit_logs");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserDeactivation", b =>
+ {
+ b.Property("DeactivatedUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("deactivated_user_id");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("DeactivatedByUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("deactivated_by_user_id");
+
+ b.Property("DeleteLater")
+ .HasColumnType("boolean")
+ .HasColumnName("delete_later");
+
+ b.Property("UserModerationId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_moderation_id");
+
+ b.HasKey("DeactivatedUserId")
+ .HasName("user_deactivations_pkey");
+
+ b.HasIndex("DeactivatedByUserId");
+
+ b.ToTable("user_deactivations");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserEmailChange", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("NewEmail")
+ .IsRequired()
+ .HasMaxLength(320)
+ .HasColumnType("character varying(320)")
+ .HasColumnName("email_new");
+
+ b.Property("OldEmail")
+ .IsRequired()
+ .HasMaxLength(320)
+ .HasColumnType("character varying(320)")
+ .HasColumnName("email_old");
+
+ b.Property("SecurityStampAtCreate")
+ .HasColumnType("uuid")
+ .HasColumnName("security_stamp_at_create");
+
+ b.Property("TokenHash")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)")
+ .HasColumnName("token_hash")
+ .UseCollation("C");
+
+ b.Property("UsedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("used_at");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("user_email_changes_pkey");
+
+ b.HasIndex("CreatedAt");
+
+ b.HasIndex("UsedAt");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("user_email_changes");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserNameBlacklist", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("MatchType")
+ .HasColumnType("match_type_enum")
+ .HasColumnName("match_type");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)")
+ .HasColumnName("value")
+ .UseCollation("ndcoll");
+
+ b.HasKey("Id")
+ .HasName("user_name_blacklist_pkey");
+
+ var index = b.HasIndex("Value")
+ .IsUnique();
+
+ NpgsqlIndexBuilderExtensions.UseCollation(index, new[] { "ndcoll" });
+
+ b.ToTable("user_name_blacklist");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserNameChange", b =>
+ {
+ var id = b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(id);
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("OldName")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)")
+ .HasColumnName("old_name");
+
+ b.HasKey("Id", "UserId")
+ .HasName("user_name_changes_pkey");
+
+ b.HasIndex("CreatedAt");
+
+ b.HasIndex("OldName");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("user_name_changes");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserOAuthConnection", b =>
+ {
+ b.Property("ProviderKey")
+ .HasMaxLength(16)
+ .HasColumnType("character varying(16)")
+ .HasColumnName("provider_key")
+ .UseCollation("C");
+
+ b.Property("ExternalId")
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)")
+ .HasColumnName("external_id");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("DisplayName")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("display_name");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("ProviderKey", "ExternalId")
+ .HasName("user_oauth_connections_pkey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("user_oauth_connections");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserPasswordReset", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("SecurityStampAtCreate")
+ .HasColumnType("uuid")
+ .HasColumnName("security_stamp_at_create");
+
+ b.Property("TokenHash")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("token_hash")
+ .UseCollation("C");
+
+ b.Property("UsedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("used_at");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("user_password_resets_pkey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("user_password_resets");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserShare", b =>
+ {
+ b.Property("SharedWithUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("shared_with_user_id");
+
+ b.Property("ShockerId")
+ .HasColumnType("uuid")
+ .HasColumnName("shocker_id");
+
+ b.Property("AllowLiveControl")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true)
+ .HasColumnName("allow_livecontrol");
+
+ b.Property("AllowShock")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true)
+ .HasColumnName("allow_shock");
+
+ b.Property("AllowSound")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true)
+ .HasColumnName("allow_sound");
+
+ b.Property("AllowVibrate")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true)
+ .HasColumnName("allow_vibrate");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("IsPaused")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(false)
+ .HasColumnName("is_paused");
+
+ b.Property("MaxDuration")
+ .HasColumnType("integer")
+ .HasColumnName("max_duration");
+
+ b.Property("MaxIntensity")
+ .HasColumnType("smallint")
+ .HasColumnName("max_intensity");
+
+ b.HasKey("SharedWithUserId", "ShockerId")
+ .HasName("user_shares_pkey");
+
+ b.HasIndex("SharedWithUserId");
+
+ b.HasIndex("ShockerId");
+
+ b.ToTable("user_shares");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserShareInvite", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("OwnerId")
+ .HasColumnType("uuid")
+ .HasColumnName("owner_id");
+
+ b.Property("RecipientUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("user_share_invites_pkey");
+
+ b.HasIndex("OwnerId");
+
+ b.HasIndex("RecipientUserId");
+
+ b.ToTable("user_share_invites");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserShareInviteShocker", b =>
+ {
+ b.Property("InviteId")
+ .HasColumnType("uuid")
+ .HasColumnName("invite_id");
+
+ b.Property("ShockerId")
+ .HasColumnType("uuid")
+ .HasColumnName("shocker_id");
+
+ b.Property("AllowLiveControl")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true)
+ .HasColumnName("allow_livecontrol");
+
+ b.Property("AllowShock")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true)
+ .HasColumnName("allow_shock");
+
+ b.Property("AllowSound")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true)
+ .HasColumnName("allow_sound");
+
+ b.Property("AllowVibrate")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true)
+ .HasColumnName("allow_vibrate");
+
+ b.Property("IsPaused")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(false)
+ .HasColumnName("is_paused");
+
+ b.Property("MaxDuration")
+ .HasColumnType("integer")
+ .HasColumnName("max_duration");
+
+ b.Property("MaxIntensity")
+ .HasColumnType("smallint")
+ .HasColumnName("max_intensity");
+
+ b.HasKey("InviteId", "ShockerId")
+ .HasName("user_share_invite_shockers_pkey");
+
+ b.HasIndex("ShockerId");
+
+ b.ToTable("user_share_invite_shockers");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.ApiToken", b =>
+ {
+ b.HasOne("OpenShock.Common.OpenShockDb.User", "User")
+ .WithMany("ApiTokens")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_api_tokens_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.ApiTokenReport", b =>
+ {
+ b.HasOne("OpenShock.Common.OpenShockDb.User", "ReportedByUser")
+ .WithMany("ReportedApiTokens")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_api_token_reports_reported_by_user_id");
+
+ b.Navigation("ReportedByUser");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.ControlStat", b =>
+ {
+ b.HasOne("OpenShock.Common.OpenShockDb.User", "Owner")
+ .WithMany("ControlStats")
+ .HasForeignKey("OwnerUserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_control_stats_owner_user_id");
+
+ b.HasOne("OpenShock.Common.OpenShockDb.Shocker", "Shocker")
+ .WithMany("ControlStats")
+ .HasForeignKey("ShockerId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("fk_control_stats_shocker_id");
+
+ b.Navigation("Owner");
+
+ b.Navigation("Shocker");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.Device", b =>
+ {
+ b.HasOne("OpenShock.Common.OpenShockDb.User", "Owner")
+ .WithMany("Devices")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_devices_owner_id");
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.DeviceOtaUpdate", b =>
+ {
+ b.HasOne("OpenShock.Common.OpenShockDb.Device", "Device")
+ .WithMany("OtaUpdates")
+ .HasForeignKey("DeviceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_device_ota_updates_device_id");
+
+ b.Navigation("Device");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.PublicShare", b =>
+ {
+ b.HasOne("OpenShock.Common.OpenShockDb.User", "Owner")
+ .WithMany("OwnedPublicShares")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_public_shares_owner_id");
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.PublicShareShocker", b =>
+ {
+ b.HasOne("OpenShock.Common.OpenShockDb.PublicShare", "PublicShare")
+ .WithMany("ShockerMappings")
+ .HasForeignKey("PublicShareId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_public_share_shockers_public_share_id");
+
+ b.HasOne("OpenShock.Common.OpenShockDb.Shocker", "Shocker")
+ .WithMany("PublicShareMappings")
+ .HasForeignKey("ShockerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_public_share_shockers_shocker_id");
+
+ b.Navigation("PublicShare");
+
+ b.Navigation("Shocker");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.Shocker", b =>
+ {
+ b.HasOne("OpenShock.Common.OpenShockDb.Device", "Device")
+ .WithMany("Shockers")
+ .HasForeignKey("DeviceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_shockers_device_id");
+
+ b.Navigation("Device");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerControlLog", b =>
+ {
+ b.HasOne("OpenShock.Common.OpenShockDb.User", "ControlledByUser")
+ .WithMany("ShockerControlLogs")
+ .HasForeignKey("ControlledByUserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .HasConstraintName("fk_shocker_control_logs_controlled_by_user_id");
+
+ b.HasOne("OpenShock.Common.OpenShockDb.Shocker", "Shocker")
+ .WithMany("ShockerControlLogs")
+ .HasForeignKey("ShockerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_shocker_control_logs_shocker_id");
+
+ b.Navigation("ControlledByUser");
+
+ b.Navigation("Shocker");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerShareCode", b =>
+ {
+ b.HasOne("OpenShock.Common.OpenShockDb.Shocker", "Shocker")
+ .WithMany("ShockerShareCodes")
+ .HasForeignKey("ShockerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_shocker_share_codes_shocker_id");
+
+ b.Navigation("Shocker");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.User", b =>
+ {
+ b.HasOne("OpenShock.Common.OpenShockDb.AutomationToken", "CreatedByAutomationToken")
+ .WithMany("CreatedUsers")
+ .HasForeignKey("CreatedByAutomationTokenId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .HasConstraintName("fk_users_created_by_automation_token_id");
+
+ b.Navigation("CreatedByAutomationToken");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserActivationRequest", b =>
+ {
+ b.HasOne("OpenShock.Common.OpenShockDb.User", "User")
+ .WithOne("UserActivationRequest")
+ .HasForeignKey("OpenShock.Common.OpenShockDb.UserActivationRequest", "UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_user_activation_requests_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserAuditLog", b =>
+ {
+ b.HasOne("OpenShock.Common.OpenShockDb.User", "Actor")
+ .WithMany("ActorAuditLogs")
+ .HasForeignKey("ActorId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("fk_user_audit_logs_actor_id");
+
+ b.HasOne("OpenShock.Common.OpenShockDb.User", "User")
+ .WithMany("AuditLogs")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_user_audit_logs_user_id");
+
+ b.Navigation("Actor");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserDeactivation", b =>
+ {
+ b.HasOne("OpenShock.Common.OpenShockDb.User", "DeactivatedByUser")
+ .WithMany()
+ .HasForeignKey("DeactivatedByUserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_user_deactivations_deactivated_by_user_id");
+
+ b.HasOne("OpenShock.Common.OpenShockDb.User", "DeactivatedUser")
+ .WithOne("UserDeactivation")
+ .HasForeignKey("OpenShock.Common.OpenShockDb.UserDeactivation", "DeactivatedUserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_user_deactivations_deactivated_user_id");
+
+ b.Navigation("DeactivatedByUser");
+
+ b.Navigation("DeactivatedUser");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserEmailChange", b =>
+ {
+ b.HasOne("OpenShock.Common.OpenShockDb.User", "User")
+ .WithMany("EmailChanges")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_user_email_changes_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserNameChange", b =>
+ {
+ b.HasOne("OpenShock.Common.OpenShockDb.User", "User")
+ .WithMany("NameChanges")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_user_name_changes_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserOAuthConnection", b =>
+ {
+ b.HasOne("OpenShock.Common.OpenShockDb.User", "User")
+ .WithMany("OAuthConnections")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_user_oauth_connections_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserPasswordReset", b =>
+ {
+ b.HasOne("OpenShock.Common.OpenShockDb.User", "User")
+ .WithMany("PasswordResets")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_user_password_resets_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserShare", b =>
+ {
+ b.HasOne("OpenShock.Common.OpenShockDb.User", "SharedWithUser")
+ .WithMany("IncomingUserShares")
+ .HasForeignKey("SharedWithUserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_user_shares_shared_with_user_id");
+
+ b.HasOne("OpenShock.Common.OpenShockDb.Shocker", "Shocker")
+ .WithMany("UserShares")
+ .HasForeignKey("ShockerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_user_shares_shocker_id");
+
+ b.Navigation("SharedWithUser");
+
+ b.Navigation("Shocker");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserShareInvite", b =>
+ {
+ b.HasOne("OpenShock.Common.OpenShockDb.User", "Owner")
+ .WithMany("OutgoingUserShareInvites")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_user_share_invites_owner_id");
+
+ b.HasOne("OpenShock.Common.OpenShockDb.User", "RecipientUser")
+ .WithMany("IncomingUserShareInvites")
+ .HasForeignKey("RecipientUserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .HasConstraintName("fk_user_share_invites_recipient_user_id");
+
+ b.Navigation("Owner");
+
+ b.Navigation("RecipientUser");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserShareInviteShocker", b =>
+ {
+ b.HasOne("OpenShock.Common.OpenShockDb.UserShareInvite", "Invite")
+ .WithMany("ShockerMappings")
+ .HasForeignKey("InviteId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_user_share_invite_shockers_invite_id");
+
+ b.HasOne("OpenShock.Common.OpenShockDb.Shocker", "Shocker")
+ .WithMany("UserShareInviteShockerMappings")
+ .HasForeignKey("ShockerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_user_share_invite_shockers_shocker_id");
+
+ b.Navigation("Invite");
+
+ b.Navigation("Shocker");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.AutomationToken", b =>
+ {
+ b.Navigation("CreatedUsers");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.Device", b =>
+ {
+ b.Navigation("OtaUpdates");
+
+ b.Navigation("Shockers");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.PublicShare", b =>
+ {
+ b.Navigation("ShockerMappings");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.Shocker", b =>
+ {
+ b.Navigation("ControlStats");
+
+ b.Navigation("PublicShareMappings");
+
+ b.Navigation("ShockerControlLogs");
+
+ b.Navigation("ShockerShareCodes");
+
+ b.Navigation("UserShareInviteShockerMappings");
+
+ b.Navigation("UserShares");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.User", b =>
+ {
+ b.Navigation("ActorAuditLogs");
+
+ b.Navigation("ApiTokens");
+
+ b.Navigation("AuditLogs");
+
+ b.Navigation("ControlStats");
+
+ b.Navigation("Devices");
+
+ b.Navigation("EmailChanges");
+
+ b.Navigation("IncomingUserShareInvites");
+
+ b.Navigation("IncomingUserShares");
+
+ b.Navigation("NameChanges");
+
+ b.Navigation("OAuthConnections");
+
+ b.Navigation("OutgoingUserShareInvites");
+
+ b.Navigation("OwnedPublicShares");
+
+ b.Navigation("PasswordResets");
+
+ b.Navigation("ReportedApiTokens");
+
+ b.Navigation("ShockerControlLogs");
+
+ b.Navigation("UserActivationRequest");
+
+ b.Navigation("UserDeactivation");
+ });
+
+ modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserShareInvite", b =>
+ {
+ b.Navigation("ShockerMappings");
+ });
+#pragma warning restore 612, 618
+ }
+}
diff --git a/Common/Migrations/20261113184758_AddStringLengthLimits.cs b/Common/Migrations/20261113184758_AddStringLengthLimits.cs
new file mode 100644
index 000000000..fe16169ce
--- /dev/null
+++ b/Common/Migrations/20261113184758_AddStringLengthLimits.cs
@@ -0,0 +1,213 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+using OpenShock.Common.OpenShockDb;
+
+#nullable disable
+
+namespace OpenShock.Common.Migrations;
+
+///
+public partial class _20261113184758_AddStringLengthLimits : Migration
+{
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ // The columns below were unbounded text. A truncated credential is useless, so abort with a clear
+ // error if any existing webhook token exceeds the new limit.
+ migrationBuilder.Sql(MigrationQueries.FailIfLonger("discord_webhooks", "webhook_token", 255));
+
+ // Truncate anything else longer than the new limit first, otherwise the type change fails on existing rows.
+ migrationBuilder.Sql("UPDATE user_oauth_connections SET display_name = left(display_name, 64) WHERE char_length(display_name) > 64;");
+
+ migrationBuilder.AlterColumn(
+ name: "display_name",
+ table: "user_oauth_connections",
+ type: "character varying(64)",
+ maxLength: 64,
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "text",
+ oldNullable: true);
+
+ migrationBuilder.Sql("UPDATE user_oauth_connections SET external_id = left(external_id, 255) WHERE char_length(external_id) > 255;");
+
+ migrationBuilder.AlterColumn(
+ name: "external_id",
+ table: "user_oauth_connections",
+ type: "character varying(255)",
+ maxLength: 255,
+ nullable: false,
+ oldClrType: typeof(string),
+ oldType: "text");
+
+ migrationBuilder.Sql("UPDATE user_oauth_connections SET provider_key = left(provider_key, 16) WHERE char_length(provider_key) > 16;");
+
+ migrationBuilder.AlterColumn(
+ name: "provider_key",
+ table: "user_oauth_connections",
+ type: "character varying(16)",
+ maxLength: 16,
+ nullable: false,
+ collation: "C",
+ oldClrType: typeof(string),
+ oldType: "text",
+ oldCollation: "C");
+
+ migrationBuilder.AlterColumn(
+ name: "webhook_token",
+ table: "discord_webhooks",
+ type: "character varying(255)",
+ maxLength: 255,
+ nullable: false,
+ oldClrType: typeof(string),
+ oldType: "text");
+
+ migrationBuilder.Sql("UPDATE discord_webhooks SET name = left(name, 64) WHERE char_length(name) > 64;");
+
+ migrationBuilder.AlterColumn(
+ name: "name",
+ table: "discord_webhooks",
+ type: "character varying(64)",
+ maxLength: 64,
+ nullable: false,
+ oldClrType: typeof(string),
+ oldType: "text");
+
+ migrationBuilder.Sql("UPDATE configuration SET value = left(value, 4096) WHERE char_length(value) > 4096;");
+
+ migrationBuilder.AlterColumn(
+ name: "value",
+ table: "configuration",
+ type: "character varying(4096)",
+ maxLength: 4096,
+ nullable: false,
+ oldClrType: typeof(string),
+ oldType: "text");
+
+ migrationBuilder.Sql("UPDATE configuration SET description = left(description, 255) WHERE char_length(description) > 255;");
+
+ migrationBuilder.AlterColumn(
+ name: "description",
+ table: "configuration",
+ type: "character varying(255)",
+ maxLength: 255,
+ nullable: false,
+ oldClrType: typeof(string),
+ oldType: "text");
+
+ migrationBuilder.Sql("UPDATE configuration SET name = left(name, 64) WHERE char_length(name) > 64;");
+
+ migrationBuilder.AlterColumn(
+ name: "name",
+ table: "configuration",
+ type: "character varying(64)",
+ maxLength: 64,
+ nullable: false,
+ collation: "C",
+ oldClrType: typeof(string),
+ oldType: "text",
+ oldCollation: "C");
+
+ migrationBuilder.Sql("UPDATE api_token_reports SET ip_country = left(ip_country, 4) WHERE char_length(ip_country) > 4;");
+
+ migrationBuilder.AlterColumn(
+ name: "ip_country",
+ table: "api_token_reports",
+ type: "character varying(4)",
+ maxLength: 4,
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "text",
+ oldNullable: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterColumn(
+ name: "display_name",
+ table: "user_oauth_connections",
+ type: "text",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "character varying(64)",
+ oldMaxLength: 64,
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn(
+ name: "external_id",
+ table: "user_oauth_connections",
+ type: "text",
+ nullable: false,
+ oldClrType: typeof(string),
+ oldType: "character varying(255)",
+ oldMaxLength: 255);
+
+ migrationBuilder.AlterColumn(
+ name: "provider_key",
+ table: "user_oauth_connections",
+ type: "text",
+ nullable: false,
+ collation: "C",
+ oldClrType: typeof(string),
+ oldType: "character varying(16)",
+ oldMaxLength: 16,
+ oldCollation: "C");
+
+ migrationBuilder.AlterColumn(
+ name: "webhook_token",
+ table: "discord_webhooks",
+ type: "text",
+ nullable: false,
+ oldClrType: typeof(string),
+ oldType: "character varying(255)",
+ oldMaxLength: 255);
+
+ migrationBuilder.AlterColumn(
+ name: "name",
+ table: "discord_webhooks",
+ type: "text",
+ nullable: false,
+ oldClrType: typeof(string),
+ oldType: "character varying(64)",
+ oldMaxLength: 64);
+
+ migrationBuilder.AlterColumn(
+ name: "value",
+ table: "configuration",
+ type: "text",
+ nullable: false,
+ oldClrType: typeof(string),
+ oldType: "character varying(4096)",
+ oldMaxLength: 4096);
+
+ migrationBuilder.AlterColumn(
+ name: "description",
+ table: "configuration",
+ type: "text",
+ nullable: false,
+ oldClrType: typeof(string),
+ oldType: "character varying(255)",
+ oldMaxLength: 255);
+
+ migrationBuilder.AlterColumn(
+ name: "name",
+ table: "configuration",
+ type: "text",
+ nullable: false,
+ collation: "C",
+ oldClrType: typeof(string),
+ oldType: "character varying(64)",
+ oldMaxLength: 64,
+ oldCollation: "C");
+
+ migrationBuilder.AlterColumn(
+ name: "ip_country",
+ table: "api_token_reports",
+ type: "text",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "character varying(4)",
+ oldMaxLength: 4,
+ oldNullable: true);
+ }
+}
diff --git a/Common/Migrations/OpenShockContextModelSnapshot.cs b/Common/Migrations/OpenShockContextModelSnapshot.cs
index 296a8fb56..b7777ff19 100644
--- a/Common/Migrations/OpenShockContextModelSnapshot.cs
+++ b/Common/Migrations/OpenShockContextModelSnapshot.cs
@@ -18,14 +18,14 @@ partial class OpenShockContextModelSnapshot : ModelSnapshot
// If you encounter a merge conflict in the line below, it means you need to
// discard one of the migration branches and recreate its migrations on top of
// the other branch. See https://aka.ms/efcore-docs-migrations-conflicts for more info.
- public override string LastMigrationId => "20261016142540_AddControlStats";
+ public override string LastMigrationId => "20261113184758_AddStringLengthLimits";
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Npgsql:CollationDefinition:public.ndcoll", "und-u-ks-level2,und-u-ks-level2,icu,False")
- .HasAnnotation("ProductVersion", "11.0.0-rc.1.26425.128")
+ .HasAnnotation("ProductVersion", "11.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "audit_action", new[] { "login", "logout", "password_changed", "email_change_requested", "email_changed", "username_changed", "api_token_created", "api_token_deleted", "oauth_connected", "oauth_disconnected", "account_deactivated", "account_reactivated", "account_deleted", "automation_token_created", "automation_token_updated", "automation_token_rotated", "automation_token_deleted", "automation_token_used" });
@@ -151,6 +151,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
modelBuilder.Entity("OpenShock.Common.OpenShockDb.ApiToken", b =>
{
b.Property("Id")
+ .ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
@@ -253,6 +254,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
modelBuilder.Entity("OpenShock.Common.OpenShockDb.ApiTokenReport", b =>
{
b.Property("Id")
+ .ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
@@ -272,7 +274,8 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.HasColumnName("ip_address");
b.Property("IpCountry")
- .HasColumnType("text")
+ .HasMaxLength(4)
+ .HasColumnType("character varying(4)")
.HasColumnName("ip_country");
b.Property("SubmittedCount")
@@ -294,6 +297,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
modelBuilder.Entity("OpenShock.Common.OpenShockDb.AutomationToken", b =>
{
b.Property("Id")
+ .ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
@@ -357,7 +361,8 @@ protected override void BuildModel(ModelBuilder modelBuilder)
modelBuilder.Entity("OpenShock.Common.OpenShockDb.ConfigurationItem", b =>
{
b.Property("Name")
- .HasColumnType("text")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
.HasColumnName("name")
.UseCollation("C");
@@ -369,7 +374,8 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property