diff --git a/API/Controller/Tokens/TokenController.cs b/API/Controller/Tokens/TokenController.cs index 194a74fa..d83e7d5d 100644 --- a/API/Controller/Tokens/TokenController.cs +++ b/API/Controller/Tokens/TokenController.cs @@ -107,23 +107,25 @@ public async Task DeleteToken([FromRoute] Guid tokenId) [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] public async Task CreateToken([FromBody] CreateTokenRequest body) { - var token = new ApiToken + string token = CryptoUtils.RandomString(HardLimits.ApiKeyTokenLength); + + var tokenDto = new ApiToken { UserId = CurrentUser.DbUser.Id, - Token = CryptoUtils.RandomString(HardLimits.ApiKeyTokenMaxLength), + TokenHash = HashingUtils.HashSha256(token), CreatedByIp = HttpContext.GetRemoteIP().ToString(), Permissions = body.Permissions.Distinct().ToList(), Id = Guid.NewGuid(), Name = body.Name, ValidUntil = body.ValidUntil?.ToUniversalTime() }; - _db.ApiTokens.Add(token); + _db.ApiTokens.Add(tokenDto); await _db.SaveChangesAsync(); return new TokenCreatedResponse { - Token = token.Token, - Id = token.Id + Token = token, + Id = tokenDto.Id }; } @@ -153,7 +155,7 @@ public async Task EditToken([FromRoute] Guid tokenId, [FromBody] public class EditTokenRequest { - [StringLength(HardLimits.ApiKeyTokenMaxLength, MinimumLength = HardLimits.ApiKeyTokenMinLength, ErrorMessage = "API token length must be between {1} and {2}")] + [StringLength(HardLimits.ApiKeyNameMaxLength, MinimumLength = 1, ErrorMessage = "API token length must be between {1} and {2}")] public required string Name { get; set; } [MaxLength(HardLimits.ApiKeyMaxPermissions, ErrorMessage = "API token permissions must be between {1} and {2}")] diff --git a/Common/Authentication/Handlers/LoginSessionAuthentication.cs b/Common/Authentication/Handlers/LoginSessionAuthentication.cs index a7e5a30f..55c3037e 100644 --- a/Common/Authentication/Handlers/LoginSessionAuthentication.cs +++ b/Common/Authentication/Handlers/LoginSessionAuthentication.cs @@ -65,7 +65,9 @@ protected override Task HandleAuthenticateAsync() private async Task TokenAuth(string token) { - var tokenDto = await _db.ApiTokens.Include(x => x.User).FirstOrDefaultAsync(x => x.Token == token && + string tokenHash = HashingUtils.HashSha256(token); + + var tokenDto = await _db.ApiTokens.Include(x => x.User).FirstOrDefaultAsync(x => x.TokenHash == tokenHash && (x.ValidUntil == null || x.ValidUntil >= DateTime.UtcNow)); if (tokenDto == null) return Fail(AuthResultError.TokenInvalid); diff --git a/Common/Constants/HardLimits.cs b/Common/Constants/HardLimits.cs index 2685c1d1..34c1063d 100644 --- a/Common/Constants/HardLimits.cs +++ b/Common/Constants/HardLimits.cs @@ -20,8 +20,7 @@ public static class HardLimits public const int UserAgentMaxLength = 1024; public const int ApiKeyNameMaxLength = 64; - public const int ApiKeyTokenMinLength = 1; - public const int ApiKeyTokenMaxLength = 64; + public const int ApiKeyTokenLength = 64; public const int ApiKeyMaxPermissions = 256; public const int HubNameMinLength = 1; @@ -34,9 +33,10 @@ public static class HardLimits public const int ShockerShareLinkNameMinLength = 1; public const int ShockerShareLinkNameMaxLength = 64; + public const int SemVerMaxLength = 64; public const int IpAddressMaxLength = 40; + public const int Sha256HashHexLength = 64; - public const int SemVerMaxLength = 64; public const int OtaUpdateMessageMaxLength = 128; public const int PasswordHashMaxLength = 100; diff --git a/Common/Migrations/20241123181710_Hash API tokens.Designer.cs b/Common/Migrations/20241123181710_Hash API tokens.Designer.cs new file mode 100644 index 00000000..f9018436 --- /dev/null +++ b/Common/Migrations/20241123181710_Hash API tokens.Designer.cs @@ -0,0 +1,1129 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using OpenShock.Common.OpenShockDb; + +#nullable disable + +namespace OpenShock.Common.Migrations +{ + [DbContext(typeof(OpenShockContext))] + [Migration("20241123181710_Hash API tokens")] + partial class HashAPItokens + { + /// + 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", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "control_type", new[] { "sound", "vibrate", "shock", "stop" }); + 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, "permission_type", new[] { "shockers.use", "shockers.edit", "shockers.pause", "devices.edit", "devices.auth" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "rank_type", new[] { "user", "support", "staff", "admin", "system" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "shocker_model_type", new[] { "caiXianlin", "petTrainer", "petrainer998DR" }); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.AdminUsersView", b => + { + b.Property("ApiTokenCount") + .HasColumnType("integer") + .HasColumnName("api_token_count"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeviceCount") + .HasColumnType("integer") + .HasColumnName("device_count"); + + b.Property("Email") + .IsRequired() + .HasColumnType("character varying") + .HasColumnName("email"); + + b.Property("EmailActivated") + .HasColumnType("boolean") + .HasColumnName("email_actived"); + + 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") + .IsRequired() + .HasColumnType("character varying") + .HasColumnName("password_hash_type"); + + b.Property("PasswordResetCount") + .HasColumnType("integer") + .HasColumnName("password_reset_count"); + + b.Property("Rank") + .HasColumnType("rank_type") + .HasColumnName("rank"); + + b.Property("ShockerControlLogCount") + .HasColumnType("integer") + .HasColumnName("shocker_control_log_count"); + + b.Property("ShockerCount") + .HasColumnType("integer") + .HasColumnName("shocker_count"); + + b.Property("ShockerShareCount") + .HasColumnType("integer") + .HasColumnName("shocker_share_count"); + + b.Property("ShockerShareLinkCount") + .HasColumnType("integer") + .HasColumnName("shocker_share_link_count"); + + b.Property("UserActivationCount") + .HasColumnType("integer") + .HasColumnName("user_activation_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("CreatedByIp") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("created_by_ip"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("LastUsed") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_used") + .HasDefaultValueSql("'-infinity'::timestamp without time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("name"); + + b.PrimitiveCollection("Permissions") + .IsRequired() + .HasColumnType("permission_type[]") + .HasColumnName("permissions"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("token_hash"); + + 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") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.HasIndex("ValidUntil") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.ToTable("api_tokens", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Device", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("name"); + + b.Property("Owner") + .HasColumnType("uuid") + .HasColumnName("owner"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("token"); + + b.HasKey("Id") + .HasName("devices_pkey"); + + b.HasIndex("Owner") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.HasIndex("Token") + .IsUnique(); + + b.ToTable("devices", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.DeviceOtaUpdate", b => + { + b.Property("Device") + .HasColumnType("uuid") + .HasColumnName("device"); + + b.Property("UpdateId") + .HasColumnType("integer") + .HasColumnName("update_id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .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("Device", "UpdateId") + .HasName("device_ota_updates_pkey"); + + b.HasIndex(new[] { "CreatedOn" }, "device_ota_updates_created_on_idx") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.ToTable("device_ota_updates", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.PasswordReset", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Secret") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("secret"); + + b.Property("UsedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("used_on"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("password_resets_pkey"); + + b.HasIndex("UserId") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.ToTable("password_resets", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShareRequest", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Owner") + .HasColumnType("uuid") + .HasColumnName("owner"); + + b.Property("User") + .HasColumnType("uuid") + .HasColumnName("user"); + + b.HasKey("Id") + .HasName("shares_codes_pkey"); + + b.HasIndex("Owner") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.HasIndex("User"); + + b.ToTable("share_requests", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShareRequestsShocker", b => + { + b.Property("ShareRequest") + .HasColumnType("uuid") + .HasColumnName("share_request"); + + b.Property("Shocker") + .HasColumnType("uuid") + .HasColumnName("shocker"); + + b.Property("LimitDuration") + .HasColumnType("integer") + .HasColumnName("limit_duration"); + + b.Property("LimitIntensity") + .HasColumnType("smallint") + .HasColumnName("limit_intensity"); + + b.Property("PermLive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_live"); + + b.Property("PermShock") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_shock"); + + b.Property("PermSound") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_sound"); + + b.Property("PermVibrate") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_vibrate"); + + b.HasKey("ShareRequest", "Shocker") + .HasName("share_requests_shockers_pkey"); + + b.HasIndex("Shocker"); + + b.ToTable("share_requests_shockers", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Shocker", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Device") + .HasColumnType("uuid") + .HasColumnName("device"); + + b.Property("Model") + .HasColumnType("shocker_model_type") + .HasColumnName("model"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("name"); + + b.Property("Paused") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("paused"); + + b.Property("RfId") + .HasColumnType("integer") + .HasColumnName("rf_id"); + + b.HasKey("Id") + .HasName("shockers_pkey"); + + b.HasIndex("Device") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.ToTable("shockers", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerControlLog", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ControlledBy") + .HasColumnType("uuid") + .HasColumnName("controlled_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .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("ControlledBy"); + + b.HasIndex("ShockerId") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.ToTable("shocker_control_logs", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerShare", b => + { + b.Property("ShockerId") + .HasColumnType("uuid") + .HasColumnName("shocker_id"); + + b.Property("SharedWith") + .HasColumnType("uuid") + .HasColumnName("shared_with"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("LimitDuration") + .HasColumnType("integer") + .HasColumnName("limit_duration"); + + b.Property("LimitIntensity") + .HasColumnType("smallint") + .HasColumnName("limit_intensity"); + + b.Property("Paused") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("paused"); + + b.Property("PermLive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_live"); + + b.Property("PermShock") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_shock"); + + b.Property("PermSound") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_sound"); + + b.Property("PermVibrate") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_vibrate"); + + b.HasKey("ShockerId", "SharedWith") + .HasName("shocker_shares_pkey"); + + b.HasIndex("SharedWith") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.ToTable("shocker_shares", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerShareCode", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("LimitDuration") + .HasColumnType("integer") + .HasColumnName("limit_duration"); + + b.Property("LimitIntensity") + .HasColumnType("smallint") + .HasColumnName("limit_intensity"); + + b.Property("PermShock") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_shock"); + + b.Property("PermSound") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_sound"); + + b.Property("PermVibrate") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_vibrate"); + + 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.ShockerSharesLink", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("ExpiresOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("name"); + + b.Property("OwnerId") + .HasColumnType("uuid") + .HasColumnName("owner_id"); + + b.HasKey("Id") + .HasName("shocker_shares_links_pkey"); + + b.HasIndex("OwnerId") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.ToTable("shocker_shares_links", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerSharesLinksShocker", b => + { + b.Property("ShareLinkId") + .HasColumnType("uuid") + .HasColumnName("share_link_id"); + + b.Property("ShockerId") + .HasColumnType("uuid") + .HasColumnName("shocker_id"); + + b.Property("Cooldown") + .HasColumnType("integer") + .HasColumnName("cooldown"); + + b.Property("LimitDuration") + .HasColumnType("integer") + .HasColumnName("limit_duration"); + + b.Property("LimitIntensity") + .HasColumnType("smallint") + .HasColumnName("limit_intensity"); + + b.Property("Paused") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("paused"); + + b.Property("PermLive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("perm_live"); + + b.Property("PermShock") + .HasColumnType("boolean") + .HasColumnName("perm_shock"); + + b.Property("PermSound") + .HasColumnType("boolean") + .HasColumnName("perm_sound"); + + b.Property("PermVibrate") + .HasColumnType("boolean") + .HasColumnName("perm_vibrate"); + + b.HasKey("ShareLinkId", "ShockerId") + .HasName("shocker_shares_links_shockers_pkey"); + + b.HasIndex("ShockerId"); + + b.ToTable("shocker_shares_links_shockers", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.User", 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("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)") + .HasColumnName("email"); + + b.Property("EmailActived") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("email_actived"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("name") + .UseCollation("ndcoll"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("password_hash"); + + b.Property("Rank") + .HasColumnType("rank_type") + .HasColumnName("rank"); + + 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.UsersActivation", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Secret") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("secret"); + + b.Property("UsedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("used_on"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("users_activation_pkey"); + + b.HasIndex("UserId"); + + b.ToTable("users_activation", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UsersEmailChange", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)") + .HasColumnName("email"); + + b.Property("Secret") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("secret"); + + b.Property("UsedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("used_on"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("users_email_change_pkey"); + + b.HasIndex("CreatedOn") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.HasIndex("UsedOn") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.HasIndex("UserId"); + + b.ToTable("users_email_changes", (string)null); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UsersNameChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("OldName") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("old_name"); + + b.HasKey("Id", "UserId") + .HasName("users_name_changes_pkey"); + + b.HasIndex("CreatedOn") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.HasIndex("OldName") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.HasIndex("UserId") + .HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); + + b.ToTable("users_name_changes", (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_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Device", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "OwnerNavigation") + .WithMany("Devices") + .HasForeignKey("Owner") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("owner_user_id"); + + b.Navigation("OwnerNavigation"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.DeviceOtaUpdate", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.Device", "DeviceNavigation") + .WithMany("DeviceOtaUpdates") + .HasForeignKey("Device") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("device_ota_updates_device"); + + b.Navigation("DeviceNavigation"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.PasswordReset", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "User") + .WithMany("PasswordResets") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShareRequest", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "OwnerNavigation") + .WithMany("ShareRequestOwnerNavigations") + .HasForeignKey("Owner") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_share_requests_owner"); + + b.HasOne("OpenShock.Common.OpenShockDb.User", "UserNavigation") + .WithMany("ShareRequestUserNavigations") + .HasForeignKey("User") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_share_requests_user"); + + b.Navigation("OwnerNavigation"); + + b.Navigation("UserNavigation"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShareRequestsShocker", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.ShareRequest", "ShareRequestNavigation") + .WithMany("ShareRequestsShockers") + .HasForeignKey("ShareRequest") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_share_requests_shockers_share_request"); + + b.HasOne("OpenShock.Common.OpenShockDb.Shocker", "ShockerNavigation") + .WithMany("ShareRequestsShockers") + .HasForeignKey("Shocker") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_share_requests_shockers_shocker"); + + b.Navigation("ShareRequestNavigation"); + + b.Navigation("ShockerNavigation"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Shocker", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.Device", "DeviceNavigation") + .WithMany("Shockers") + .HasForeignKey("Device") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("device_id"); + + b.Navigation("DeviceNavigation"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerControlLog", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "ControlledByNavigation") + .WithMany("ShockerControlLogs") + .HasForeignKey("ControlledBy") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_controlled_by"); + + b.HasOne("OpenShock.Common.OpenShockDb.Shocker", "Shocker") + .WithMany("ShockerControlLogs") + .HasForeignKey("ShockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_shocker_id"); + + b.Navigation("ControlledByNavigation"); + + b.Navigation("Shocker"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerShare", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "SharedWithNavigation") + .WithMany("ShockerShares") + .HasForeignKey("SharedWith") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("shared_with_user_id"); + + b.HasOne("OpenShock.Common.OpenShockDb.Shocker", "Shocker") + .WithMany("ShockerShares") + .HasForeignKey("ShockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("ref_shocker_id"); + + b.Navigation("SharedWithNavigation"); + + 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_id"); + + b.Navigation("Shocker"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerSharesLink", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "Owner") + .WithMany("ShockerSharesLinks") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("owner_id"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerSharesLinksShocker", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.ShockerSharesLink", "ShareLink") + .WithMany("ShockerSharesLinksShockers") + .HasForeignKey("ShareLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("share_link_id"); + + b.HasOne("OpenShock.Common.OpenShockDb.Shocker", "Shocker") + .WithMany("ShockerSharesLinksShockers") + .HasForeignKey("ShockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("shocker_id"); + + b.Navigation("ShareLink"); + + b.Navigation("Shocker"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UsersActivation", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "User") + .WithMany("UsersActivations") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UsersEmailChange", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "User") + .WithMany("UsersEmailChanges") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.UsersNameChange", b => + { + b.HasOne("OpenShock.Common.OpenShockDb.User", "User") + .WithMany("UsersNameChanges") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Device", b => + { + b.Navigation("DeviceOtaUpdates"); + + b.Navigation("Shockers"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShareRequest", b => + { + b.Navigation("ShareRequestsShockers"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.Shocker", b => + { + b.Navigation("ShareRequestsShockers"); + + b.Navigation("ShockerControlLogs"); + + b.Navigation("ShockerShareCodes"); + + b.Navigation("ShockerShares"); + + b.Navigation("ShockerSharesLinksShockers"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.ShockerSharesLink", b => + { + b.Navigation("ShockerSharesLinksShockers"); + }); + + modelBuilder.Entity("OpenShock.Common.OpenShockDb.User", b => + { + b.Navigation("ApiTokens"); + + b.Navigation("Devices"); + + b.Navigation("PasswordResets"); + + b.Navigation("ShareRequestOwnerNavigations"); + + b.Navigation("ShareRequestUserNavigations"); + + b.Navigation("ShockerControlLogs"); + + b.Navigation("ShockerShares"); + + b.Navigation("ShockerSharesLinks"); + + b.Navigation("UsersActivations"); + + b.Navigation("UsersEmailChanges"); + + b.Navigation("UsersNameChanges"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Common/Migrations/20241123181710_Hash API tokens.cs b/Common/Migrations/20241123181710_Hash API tokens.cs new file mode 100644 index 00000000..559395b9 --- /dev/null +++ b/Common/Migrations/20241123181710_Hash API tokens.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace OpenShock.Common.Migrations +{ + /// + public partial class HashAPItokens : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "token", + table: "api_tokens", + newName: "token_hash"); + + migrationBuilder.RenameIndex( + name: "IX_api_tokens_token", + table: "api_tokens", + newName: "IX_api_tokens_token_hash"); + + migrationBuilder.Sql( + $""" + UPDATE api_tokens SET token_hash = encode(digest(token_hash, 'sha256'), 'hex') + """ + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + throw new InvalidOperationException("This migration cannot be reverted because token hashing is irreversible."); + } + } +} diff --git a/Common/Migrations/OpenShockContextModelSnapshot.cs b/Common/Migrations/OpenShockContextModelSnapshot.cs index 17e40578..839300bd 100644 --- a/Common/Migrations/OpenShockContextModelSnapshot.cs +++ b/Common/Migrations/OpenShockContextModelSnapshot.cs @@ -142,11 +142,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("permission_type[]") .HasColumnName("permissions"); - b.Property("Token") + b.Property("TokenHash") .IsRequired() .HasMaxLength(64) .HasColumnType("character varying(64)") - .HasColumnName("token"); + .HasColumnName("token_hash"); b.Property("UserId") .HasColumnType("uuid") @@ -159,7 +159,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id") .HasName("api_tokens_pkey"); - b.HasIndex("Token") + b.HasIndex("TokenHash") .IsUnique(); b.HasIndex("UserId") diff --git a/Common/OpenShockDb/ApiToken.cs b/Common/OpenShockDb/ApiToken.cs index c7c770bf..d3a00b70 100644 --- a/Common/OpenShockDb/ApiToken.cs +++ b/Common/OpenShockDb/ApiToken.cs @@ -10,7 +10,7 @@ public partial class ApiToken public string Name { get; set; } = null!; - public string Token { get; set; } = null!; + public string TokenHash { get; set; } = null!; public Guid UserId { get; set; } diff --git a/Common/OpenShockDb/OpenShockContext.cs b/Common/OpenShockDb/OpenShockContext.cs index 7545658a..f7264f6d 100644 --- a/Common/OpenShockDb/OpenShockContext.cs +++ b/Common/OpenShockDb/OpenShockContext.cs @@ -77,7 +77,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.HasIndex(e => e.UserId).HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); entity.HasIndex(e => e.ValidUntil).HasAnnotation("Npgsql:StorageParameter:deduplicate_items", "true"); - entity.HasIndex(e => e.Token).IsUnique(); + entity.HasIndex(e => e.TokenHash).IsUnique(); entity.Property(e => e.Id) .ValueGeneratedNever() @@ -94,9 +94,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.Name) .HasMaxLength(HardLimits.ApiKeyNameMaxLength) .HasColumnName("name"); - entity.Property(e => e.Token) - .HasMaxLength(HardLimits.ApiKeyTokenMaxLength) - .HasColumnName("token"); + entity.Property(e => e.TokenHash) + .HasMaxLength(HardLimits.Sha256HashHexLength) + .HasColumnName("token_hash"); entity.Property(e => e.UserId).HasColumnName("user_id"); entity.Property(e => e.ValidUntil).HasColumnName("valid_until");