diff --git a/API.IntegrationTests/Helpers/TestHelper.cs b/API.IntegrationTests/Helpers/TestHelper.cs index 71947eb2..49b53e35 100644 --- a/API.IntegrationTests/Helpers/TestHelper.cs +++ b/API.IntegrationTests/Helpers/TestHelper.cs @@ -2,6 +2,7 @@ using System.Net; using System.Text; using System.Text.Json; +using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; using OpenShock.Common.Constants; using OpenShock.Common.OpenShockDb; @@ -50,7 +51,7 @@ public static async Task CreateAndLoginUser( /// public static HttpClient CreateAuthenticatedClient(WebApplicationFactory factory, string sessionToken) { - var client = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions + var client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false, HandleCookies = false @@ -64,7 +65,7 @@ public static HttpClient CreateAuthenticatedClient(WebApplicationFactory factory /// public static HttpClient CreateApiTokenClient(WebApplicationFactory factory, string apiToken) { - var client = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions + var client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false, HandleCookies = false @@ -78,7 +79,7 @@ public static HttpClient CreateApiTokenClient(WebApplicationFactory factory, str /// public static HttpClient CreateHubTokenClient(WebApplicationFactory factory, string hubToken) { - var client = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions + var client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false, HandleCookies = false @@ -146,7 +147,7 @@ public static async Task CreateUserInDb( WebApplicationFactory factory, Guid userId, string name = "TestToken", - List? permissions = null) + List? permissions = null) { await using var scope = factory.Services.CreateAsyncScope(); var db = scope.ServiceProvider.GetRequiredService(); @@ -160,7 +161,7 @@ public static async Task CreateUserInDb( Name = name, TokenHash = HashingUtils.HashToken(rawToken), CreatedByIp = IPAddress.Loopback, - Permissions = permissions ?? [Common.Models.PermissionType.Shockers_Use] + Permissions = permissions ?? [PermissionType.Shockers_Use] }); await db.SaveChangesAsync(); return (tokenId, rawToken); diff --git a/API.IntegrationTests/Tests/TokensTests.cs b/API.IntegrationTests/Tests/TokensTests.cs index ef01677b..b6dd3b8e 100644 --- a/API.IntegrationTests/Tests/TokensTests.cs +++ b/API.IntegrationTests/Tests/TokensTests.cs @@ -1,6 +1,7 @@ using System.Net; using System.Text.Json; using OpenShock.API.IntegrationTests.Helpers; +using OpenShock.Common.OpenShockDb; namespace OpenShock.API.IntegrationTests.Tests; @@ -348,7 +349,7 @@ public async Task ApiTokenAuth_CanAccessDevices() { var userId = await TestHelper.CreateUserInDb(WebApplicationFactory, "tokauth", "tokauth@test.org", "SecurePassword123#"); var (_, rawToken) = await TestHelper.CreateApiTokenInDb(WebApplicationFactory, userId, "AuthToken", - [Common.Models.PermissionType.Shockers_Use, Common.Models.PermissionType.Devices_Edit]); + [PermissionType.Shockers_Use, PermissionType.Devices_Edit]); using var client = TestHelper.CreateApiTokenClient(WebApplicationFactory, rawToken); var response = await client.GetAsync("/1/devices"); diff --git a/API/Controller/Account/Authenticated/ChangeUsername.cs b/API/Controller/Account/Authenticated/ChangeUsername.cs index 759fa11c..e346d20f 100644 --- a/API/Controller/Account/Authenticated/ChangeUsername.cs +++ b/API/Controller/Account/Authenticated/ChangeUsername.cs @@ -1,9 +1,9 @@ -using Microsoft.AspNetCore.Mvc; +using System.Net.Mime; +using Microsoft.AspNetCore.Mvc; using OpenShock.API.Models.Requests; using OpenShock.Common.Errors; -using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; using OpenShock.Common.Problems; -using System.Net.Mime; namespace OpenShock.API.Controller.Account.Authenticated; diff --git a/API/Controller/Devices/DevicesController.cs b/API/Controller/Devices/DevicesController.cs index 7320d064..46539308 100644 --- a/API/Controller/Devices/DevicesController.cs +++ b/API/Controller/Devices/DevicesController.cs @@ -1,18 +1,20 @@ -using Asp.Versioning; +using System.Net.Mime; +using System.Security.Cryptography; +using Asp.Versioning; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using OpenShock.API.Models.Requests; +using OpenShock.API.Models.Response; +using OpenShock.API.Services.DeviceUpdate; using OpenShock.Common.Authentication.Attributes; using OpenShock.Common.Constants; using OpenShock.Common.Errors; using OpenShock.Common.Extensions; using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; using OpenShock.Common.Problems; using OpenShock.Common.Redis; using OpenShock.Common.Utils; -using System.Net.Mime; -using System.Security.Cryptography; -using OpenShock.API.Services.DeviceUpdate; using Redis.OM; namespace OpenShock.API.Controller.Devices; @@ -25,12 +27,12 @@ public sealed partial class DevicesController /// All devices for the current user [HttpGet] [MapToApiVersion("1")] - [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] public IActionResult ListDevices() { var devices = _db.Devices .Where(x => x.OwnerId == CurrentUser.Id) - .Select(x => new Models.Response.DeviceResponse + .Select(x => new DeviceResponse { Id = x.Id, Name = x.Name, @@ -47,7 +49,7 @@ public IActionResult ListDevices() /// /// The device [HttpGet("{deviceId}")] - [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // DeviceNotFound [MapToApiVersion("1")] public async Task GetDeviceById([FromRoute] Guid deviceId) @@ -56,7 +58,7 @@ public async Task GetDeviceById([FromRoute] Guid deviceId) var device = await _db.Devices.Where(x => x.OwnerId == CurrentUser.Id && x.Id == deviceId) - .Select(x => new Models.Response.DeviceWithTokenResponse + .Select(x => new DeviceWithTokenResponse { Id = x.Id, Name = x.Name, diff --git a/API/Controller/Sessions/DeleteSessions.cs b/API/Controller/Sessions/DeleteSessions.cs index 74c5b87c..b950141f 100644 --- a/API/Controller/Sessions/DeleteSessions.cs +++ b/API/Controller/Sessions/DeleteSessions.cs @@ -1,9 +1,9 @@ -using Microsoft.AspNetCore.Mvc; +using System.Net.Mime; +using Microsoft.AspNetCore.Mvc; using OpenShock.Common.Errors; using OpenShock.Common.Extensions; -using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; using OpenShock.Common.Problems; -using System.Net.Mime; namespace OpenShock.API.Controller.Sessions; diff --git a/API/Controller/Shares/UserShares/UpdateShockerShares.cs b/API/Controller/Shares/UserShares/UpdateShockerShares.cs index 0d9037ab..cb6d21ab 100644 --- a/API/Controller/Shares/UserShares/UpdateShockerShares.cs +++ b/API/Controller/Shares/UserShares/UpdateShockerShares.cs @@ -7,6 +7,7 @@ using OpenShock.Common.Authentication.Attributes; using OpenShock.Common.Errors; using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; using OpenShock.Common.Problems; namespace OpenShock.API.Controller.Shares.UserShares; diff --git a/API/Controller/Shockers/EditShocker.cs b/API/Controller/Shockers/EditShocker.cs index 42b3a934..6cafad77 100644 --- a/API/Controller/Shockers/EditShocker.cs +++ b/API/Controller/Shockers/EditShocker.cs @@ -7,6 +7,7 @@ using OpenShock.Common.Authentication.Attributes; using OpenShock.Common.Errors; using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; using OpenShock.Common.Problems; namespace OpenShock.API.Controller.Shockers; diff --git a/API/Controller/Shockers/PauseShocker.cs b/API/Controller/Shockers/PauseShocker.cs index 9c61a214..257c2d5d 100644 --- a/API/Controller/Shockers/PauseShocker.cs +++ b/API/Controller/Shockers/PauseShocker.cs @@ -7,6 +7,7 @@ using OpenShock.Common.Authentication.Attributes; using OpenShock.Common.Errors; using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; using OpenShock.Common.Problems; namespace OpenShock.API.Controller.Shockers; diff --git a/API/Controller/Shockers/RemoveShocker.cs b/API/Controller/Shockers/RemoveShocker.cs index 8aa9438a..6cf852b9 100644 --- a/API/Controller/Shockers/RemoveShocker.cs +++ b/API/Controller/Shockers/RemoveShocker.cs @@ -7,6 +7,7 @@ using OpenShock.Common.Errors; using OpenShock.Common.Extensions; using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; using OpenShock.Common.Problems; namespace OpenShock.API.Controller.Shockers; diff --git a/API/Controller/Users/GetSelf.cs b/API/Controller/Users/GetSelf.cs index 396f2c41..b3a3ced8 100644 --- a/API/Controller/Users/GetSelf.cs +++ b/API/Controller/Users/GetSelf.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc; using OpenShock.Common.Extensions; using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; namespace OpenShock.API.Controller.Users; diff --git a/API/Models/Requests/EditTokenRequest.cs b/API/Models/Requests/EditTokenRequest.cs index fd64005a..cebfdfb4 100644 --- a/API/Models/Requests/EditTokenRequest.cs +++ b/API/Models/Requests/EditTokenRequest.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; using OpenShock.Common.Constants; -using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; namespace OpenShock.API.Models.Requests; diff --git a/API/Models/Requests/EditTokenRequestV2.cs b/API/Models/Requests/EditTokenRequestV2.cs index fb50f456..c9276bb1 100644 --- a/API/Models/Requests/EditTokenRequestV2.cs +++ b/API/Models/Requests/EditTokenRequestV2.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; using OpenShock.Common.Constants; -using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; namespace OpenShock.API.Models.Requests; diff --git a/API/Models/Requests/NewShocker.cs b/API/Models/Requests/NewShocker.cs index 3b1f0be2..01ea346e 100644 --- a/API/Models/Requests/NewShocker.cs +++ b/API/Models/Requests/NewShocker.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; using OpenShock.Common.Constants; -using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; namespace OpenShock.API.Models.Requests; diff --git a/API/Models/Response/LogEntry.cs b/API/Models/Response/LogEntry.cs index 585b8068..0bfdc0db 100644 --- a/API/Models/Response/LogEntry.cs +++ b/API/Models/Response/LogEntry.cs @@ -1,4 +1,5 @@ using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; namespace OpenShock.API.Models.Response; diff --git a/API/Models/Response/LogEntryWithHub.cs b/API/Models/Response/LogEntryWithHub.cs index d429b73b..82e91920 100644 --- a/API/Models/Response/LogEntryWithHub.cs +++ b/API/Models/Response/LogEntryWithHub.cs @@ -1,4 +1,5 @@ using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; namespace OpenShock.API.Models.Response; diff --git a/API/Models/Response/ShockerResponse.cs b/API/Models/Response/ShockerResponse.cs index 6b0e5bac..1bb68783 100644 --- a/API/Models/Response/ShockerResponse.cs +++ b/API/Models/Response/ShockerResponse.cs @@ -1,4 +1,4 @@ -using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; namespace OpenShock.API.Models.Response; diff --git a/API/Models/Response/TokenCreatedResponse.cs b/API/Models/Response/TokenCreatedResponse.cs index e2fe87c9..27564902 100644 --- a/API/Models/Response/TokenCreatedResponse.cs +++ b/API/Models/Response/TokenCreatedResponse.cs @@ -1,4 +1,4 @@ -using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; namespace OpenShock.API.Models.Response; diff --git a/API/Models/Response/TokenCreatedResponseV2.cs b/API/Models/Response/TokenCreatedResponseV2.cs index 9ee5ca3b..286b774f 100644 --- a/API/Models/Response/TokenCreatedResponseV2.cs +++ b/API/Models/Response/TokenCreatedResponseV2.cs @@ -1,5 +1,4 @@ -using OpenShock.API.Models; -using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; namespace OpenShock.API.Models.Response; diff --git a/API/Models/Response/TokenResponse.cs b/API/Models/Response/TokenResponse.cs index 73676b0e..ac457104 100644 --- a/API/Models/Response/TokenResponse.cs +++ b/API/Models/Response/TokenResponse.cs @@ -1,4 +1,4 @@ -using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; namespace OpenShock.API.Models.Response; diff --git a/API/Models/Response/TokenResponseV2.cs b/API/Models/Response/TokenResponseV2.cs index 9256b575..88c1f743 100644 --- a/API/Models/Response/TokenResponseV2.cs +++ b/API/Models/Response/TokenResponseV2.cs @@ -1,5 +1,4 @@ -using OpenShock.API.Models; -using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; namespace OpenShock.API.Models.Response; diff --git a/Common.Tests/Models/ApiTokenControlLimitsTests.cs b/Common.Tests/Models/ApiTokenControlLimitsTests.cs index 6d83d69d..cdf3436a 100644 --- a/Common.Tests/Models/ApiTokenControlLimitsTests.cs +++ b/Common.Tests/Models/ApiTokenControlLimitsTests.cs @@ -1,4 +1,5 @@ using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; namespace OpenShock.Common.Tests.Models; diff --git a/Common.Tests/Utils/HashingUtilsTests.cs b/Common.Tests/Utils/HashingUtilsTests.cs index 698a2a0e..099b0dec 100644 --- a/Common.Tests/Utils/HashingUtilsTests.cs +++ b/Common.Tests/Utils/HashingUtilsTests.cs @@ -1,4 +1,4 @@ -using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; using OpenShock.Common.Utils; namespace OpenShock.Common.Tests.Utils; diff --git a/Common.Tests/Utils/PermissionUtilsTests.cs b/Common.Tests/Utils/PermissionUtilsTests.cs index de808de2..218f5e1a 100644 --- a/Common.Tests/Utils/PermissionUtilsTests.cs +++ b/Common.Tests/Utils/PermissionUtilsTests.cs @@ -1,4 +1,5 @@ using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; using OpenShock.Common.Utils; namespace OpenShock.Common.Tests.Utils; diff --git a/Common/Authentication/Requirements/ApiTokenPermissionRequirement.cs b/Common/Authentication/Requirements/ApiTokenPermissionRequirement.cs index 275d565f..a5c372aa 100644 --- a/Common/Authentication/Requirements/ApiTokenPermissionRequirement.cs +++ b/Common/Authentication/Requirements/ApiTokenPermissionRequirement.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Authorization; -using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; namespace OpenShock.Common.Authentication.Requirements; diff --git a/Common/DeviceControl/ControlShockerObj.cs b/Common/DeviceControl/ControlShockerObj.cs index fd94132f..6ddba91c 100644 --- a/Common/DeviceControl/ControlShockerObj.cs +++ b/Common/DeviceControl/ControlShockerObj.cs @@ -1,4 +1,5 @@ using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; namespace OpenShock.Common.DeviceControl; diff --git a/Common/Errors/AuthorizationError.cs b/Common/Errors/AuthorizationError.cs index fb4d8ef2..7753103b 100644 --- a/Common/Errors/AuthorizationError.cs +++ b/Common/Errors/AuthorizationError.cs @@ -1,5 +1,5 @@ using System.Net; -using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; using OpenShock.Common.Problems; using OpenShock.Common.Problems.CustomProblems; diff --git a/Common/Extensions/ClaimsPrincipalExtensions.cs b/Common/Extensions/ClaimsPrincipalExtensions.cs index 1cabfdce..d0e7fbcc 100644 --- a/Common/Extensions/ClaimsPrincipalExtensions.cs +++ b/Common/Extensions/ClaimsPrincipalExtensions.cs @@ -1,6 +1,6 @@ using System.Security.Claims; using OpenShock.Common.Authentication; -using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; namespace OpenShock.Common.Extensions; diff --git a/Common/JsonSerialization/PermissionTypeConverter.cs b/Common/JsonSerialization/PermissionTypeConverter.cs index d8ceb7b3..74445584 100644 --- a/Common/JsonSerialization/PermissionTypeConverter.cs +++ b/Common/JsonSerialization/PermissionTypeConverter.cs @@ -1,6 +1,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; namespace OpenShock.Common.JsonSerialization; diff --git a/Common/Migrations/20260603134636_AddApiTokenShockerControl.cs b/Common/Migrations/20260603134636_AddApiTokenShockerControl.cs index d78e72a2..05c6a692 100644 --- a/Common/Migrations/20260603134636_AddApiTokenShockerControl.cs +++ b/Common/Migrations/20260603134636_AddApiTokenShockerControl.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; #nullable disable diff --git a/Common/Migrations/20260630210423_AddEmailOutbox.cs b/Common/Migrations/20260630210423_AddEmailOutbox.cs index 8f0ca811..a2e98620 100644 --- a/Common/Migrations/20260630210423_AddEmailOutbox.cs +++ b/Common/Migrations/20260630210423_AddEmailOutbox.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Migrations; using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; #nullable disable diff --git a/Common/Migrations/20260630233834_RefactorPgEnums.Designer.cs b/Common/Migrations/20260630233834_RefactorPgEnums.Designer.cs new file mode 100644 index 00000000..c88a0825 --- /dev/null +++ b/Common/Migrations/20260630233834_RefactorPgEnums.Designer.cs @@ -0,0 +1,1599 @@ +// +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.Models; +using OpenShock.Common.OpenShockDb; + +#nullable disable + +namespace OpenShock.Common.Migrations +{ + [DbContext(typeof(MigrationOpenShockContext))] + [Migration("20260630233834_RefactorPgEnums")] + partial class RefactorPgEnums + { + /// + 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", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + 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" }); + 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 => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("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("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") + .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.PrimitiveCollection>("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", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ApiTokenReport", b => + { + b.Property("Id") + .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") + .HasColumnType("text") + .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", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ConfigurationItem", b => + { + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name") + .UseCollation("C"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .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() + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Name") + .HasName("configuration_pkey"); + + b.ToTable("configuration", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Device", b => + { + b.Property("Id") + .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", (string)null); + }); + + 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", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.DiscordWebhook", b => + { + b.Property("Name") + .HasColumnType("text") + .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() + .HasColumnType("text") + .HasColumnName("webhook_token"); + + b.HasKey("Name") + .HasName("discord_webhooks_pkey"); + + b.ToTable("discord_webhooks", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.EmailOutboxMessage", b => + { + b.Property("Id") + .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", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.EmailProviderBlacklist", b => + { + b.Property("Id") + .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"); + + b.HasIndex("Domain") + .IsUnique(); + + NpgsqlIndexBuilderExtensions.UseCollation(b.HasIndex("Domain"), new[] { "ndcoll" }); + + b.ToTable("email_provider_blacklist", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.PublicShare", b => + { + b.Property("Id") + .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", (string)null); + }); + + 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", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Shocker", b => + { + b.Property("Id") + .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", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerControlLog", b => + { + b.Property("Id") + .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", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerShareCode", b => + { + b.Property("Id") + .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", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.User", b => + { + b.Property("Id") + .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("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.PrimitiveCollection>("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("Email") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + NpgsqlIndexBuilderExtensions.UseCollation(b.HasIndex("Name"), new[] { "ndcoll" }); + + b.ToTable("users", (string)null); + }); + + 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", (string)null); + }); + + 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", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserEmailChange", b => + { + b.Property("Id") + .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", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserNameBlacklist", b => + { + b.Property("Id") + .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"); + + b.HasIndex("Value") + .IsUnique(); + + NpgsqlIndexBuilderExtensions.UseCollation(b.HasIndex("Value"), new[] { "ndcoll" }); + + b.ToTable("user_name_blacklist", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserNameChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("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", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserOAuthConnection", b => + { + b.Property("ProviderKey") + .HasColumnType("text") + .HasColumnName("provider_key") + .UseCollation("C"); + + b.Property("ExternalId") + .HasColumnType("text") + .HasColumnName("external_id"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("DisplayName") + .HasColumnType("text") + .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", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserPasswordReset", b => + { + b.Property("Id") + .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", (string)null); + }); + + 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", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UserShareInvite", b => + { + b.Property("Id") + .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", (string)null); + }); + + 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", (string)null); + }); + + 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.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.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.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.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("PublicShareMappings"); + + b.Navigation("ShockerControlLogs"); + + b.Navigation("ShockerShareCodes"); + + b.Navigation("UserShareInviteShockerMappings"); + + b.Navigation("UserShares"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.User", b => + { + b.Navigation("ApiTokens"); + + 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/20260630233834_RefactorPgEnums.cs b/Common/Migrations/20260630233834_RefactorPgEnums.cs new file mode 100644 index 00000000..50d211c5 --- /dev/null +++ b/Common/Migrations/20260630233834_RefactorPgEnums.cs @@ -0,0 +1,77 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace OpenShock.Common.Migrations +{ + /// + public partial class RefactorPgEnums : Migration + { + // Postgres cannot reorder or remove labels of an existing enum type in place, and + // ALTER TYPE ... ADD VALUE is the only thing EF's annotation diff emits. So any change + // beyond appending a label silently no-ops (reorder) or leaves stale labels behind + // (rename). To apply these changes robustly we recreate each affected type: + // detach columns to text -> drop the old type -> (remap values) -> create the new type -> reattach. + // This works regardless of the current label set/order and is fully reversible. + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // control_type: reorder labels to match the C# enum's declaration order. + migrationBuilder.Sql("ALTER TABLE shocker_control_logs ALTER COLUMN type TYPE text USING type::text;"); + migrationBuilder.Sql("DROP TYPE control_type;"); + migrationBuilder.Sql("CREATE TYPE control_type AS ENUM ('stop', 'shock', 'vibrate', 'sound');"); + migrationBuilder.Sql("ALTER TABLE shocker_control_logs ALTER COLUMN type TYPE control_type USING type::control_type;"); + + // password_encryption_type: reorder labels. Orphan type (no column maps to it). + migrationBuilder.Sql("DROP TYPE password_encryption_type;"); + migrationBuilder.Sql("CREATE TYPE password_encryption_type AS ENUM ('bcrypt_enhanced', 'pbkdf2');"); + + // shocker_model_type: rename labels to snake_case (and fix the petTrainer typo). + migrationBuilder.Sql("ALTER TABLE shockers ALTER COLUMN model TYPE text USING model::text;"); + migrationBuilder.Sql("DROP TYPE shocker_model_type;"); + migrationBuilder.Sql( + """ + UPDATE shockers SET model = CASE model + WHEN 'caiXianlin' THEN 'cai_xianlin' + WHEN 'petTrainer' THEN 'petrainer' + WHEN 'petrainer998DR' THEN 'petrainer_998dr' + WHEN 'wellturnT330' THEN 'wellturn_t330' + ELSE model + END; + """); + migrationBuilder.Sql("CREATE TYPE shocker_model_type AS ENUM ('cai_xianlin', 'petrainer', 'petrainer_998dr', 'wellturn_t330');"); + migrationBuilder.Sql("ALTER TABLE shockers ALTER COLUMN model TYPE shocker_model_type USING model::shocker_model_type;"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // control_type: restore the original label order. + migrationBuilder.Sql("ALTER TABLE shocker_control_logs ALTER COLUMN type TYPE text USING type::text;"); + migrationBuilder.Sql("DROP TYPE control_type;"); + migrationBuilder.Sql("CREATE TYPE control_type AS ENUM ('sound', 'vibrate', 'shock', 'stop');"); + migrationBuilder.Sql("ALTER TABLE shocker_control_logs ALTER COLUMN type TYPE control_type USING type::control_type;"); + + // password_encryption_type: restore the original label order. + migrationBuilder.Sql("DROP TYPE password_encryption_type;"); + migrationBuilder.Sql("CREATE TYPE password_encryption_type AS ENUM ('pbkdf2', 'bcrypt_enhanced');"); + + // shocker_model_type: restore the original camelCase labels. + migrationBuilder.Sql("ALTER TABLE shockers ALTER COLUMN model TYPE text USING model::text;"); + migrationBuilder.Sql("DROP TYPE shocker_model_type;"); + migrationBuilder.Sql( + """ + UPDATE shockers SET model = CASE model + WHEN 'cai_xianlin' THEN 'caiXianlin' + WHEN 'petrainer' THEN 'petTrainer' + WHEN 'petrainer_998dr' THEN 'petrainer998DR' + WHEN 'wellturn_t330' THEN 'wellturnT330' + ELSE model + END; + """); + migrationBuilder.Sql("CREATE TYPE shocker_model_type AS ENUM ('caiXianlin', 'petTrainer', 'petrainer998DR', 'wellturnT330');"); + migrationBuilder.Sql("ALTER TABLE shockers ALTER COLUMN model TYPE shocker_model_type USING model::shocker_model_type;"); + } + } +} diff --git a/Common/Migrations/OpenShockContextModelSnapshot.cs b/Common/Migrations/OpenShockContextModelSnapshot.cs index ee0dea15..da71049c 100644 --- a/Common/Migrations/OpenShockContextModelSnapshot.cs +++ b/Common/Migrations/OpenShockContextModelSnapshot.cs @@ -6,7 +6,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using OpenShock.Common.Models; using OpenShock.Common.OpenShockDb; #nullable disable @@ -26,15 +25,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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[] { "sound", "vibrate", "shock", "stop" }); + 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[] { "pbkdf2", "bcrypt_enhanced" }); + 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" }); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "role_type", new[] { "support", "staff", "admin", "system" }); - NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "shocker_model_type", new[] { "caiXianlin", "petTrainer", "petrainer998DR", "wellturnT330" }); + 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 => diff --git a/Common/Models/ControlType.cs b/Common/Models/ControlType.cs deleted file mode 100644 index c10b7221..00000000 --- a/Common/Models/ControlType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace OpenShock.Common.Models; - -public enum ControlType -{ - Stop = 0, - Shock = 1, - Vibrate = 2, - Sound = 3 -} \ No newline at end of file diff --git a/Common/Models/OtaUpdateStatus.cs b/Common/Models/OtaUpdateStatus.cs deleted file mode 100644 index 27c91151..00000000 --- a/Common/Models/OtaUpdateStatus.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace OpenShock.Common.Models; - -public enum OtaUpdateStatus -{ - Started, - Running, - Finished, - Error, - Timeout -} \ No newline at end of file diff --git a/Common/Models/PasswordHashingAlgorithm.cs b/Common/Models/PasswordHashingAlgorithm.cs deleted file mode 100644 index 4b19f361..00000000 --- a/Common/Models/PasswordHashingAlgorithm.cs +++ /dev/null @@ -1,9 +0,0 @@ -// ReSharper disable InconsistentNaming -namespace OpenShock.Common.Models; - -public enum PasswordHashingAlgorithm -{ - Unknown = -1, - BCrypt = 0, - PBKDF2 = 1, -}; \ No newline at end of file diff --git a/Common/Models/PermissionType.cs b/Common/Models/PermissionTypeExtensions.cs similarity index 86% rename from Common/Models/PermissionType.cs rename to Common/Models/PermissionTypeExtensions.cs index f1fa6e03..d09bbf98 100644 --- a/Common/Models/PermissionType.cs +++ b/Common/Models/PermissionTypeExtensions.cs @@ -1,26 +1,11 @@ -using System.Reflection; -using System.Text.Json.Serialization; +using System.Reflection; using NpgsqlTypes; -using OpenShock.Common.JsonSerialization; +using OpenShock.Common.OpenShockDb; // ReSharper disable InconsistentNaming namespace OpenShock.Common.Models; -[JsonConverter(typeof(PermissionTypeConverter))] -public enum PermissionType -{ - [PgName("shockers.use")] Shockers_Use, - - [PgName("shockers.edit")] Shockers_Edit, - - [PgName("shockers.pause")] Shockers_Pause, - - [PgName("devices.edit")] Devices_Edit, - - [PgName("devices.auth")] Devices_Auth -} - public static class PermissionTypeExtensions { public static bool IsAllowed(this PermissionType permissionType, IReadOnlyList permissions) => diff --git a/Common/Models/RoleType.cs b/Common/Models/RoleType.cs deleted file mode 100644 index 1cb64b97..00000000 --- a/Common/Models/RoleType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace OpenShock.Common.Models; - -public enum RoleType -{ - Support, - Staff, - Admin, - System -} \ No newline at end of file diff --git a/Common/Models/Services/Ota/OtaItem.cs b/Common/Models/Services/Ota/OtaItem.cs index 08590594..de1b4a68 100644 --- a/Common/Models/Services/Ota/OtaItem.cs +++ b/Common/Models/Services/Ota/OtaItem.cs @@ -1,5 +1,6 @@ using System.Text.Json.Serialization; using OpenShock.Common.JsonSerialization; +using OpenShock.Common.OpenShockDb; namespace OpenShock.Common.Models.Services.Ota; diff --git a/Common/Models/ShockerModelType.cs b/Common/Models/ShockerModelType.cs deleted file mode 100644 index 84690157..00000000 --- a/Common/Models/ShockerModelType.cs +++ /dev/null @@ -1,11 +0,0 @@ -using NpgsqlTypes; - -namespace OpenShock.Common.Models; - -public enum ShockerModelType -{ - [PgName("caiXianlin")] CaiXianlin = 0, - [PgName("petTrainer")] PetTrainer = 1, // Misspelled, should be "petrainer", - [PgName("petrainer998DR")] Petrainer998DR = 2, - [PgName("wellturnT330")] WellturnT330 = 3, -} \ No newline at end of file diff --git a/Common/Models/WebSocket/ControlResponse.cs b/Common/Models/WebSocket/ControlResponse.cs index f5d0bcf8..d9d87782 100644 --- a/Common/Models/WebSocket/ControlResponse.cs +++ b/Common/Models/WebSocket/ControlResponse.cs @@ -1,4 +1,6 @@ -namespace OpenShock.Common.Models.WebSocket; +using OpenShock.Common.OpenShockDb; + +namespace OpenShock.Common.Models.WebSocket; public sealed class ControlResponse { diff --git a/Common/Models/WebSocket/LCG/ClientLiveFrame.cs b/Common/Models/WebSocket/LCG/ClientLiveFrame.cs index 35a4ec12..ae3f336e 100644 --- a/Common/Models/WebSocket/LCG/ClientLiveFrame.cs +++ b/Common/Models/WebSocket/LCG/ClientLiveFrame.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using OpenShock.Common.OpenShockDb; namespace OpenShock.Common.Models.WebSocket.LCG; diff --git a/Common/Models/WebSocket/User/Control.cs b/Common/Models/WebSocket/User/Control.cs index 9400133c..21e3b32f 100644 --- a/Common/Models/WebSocket/User/Control.cs +++ b/Common/Models/WebSocket/User/Control.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using OpenShock.Common.Constants; +using OpenShock.Common.OpenShockDb; namespace OpenShock.Common.Models.WebSocket.User; diff --git a/Common/Models/WebSocket/User/ControlLog.cs b/Common/Models/WebSocket/User/ControlLog.cs index 424f9adc..3e06b99a 100644 --- a/Common/Models/WebSocket/User/ControlLog.cs +++ b/Common/Models/WebSocket/User/ControlLog.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using OpenShock.Common.Constants; +using OpenShock.Common.OpenShockDb; namespace OpenShock.Common.Models.WebSocket.User; diff --git a/Common/OpenShockDb/Enums/ConfigurationValueType.cs b/Common/OpenShockDb/Enums/ConfigurationValueType.cs new file mode 100644 index 00000000..d53ce7a7 --- /dev/null +++ b/Common/OpenShockDb/Enums/ConfigurationValueType.cs @@ -0,0 +1,14 @@ +using NpgsqlTypes; +using OpenShock.Common.Utils; + +namespace OpenShock.Common.OpenShockDb; + +[PgEnum(Name = "configuration_value_type")] +public enum ConfigurationValueType +{ + [PgName("string")] String = 0, + [PgName("bool")] Bool = 1, + [PgName("int")] Int = 2, + [PgName("float")] Float = 3, + [PgName("json")] Json = 4 +} diff --git a/Common/Models/ControlLimitMode.cs b/Common/OpenShockDb/Enums/ControlLimitMode.cs similarity index 64% rename from Common/Models/ControlLimitMode.cs rename to Common/OpenShockDb/Enums/ControlLimitMode.cs index b2de7f81..e921a55b 100644 --- a/Common/Models/ControlLimitMode.cs +++ b/Common/OpenShockDb/Enums/ControlLimitMode.cs @@ -1,17 +1,21 @@ -namespace OpenShock.Common.Models; +using NpgsqlTypes; +using OpenShock.Common.Utils; + +namespace OpenShock.Common.OpenShockDb; /// /// Determines how a per-token min/max limit is applied to an incoming control value. /// +[PgEnum(Name = "control_limit_mode")] public enum ControlLimitMode { /// /// Clamp the incoming value into the [min, max] range. /// - Clamp = 0, + [PgName("clamp")] Clamp = 0, /// /// Linearly remap the full input range onto [min, max]. /// - Lerp = 1 + [PgName("lerp")] Lerp = 1 } diff --git a/Common/OpenShockDb/Enums/ControlType.cs b/Common/OpenShockDb/Enums/ControlType.cs new file mode 100644 index 00000000..717bfec6 --- /dev/null +++ b/Common/OpenShockDb/Enums/ControlType.cs @@ -0,0 +1,13 @@ +using NpgsqlTypes; +using OpenShock.Common.Utils; + +namespace OpenShock.Common.OpenShockDb; + +[PgEnum(Name = "control_type")] +public enum ControlType +{ + [PgName("stop")] Stop = 0, + [PgName("shock")] Shock = 1, + [PgName("vibrate")] Vibrate = 2, + [PgName("sound")] Sound = 3 +} diff --git a/Common/Models/EmailStatus.cs b/Common/OpenShockDb/Enums/EmailStatus.cs similarity index 70% rename from Common/Models/EmailStatus.cs rename to Common/OpenShockDb/Enums/EmailStatus.cs index f3c5a8d5..4ed0beec 100644 --- a/Common/Models/EmailStatus.cs +++ b/Common/OpenShockDb/Enums/EmailStatus.cs @@ -1,7 +1,10 @@ -namespace OpenShock.Common.Models; +using NpgsqlTypes; +using OpenShock.Common.Utils; + +namespace OpenShock.Common.OpenShockDb; /// -/// Delivery state of an . +/// Delivery state of an . /// /// /// The row is written by the API as in the same transaction as the business @@ -9,37 +12,38 @@ namespace OpenShock.Common.Models; /// under a lease), hands it to the email provider, and records the outcome /// directly on the row - a terminal , , or , /// or back to with a future retry time. Retry count and scheduling live in the -/// row itself ( / -/// ), so the table is the complete, queryable +/// row itself ( / +/// ), so the table is the complete, queryable /// source of truth for every email's delivery state. /// +[PgEnum(Name = "email_status")] public enum EmailStatus { /// /// Due for delivery: either freshly enqueued, or a transient failure scheduled for a later retry - /// (distinguished by being in the - /// future and > 0). A row is eligible + /// (distinguished by being in the + /// future and > 0). A row is eligible /// to be claimed once its next-attempt time has passed. /// - Pending, + [PgName("pending")] Pending = 0, /// /// Claimed by an executor and currently being delivered. The claim carries a lease - /// ( set to the lease expiry); if the + /// ( set to the lease expiry); if the /// process dies mid-send, the lease lapses and the row is reclaimed - so a crash cannot strand a /// message in this state. /// - Sending, + [PgName("sending")] Sending = 1, /// The message was handed to the email provider successfully. Terminal. - Sent, + [PgName("sent")] Sent = 2, /// /// Delivery was abandoned: it exhausted its retry budget or hit a permanent provider error. The /// row is kept (never auto-deleted) so it stays inspectable and can be requeued by an operator. /// Terminal. /// - Failed, + [PgName("failed")] Failed = 3, /// /// The email was intentionally not sent because the underlying request no longer needs it (the @@ -47,5 +51,5 @@ public enum EmailStatus /// is a successful no-op, kept distinct from so operators never mistake it for /// a delivery problem or requeue it. Terminal. /// - Skipped + [PgName("skipped")] Skipped = 4 } diff --git a/Common/Models/EmailType.cs b/Common/OpenShockDb/Enums/EmailType.cs similarity index 60% rename from Common/Models/EmailType.cs rename to Common/OpenShockDb/Enums/EmailType.cs index bb9dc693..abc6fe8e 100644 --- a/Common/Models/EmailType.cs +++ b/Common/OpenShockDb/Enums/EmailType.cs @@ -1,22 +1,26 @@ -namespace OpenShock.Common.Models; +using NpgsqlTypes; +using OpenShock.Common.Utils; + +namespace OpenShock.Common.OpenShockDb; /// -/// The kind of email an represents. The outbox stores +/// The kind of email an represents. The outbox stores /// only the intent to send one of these, never a rendered body or a usable secret, the /// consumer maps the type to the matching template and (for token-bearing types) mints a fresh /// secret at send time. /// +[PgEnum(Name = "email_type")] public enum EmailType { /// Account activation / email confirmation for a newly created account. - AccountActivation, + [PgName("account_activation")] AccountActivation = 0, /// Password reset link. - PasswordReset, + [PgName("password_reset")] PasswordReset = 1, /// Verification of a newly requested email address (sent to the new address). - EmailVerification, + [PgName("email_verification")] EmailVerification = 2, /// Informational notice sent to the previous address when an email change is requested. Carries no secret. - EmailChangeNotice + [PgName("email_change_notice")] EmailChangeNotice = 3 } diff --git a/Common/OpenShockDb/Enums/MatchTypeEnum.cs b/Common/OpenShockDb/Enums/MatchTypeEnum.cs new file mode 100644 index 00000000..06a0e31a --- /dev/null +++ b/Common/OpenShockDb/Enums/MatchTypeEnum.cs @@ -0,0 +1,11 @@ +using NpgsqlTypes; +using OpenShock.Common.Utils; + +namespace OpenShock.Common.OpenShockDb; + +[PgEnum(Name = "match_type_enum")] +public enum MatchTypeEnum +{ + [PgName("exact")] Exact = 0, + [PgName("contains")] Contains = 1, +} diff --git a/Common/OpenShockDb/Enums/OtaUpdateStatus.cs b/Common/OpenShockDb/Enums/OtaUpdateStatus.cs new file mode 100644 index 00000000..104c58b1 --- /dev/null +++ b/Common/OpenShockDb/Enums/OtaUpdateStatus.cs @@ -0,0 +1,14 @@ +using NpgsqlTypes; +using OpenShock.Common.Utils; + +namespace OpenShock.Common.OpenShockDb; + +[PgEnum(Name = "ota_update_status")] +public enum OtaUpdateStatus +{ + [PgName("started")] Started = 0, + [PgName("running")] Running = 1, + [PgName("finished")] Finished = 2, + [PgName("error")] Error = 3, + [PgName("timeout")] Timeout = 4 +} diff --git a/Common/OpenShockDb/Enums/PasswordHashingAlgorithm.cs b/Common/OpenShockDb/Enums/PasswordHashingAlgorithm.cs new file mode 100644 index 00000000..3198670a --- /dev/null +++ b/Common/OpenShockDb/Enums/PasswordHashingAlgorithm.cs @@ -0,0 +1,14 @@ +// ReSharper disable InconsistentNaming + +using NpgsqlTypes; +using OpenShock.Common.Utils; + +namespace OpenShock.Common.OpenShockDb; + +[PgEnum(Name = "password_encryption_type")] +public enum PasswordHashingAlgorithm +{ + Unknown = -1, + [PgName("bcrypt_enhanced")] BCrypt = 0, + [PgName("pbkdf2")] PBKDF2 = 1 +} diff --git a/Common/OpenShockDb/Enums/PermissionType.cs b/Common/OpenShockDb/Enums/PermissionType.cs new file mode 100644 index 00000000..7e86ca6c --- /dev/null +++ b/Common/OpenShockDb/Enums/PermissionType.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; +using NpgsqlTypes; +using OpenShock.Common.JsonSerialization; +using OpenShock.Common.Utils; + +// ReSharper disable InconsistentNaming + +namespace OpenShock.Common.OpenShockDb; + +[PgEnum(Name = "permission_type")] +[JsonConverter(typeof(PermissionTypeConverter))] +public enum PermissionType +{ + [PgName("shockers.use")] Shockers_Use = 0, + + [PgName("shockers.edit")] Shockers_Edit = 1, + + [PgName("shockers.pause")] Shockers_Pause = 2, + + [PgName("devices.edit")] Devices_Edit = 3, + + [PgName("devices.auth")] Devices_Auth = 4 +} diff --git a/Common/OpenShockDb/Enums/RoleType.cs b/Common/OpenShockDb/Enums/RoleType.cs new file mode 100644 index 00000000..ef68b4cb --- /dev/null +++ b/Common/OpenShockDb/Enums/RoleType.cs @@ -0,0 +1,13 @@ +using NpgsqlTypes; +using OpenShock.Common.Utils; + +namespace OpenShock.Common.OpenShockDb; + +[PgEnum(Name = "role_type")] +public enum RoleType +{ + [PgName("support")] Support = 0, + [PgName("staff")] Staff = 1, + [PgName("admin")] Admin = 2, + [PgName("system")] System = 3, +} diff --git a/Common/OpenShockDb/Enums/ShockerModelType.cs b/Common/OpenShockDb/Enums/ShockerModelType.cs new file mode 100644 index 00000000..1629c902 --- /dev/null +++ b/Common/OpenShockDb/Enums/ShockerModelType.cs @@ -0,0 +1,13 @@ +using NpgsqlTypes; +using OpenShock.Common.Utils; + +namespace OpenShock.Common.OpenShockDb; + +[PgEnum(Name = "shocker_model_type")] +public enum ShockerModelType +{ + [PgName("cai_xianlin")] CaiXianlin = 0, + [PgName("petrainer")] PetTrainer = 1, + [PgName("petrainer_998dr")] Petrainer998DR = 2, + [PgName("wellturn_t330")] WellturnT330 = 3 +} \ No newline at end of file diff --git a/Common/OpenShockDb/Extensions/NpgsqlEnumExtensions.cs b/Common/OpenShockDb/Extensions/NpgsqlEnumExtensions.cs new file mode 100644 index 00000000..583d8b1b --- /dev/null +++ b/Common/OpenShockDb/Extensions/NpgsqlEnumExtensions.cs @@ -0,0 +1,63 @@ +using System.Reflection; +using Microsoft.EntityFrameworkCore; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using NpgsqlTypes; +using OpenShock.Common.Utils; + +namespace OpenShock.Common.OpenShockDb; + +public static class NpgsqlEnumExtensions +{ + private readonly record struct PgEnumInfo( + Action Map, + Action Register); + + private static PgEnumInfo BuildInfo() where TEnum : struct, Enum + { + var type = typeof(TEnum); + var attr = type.GetCustomAttribute() + ?? throw new InvalidOperationException($"{type.Name} is missing [PgEnum]"); + + var pgTypeName = attr.Name; + + var members = type + .GetFields(BindingFlags.Public | BindingFlags.Static) + .Select(f => f.GetCustomAttribute()?.PgName) + .Where(n => n is not null) + .Cast() + .ToArray(); + + return new PgEnumInfo( + Map: b => b.MapEnum(pgTypeName, attr.Schema), + Register: m => m.HasPostgresEnum(attr.Schema, pgTypeName, members)); + } + + private static readonly PgEnumInfo[] Enums = + [ + BuildInfo(), + BuildInfo(), + BuildInfo(), + BuildInfo(), + BuildInfo(), + BuildInfo(), + BuildInfo(), + BuildInfo(), + BuildInfo(), + BuildInfo(), + BuildInfo(), + ]; + + public static NpgsqlDbContextOptionsBuilder MapPgEnums(this NpgsqlDbContextOptionsBuilder builder) + { + foreach (var info in Enums) + info.Map(builder); + return builder; + } + + public static ModelBuilder RegisterPgEnums(this ModelBuilder modelBuilder) + { + foreach (var info in Enums) + info.Register(modelBuilder); + return modelBuilder; + } +} diff --git a/Common/OpenShockDb/EmailOutboxCoalesceKeys.cs b/Common/OpenShockDb/Keys/EmailOutboxCoalesceKeys.cs similarity index 93% rename from Common/OpenShockDb/EmailOutboxCoalesceKeys.cs rename to Common/OpenShockDb/Keys/EmailOutboxCoalesceKeys.cs index c7e5e31e..5986e959 100644 --- a/Common/OpenShockDb/EmailOutboxCoalesceKeys.cs +++ b/Common/OpenShockDb/Keys/EmailOutboxCoalesceKeys.cs @@ -11,7 +11,7 @@ namespace OpenShock.Common.OpenShockDb; /// The kind prefix is baked into the key so different email kinds for the same user never coalesce /// into each other (a pending reset must not suppress a pending activation). Scope is per user: only /// the newest activation / reset / verification for a given user is delivered. -/// intentionally has no builder - it is an +/// intentionally has no builder - it is an /// always-deliver type, so it carries a null key. /// public static class EmailOutboxCoalesceKeys diff --git a/Common/OpenShockDb/EmailOutboxPayloadKeys.cs b/Common/OpenShockDb/Keys/EmailOutboxPayloadKeys.cs similarity index 78% rename from Common/OpenShockDb/EmailOutboxPayloadKeys.cs rename to Common/OpenShockDb/Keys/EmailOutboxPayloadKeys.cs index 3e189664..ba9781e3 100644 --- a/Common/OpenShockDb/EmailOutboxPayloadKeys.cs +++ b/Common/OpenShockDb/Keys/EmailOutboxPayloadKeys.cs @@ -1,3 +1,4 @@ + namespace OpenShock.Common.OpenShockDb; /// @@ -9,15 +10,15 @@ namespace OpenShock.Common.OpenShockDb; /// public static class EmailOutboxPayloadKeys { - /// Target user id. Used by . + /// Target user id. Used by . public const string UserId = "userId"; - /// id. Used by . + /// id. Used by . public const string PasswordResetId = "passwordResetId"; - /// id. Used by . + /// id. Used by . public const string EmailChangeId = "emailChangeId"; - /// The newly requested address. Used by . + /// The newly requested address. Used by . public const string NewEmail = "newEmail"; } diff --git a/Common/OpenShockDb/AdminUsersView.cs b/Common/OpenShockDb/Models/AdminUsersView.cs similarity index 93% rename from Common/OpenShockDb/AdminUsersView.cs rename to Common/OpenShockDb/Models/AdminUsersView.cs index 536a57ec..0d7f64e8 100644 --- a/Common/OpenShockDb/AdminUsersView.cs +++ b/Common/OpenShockDb/Models/AdminUsersView.cs @@ -1,5 +1,4 @@ -using OpenShock.Common.Models; -// We are in a view, no need to restrict lengths lol +// We are in a view, no need to restrict lengths lol // ReSharper disable EntityFramework.ModelValidation.UnlimitedStringLength namespace OpenShock.Common.OpenShockDb; diff --git a/Common/OpenShockDb/ApiToken.cs b/Common/OpenShockDb/Models/ApiToken.cs similarity index 97% rename from Common/OpenShockDb/ApiToken.cs rename to Common/OpenShockDb/Models/ApiToken.cs index b1e3b139..343e957b 100644 --- a/Common/OpenShockDb/ApiToken.cs +++ b/Common/OpenShockDb/Models/ApiToken.cs @@ -1,6 +1,5 @@ using System.Net; using OpenShock.Common.Constants; -using OpenShock.Common.Models; namespace OpenShock.Common.OpenShockDb; diff --git a/Common/OpenShockDb/ApiTokenReport.cs b/Common/OpenShockDb/Models/ApiTokenReport.cs similarity index 100% rename from Common/OpenShockDb/ApiTokenReport.cs rename to Common/OpenShockDb/Models/ApiTokenReport.cs diff --git a/Common/OpenShockDb/ConfigurationItem.cs b/Common/OpenShockDb/Models/ConfigurationItem.cs similarity index 71% rename from Common/OpenShockDb/ConfigurationItem.cs rename to Common/OpenShockDb/Models/ConfigurationItem.cs index eb7c9959..f9261705 100644 --- a/Common/OpenShockDb/ConfigurationItem.cs +++ b/Common/OpenShockDb/Models/ConfigurationItem.cs @@ -1,13 +1,4 @@ -namespace OpenShock.Common.OpenShockDb; - -public enum ConfigurationValueType -{ - String, - Bool, - Int, - Float, - Json -} +namespace OpenShock.Common.OpenShockDb; public sealed class ConfigurationItem { diff --git a/Common/OpenShockDb/Device.cs b/Common/OpenShockDb/Models/Device.cs similarity index 100% rename from Common/OpenShockDb/Device.cs rename to Common/OpenShockDb/Models/Device.cs diff --git a/Common/OpenShockDb/DeviceOtaUpdate.cs b/Common/OpenShockDb/Models/DeviceOtaUpdate.cs similarity index 84% rename from Common/OpenShockDb/DeviceOtaUpdate.cs rename to Common/OpenShockDb/Models/DeviceOtaUpdate.cs index a2e4fea4..edab467b 100644 --- a/Common/OpenShockDb/DeviceOtaUpdate.cs +++ b/Common/OpenShockDb/Models/DeviceOtaUpdate.cs @@ -1,6 +1,4 @@ -using OpenShock.Common.Models; - -namespace OpenShock.Common.OpenShockDb; +namespace OpenShock.Common.OpenShockDb; public sealed class DeviceOtaUpdate { diff --git a/Common/OpenShockDb/DiscordWebhook.cs b/Common/OpenShockDb/Models/DiscordWebhook.cs similarity index 100% rename from Common/OpenShockDb/DiscordWebhook.cs rename to Common/OpenShockDb/Models/DiscordWebhook.cs diff --git a/Common/OpenShockDb/EmailOutboxMessage.cs b/Common/OpenShockDb/Models/EmailOutboxMessage.cs similarity index 99% rename from Common/OpenShockDb/EmailOutboxMessage.cs rename to Common/OpenShockDb/Models/EmailOutboxMessage.cs index 3b0b3342..53bc5f79 100644 --- a/Common/OpenShockDb/EmailOutboxMessage.cs +++ b/Common/OpenShockDb/Models/EmailOutboxMessage.cs @@ -1,5 +1,3 @@ -using OpenShock.Common.Models; - namespace OpenShock.Common.OpenShockDb; /// diff --git a/Common/OpenShockDb/EmailProviderBlacklist.cs b/Common/OpenShockDb/Models/EmailProviderBlacklist.cs similarity index 100% rename from Common/OpenShockDb/EmailProviderBlacklist.cs rename to Common/OpenShockDb/Models/EmailProviderBlacklist.cs diff --git a/Common/OpenShockDb/PublicShare.cs b/Common/OpenShockDb/Models/PublicShare.cs similarity index 100% rename from Common/OpenShockDb/PublicShare.cs rename to Common/OpenShockDb/Models/PublicShare.cs diff --git a/Common/OpenShockDb/PublicShareShocker.cs b/Common/OpenShockDb/Models/PublicShareShocker.cs similarity index 100% rename from Common/OpenShockDb/PublicShareShocker.cs rename to Common/OpenShockDb/Models/PublicShareShocker.cs diff --git a/Common/OpenShockDb/SafetySettings.cs b/Common/OpenShockDb/Models/SafetySettings.cs similarity index 100% rename from Common/OpenShockDb/SafetySettings.cs rename to Common/OpenShockDb/Models/SafetySettings.cs diff --git a/Common/OpenShockDb/Shocker.cs b/Common/OpenShockDb/Models/Shocker.cs similarity index 91% rename from Common/OpenShockDb/Shocker.cs rename to Common/OpenShockDb/Models/Shocker.cs index 89100c29..906ee9c2 100644 --- a/Common/OpenShockDb/Shocker.cs +++ b/Common/OpenShockDb/Models/Shocker.cs @@ -1,6 +1,4 @@ -using OpenShock.Common.Models; - -namespace OpenShock.Common.OpenShockDb; +namespace OpenShock.Common.OpenShockDb; public sealed class Shocker { diff --git a/Common/OpenShockDb/ShockerControlLog.cs b/Common/OpenShockDb/Models/ShockerControlLog.cs similarity index 89% rename from Common/OpenShockDb/ShockerControlLog.cs rename to Common/OpenShockDb/Models/ShockerControlLog.cs index d007f27d..2c2ab0f9 100644 --- a/Common/OpenShockDb/ShockerControlLog.cs +++ b/Common/OpenShockDb/Models/ShockerControlLog.cs @@ -1,6 +1,4 @@ -using OpenShock.Common.Models; - -namespace OpenShock.Common.OpenShockDb; +namespace OpenShock.Common.OpenShockDb; public sealed class ShockerControlLog { diff --git a/Common/OpenShockDb/ShockerShareCode.cs b/Common/OpenShockDb/Models/ShockerShareCode.cs similarity index 100% rename from Common/OpenShockDb/ShockerShareCode.cs rename to Common/OpenShockDb/Models/ShockerShareCode.cs diff --git a/Common/OpenShockDb/User.cs b/Common/OpenShockDb/Models/User.cs similarity index 96% rename from Common/OpenShockDb/User.cs rename to Common/OpenShockDb/Models/User.cs index bf5a2990..df26d85a 100644 --- a/Common/OpenShockDb/User.cs +++ b/Common/OpenShockDb/Models/User.cs @@ -1,6 +1,4 @@ -using OpenShock.Common.Models; - -namespace OpenShock.Common.OpenShockDb; +namespace OpenShock.Common.OpenShockDb; public sealed class User { diff --git a/Common/OpenShockDb/UserActivationRequest.cs b/Common/OpenShockDb/Models/UserActivationRequest.cs similarity index 100% rename from Common/OpenShockDb/UserActivationRequest.cs rename to Common/OpenShockDb/Models/UserActivationRequest.cs diff --git a/Common/OpenShockDb/UserDeactivation.cs b/Common/OpenShockDb/Models/UserDeactivation.cs similarity index 100% rename from Common/OpenShockDb/UserDeactivation.cs rename to Common/OpenShockDb/Models/UserDeactivation.cs diff --git a/Common/OpenShockDb/UserEmailChange.cs b/Common/OpenShockDb/Models/UserEmailChange.cs similarity index 100% rename from Common/OpenShockDb/UserEmailChange.cs rename to Common/OpenShockDb/Models/UserEmailChange.cs diff --git a/Common/OpenShockDb/UserNameBlacklist.cs b/Common/OpenShockDb/Models/UserNameBlacklist.cs similarity index 84% rename from Common/OpenShockDb/UserNameBlacklist.cs rename to Common/OpenShockDb/Models/UserNameBlacklist.cs index 9f344af1..7bce6b86 100644 --- a/Common/OpenShockDb/UserNameBlacklist.cs +++ b/Common/OpenShockDb/Models/UserNameBlacklist.cs @@ -1,10 +1,4 @@ -namespace OpenShock.Common.OpenShockDb; - -public enum MatchTypeEnum -{ - Exact, - Contains, -} +namespace OpenShock.Common.OpenShockDb; public sealed class UserNameBlacklist { diff --git a/Common/OpenShockDb/UserNameChange.cs b/Common/OpenShockDb/Models/UserNameChange.cs similarity index 100% rename from Common/OpenShockDb/UserNameChange.cs rename to Common/OpenShockDb/Models/UserNameChange.cs diff --git a/Common/OpenShockDb/UserOAuthConnection.cs b/Common/OpenShockDb/Models/UserOAuthConnection.cs similarity index 100% rename from Common/OpenShockDb/UserOAuthConnection.cs rename to Common/OpenShockDb/Models/UserOAuthConnection.cs diff --git a/Common/OpenShockDb/UserPasswordReset.cs b/Common/OpenShockDb/Models/UserPasswordReset.cs similarity index 100% rename from Common/OpenShockDb/UserPasswordReset.cs rename to Common/OpenShockDb/Models/UserPasswordReset.cs diff --git a/Common/OpenShockDb/UserShare.cs b/Common/OpenShockDb/Models/UserShare.cs similarity index 100% rename from Common/OpenShockDb/UserShare.cs rename to Common/OpenShockDb/Models/UserShare.cs diff --git a/Common/OpenShockDb/UserShareInvite.cs b/Common/OpenShockDb/Models/UserShareInvite.cs similarity index 100% rename from Common/OpenShockDb/UserShareInvite.cs rename to Common/OpenShockDb/Models/UserShareInvite.cs diff --git a/Common/OpenShockDb/UserShareInviteShocker.cs b/Common/OpenShockDb/Models/UserShareInviteShocker.cs similarity index 100% rename from Common/OpenShockDb/UserShareInviteShocker.cs rename to Common/OpenShockDb/Models/UserShareInviteShocker.cs diff --git a/Common/OpenShockDb/OpenShockContext.cs b/Common/OpenShockDb/OpenShockContext.cs index d0475343..d419acb7 100644 --- a/Common/OpenShockDb/OpenShockContext.cs +++ b/Common/OpenShockDb/OpenShockContext.cs @@ -67,16 +67,7 @@ public static void ConfigureOptionsBuilder(DbContextOptionsBuilder optionsBuilde // to its jsonb column. Without dynamic JSON, Npgsql maps Dictionary to // hstore by default and writing it to a jsonb column fails at runtime. npgsqlBuilder.ConfigureDataSource(dataSourceBuilder => dataSourceBuilder.EnableDynamicJson()); - npgsqlBuilder.MapEnum(); - npgsqlBuilder.MapEnum(); - npgsqlBuilder.MapEnum(); - npgsqlBuilder.MapEnum(); - npgsqlBuilder.MapEnum(); - npgsqlBuilder.MapEnum(); - npgsqlBuilder.MapEnum(); - npgsqlBuilder.MapEnum(); - npgsqlBuilder.MapEnum(); - npgsqlBuilder.MapEnum(); + npgsqlBuilder.MapPgEnums(); }); if (debug) @@ -149,18 +140,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder - .HasPostgresEnum("control_type", ["sound", "vibrate", "shock", "stop"]) - .HasPostgresEnum("control_limit_mode", ["clamp", "lerp"]) - .HasPostgresEnum("ota_update_status", ["started", "running", "finished", "error", "timeout"]) - .HasPostgresEnum("password_encryption_type", ["pbkdf2", "bcrypt_enhanced"]) - .HasPostgresEnum("permission_type", - ["shockers.use", "shockers.edit", "shockers.pause", "devices.edit", "devices.auth"]) - .HasPostgresEnum("role_type", ["support", "staff", "admin", "system"]) - .HasPostgresEnum("shocker_model_type", ["caiXianlin", "petTrainer", "petrainer998DR", "wellturnT330"]) - .HasPostgresEnum("match_type_enum", ["exact", "contains"]) - .HasPostgresEnum("configuration_value_type", ["string", "bool", "int", "float", "json"]) - .HasPostgresEnum("email_type", ["account_activation", "password_reset", "email_verification", "email_change_notice"]) - .HasPostgresEnum("email_status", ["pending", "sending", "sent", "failed", "skipped"]) + .RegisterPgEnums() .HasCollation("public", "ndcoll", "und-u-ks-level2", "icu", false); // Add case-insensitive, accent-sensitive comparison collation modelBuilder.Entity(entity => diff --git a/Common/OpenShockDb/EmailOutboxQueries.cs b/Common/OpenShockDb/Queries/EmailOutboxQueries.cs similarity index 98% rename from Common/OpenShockDb/EmailOutboxQueries.cs rename to Common/OpenShockDb/Queries/EmailOutboxQueries.cs index ade6d04a..f6239d8b 100644 --- a/Common/OpenShockDb/EmailOutboxQueries.cs +++ b/Common/OpenShockDb/Queries/EmailOutboxQueries.cs @@ -1,5 +1,4 @@ using Microsoft.EntityFrameworkCore; -using OpenShock.Common.Models; namespace OpenShock.Common.OpenShockDb; diff --git a/Common/Problems/CustomProblems/TokenPermissionProblem.cs b/Common/Problems/CustomProblems/TokenPermissionProblem.cs index d23b0598..334179bb 100644 --- a/Common/Problems/CustomProblems/TokenPermissionProblem.cs +++ b/Common/Problems/CustomProblems/TokenPermissionProblem.cs @@ -1,5 +1,5 @@ using System.Net; -using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; namespace OpenShock.Common.Problems.CustomProblems; diff --git a/Common/Redis/PubSub/DeviceMessage.cs b/Common/Redis/PubSub/DeviceMessage.cs index fb64859f..10635afd 100644 --- a/Common/Redis/PubSub/DeviceMessage.cs +++ b/Common/Redis/PubSub/DeviceMessage.cs @@ -1,5 +1,6 @@ using MessagePack; using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; namespace OpenShock.Common.Redis.PubSub; diff --git a/Common/Utils/HashingUtils.cs b/Common/Utils/HashingUtils.cs index c9574bcb..b375b538 100644 --- a/Common/Utils/HashingUtils.cs +++ b/Common/Utils/HashingUtils.cs @@ -1,9 +1,8 @@ -using System.Buffers; -using System.Diagnostics; +using System.Diagnostics; using System.Security.Cryptography; using System.Text; using BCrypt.Net; -using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; namespace OpenShock.Common.Utils; diff --git a/Common/Utils/PermissionUtils.cs b/Common/Utils/PermissionUtils.cs index 99307465..db3fd7ef 100644 --- a/Common/Utils/PermissionUtils.cs +++ b/Common/Utils/PermissionUtils.cs @@ -1,4 +1,5 @@ using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; namespace OpenShock.Common.Utils; diff --git a/Common/Utils/PgEnumAttribute.cs b/Common/Utils/PgEnumAttribute.cs new file mode 100644 index 00000000..76ce7341 --- /dev/null +++ b/Common/Utils/PgEnumAttribute.cs @@ -0,0 +1,15 @@ +namespace OpenShock.Common.Utils; + +/// +/// Marks an enum as a Postgres native enum type so it is automatically registered +/// with the EF Core model via . +/// +[AttributeUsage(AttributeTargets.Enum)] +public sealed class PgEnumAttribute : Attribute +{ + /// The Postgres type name. Always specified explicitly, e.g. [PgEnum(Name = "control_type")]. + public required string Name { get; init; } + + /// Postgres schema; defaults to the model's default schema when null. + public string? Schema { get; init; } +} diff --git a/Cron/Services/Email/Outbox/EmailOutboxRetryPolicy.cs b/Cron/Services/Email/Outbox/EmailOutboxRetryPolicy.cs index b462c33e..29f84c8f 100644 --- a/Cron/Services/Email/Outbox/EmailOutboxRetryPolicy.cs +++ b/Cron/Services/Email/Outbox/EmailOutboxRetryPolicy.cs @@ -10,7 +10,7 @@ public static class EmailOutboxRetryPolicy { /// /// Maximum number of delivery attempts before a transiently-failing message is given up as - /// . An attempt is counted when the row is + /// . An attempt is counted when the row is /// claimed, so this also bounds crash/lease-expiry reclaim loops. Matches the prior Hangfire policy /// (one initial run plus ten retries). /// diff --git a/LiveControlGateway/Controllers/HubV1Controller.cs b/LiveControlGateway/Controllers/HubV1Controller.cs index 0d7f7115..9d3a067f 100644 --- a/LiveControlGateway/Controllers/HubV1Controller.cs +++ b/LiveControlGateway/Controllers/HubV1Controller.cs @@ -5,12 +5,15 @@ using OpenShock.Common.Authentication; using OpenShock.Common.Hubs; using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; using OpenShock.Common.Services.Ota; using OpenShock.LiveControlGateway.LifetimeManager; using OpenShock.LiveControlGateway.Options; using OpenShock.Serialization.Deprecated.DoNotUse.V1; using OpenShock.Serialization.Types; using Serilog; +using ShockerCommand = OpenShock.Serialization.Gateway.ShockerCommand; +using ShockerModelType = OpenShock.Serialization.Types.ShockerModelType; namespace OpenShock.LiveControlGateway.Controllers; @@ -163,16 +166,16 @@ await otaService.Error(CurrentHubId, payload.BootStatus.OtaUpdateId, false, /// [NonAction] - public override ValueTask Control(IList controlCommands) + public override ValueTask Control(IList controlCommands) => QueueMessage(new GatewayToHubMessage { Payload = new GatewayToHubMessagePayload(new ShockerCommandList { - Commands = [.. controlCommands.Select(x => new ShockerCommand() + Commands = [.. controlCommands.Select(x => new Serialization.Deprecated.DoNotUse.V1.ShockerCommand() { Duration = x.Duration, Type = x.Type, - Id = x.Model == Serialization.Types.ShockerModelType.Petrainer998DR ? (ushort)(x.Id >> 1) : x.Id, // Fix for old hubs, their ids was serialized wrongly in the RFTransmitter, the V1 endpoint is being phased out, so this wont stay here forever + Id = x.Model == ShockerModelType.Petrainer998DR ? (ushort)(x.Id >> 1) : x.Id, // Fix for old hubs, their ids was serialized wrongly in the RFTransmitter, the V1 endpoint is being phased out, so this wont stay here forever Intensity = x.Intensity, Model = x.Model })] diff --git a/LiveControlGateway/Controllers/HubV2Controller.cs b/LiveControlGateway/Controllers/HubV2Controller.cs index e0600222..a7dc7d4d 100644 --- a/LiveControlGateway/Controllers/HubV2Controller.cs +++ b/LiveControlGateway/Controllers/HubV2Controller.cs @@ -1,4 +1,5 @@ -using Asp.Versioning; +using System.Diagnostics; +using Asp.Versioning; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; @@ -6,13 +7,13 @@ using OpenShock.Common.Constants; using OpenShock.Common.Hubs; using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; using OpenShock.Common.Services.Ota; using OpenShock.LiveControlGateway.LifetimeManager; using OpenShock.LiveControlGateway.Options; using OpenShock.Serialization.Gateway; using OpenShock.Serialization.Types; using Serilog; -using System.Diagnostics; namespace OpenShock.LiveControlGateway.Controllers; //TODO: Implement new keep alive ping pong mechanism diff --git a/LiveControlGateway/LifetimeManager/ShockerState.cs b/LiveControlGateway/LifetimeManager/ShockerState.cs index f8470dfc..fa174de5 100644 --- a/LiveControlGateway/LifetimeManager/ShockerState.cs +++ b/LiveControlGateway/LifetimeManager/ShockerState.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; using OpenShock.Common.Constants; -using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; namespace OpenShock.LiveControlGateway.LifetimeManager; diff --git a/LiveControlGateway/Mappers/FbsMapper.cs b/LiveControlGateway/Mappers/FbsMapper.cs index 11c54de6..a751c196 100644 --- a/LiveControlGateway/Mappers/FbsMapper.cs +++ b/LiveControlGateway/Mappers/FbsMapper.cs @@ -1,20 +1,21 @@ -using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; using OpenShock.Common.Redis.PubSub; using OpenShock.Serialization.Gateway; using OpenShock.Serialization.Types; +using ShockerModelType = OpenShock.Serialization.Types.ShockerModelType; namespace OpenShock.LiveControlGateway.Mappers; public static class FbsMapper { - public static Serialization.Types.ShockerModelType ToFbsModelType(Common.Models.ShockerModelType type) + public static ShockerModelType ToFbsModelType(Common.OpenShockDb.ShockerModelType type) { return type switch { - Common.Models.ShockerModelType.CaiXianlin => Serialization.Types.ShockerModelType.CaiXianlin, - Common.Models.ShockerModelType.PetTrainer => Serialization.Types.ShockerModelType.Petrainer, - Common.Models.ShockerModelType.Petrainer998DR => Serialization.Types.ShockerModelType.Petrainer998DR, - Common.Models.ShockerModelType.WellturnT330 => Serialization.Types.ShockerModelType.WellturnT330, + Common.OpenShockDb.ShockerModelType.CaiXianlin => ShockerModelType.CaiXianlin, + Common.OpenShockDb.ShockerModelType.PetTrainer => ShockerModelType.Petrainer, + Common.OpenShockDb.ShockerModelType.Petrainer998DR => ShockerModelType.Petrainer998DR, + Common.OpenShockDb.ShockerModelType.WellturnT330 => ShockerModelType.WellturnT330, _ => throw new NotImplementedException(), }; }