From 56633d48bd78a3226e2efbec84972028008883eb Mon Sep 17 00:00:00 2001 From: HeavenVR Date: Fri, 13 Nov 2026 19:46:35 +0100 Subject: [PATCH 1/4] feat(db): add length limits to unbounded text columns Give the configuration, Discord webhook, OAuth connection and token report country columns explicit maximum lengths instead of unbounded text. The migration truncates any longer existing value before changing each column type. Requests are validated against the same limits, the OAuth display name is truncated on write, and webhook URLs with an over-long token are rejected. --- API/Controller/Admin/DTOs/AddWebhookDto.cs | 6 +- .../Admin/DTOs/ConfigurationAddItemRequest.cs | 8 +- .../DTOs/ConfigurationUpdateItemRequest.cs | 8 +- API/Services/Account/AccountService.cs | 2 +- .../OAuthConnection/OAuthConnectionService.cs | 4 +- Common/Constants/ApiHardLimits.cs | 13 +- ...13184758_AddStringLengthLimits.Designer.cs | 1923 +++++++++++++++++ .../20261113184758_AddStringLengthLimits.cs | 211 ++ .../OpenShockContextModelSnapshot.cs | 47 +- Common/OpenShockDb/OpenShockContext.cs | 10 +- Common/Services/Webhook/WebhookService.cs | 4 +- 11 files changed, 2214 insertions(+), 22 deletions(-) create mode 100644 Common/Migrations/20261113184758_AddStringLengthLimits.Designer.cs create mode 100644 Common/Migrations/20261113184758_AddStringLengthLimits.cs diff --git a/API/Controller/Admin/DTOs/AddWebhookDto.cs b/API/Controller/Admin/DTOs/AddWebhookDto.cs index 583a3fe0e..4c804c033 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; } } \ No newline at end of file diff --git a/API/Controller/Admin/DTOs/ConfigurationAddItemRequest.cs b/API/Controller/Admin/DTOs/ConfigurationAddItemRequest.cs index 3bc174983..830a168a0 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; } } \ No newline at end of file diff --git a/API/Controller/Admin/DTOs/ConfigurationUpdateItemRequest.cs b/API/Controller/Admin/DTOs/ConfigurationUpdateItemRequest.cs index e9b59c302..5d2941bb1 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; } } \ No newline at end of file diff --git a/API/Services/Account/AccountService.cs b/API/Services/Account/AccountService.cs index 1f5083858..487911f33 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/Constants/ApiHardLimits.cs b/Common/Constants/ApiHardLimits.cs index 23e01a901..afd0c6b5c 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 = 256; + 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 = 128; + 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..5e6d58b27 --- /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(256) + .HasColumnType("character varying(256)") + .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(128) + .HasColumnType("character varying(128)") + .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..cc6235d9c --- /dev/null +++ b/Common/Migrations/20261113184758_AddStringLengthLimits.cs @@ -0,0 +1,211 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace OpenShock.Common.Migrations; + +/// +public partial class _20261113184758_AddStringLengthLimits : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // The columns below were unbounded text. Truncate anything 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.Sql("UPDATE discord_webhooks SET webhook_token = left(webhook_token, 128) WHERE char_length(webhook_token) > 128;"); + + migrationBuilder.AlterColumn( + name: "webhook_token", + table: "discord_webhooks", + type: "character varying(128)", + maxLength: 128, + 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, 256) WHERE char_length(description) > 256;"); + + migrationBuilder.AlterColumn( + name: "description", + table: "configuration", + type: "character varying(256)", + maxLength: 256, + 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(128)", + oldMaxLength: 128); + + 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(256)", + oldMaxLength: 256); + + 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..05f26a5bb 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("Description") .IsRequired() - .HasColumnType("text") + .HasMaxLength(256) + .HasColumnType("character varying(256)") .HasColumnName("description"); b.Property("Type") @@ -382,7 +388,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Value") .IsRequired() - .HasColumnType("text") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") .HasColumnName("value"); b.HasKey("Name") @@ -501,6 +508,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("OpenShock.Common.OpenShockDb.Device", b => { b.Property("Id") + .ValueGeneratedOnAdd() .HasColumnType("uuid") .HasColumnName("id"); @@ -580,7 +588,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("OpenShock.Common.OpenShockDb.DiscordWebhook", b => { b.Property("Name") - .HasColumnType("text") + .HasMaxLength(64) + .HasColumnType("character varying(64)") .HasColumnName("name"); b.Property("CreatedAt") @@ -599,7 +608,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("WebhookToken") .IsRequired() - .HasColumnType("text") + .HasMaxLength(128) + .HasColumnType("character varying(128)") .HasColumnName("webhook_token"); b.HasKey("Name") @@ -611,6 +621,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("OpenShock.Common.OpenShockDb.EmailOutboxMessage", b => { b.Property("Id") + .ValueGeneratedOnAdd() .HasColumnType("uuid") .HasColumnName("id"); @@ -690,6 +701,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("OpenShock.Common.OpenShockDb.EmailProviderBlacklist", b => { b.Property("Id") + .ValueGeneratedOnAdd() .HasColumnType("uuid") .HasColumnName("id"); @@ -720,6 +732,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("OpenShock.Common.OpenShockDb.PublicShare", b => { b.Property("Id") + .ValueGeneratedOnAdd() .HasColumnType("uuid") .HasColumnName("id"); @@ -814,6 +827,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("OpenShock.Common.OpenShockDb.Shocker", b => { b.Property("Id") + .ValueGeneratedOnAdd() .HasColumnType("uuid") .HasColumnName("id"); @@ -858,6 +872,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerControlLog", b => { b.Property("Id") + .ValueGeneratedOnAdd() .HasColumnType("uuid") .HasColumnName("id"); @@ -911,6 +926,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerShareCode", b => { b.Property("Id") + .ValueGeneratedOnAdd() .HasColumnType("uuid") .HasColumnName("id"); @@ -973,6 +989,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("OpenShock.Common.OpenShockDb.User", b => { b.Property("Id") + .ValueGeneratedOnAdd() .HasColumnType("uuid") .HasColumnName("id"); @@ -1071,6 +1088,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserAuditLog", b => { b.Property("Id") + .ValueGeneratedOnAdd() .HasColumnType("uuid") .HasColumnName("id"); @@ -1158,6 +1176,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserEmailChange", b => { b.Property("Id") + .ValueGeneratedOnAdd() .HasColumnType("uuid") .HasColumnName("id"); @@ -1213,6 +1232,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserNameBlacklist", b => { b.Property("Id") + .ValueGeneratedOnAdd() .HasColumnType("uuid") .HasColumnName("id"); @@ -1284,12 +1304,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserOAuthConnection", b => { b.Property("ProviderKey") - .HasColumnType("text") + .HasMaxLength(16) + .HasColumnType("character varying(16)") .HasColumnName("provider_key") .UseCollation("C"); b.Property("ExternalId") - .HasColumnType("text") + .HasMaxLength(255) + .HasColumnType("character varying(255)") .HasColumnName("external_id"); b.Property("CreatedAt") @@ -1299,7 +1321,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasDefaultValueSql("CURRENT_TIMESTAMP"); b.Property("DisplayName") - .HasColumnType("text") + .HasMaxLength(64) + .HasColumnType("character varying(64)") .HasColumnName("display_name"); b.Property("UserId") @@ -1317,6 +1340,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserPasswordReset", b => { b.Property("Id") + .ValueGeneratedOnAdd() .HasColumnType("uuid") .HasColumnName("id"); @@ -1420,6 +1444,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserShareInvite", b => { b.Property("Id") + .ValueGeneratedOnAdd() .HasColumnType("uuid") .HasColumnName("id"); diff --git a/Common/OpenShockDb/OpenShockContext.cs b/Common/OpenShockDb/OpenShockContext.cs index 234f69209..b28813927 100644 --- a/Common/OpenShockDb/OpenShockContext.cs +++ b/Common/OpenShockDb/OpenShockContext.cs @@ -3,7 +3,6 @@ using Microsoft.EntityFrameworkCore; using OpenShock.Common.Constants; using OpenShock.Common.Models; - using OpenShock.Internal.Common.Constants; namespace OpenShock.Common.OpenShockDb; @@ -232,6 +231,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.IpAddress) .HasColumnName("ip_address"); entity.Property(e => e.IpCountry) + .HasMaxLength(4) .HasColumnName("ip_country"); entity.Property(e => e.CreatedAt) .HasDefaultValueSql("CURRENT_TIMESTAMP") @@ -729,10 +729,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasColumnName("user_id"); entity.Property(e => e.ProviderKey) .UseCollation("C") + .HasMaxLength(ApiHardLimits.OAuthProviderKeyMaxLength) .HasColumnName("provider_key"); entity.Property(e => e.ExternalId) + .HasMaxLength(ApiHardLimits.OAuthExternalIdMaxLength) .HasColumnName("external_id"); entity.Property(e => e.DisplayName) + .HasMaxLength(ApiHardLimits.OAuthDisplayNameMaxLength) .HasColumnName("display_name"); entity.Property(e => e.CreatedAt) .HasDefaultValueSql("CURRENT_TIMESTAMP") @@ -869,10 +872,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.Id) .HasColumnName("id"); entity.Property(e => e.Name) + .HasMaxLength(ApiHardLimits.DiscordWebhookNameMaxLength) .HasColumnName("name"); entity.Property(e => e.WebhookId) .HasColumnName("webhook_id"); entity.Property(e => e.WebhookToken) + .HasMaxLength(ApiHardLimits.DiscordWebhookTokenMaxLength) .HasColumnName("webhook_token"); entity.Property(e => e.CreatedAt) .HasDefaultValueSql("CURRENT_TIMESTAMP") @@ -887,12 +892,15 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.Name) .UseCollation("C") + .HasMaxLength(ApiHardLimits.ConfigurationNameMaxLength) .HasColumnName("name"); entity.Property(e => e.Description) + .HasMaxLength(ApiHardLimits.ConfigurationDescriptionMaxLength) .HasColumnName("description"); entity.Property(e => e.Type) .HasColumnName("type"); entity.Property(e => e.Value) + .HasMaxLength(ApiHardLimits.ConfigurationValueMaxLength) .HasColumnName("value"); entity.Property(e => e.UpdatedAt) .HasColumnName("updated_at"); diff --git a/Common/Services/Webhook/WebhookService.cs b/Common/Services/Webhook/WebhookService.cs index 0671ae3e2..a278a663b 100644 --- a/Common/Services/Webhook/WebhookService.cs +++ b/Common/Services/Webhook/WebhookService.cs @@ -1,5 +1,6 @@ using System.Drawing; using Microsoft.EntityFrameworkCore; +using OpenShock.Common.Constants; using OpenShock.Common.Models; using OpenShock.Common.OpenShockDb; using OpenShock.Common.Results; @@ -28,7 +29,8 @@ public async Task, UnsupportedWebhookUrl>> AddWebhook DnsSafeHost: "discord.com", Segments: ["/", "api/", "webhooks/", {} webhookIdStr, {} webhookToken] } || - !long.TryParse(webhookIdStr[..^1], out var webhookId) + !long.TryParse(webhookIdStr[..^1], out var webhookId) || + webhookToken.Length > ApiHardLimits.DiscordWebhookTokenMaxLength ) { return new UnsupportedWebhookUrl(); From c0de257532b994cf30d3b2f8c6f0130659ada400 Mon Sep 17 00:00:00 2001 From: HeavenVR Date: Fri, 13 Nov 2026 22:12:20 +0100 Subject: [PATCH 2/4] fix: never split a surrogate pair when truncating strings --- Common.Tests/Utils/StringUtilsTests.cs | 14 ++++++++++++++ Common/Utils/StringUtils.cs | 7 ++++++- 2 files changed, 20 insertions(+), 1 deletion(-) 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/Utils/StringUtils.cs b/Common/Utils/StringUtils.cs index e569d4f6c..797ba763d 100644 --- a/Common/Utils/StringUtils.cs +++ b/Common/Utils/StringUtils.cs @@ -4,6 +4,11 @@ public static class StringUtils { public static string Truncate(this string input, int maxLength) { - return input.Length <= maxLength ? input : input[..maxLength]; + if (input.Length <= maxLength) return input; + + // Never cut a surrogate pair in half, that would leave an invalid UTF-16 string. + if (maxLength > 0 && char.IsHighSurrogate(input[maxLength - 1])) maxLength--; + + return input[..maxLength]; } } From c4bce00d53f3437a53615f512f744b1105dde94e Mon Sep 17 00:00:00 2001 From: HeavenVR Date: Fri, 13 Nov 2026 22:12:20 +0100 Subject: [PATCH 3/4] fix(db): fail the migration instead of truncating webhook tokens --- .../20261113184758_AddStringLengthLimits.cs | 10 ++++---- .../OpenShockDb/Queries/MigrationQueries.cs | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 Common/OpenShockDb/Queries/MigrationQueries.cs diff --git a/Common/Migrations/20261113184758_AddStringLengthLimits.cs b/Common/Migrations/20261113184758_AddStringLengthLimits.cs index cc6235d9c..9704c157c 100644 --- a/Common/Migrations/20261113184758_AddStringLengthLimits.cs +++ b/Common/Migrations/20261113184758_AddStringLengthLimits.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore.Migrations; +using OpenShock.Common.OpenShockDb; #nullable disable @@ -10,8 +11,11 @@ public partial class _20261113184758_AddStringLengthLimits : Migration /// protected override void Up(MigrationBuilder migrationBuilder) { - // The columns below were unbounded text. Truncate anything longer than the new limit first, - // otherwise the type change fails on existing rows. + // 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", 128)); + + // 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( @@ -48,8 +52,6 @@ protected override void Up(MigrationBuilder migrationBuilder) oldType: "text", oldCollation: "C"); - migrationBuilder.Sql("UPDATE discord_webhooks SET webhook_token = left(webhook_token, 128) WHERE char_length(webhook_token) > 128;"); - migrationBuilder.AlterColumn( name: "webhook_token", table: "discord_webhooks", diff --git a/Common/OpenShockDb/Queries/MigrationQueries.cs b/Common/OpenShockDb/Queries/MigrationQueries.cs new file mode 100644 index 000000000..fc88a6751 --- /dev/null +++ b/Common/OpenShockDb/Queries/MigrationQueries.cs @@ -0,0 +1,24 @@ +namespace OpenShock.Common.OpenShockDb; + +/// +/// Reusable raw SQL for migrations. +/// +public static class MigrationQueries +{ + /// + /// SQL that aborts the migration with a clear error if any existing value in is longer + /// than characters. Run it before narrowing a column whose values must not be + /// truncated, like credentials. Table and column names are inlined into the SQL, only pass constants. + /// + public static string FailIfLonger(string table, string column, int maxLength) => + $""" + DO $$ + DECLARE offending bigint; + BEGIN + SELECT count(*) INTO offending FROM {table} WHERE char_length({column}) > {maxLength}; + IF offending > 0 THEN + RAISE EXCEPTION '% row(s) in {table}.{column} exceed the new limit of {maxLength} characters. Fix or remove them manually before re-running this migration.', offending; + END IF; + END $$; + """; +} From f1fb78036bae872ab4093112d05b02efa246ac45 Mon Sep 17 00:00:00 2001 From: HeavenVR Date: Sat, 14 Nov 2026 08:59:56 +0100 Subject: [PATCH 4/4] adjust lengths --- Common/Constants/ApiHardLimits.cs | 4 ++-- ...13184758_AddStringLengthLimits.Designer.cs | 8 ++++---- .../20261113184758_AddStringLengthLimits.cs | 20 +++++++++---------- .../OpenShockContextModelSnapshot.cs | 8 ++++---- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Common/Constants/ApiHardLimits.cs b/Common/Constants/ApiHardLimits.cs index afd0c6b5c..d3bd51a45 100644 --- a/Common/Constants/ApiHardLimits.cs +++ b/Common/Constants/ApiHardLimits.cs @@ -26,7 +26,7 @@ public static class ApiHardLimits public const int AuditReasonMaxLength = 512; public const int ConfigurationNameMaxLength = 64; - public const int ConfigurationDescriptionMaxLength = 256; + public const int ConfigurationDescriptionMaxLength = 255; public const int ConfigurationValueMaxLength = 4096; public const int OAuthProviderKeyMaxLength = 16; @@ -34,7 +34,7 @@ public static class ApiHardLimits public const int OAuthDisplayNameMaxLength = 64; public const int DiscordWebhookNameMaxLength = 64; - public const int DiscordWebhookTokenMaxLength = 128; + public const int DiscordWebhookTokenMaxLength = 255; public const int MaxTurnstileResponseTokenLength = 2048; // https://developers.cloudflare.com/turnstile/get-started/server-side-validation/ diff --git a/Common/Migrations/20261113184758_AddStringLengthLimits.Designer.cs b/Common/Migrations/20261113184758_AddStringLengthLimits.Designer.cs index 5e6d58b27..49d4b587d 100644 --- a/Common/Migrations/20261113184758_AddStringLengthLimits.Designer.cs +++ b/Common/Migrations/20261113184758_AddStringLengthLimits.Designer.cs @@ -372,8 +372,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("Description") .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)") + .HasMaxLength(255) + .HasColumnType("character varying(255)") .HasColumnName("description"); b.Property("Type") @@ -606,8 +606,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("WebhookToken") .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)") + .HasMaxLength(255) + .HasColumnType("character varying(255)") .HasColumnName("webhook_token"); b.HasKey("Name") diff --git a/Common/Migrations/20261113184758_AddStringLengthLimits.cs b/Common/Migrations/20261113184758_AddStringLengthLimits.cs index 9704c157c..fe16169ce 100644 --- a/Common/Migrations/20261113184758_AddStringLengthLimits.cs +++ b/Common/Migrations/20261113184758_AddStringLengthLimits.cs @@ -13,7 +13,7 @@ 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", 128)); + 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;"); @@ -55,8 +55,8 @@ protected override void Up(MigrationBuilder migrationBuilder) migrationBuilder.AlterColumn( name: "webhook_token", table: "discord_webhooks", - type: "character varying(128)", - maxLength: 128, + type: "character varying(255)", + maxLength: 255, nullable: false, oldClrType: typeof(string), oldType: "text"); @@ -83,13 +83,13 @@ protected override void Up(MigrationBuilder migrationBuilder) oldClrType: typeof(string), oldType: "text"); - migrationBuilder.Sql("UPDATE configuration SET description = left(description, 256) WHERE char_length(description) > 256;"); + migrationBuilder.Sql("UPDATE configuration SET description = left(description, 255) WHERE char_length(description) > 255;"); migrationBuilder.AlterColumn( name: "description", table: "configuration", - type: "character varying(256)", - maxLength: 256, + type: "character varying(255)", + maxLength: 255, nullable: false, oldClrType: typeof(string), oldType: "text"); @@ -159,8 +159,8 @@ protected override void Down(MigrationBuilder migrationBuilder) type: "text", nullable: false, oldClrType: typeof(string), - oldType: "character varying(128)", - oldMaxLength: 128); + oldType: "character varying(255)", + oldMaxLength: 255); migrationBuilder.AlterColumn( name: "name", @@ -186,8 +186,8 @@ protected override void Down(MigrationBuilder migrationBuilder) type: "text", nullable: false, oldClrType: typeof(string), - oldType: "character varying(256)", - oldMaxLength: 256); + oldType: "character varying(255)", + oldMaxLength: 255); migrationBuilder.AlterColumn( name: "name", diff --git a/Common/Migrations/OpenShockContextModelSnapshot.cs b/Common/Migrations/OpenShockContextModelSnapshot.cs index 05f26a5bb..b7777ff19 100644 --- a/Common/Migrations/OpenShockContextModelSnapshot.cs +++ b/Common/Migrations/OpenShockContextModelSnapshot.cs @@ -374,8 +374,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Description") .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)") + .HasMaxLength(255) + .HasColumnType("character varying(255)") .HasColumnName("description"); b.Property("Type") @@ -608,8 +608,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("WebhookToken") .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)") + .HasMaxLength(255) + .HasColumnType("character varying(255)") .HasColumnName("webhook_token"); b.HasKey("Name")