From ab6e2e068f399a34d37489b92f8cd60d69471c80 Mon Sep 17 00:00:00 2001 From: HeavenVR Date: Sat, 23 Nov 2024 19:11:44 +0100 Subject: [PATCH 01/22] Add <= 1.4.0 hubs P998DR RFID fix (#134) * Add RFID migrations * Shift rigth on send --- ...14013_Fix Petrainer998DR RFIDs.Designer.cs | 1129 +++++++++++++++++ ...20241122214013_Fix Petrainer998DR RFIDs.cs | 38 + .../OpenShockContextModelSnapshot.cs | 6 +- .../Controllers/HubV1Controller.cs | 2 +- 4 files changed, 1171 insertions(+), 4 deletions(-) create mode 100644 Common/Migrations/20241122214013_Fix Petrainer998DR RFIDs.Designer.cs create mode 100644 Common/Migrations/20241122214013_Fix Petrainer998DR RFIDs.cs diff --git a/Common/Migrations/20241122214013_Fix Petrainer998DR RFIDs.Designer.cs b/Common/Migrations/20241122214013_Fix Petrainer998DR RFIDs.Designer.cs new file mode 100644 index 00000000..66b58cae --- /dev/null +++ b/Common/Migrations/20241122214013_Fix Petrainer998DR RFIDs.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("20241122214013_Fix Petrainer998DR RFIDs")] + partial class FixPetrainer998DRRFIDs + { + /// + 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("Token") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("token"); + + 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("Token") + .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/20241122214013_Fix Petrainer998DR RFIDs.cs b/Common/Migrations/20241122214013_Fix Petrainer998DR RFIDs.cs new file mode 100644 index 00000000..985b5e06 --- /dev/null +++ b/Common/Migrations/20241122214013_Fix Petrainer998DR RFIDs.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace OpenShock.Common.Migrations +{ + /// + public partial class FixPetrainer998DRRFIDs : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + $""" + UPDATE shockers + SET + rf_id = ((rf_id)::bit(32) << 1)::integer + WHERE + model = 'petrainer998DR' + """ + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + $""" + UPDATE shockers + SET + rf_id = ((rf_id)::bit(32) >> 1)::integer + WHERE + model = 'petrainer998DR' + """ + ); + } + } +} diff --git a/Common/Migrations/OpenShockContextModelSnapshot.cs b/Common/Migrations/OpenShockContextModelSnapshot.cs index 4d84fc17..17e40578 100644 --- a/Common/Migrations/OpenShockContextModelSnapshot.cs +++ b/Common/Migrations/OpenShockContextModelSnapshot.cs @@ -18,7 +18,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasAnnotation("Npgsql:CollationDefinition:public.ndcoll", "und-u-ks-level2,und-u-ks-level2,icu,False") - .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("ProductVersion", "9.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "control_type", new[] { "sound", "vibrate", "shock", "stop" }); @@ -48,7 +48,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying") .HasColumnName("email"); - b.Property("EmailActived") + b.Property("EmailActivated") .HasColumnType("boolean") .HasColumnName("email_actived"); @@ -137,7 +137,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(64)") .HasColumnName("name"); - b.Property("Permissions") + b.PrimitiveCollection("Permissions") .IsRequired() .HasColumnType("permission_type[]") .HasColumnName("permissions"); diff --git a/LiveControlGateway/Controllers/HubV1Controller.cs b/LiveControlGateway/Controllers/HubV1Controller.cs index 13b5f80f..e157e72e 100644 --- a/LiveControlGateway/Controllers/HubV1Controller.cs +++ b/LiveControlGateway/Controllers/HubV1Controller.cs @@ -168,7 +168,7 @@ public override ValueTask Control(List> 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 }).ToList() From d396efe6a8058362f6268b3640be7c630da9970f Mon Sep 17 00:00:00 2001 From: HeavenVR Date: Sat, 23 Nov 2024 20:22:10 +0100 Subject: [PATCH 02/22] Hash stored tokens (#128) * Hash tokens before storage in database * Dont hash device tokens * oops * Adjust limits and add migrations --- API/Controller/Tokens/TokenController.cs | 14 +- .../Handlers/LoginSessionAuthentication.cs | 4 +- Common/Constants/HardLimits.cs | 6 +- ...20241123181710_Hash API tokens.Designer.cs | 1129 +++++++++++++++++ .../20241123181710_Hash API tokens.cs | 36 + .../OpenShockContextModelSnapshot.cs | 6 +- Common/OpenShockDb/ApiToken.cs | 2 +- Common/OpenShockDb/OpenShockContext.cs | 8 +- 8 files changed, 1187 insertions(+), 18 deletions(-) create mode 100644 Common/Migrations/20241123181710_Hash API tokens.Designer.cs create mode 100644 Common/Migrations/20241123181710_Hash API tokens.cs 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"); From af61aa7ceb8112d367051390921bbfa11ffdd2ab Mon Sep 17 00:00:00 2001 From: hhvrc Date: Sat, 23 Nov 2024 22:52:27 +0100 Subject: [PATCH 03/22] IPAddress data and naming cleanup --- API/Controller/Tokens/TokenController.cs | 2 +- API/Services/Account/AccountService.cs | 2 +- ...214013_Type and naming cleanup.Designer.cs | 1129 +++++++++++++++++ .../20241123214013_Type and naming cleanup.cs | 41 + .../OpenShockContextModelSnapshot.cs | 12 +- Common/OpenShockDb/ApiToken.cs | 3 +- Common/OpenShockDb/OpenShockContext.cs | 7 +- Common/OpenShockDb/User.cs | 2 +- 8 files changed, 1184 insertions(+), 14 deletions(-) create mode 100644 Common/Migrations/20241123214013_Type and naming cleanup.Designer.cs create mode 100644 Common/Migrations/20241123214013_Type and naming cleanup.cs diff --git a/API/Controller/Tokens/TokenController.cs b/API/Controller/Tokens/TokenController.cs index d83e7d5d..ace7625c 100644 --- a/API/Controller/Tokens/TokenController.cs +++ b/API/Controller/Tokens/TokenController.cs @@ -113,7 +113,7 @@ public async Task CreateToken([FromBody] CreateTokenReques { UserId = CurrentUser.DbUser.Id, TokenHash = HashingUtils.HashSha256(token), - CreatedByIp = HttpContext.GetRemoteIP().ToString(), + CreatedByIp = HttpContext.GetRemoteIP(), Permissions = body.Permissions.Distinct().ToList(), Id = Guid.NewGuid(), Name = body.Name, diff --git a/API/Services/Account/AccountService.cs b/API/Services/Account/AccountService.cs index 4cd17bcc..f6a10bc3 100644 --- a/API/Services/Account/AccountService.cs +++ b/API/Services/Account/AccountService.cs @@ -64,7 +64,7 @@ private async Task, AccountWithEmailOrUsernameExists>> Creat Name = username, Email = email.ToLowerInvariant(), PasswordHash = PasswordHashingUtils.HashPassword(password), - EmailActived = emailActivated + EmailActivated = emailActivated }; _db.Users.Add(user); diff --git a/Common/Migrations/20241123214013_Type and naming cleanup.Designer.cs b/Common/Migrations/20241123214013_Type and naming cleanup.Designer.cs new file mode 100644 index 00000000..7ff6de0d --- /dev/null +++ b/Common/Migrations/20241123214013_Type and naming cleanup.Designer.cs @@ -0,0 +1,1129 @@ +// +using System; +using System.Net; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using OpenShock.Common.OpenShockDb; + +#nullable disable + +namespace OpenShock.Common.Migrations +{ + [DbContext(typeof(OpenShockContext))] + [Migration("20241123214013_Type and naming cleanup")] + partial class Typeandnamingcleanup + { + /// + 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_activated"); + + 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() + .HasColumnType("inet") + .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("EmailActivated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("email_activated"); + + 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/20241123214013_Type and naming cleanup.cs b/Common/Migrations/20241123214013_Type and naming cleanup.cs new file mode 100644 index 00000000..a4f07fc6 --- /dev/null +++ b/Common/Migrations/20241123214013_Type and naming cleanup.cs @@ -0,0 +1,41 @@ +using System.Net; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace OpenShock.Common.Migrations +{ + /// + public partial class Typeandnamingcleanup : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "email_actived", + table: "users", + newName: "email_activated"); + + migrationBuilder.Sql( + $""" + ALTER TABLE api_tokens ALTER COLUMN created_by_ip TYPE inet USING CAST(created_by_ip AS inet); + """ + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "email_activated", + table: "users", + newName: "email_actived"); + + migrationBuilder.Sql( + $""" + ALTER TABLE api_tokens ALTER COLUMN created_by_ip TYPE character varying(40) USING CAST(created_by_ip AS character varying(40)); + """ + ); + } + } +} diff --git a/Common/Migrations/OpenShockContextModelSnapshot.cs b/Common/Migrations/OpenShockContextModelSnapshot.cs index 839300bd..0f5f566a 100644 --- a/Common/Migrations/OpenShockContextModelSnapshot.cs +++ b/Common/Migrations/OpenShockContextModelSnapshot.cs @@ -1,5 +1,6 @@ // using System; +using System.Net; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -50,7 +51,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("EmailActivated") .HasColumnType("boolean") - .HasColumnName("email_actived"); + .HasColumnName("email_activated"); b.Property("EmailChangeRequestCount") .HasColumnType("integer") @@ -113,10 +114,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid") .HasColumnName("id"); - b.Property("CreatedByIp") + b.Property("CreatedByIp") .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") + .HasColumnType("inet") .HasColumnName("created_by_ip"); b.Property("CreatedOn") @@ -686,11 +686,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(320)") .HasColumnName("email"); - b.Property("EmailActived") + b.Property("EmailActivated") .ValueGeneratedOnAdd() .HasColumnType("boolean") .HasDefaultValue(false) - .HasColumnName("email_actived"); + .HasColumnName("email_activated"); b.Property("Name") .IsRequired() diff --git a/Common/OpenShockDb/ApiToken.cs b/Common/OpenShockDb/ApiToken.cs index d3a00b70..f5274381 100644 --- a/Common/OpenShockDb/ApiToken.cs +++ b/Common/OpenShockDb/ApiToken.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Net; using OpenShock.Common.Models; namespace OpenShock.Common.OpenShockDb; @@ -16,7 +17,7 @@ public partial class ApiToken public DateTime CreatedOn { get; set; } - public string CreatedByIp { get; set; } = null!; + public IPAddress CreatedByIp { get; set; } = null!; public DateTime? ValidUntil { get; set; } diff --git a/Common/OpenShockDb/OpenShockContext.cs b/Common/OpenShockDb/OpenShockContext.cs index f7264f6d..3212bda5 100644 --- a/Common/OpenShockDb/OpenShockContext.cs +++ b/Common/OpenShockDb/OpenShockContext.cs @@ -83,7 +83,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .ValueGeneratedNever() .HasColumnName("id"); entity.Property(e => e.CreatedByIp) - .VarCharWithLength(HardLimits.IpAddressMaxLength) .HasColumnName("created_by_ip"); entity.Property(e => e.CreatedOn) .HasDefaultValueSql("CURRENT_TIMESTAMP") @@ -456,9 +455,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.Email) .VarCharWithLength(HardLimits.EmailAddressMaxLength) .HasColumnName("email"); - entity.Property(e => e.EmailActived) + entity.Property(e => e.EmailActivated) .HasDefaultValue(false) - .HasColumnName("email_actived"); + .HasColumnName("email_activated"); entity.Property(e => e.Name) .UseCollation("ndcoll") .VarCharWithLength(HardLimits.UsernameMaxLength) @@ -577,7 +576,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.CreatedAt) .HasColumnName("created_at"); entity.Property(e => e.EmailActivated) - .HasColumnName("email_actived"); + .HasColumnName("email_activated"); entity.Property(e => e.Rank) .HasColumnType("rank_type") .HasColumnName("rank"); diff --git a/Common/OpenShockDb/User.cs b/Common/OpenShockDb/User.cs index afc3426a..a0036816 100644 --- a/Common/OpenShockDb/User.cs +++ b/Common/OpenShockDb/User.cs @@ -16,7 +16,7 @@ public partial class User public DateTime CreatedAt { get; set; } - public bool EmailActived { get; set; } + public bool EmailActivated { get; set; } public RankType Rank { get; set; } From 560c8aaaca074fc789c9dff2aa520d0982894ce2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 09:50:46 +0100 Subject: [PATCH 04/22] build(deps): Bump the nuget-dependencies group with 7 updates (#136) Bumps the nuget-dependencies group with 7 updates: | Package | From | To | | --- | --- | --- | | [Fluid.Core](https://github.com/sebastienros/fluid) | `2.12.0` | `2.13.1` | | [Microsoft.EntityFrameworkCore](https://github.com/dotnet/efcore) | `9.0.0` | `9.0.0` | | [Microsoft.EntityFrameworkCore.Relational](https://github.com/dotnet/efcore) | `9.0.0` | `9.0.0` | | [Npgsql.EntityFrameworkCore.PostgreSQL](https://github.com/npgsql/efcore.pg) | `9.0.0` | `9.0.1` | | [Scalar.AspNetCore](https://github.com/scalar/scalar) | `1.2.39` | `1.2.44` | | [TUnit](https://github.com/thomhurst/TUnit) | `0.3.31` | `0.4.1` | | NRedisStack | `0.13.0` | `0.13.1` | Updates `Fluid.Core` from 2.12.0 to 2.13.1 - [Release notes](https://github.com/sebastienros/fluid/releases) - [Commits](https://github.com/sebastienros/fluid/compare/v2.12.0...v2.13.1) Updates `Microsoft.EntityFrameworkCore` from 9.0.0 to 9.0.0 - [Release notes](https://github.com/dotnet/efcore/releases) - [Commits](https://github.com/dotnet/efcore/compare/v9.0.0...v9.0.0) Updates `Microsoft.EntityFrameworkCore.Relational` from 9.0.0 to 9.0.0 - [Release notes](https://github.com/dotnet/efcore/releases) - [Commits](https://github.com/dotnet/efcore/compare/v9.0.0...v9.0.0) Updates `Npgsql.EntityFrameworkCore.PostgreSQL` from 9.0.0 to 9.0.1 - [Release notes](https://github.com/npgsql/efcore.pg/releases) - [Commits](https://github.com/npgsql/efcore.pg/compare/v9.0.0...v9.0.1) Updates `Scalar.AspNetCore` from 1.2.39 to 1.2.44 - [Changelog](https://github.com/scalar/scalar/blob/main/RELEASE.md) - [Commits](https://github.com/scalar/scalar/commits) Updates `TUnit` from 0.3.31 to 0.4.1 - [Commits](https://github.com/thomhurst/TUnit/commits) Updates `NRedisStack` from 0.13.0 to 0.13.1 --- updated-dependencies: - dependency-name: Fluid.Core dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-dependencies - dependency-name: Microsoft.EntityFrameworkCore dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-dependencies - dependency-name: Microsoft.EntityFrameworkCore.Relational dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-dependencies - dependency-name: Npgsql.EntityFrameworkCore.PostgreSQL dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-dependencies - dependency-name: Scalar.AspNetCore dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-dependencies - dependency-name: TUnit dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-dependencies - dependency-name: NRedisStack dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- API/API.csproj | 6 +++--- Common.Tests/Common.Tests.csproj | 2 +- Common/Common.csproj | 4 ++-- LiveControlGateway/LiveControlGateway.csproj | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/API/API.csproj b/API/API.csproj index a7a6cd61..5e91d68a 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -23,7 +23,7 @@ - + @@ -37,10 +37,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + - + diff --git a/Common.Tests/Common.Tests.csproj b/Common.Tests/Common.Tests.csproj index 1f721fb9..f27d4e36 100644 --- a/Common.Tests/Common.Tests.csproj +++ b/Common.Tests/Common.Tests.csproj @@ -13,7 +13,7 @@ - + diff --git a/Common/Common.csproj b/Common/Common.csproj index cb312cd3..6cefdf4c 100644 --- a/Common/Common.csproj +++ b/Common/Common.csproj @@ -30,8 +30,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/LiveControlGateway/LiveControlGateway.csproj b/LiveControlGateway/LiveControlGateway.csproj index 46ba6374..186bfd2f 100644 --- a/LiveControlGateway/LiveControlGateway.csproj +++ b/LiveControlGateway/LiveControlGateway.csproj @@ -16,7 +16,7 @@ - + From 7739ef8915048e05b3bf1041f10ea3b5cfbdefbc Mon Sep 17 00:00:00 2001 From: HeavenVR Date: Mon, 25 Nov 2024 11:05:48 +0100 Subject: [PATCH 05/22] Create codeql.yml --- .github/workflows/codeql.yml | 90 ++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..88728ecf --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,90 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "develop", "master" ] + pull_request: + branches: [ "develop", "master" ] + schedule: + - cron: '0 6 * * 1' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: csharp + build-mode: manual + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + dotnet restore + dotnet publish API/API.csproj -c Release + dotnet publish LiveControlGateway/LiveControlGateway.csproj -c Release + dotnet publish Cron/Cron.csproj -c Release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" From cd22c19839e4bb5371bae2d3c585c4a655924b8f Mon Sep 17 00:00:00 2001 From: HeavenVR Date: Mon, 25 Nov 2024 11:15:23 +0100 Subject: [PATCH 06/22] Update codeql.yml --- .github/workflows/codeql.yml | 61 ++++++++---------------------------- 1 file changed, 13 insertions(+), 48 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 88728ecf..c9d76b53 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,14 +1,3 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# name: "CodeQL Advanced" on: @@ -19,15 +8,13 @@ on: schedule: - cron: '0 6 * * 1' +env: + DOTNET_VERSION: 9.x.x + jobs: analyze: - name: Analyze (${{ matrix.language }}) - # Runner size impacts CodeQL analysis time. To learn more, please see: - # - https://gh.io/recommended-hardware-resources-for-running-codeql - # - https://gh.io/supported-runners-and-hardware-resources - # - https://gh.io/using-larger-runners (GitHub.com only) - # Consider using larger runners or machines with greater resources for possible analysis time improvements. - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + name: Analyze (csharp) + runs-on: 'ubuntu-latest' permissions: # required for all workflows security-events: write @@ -38,21 +25,6 @@ jobs: # only required for workflows in private repositories actions: read contents: read - - strategy: - fail-fast: false - matrix: - include: - - language: csharp - build-mode: manual - # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' - # Use `c-cpp` to analyze code written in C, C++ or both - # Use 'java-kotlin' to analyze code written in Java, Kotlin or both - # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both - # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, - # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. - # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how - # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository uses: actions/checkout@v4 @@ -61,22 +33,15 @@ jobs: - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality + languages: csharp + build-mode: manual + + - name: Setup .NET SDK ${{ env.DOTNET_VERSION }} + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} - # If the analyze step fails for one of the languages you are analyzing with - # "We were unable to automatically build your code", modify the matrix above - # to set the build mode to "manual" for that language. Then modify this step - # to build your code. - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - if: matrix.build-mode == 'manual' + - name: Build .NET shell: bash run: | dotnet restore From 1591022b74d7f97b2581e9fd50d87786f8a17574 Mon Sep 17 00:00:00 2001 From: HeavenVR Date: Mon, 25 Nov 2024 11:18:34 +0100 Subject: [PATCH 07/22] Update codeql.yml --- .github/workflows/codeql.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c9d76b53..a24c774d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -52,4 +52,4 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: - category: "/language:${{matrix.language}}" + category: "/language:csharp" From f8e346bb2a217c7472a7c66e650ed76106374400 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Mon, 25 Nov 2024 17:45:28 +0100 Subject: [PATCH 08/22] Clean up migrations --- .../Migrations/20241105235041_RestrictFieldLengths.cs | 7 +++++-- .../20241123214013_Type and naming cleanup.cs | 10 +++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Common/Migrations/20241105235041_RestrictFieldLengths.cs b/Common/Migrations/20241105235041_RestrictFieldLengths.cs index bc54746c..8c4d95eb 100644 --- a/Common/Migrations/20241105235041_RestrictFieldLengths.cs +++ b/Common/Migrations/20241105235041_RestrictFieldLengths.cs @@ -11,8 +11,11 @@ public partial class RestrictFieldLengths : Migration protected override void Up(MigrationBuilder migrationBuilder) { // Truncate shocker sharelinks names to 64 characters - migrationBuilder.Sql("UPDATE public.shocker_shares_links SET name = LEFT(name, 64) WHERE LENGTH(name) > 64"); - migrationBuilder.Sql("UPDATE public.shocker_control_logs SET custom_name = LEFT(custom_name, 64) WHERE LENGTH(custom_name) > 64"); + migrationBuilder.Sql( + $""" + UPDATE public.shocker_shares_links SET name = LEFT(name, 64) WHERE LENGTH(name) > 64; + UPDATE public.shocker_control_logs SET custom_name = LEFT(custom_name, 64) WHERE LENGTH(custom_name) > 64; + """); // We need to drop the view to modify the target table migrationBuilder.Sql(AddAdminUsersView.AdminUsersViewDropQuery); diff --git a/Common/Migrations/20241123214013_Type and naming cleanup.cs b/Common/Migrations/20241123214013_Type and naming cleanup.cs index a4f07fc6..75bea950 100644 --- a/Common/Migrations/20241123214013_Type and naming cleanup.cs +++ b/Common/Migrations/20241123214013_Type and naming cleanup.cs @@ -26,16 +26,16 @@ protected override void Up(MigrationBuilder migrationBuilder) /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.RenameColumn( - name: "email_activated", - table: "users", - newName: "email_actived"); - migrationBuilder.Sql( $""" ALTER TABLE api_tokens ALTER COLUMN created_by_ip TYPE character varying(40) USING CAST(created_by_ip AS character varying(40)); """ ); + + migrationBuilder.RenameColumn( + name: "email_activated", + table: "users", + newName: "email_actived"); } } } From 4e5363d510f42ee2c13be0a4734c0d3188200b1b Mon Sep 17 00:00:00 2001 From: hhvrc Date: Mon, 25 Nov 2024 17:46:05 +0100 Subject: [PATCH 09/22] Create pgcrypto extension on migrate --- Common/Migrations/20241123181710_Hash API tokens.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Common/Migrations/20241123181710_Hash API tokens.cs b/Common/Migrations/20241123181710_Hash API tokens.cs index 559395b9..a688c33d 100644 --- a/Common/Migrations/20241123181710_Hash API tokens.cs +++ b/Common/Migrations/20241123181710_Hash API tokens.cs @@ -22,7 +22,8 @@ protected override void Up(MigrationBuilder migrationBuilder) migrationBuilder.Sql( $""" - UPDATE api_tokens SET token_hash = encode(digest(token_hash, 'sha256'), 'hex') + CREATE EXTENSION IF NOT EXISTS pgcrypto; + UPDATE api_tokens SET token_hash = encode(digest(token_hash, 'sha256'), 'hex'); """ ); } From 34cf939776bf74f56710237d49dc71c567336ca3 Mon Sep 17 00:00:00 2001 From: HeavenVR Date: Wed, 27 Nov 2024 01:26:35 +0100 Subject: [PATCH 10/22] Use modern startup format (#137) * Remove global static logging singleton * Use modern startup files * Use common helper for configuring webapplicationbuilder * Only use Serilog base logger during startup * Use proper serilog registration call * Remove redundant configuration source adds * Create common application builder * Clean up launch settings * Specifically only add back whats needed for https in debug mode --- API/API.csproj | 1 - API/Program.cs | 152 ++++++++---- API/Properties/launchSettings.json | 12 +- API/Startup.cs | 216 ------------------ Common/ApplicationLogging.cs | 11 - Common/Common.csproj | 13 +- Common/ExceptionHandle/ExceptionHandler.cs | 15 +- Common/Extensions/ConfigurationExtensions.cs | 45 ++++ Common/Extensions/SwaggerGenExtensions.cs | 74 ++++++ Common/OpenShockApplication.cs | 42 ++++ Common/Utils/LucTask.cs | 14 +- Cron/Program.cs | 85 +++---- Cron/Properties/launchSettings.json | 1 - Cron/Startup.cs | 53 ----- .../LifetimeManager/HubLifetime.cs | 16 +- .../LifetimeManager/HubLifetimeManager.cs | 15 +- LiveControlGateway/Program.cs | 92 ++++---- .../Properties/launchSettings.json | 8 +- LiveControlGateway/Startup.cs | 141 ------------ 19 files changed, 397 insertions(+), 609 deletions(-) delete mode 100644 API/Startup.cs delete mode 100644 Common/ApplicationLogging.cs create mode 100644 Common/Extensions/ConfigurationExtensions.cs create mode 100644 Common/Extensions/SwaggerGenExtensions.cs create mode 100644 Common/OpenShockApplication.cs delete mode 100644 Cron/Startup.cs delete mode 100644 LiveControlGateway/Startup.cs diff --git a/API/API.csproj b/API/API.csproj index 5e91d68a..57b7be35 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -36,7 +36,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/API/Program.cs b/API/Program.cs index bfcfa02b..1ed81d91 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -1,49 +1,123 @@ +using Microsoft.AspNetCore.Http.Connections; +using Microsoft.EntityFrameworkCore; using OpenShock.API; +using OpenShock.API.Realtime; +using OpenShock.API.Services; +using OpenShock.API.Services.Account; +using OpenShock.API.Services.Email.Mailjet; +using OpenShock.API.Services.Email.Smtp; +using OpenShock.Common; +using OpenShock.Common.Extensions; +using OpenShock.Common.Hubs; +using OpenShock.Common.JsonSerialization; +using OpenShock.Common.OpenShockDb; +using OpenShock.Common.Services.Device; +using OpenShock.Common.Services.LCGNodeProvisioner; +using OpenShock.Common.Services.Ota; +using OpenShock.Common.Services.Turnstile; +using OpenShock.Common.Utils; +using Scalar.AspNetCore; using Serilog; -HostBuilder builder = new(); -builder.UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureHostConfiguration(config => - { - config.AddEnvironmentVariables(prefix: "DOTNET_"); - if (args is { Length: > 0 }) config.AddCommandLine(args); - }) - .ConfigureAppConfiguration((context, config) => - { - config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false) - .AddJsonFile("appsettings.Custom.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json", optional: true, - reloadOnChange: false); - - config.AddUserSecrets(typeof(Program).Assembly); - config.AddEnvironmentVariables(); - if (args is { Length: > 0 }) config.AddCommandLine(args); - }) - .UseDefaultServiceProvider((context, options) => +var builder = OpenShockApplication.CreateDefaultBuilder(args, options => +{ + options.ListenAnyIP(80); +#if DEBUG + options.ListenAnyIP(443, options => options.UseHttps()); +#endif +}); + +var config = builder.GetAndRegisterOpenShockConfig(); +var commonServices = builder.Services.AddOpenShockServices(config); + +builder.Services.AddSignalR() + .AddOpenShockStackExchangeRedis(options => { options.Configuration = commonServices.RedisConfig; }) + .AddJsonProtocol(options => { - var isDevelopment = context.HostingEnvironment.IsDevelopment(); - options.ValidateScopes = isDevelopment; - options.ValidateOnBuild = isDevelopment; - }) - .UseSerilog((context, _, config) => { config.ReadFrom.Configuration(context.Configuration); }) - .ConfigureWebHostDefaults(webBuilder => + options.PayloadSerializerOptions.PropertyNameCaseInsensitive = true; + options.PayloadSerializerOptions.Converters.Add(new SemVersionJsonConverter()); + }); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddSwaggerExt("OpenShock.API"); + +builder.Services.AddSingleton(); + +builder.Services.AddSingleton(x => +{ + return new CloudflareTurnstileOptions { - webBuilder.UseKestrel(); - webBuilder.ConfigureKestrel(serverOptions => + SecretKey = config.Turnstile.SecretKey ?? string.Empty, + SiteKey = config.Turnstile.SiteKey ?? string.Empty + }; +}); +builder.Services.AddHttpClient(); + +// ----------------- MAIL SETUP ----------------- +var emailConfig = config.Mail; +switch (emailConfig.Type) +{ + case ApiConfig.MailConfig.MailType.Mailjet: + if (emailConfig.Mailjet == null) + throw new Exception("Mailjet config is null but mailjet is selected as mail type"); + builder.Services.AddMailjetEmailService(emailConfig.Mailjet, emailConfig.Sender); + break; + case ApiConfig.MailConfig.MailType.Smtp: + if (emailConfig.Smtp == null) + throw new Exception("SMTP config is null but SMTP is selected as mail type"); + builder.Services.AddSmtpEmailService(emailConfig.Smtp, emailConfig.Sender, new SmtpServiceTemplates { - serverOptions.ListenAnyIP(80); -#if DEBUG - serverOptions.ListenAnyIP(443, options => { options.UseHttps(); }); -#endif - serverOptions.Limits.RequestHeadersTimeout = TimeSpan.FromMilliseconds(3000); + PasswordReset = SmtpTemplate.ParseFromFileThrow("SmtpTemplates/PasswordReset.liquid").Result, + EmailVerification = SmtpTemplate.ParseFromFileThrow("SmtpTemplates/EmailVerification.liquid").Result }); - webBuilder.UseStartup(); - }); -try + break; + default: + throw new Exception("Unknown mail type"); +} + +builder.Services.ConfigureOptions(); +//services.AddHealthChecks().AddCheck("database"); + +builder.Services.AddHostedService(); + +var app = builder.Build(); + +app.UseCommonOpenShockMiddleware(); + +if (!config.Db.SkipMigration) { - await builder.Build().RunAsync(); + Log.Information("Running database migrations..."); + using var scope = app.Services.CreateScope(); + var openShockContext = scope.ServiceProvider.GetRequiredService(); + var pendingMigrations = openShockContext.Database.GetPendingMigrations().ToList(); + + if (pendingMigrations.Count > 0) + { + Log.Information("Found pending migrations, applying [{@Migrations}]", pendingMigrations); + openShockContext.Database.Migrate(); + Log.Information("Applied database migrations... proceeding with startup"); + } + else + { + Log.Information("No pending migrations found, proceeding with startup"); + } } -catch (Exception e) +else { - Console.WriteLine(e); -} \ No newline at end of file + Log.Warning("Skipping possible database migrations..."); +} + +app.UseSwaggerExt(); + +app.MapControllers(); + +app.MapHub("/1/hubs/user", options => options.Transports = HttpTransportType.WebSockets); +app.MapHub("/1/hubs/share/link/{id:guid}", options => options.Transports = HttpTransportType.WebSockets); + +app.MapScalarApiReference(options => options.OpenApiRoutePattern = "/swagger/{documentName}/swagger.json"); + +app.Run(); diff --git a/API/Properties/launchSettings.json b/API/Properties/launchSettings.json index c027337d..abfc1566 100644 --- a/API/Properties/launchSettings.json +++ b/API/Properties/launchSettings.json @@ -1,18 +1,10 @@ { "profiles": { - "ShockLink": { + "API": { "commandName": "Project", "dotnetRunMessages": true, "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DB": "Host=docker-node;Port=1337;Database=root;Username=root;Password=root;Search Path=ShockLink", - "REDIS_HOST":"docker-node", - "REDIS_PASSWORD": "", - "CF_ACC_ID": "", - "CF_IMG_KEY": "", - "CF_IMG_URL": "", - "MAILJET_KEY": "", - "MAILJET_SECRET": "" + "ASPNETCORE_ENVIRONMENT": "Development" } } } diff --git a/API/Startup.cs b/API/Startup.cs deleted file mode 100644 index ce6df6d8..00000000 --- a/API/Startup.cs +++ /dev/null @@ -1,216 +0,0 @@ -using System.Text; -using Asp.Versioning.ApiExplorer; -using Microsoft.AspNetCore.Http.Connections; -using Microsoft.EntityFrameworkCore; -using Microsoft.OpenApi.Models; -using OpenShock.API.Realtime; -using OpenShock.API.Services; -using OpenShock.API.Services.Account; -using OpenShock.API.Services.Email.Mailjet; -using OpenShock.API.Services.Email.Smtp; -using OpenShock.Common; -using OpenShock.Common.Constants; -using OpenShock.Common.DataAnnotations; -using OpenShock.Common.Hubs; -using OpenShock.Common.JsonSerialization; -using OpenShock.Common.Models; -using OpenShock.Common.OpenShockDb; -using OpenShock.Common.Redis; -using OpenShock.Common.Services.Device; -using OpenShock.Common.Services.LCGNodeProvisioner; -using OpenShock.Common.Services.Ota; -using OpenShock.Common.Services.Session; -using OpenShock.Common.Services.Turnstile; -using OpenShock.Common.Utils; -using Redis.OM; -using Redis.OM.Contracts; -using Scalar.AspNetCore; -using Semver; -using Serilog; - -namespace OpenShock.API; - -public sealed class Startup -{ - - private readonly ApiConfig _apiConfig; - - public Startup(IConfiguration configuration) - { - _apiConfig = configuration.GetChildren() - .FirstOrDefault(x => x.Key.Equals("openshock", StringComparison.InvariantCultureIgnoreCase))? - .Get() ?? - throw new Exception("Couldn't bind config, check config file"); - - var startupLogger = new LoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger(); - - MiniValidation.MiniValidator.TryValidate(_apiConfig, true, true, out var errors); - if (errors.Count > 0) - { - var sb = new StringBuilder(); - - foreach (var error in errors) - { - sb.AppendLine($"Error on field [{error.Key}] reason: {string.Join(", ", error.Value)}"); - } - - startupLogger.Error( - "Error validating config, please fix your configuration / environment variables\nFound the following errors:\n{Errors}", - sb.ToString()); - Environment.Exit(-10); - } - } - - public void ConfigureServices(IServiceCollection services) - { - services.AddSingleton(_apiConfig); - - var commonServices = services.AddOpenShockServices(_apiConfig); - - services.AddSingleton(); - - services.AddSingleton(x => - { - var config = x.GetRequiredService(); - return new CloudflareTurnstileOptions - { - SecretKey = config.Turnstile.SecretKey ?? string.Empty, - SiteKey = config.Turnstile.SiteKey ?? string.Empty - }; - }); - services.AddHttpClient(); - - // ----------------- MAIL SETUP ----------------- - var emailConfig = _apiConfig.Mail; - switch (emailConfig.Type) - { - case ApiConfig.MailConfig.MailType.Mailjet: - if (emailConfig.Mailjet == null) - throw new Exception("Mailjet config is null but mailjet is selected as mail type"); - services.AddMailjetEmailService(emailConfig.Mailjet, emailConfig.Sender); - break; - case ApiConfig.MailConfig.MailType.Smtp: - if (emailConfig.Smtp == null) - throw new Exception("SMTP config is null but SMTP is selected as mail type"); - services.AddSmtpEmailService(emailConfig.Smtp, emailConfig.Sender, new SmtpServiceTemplates - { - PasswordReset = SmtpTemplate.ParseFromFileThrow("SmtpTemplates/PasswordReset.liquid").Result, - EmailVerification = SmtpTemplate.ParseFromFileThrow("SmtpTemplates/EmailVerification.liquid").Result - }); - break; - default: - throw new Exception("Unknown mail type"); - } - - services.AddSignalR() - .AddOpenShockStackExchangeRedis(options => { options.Configuration = commonServices.RedisConfig; }) - .AddJsonProtocol(options => - { - options.PayloadSerializerOptions.PropertyNameCaseInsensitive = true; - options.PayloadSerializerOptions.Converters.Add(new SemVersionJsonConverter()); - }); - - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - services.AddSwaggerGen(options => - { - options.CustomOperationIds(e => - $"{e.ActionDescriptor.RouteValues["controller"]}_{e.ActionDescriptor.AttributeRouteInfo?.Name ?? e.ActionDescriptor.RouteValues["action"]}"); - options.SchemaFilter(); - options.ParameterFilter(); - options.OperationFilter(); - options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "OpenShock.API.xml"), true); - options.AddSecurityDefinition(AuthConstants.AuthTokenHeaderName, new OpenApiSecurityScheme - { - Name = AuthConstants.AuthTokenHeaderName, - Type = SecuritySchemeType.ApiKey, - Scheme = "ApiKeyAuth", - In = ParameterLocation.Header, - Description = "API Token Authorization header." - }); - options.AddSecurityRequirement(new OpenApiSecurityRequirement - { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = AuthConstants.AuthTokenHeaderName - } - }, - Array.Empty() - } - }); - options.AddServer(new OpenApiServer { Url = "https://api.openshock.app" }); - options.AddServer(new OpenApiServer { Url = "https://staging-api.openshock.app" }); -#if DEBUG - options.AddServer(new OpenApiServer { Url = "https://localhost" }); -#endif - options.SwaggerDoc("v1", new OpenApiInfo { Title = "OpenShock", Version = "1" }); - options.SwaggerDoc("v2", new OpenApiInfo { Title = "OpenShock", Version = "2" }); - options.MapType(() => OpenApiSchemas.SemVerSchema); - options.MapType(() => OpenApiSchemas.PauseReasonEnumSchema); - - // Avoid nullable strings everywhere - options.SupportNonNullableReferenceTypes(); - } - ); - - services.ConfigureOptions(); - //services.AddHealthChecks().AddCheck("database"); - - services.AddHostedService(); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory, - ILogger logger) - { - ApplicationLogging.LoggerFactory = loggerFactory; - - app.UseCommonOpenShockMiddleware(); - - if (!_apiConfig.Db.SkipMigration) - { - logger.LogInformation("Running database migrations..."); - using var scope = app.ApplicationServices.CreateScope(); - var openShockContext = scope.ServiceProvider.GetRequiredService(); - var pendingMigrations = openShockContext.Database.GetPendingMigrations().ToList(); - - if (pendingMigrations.Count > 0) - { - logger.LogInformation("Found pending migrations, applying [{@Migrations}]", pendingMigrations); - openShockContext.Database.Migrate(); - logger.LogInformation("Applied database migrations... proceeding with startup"); - } - else logger.LogInformation("No pending migrations found, proceeding with startup"); - } - else logger.LogWarning("Skipping possible database migrations..."); - - app.UseSwagger(); - var provider = app.ApplicationServices.GetRequiredService(); - app.UseSwaggerUI(c => - { - foreach (var description in provider.ApiVersionDescriptions) - c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", - description.GroupName.ToUpperInvariant()); - }); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - endpoints.MapHub("/1/hubs/user", - options => { options.Transports = HttpTransportType.WebSockets; }); - endpoints.MapHub("/1/hubs/share/link/{id}", - options => { options.Transports = HttpTransportType.WebSockets; }); - - endpoints.MapScalarApiReference(options => - { - options.OpenApiRoutePattern = "/swagger/{documentName}/swagger.json"; - }); - }); - } -} \ No newline at end of file diff --git a/Common/ApplicationLogging.cs b/Common/ApplicationLogging.cs deleted file mode 100644 index 2fd5be32..00000000 --- a/Common/ApplicationLogging.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace OpenShock.Common; - -public static class ApplicationLogging -{ - public static ILoggerFactory LoggerFactory { get; set; } = null!; - public static ILogger CreateLogger() => LoggerFactory.CreateLogger(); - public static ILogger CreateLogger(Type type) => LoggerFactory.CreateLogger(type); - public static ILogger CreateLogger(string categoryName) => LoggerFactory.CreateLogger(categoryName); -} \ No newline at end of file diff --git a/Common/Common.csproj b/Common/Common.csproj index 6cefdf4c..1c1a5822 100644 --- a/Common/Common.csproj +++ b/Common/Common.csproj @@ -14,7 +14,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -29,7 +29,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + + @@ -40,12 +41,12 @@ - + - + - - + + diff --git a/Common/ExceptionHandle/ExceptionHandler.cs b/Common/ExceptionHandle/ExceptionHandler.cs index bb6679d9..6c7b57c6 100644 --- a/Common/ExceptionHandle/ExceptionHandler.cs +++ b/Common/ExceptionHandle/ExceptionHandler.cs @@ -1,21 +1,18 @@ using System.Net; using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.Http.Json; -using Microsoft.Extensions.Options; using OpenShock.Common.Errors; namespace OpenShock.Common.ExceptionHandle; public sealed class OpenShockExceptionHandler : IExceptionHandler { - private static readonly ILogger Logger = ApplicationLogging.CreateLogger(typeof(OpenShockExceptionHandler)); - private static readonly ILogger LoggerRequestInfo = ApplicationLogging.CreateLogger("RequestInfo"); - private readonly IProblemDetailsService _problemDetailsService; - - public OpenShockExceptionHandler(IProblemDetailsService problemDetailsService) + private readonly ILogger _logger; + + public OpenShockExceptionHandler(IProblemDetailsService problemDetailsService, ILoggerFactory loggerFactory) { _problemDetailsService = problemDetailsService; + _logger = loggerFactory.CreateLogger("RequestInfo"); } public async ValueTask TryHandleAsync(HttpContext context, Exception exception, CancellationToken cancellationToken) @@ -34,7 +31,7 @@ public async ValueTask TryHandleAsync(HttpContext context, Exception excep }); } - private static async Task PrintRequestInfo(HttpContext context) + private async Task PrintRequestInfo(HttpContext context) { // Rewind our body reader, so we can read it again. context.Request.Body.Seek(0, SeekOrigin.Begin); @@ -57,6 +54,6 @@ private static async Task PrintRequestInfo(HttpContext context) }; // Finally log this object on Information level. - LoggerRequestInfo.LogInformation("{@RequestInfo}", requestInfo); + _logger.LogInformation("{@RequestInfo}", requestInfo); } } \ No newline at end of file diff --git a/Common/Extensions/ConfigurationExtensions.cs b/Common/Extensions/ConfigurationExtensions.cs new file mode 100644 index 00000000..90509578 --- /dev/null +++ b/Common/Extensions/ConfigurationExtensions.cs @@ -0,0 +1,45 @@ +using OpenShock.Common.Config; +using Serilog; +using System.Text; +using System.Text.Json; + +namespace OpenShock.Common.Extensions; + +public static class ConfigurationExtensions +{ + public static T GetAndRegisterOpenShockConfig(this WebApplicationBuilder builder) where T : BaseConfig + { +#if DEBUG + Console.WriteLine(builder.Configuration.GetDebugView()); +#endif + + var config = builder.Configuration + .GetChildren() + .First(x => x.Key.Equals("openshock", StringComparison.InvariantCultureIgnoreCase)) + .Get() ?? throw new Exception("Couldn't bind config, check config file"); + + MiniValidation.MiniValidator.TryValidate(config, true, true, out var errors); + if (errors.Count > 0) + { + var sb = new StringBuilder(); + + sb.AppendLine("Error validating config, please fix your configuration / environment variables"); + sb.AppendLine("Found the following errors:"); + foreach (var error in errors) + { + sb.AppendLine($"Error on field [{error.Key}] reason: {string.Join(", ", error.Value)}"); + } + + Log.Error(sb.ToString()); + Environment.Exit(-10); + } + +#if DEBUG + Console.WriteLine(JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true })); +#endif + + builder.Services.AddSingleton(config); + + return config; + } +} diff --git a/Common/Extensions/SwaggerGenExtensions.cs b/Common/Extensions/SwaggerGenExtensions.cs new file mode 100644 index 00000000..5f0c3d21 --- /dev/null +++ b/Common/Extensions/SwaggerGenExtensions.cs @@ -0,0 +1,74 @@ +using Microsoft.OpenApi.Models; +using OpenShock.Common.Constants; +using OpenShock.Common.DataAnnotations; +using OpenShock.Common.Models; +using Semver; +using Asp.Versioning.ApiExplorer; + +namespace OpenShock.Common.Extensions; + +public static class SwaggerGenExtensions +{ + public static IServiceCollection AddSwaggerExt(this IServiceCollection services, string assemblyName) + { + return services.AddSwaggerGen(options => + { + options.CustomOperationIds(e => + $"{e.ActionDescriptor.RouteValues["controller"]}_{e.ActionDescriptor.AttributeRouteInfo?.Name ?? e.ActionDescriptor.RouteValues["action"]}"); + options.SchemaFilter(); + options.ParameterFilter(); + options.OperationFilter(); + options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, assemblyName + ".xml"), true); + options.AddSecurityDefinition(AuthConstants.AuthTokenHeaderName, new OpenApiSecurityScheme + { + Name = AuthConstants.AuthTokenHeaderName, + Type = SecuritySchemeType.ApiKey, + Scheme = "ApiKeyAuth", + In = ParameterLocation.Header, + Description = "API Token Authorization header." + }); + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = AuthConstants.AuthTokenHeaderName + } + }, + Array.Empty() + } + }); + options.AddServer(new OpenApiServer { Url = "https://api.openshock.app" }); + options.AddServer(new OpenApiServer { Url = "https://staging-api.openshock.app" }); +#if DEBUG + options.AddServer(new OpenApiServer { Url = "https://localhost" }); +#endif + options.SwaggerDoc("v1", new OpenApiInfo { Title = "OpenShock", Version = "1" }); + options.SwaggerDoc("v2", new OpenApiInfo { Title = "OpenShock", Version = "2" }); + options.MapType(() => OpenApiSchemas.SemVerSchema); + options.MapType(() => OpenApiSchemas.PauseReasonEnumSchema); + + // Avoid nullable strings everywhere + options.SupportNonNullableReferenceTypes(); + }); + } + + public static IApplicationBuilder UseSwaggerExt(this WebApplication app) + { + var provider = app.Services.GetRequiredService(); + var groupNames = provider.ApiVersionDescriptions.Select(d => d.GroupName).ToList(); + + return app + .UseSwagger() + .UseSwaggerUI(c => + { + foreach (var groupName in groupNames) + { + c.SwaggerEndpoint($"/swagger/{groupName}/swagger.json", groupName.ToUpperInvariant()); + } + }); + } +} diff --git a/Common/OpenShockApplication.cs b/Common/OpenShockApplication.cs new file mode 100644 index 00000000..523fc940 --- /dev/null +++ b/Common/OpenShockApplication.cs @@ -0,0 +1,42 @@ +using System.Reflection; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Serilog; + +namespace OpenShock.Common; + +public static class OpenShockApplication +{ + public static WebApplicationBuilder CreateDefaultBuilder(string[] args, Action configurePorts) where TProgram : class + { + var builder = WebApplication.CreateSlimBuilder(args); + + builder.Configuration.Sources.Clear(); + builder.Configuration + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) + .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: false) + .AddJsonFile("appsettings.Custom.json", optional: true, reloadOnChange: false) + .AddEnvironmentVariables() + .AddUserSecrets(true) + .AddCommandLine(args); + + var isDevelopment = builder.Environment.IsDevelopment(); + builder.Host.UseDefaultServiceProvider((_, options) => + { + options.ValidateScopes = isDevelopment; + options.ValidateOnBuild = isDevelopment; + }); + + // Since we use slim builders, this allows for HTTPS during local development + if (isDevelopment) builder.WebHost.UseKestrelHttpsConfiguration(); + + builder.WebHost.ConfigureKestrel(serverOptions => + { + configurePorts(serverOptions); + serverOptions.Limits.RequestHeadersTimeout = TimeSpan.FromMilliseconds(3000); + }); + + builder.Host.UseSerilog((context, _, config) => config.ReadFrom.Configuration(context.Configuration)); + + return builder; + } +} diff --git a/Common/Utils/LucTask.cs b/Common/Utils/LucTask.cs index 44cec6f2..02a93019 100644 --- a/Common/Utils/LucTask.cs +++ b/Common/Utils/LucTask.cs @@ -1,12 +1,10 @@ -using System.Runtime.CompilerServices; -using Microsoft.Extensions.Logging; +using Serilog; +using System.Runtime.CompilerServices; namespace OpenShock.Common.Utils; public static class LucTask { - private static readonly ILogger Logger = ApplicationLogging.CreateLogger(typeof(LucTask)); - public static Task Run(Func function, [CallerFilePath] string file = "", [CallerMemberName] string member = "", [CallerLineNumber] int line = -1) => Task.Run(function).ContinueWith( t => @@ -14,9 +12,9 @@ public static Task Run(Func function, [CallerFilePath] string file = "", if (!t.IsFaulted) return; var index = file.LastIndexOf('\\'); if (index == -1) index = file.LastIndexOf('/'); - Logger.LogError(t.Exception, + Log.Error(t.Exception, "Error during task execution. {File}::{Member}:{Line} - Stack: {Stack}", - file.Substring(index + 1, file.Length - index - 1), member, line, t.Exception?.StackTrace); + file[(index + 1)..], member, line, t.Exception?.StackTrace); }, TaskContinuationOptions.OnlyOnFaulted); @@ -27,9 +25,9 @@ public static Task Run(Task? function, [CallerFilePath] string file = "", if (!t.IsFaulted) return; var index = file.LastIndexOf('\\'); if (index == -1) index = file.LastIndexOf('/'); - Logger.LogError(t.Exception, + Log.Error(t.Exception, "Error during task execution. {File}::{Member}:{Line} - Stack: {Stack}", - file.Substring(index + 1, file.Length - index - 1), member, line, t.Exception?.StackTrace); + file[(index + 1)..], member, line, t.Exception?.StackTrace); }, TaskContinuationOptions.OnlyOnFaulted); } \ No newline at end of file diff --git a/Cron/Program.cs b/Cron/Program.cs index 1dfe7397..a353f2e9 100644 --- a/Cron/Program.cs +++ b/Cron/Program.cs @@ -1,60 +1,43 @@ using Hangfire; +using Hangfire.PostgreSql; +using OpenShock.Common; +using OpenShock.Common.Extensions; using OpenShock.Cron; -using OpenShock.Cron.Jobs; using OpenShock.Cron.Utils; -using Serilog; - -HostBuilder builder = new(); -builder.UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureHostConfiguration(config => - { - config.AddEnvironmentVariables(prefix: "DOTNET_"); - if (args is { Length: > 0 }) config.AddCommandLine(args); - }) - .ConfigureAppConfiguration((context, config) => - { - config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false) - .AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json", optional: true, - reloadOnChange: false); - - config.AddUserSecrets(typeof(Program).Assembly); - config.AddEnvironmentVariables(); - if (args is { Length: > 0 }) config.AddCommandLine(args); - }) - .UseDefaultServiceProvider((context, options) => - { - var isDevelopment = context.HostingEnvironment.IsDevelopment(); - options.ValidateScopes = isDevelopment; - options.ValidateOnBuild = isDevelopment; - }) - .UseSerilog((context, _, config) => { config.ReadFrom.Configuration(context.Configuration); }) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseKestrel(); - webBuilder.ConfigureKestrel(serverOptions => - { - serverOptions.ListenAnyIP(780); + +var builder = OpenShockApplication.CreateDefaultBuilder(args, options => +{ + options.ListenAnyIP(780); #if DEBUG - serverOptions.ListenAnyIP(7443, options => { options.UseHttps("devcert.pfx"); }); + options.ListenAnyIP(7443, options => options.UseHttps("devcert.pfx")); #endif - serverOptions.Limits.RequestHeadersTimeout = TimeSpan.FromMilliseconds(3000); - }); - webBuilder.UseStartup(); - }); -try -{ - var app = builder.Build(); +}); - var jobManagerV2 = app.Services.GetRequiredService(); - foreach (var cronJob in CronJobCollector.GetAllCronJobs()) - { - jobManagerV2.AddOrUpdate(cronJob.Name, cronJob.Job, cronJob.Schedule); - } +var config = builder.GetAndRegisterOpenShockConfig(); +builder.Services.AddOpenShockServices(config); - await app.RunAsync(); +builder.Services.AddHangfire(hangfire => + hangfire.UsePostgreSqlStorage(c => + c.UseNpgsqlConnection(config.Db.Conn))); +builder.Services.AddHangfireServer(); -} -catch (Exception e) +var app = builder.Build(); + +app.UseCommonOpenShockMiddleware(); + +app.UseHangfireDashboard(options: new DashboardOptions { - Console.WriteLine(e); -} \ No newline at end of file + AsyncAuthorization = [ + new DashboardAdminAuth() + ] +}); + +app.MapControllers(); + +var jobManager = app.Services.GetRequiredService(); +foreach (var cronJob in CronJobCollector.GetAllCronJobs()) +{ + jobManager.AddOrUpdate(cronJob.Name, cronJob.Job, cronJob.Schedule); +} + +app.Run(); \ No newline at end of file diff --git a/Cron/Properties/launchSettings.json b/Cron/Properties/launchSettings.json index 19454ce9..0121b90c 100644 --- a/Cron/Properties/launchSettings.json +++ b/Cron/Properties/launchSettings.json @@ -1,5 +1,4 @@ { - "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "Cron": { "commandName": "Project", diff --git a/Cron/Startup.cs b/Cron/Startup.cs deleted file mode 100644 index 1a852840..00000000 --- a/Cron/Startup.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Hangfire; -using Hangfire.PostgreSql; -using OpenShock.Common; -using OpenTelemetry.Trace; - -namespace OpenShock.Cron; - -public sealed class Startup -{ - private CronConf _config; - - public Startup(IConfiguration configuration) - { -#if DEBUG - var root = (IConfigurationRoot)configuration; - var debugView = root.GetDebugView(); - Console.WriteLine(debugView); -#endif - _config = configuration.GetChildren() - .First(x => x.Key.Equals("openshock", StringComparison.InvariantCultureIgnoreCase)) - .Get() ?? - throw new Exception("Couldn't bind config, check config file"); - } - - public void ConfigureServices(IServiceCollection services) - { - services.AddHangfire(hangfire => - hangfire.UsePostgreSqlStorage(c => - c.UseNpgsqlConnection(_config.Db.Conn))); - services.AddHangfireServer(); - - - services.AddOpenShockServices(_config); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory, - ILogger logger) - { - app.UseCommonOpenShockMiddleware(); - - app.UseHangfireDashboard(options: new DashboardOptions - { - AsyncAuthorization = [ - new DashboardAdminAuth() - ] - }); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } -} \ No newline at end of file diff --git a/LiveControlGateway/LifetimeManager/HubLifetime.cs b/LiveControlGateway/LifetimeManager/HubLifetime.cs index bb42077a..1c50ca6c 100644 --- a/LiveControlGateway/LifetimeManager/HubLifetime.cs +++ b/LiveControlGateway/LifetimeManager/HubLifetime.cs @@ -28,7 +28,6 @@ public sealed class HubLifetime : IAsyncDisposable { private readonly TimeSpan _waitBetweenTicks; private readonly ushort _commandDuration; - private static readonly ILogger Logger = ApplicationLogging.CreateLogger(); private Dictionary _shockerStates = new(); private readonly byte _tps; @@ -39,19 +38,23 @@ public sealed class HubLifetime : IAsyncDisposable private readonly IRedisConnectionProvider _redisConnectionProvider; private readonly IRedisPubService _redisPubService; + private readonly ILogger _logger; + /// /// DI Constructor /// /// /// /// + /// /// + /// /// - /// public HubLifetime([Range(1, 10)] byte tps, IHubController hubController, IDbContextFactory dbContextFactory, IRedisConnectionProvider redisConnectionProvider, IRedisPubService redisPubService, + ILogger logger, CancellationToken cancellationToken = default) { _tps = tps; @@ -60,6 +63,7 @@ public HubLifetime([Range(1, 10)] byte tps, IHubController hubController, _dbContextFactory = dbContextFactory; _redisConnectionProvider = redisConnectionProvider; _redisPubService = redisPubService; + _logger = logger; _waitBetweenTicks = TimeSpan.FromMilliseconds(Math.Floor((float)1000 / tps)); _commandDuration = (ushort)(_waitBetweenTicks.TotalMilliseconds * 2.5); @@ -90,14 +94,14 @@ private async Task UpdateLoop() } catch (Exception e) { - Logger.LogError(e, "Error in Update()"); + _logger.LogError(e, "Error in Update()"); } var elapsed = stopwatch.Elapsed; var waitTime = _waitBetweenTicks - elapsed; if (waitTime.TotalMilliseconds < 1) { - Logger.LogWarning("Update loop running behind for device [{DeviceId}]", _hubController.Id); + _logger.LogWarning("Update loop running behind for device [{DeviceId}]", _hubController.Id); continue; } @@ -147,7 +151,7 @@ public async Task UpdateDevice() /// private async Task UpdateShockers(OpenShockContext db) { - Logger.LogDebug("Updating shockers for device [{DeviceId}]", _hubController.Id); + _logger.LogDebug("Updating shockers for device [{DeviceId}]", _hubController.Id); var ownShockers = await db.Shockers.Where(x => x.Device == _hubController.Id).Select(x => new ShockerState() { Id = x.Id, @@ -196,7 +200,7 @@ public ValueTask Control(IEnumerable shocks) { if (!_shockerStates.TryGetValue(shock.Id, out var state)) continue; - Logger.LogTrace( + _logger.LogTrace( "Control exclusive: {Exclusive}, type: {Type}, duration: {Duration}, intensity: {Intensity}", shock.Exclusive, shock.Type, shock.Duration, shock.Intensity); state.ExclusiveUntil = shock.Exclusive && shock.Type != ControlType.Stop diff --git a/LiveControlGateway/LifetimeManager/HubLifetimeManager.cs b/LiveControlGateway/LifetimeManager/HubLifetimeManager.cs index 1a462456..ee54b981 100644 --- a/LiveControlGateway/LifetimeManager/HubLifetimeManager.cs +++ b/LiveControlGateway/LifetimeManager/HubLifetimeManager.cs @@ -18,29 +18,33 @@ namespace OpenShock.LiveControlGateway.LifetimeManager; /// public sealed class HubLifetimeManager { - private readonly ILogger _logger; private readonly IDbContextFactory _dbContextFactory; private readonly IRedisConnectionProvider _redisConnectionProvider; private readonly IRedisPubService _redisPubService; + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; private readonly ConcurrentDictionary _managers = new(); /// /// DI constructor /// - /// /// /// /// + /// public HubLifetimeManager( - ILogger logger, IDbContextFactory dbContextFactory, IRedisConnectionProvider redisConnectionProvider, - IRedisPubService redisPubService) + IRedisPubService redisPubService, + ILoggerFactory loggerFactory + ) { - _logger = logger; _dbContextFactory = dbContextFactory; _redisConnectionProvider = redisConnectionProvider; _redisPubService = redisPubService; + _loggerFactory = loggerFactory; + + _logger = _loggerFactory.CreateLogger(); } /// @@ -65,6 +69,7 @@ public async Task AddDeviceConnection(byte tps, IHubController hubC _dbContextFactory, _redisConnectionProvider, _redisPubService, + _loggerFactory.CreateLogger(), cancellationToken); await using var db = await _dbContextFactory.CreateDbContextAsync(cancellationToken); diff --git a/LiveControlGateway/Program.cs b/LiveControlGateway/Program.cs index 1a6fdf73..6ddf3fb2 100644 --- a/LiveControlGateway/Program.cs +++ b/LiveControlGateway/Program.cs @@ -1,51 +1,53 @@ +using OpenShock.Common; +using OpenShock.Common.Extensions; +using OpenShock.Common.JsonSerialization; +using OpenShock.Common.Services.Device; +using OpenShock.Common.Services.Ota; +using OpenShock.Common.Utils; using OpenShock.LiveControlGateway; -using Serilog; - -HostBuilder builder = new(); -builder.UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureHostConfiguration(config => - { - config.AddEnvironmentVariables(prefix: "DOTNET_"); - if (args is { Length: > 0 }) config.AddCommandLine(args); - }) - .ConfigureAppConfiguration((context, config) => - { - config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false) - .AddJsonFile("appsettings.Custom.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json", optional: true, reloadOnChange: false); - - config.AddUserSecrets(typeof(Program).Assembly); - config.AddEnvironmentVariables(); - if (args is { Length: > 0 }) config.AddCommandLine(args); - }) - .UseDefaultServiceProvider((context, options) => - { - var isDevelopment = context.HostingEnvironment.IsDevelopment(); - options.ValidateScopes = isDevelopment; - options.ValidateOnBuild = isDevelopment; - }) - .UseSerilog((context, _, config) => { config.ReadFrom.Configuration(context.Configuration); }) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseKestrel(); - webBuilder.ConfigureKestrel(serverOptions => - { +using OpenShock.LiveControlGateway.LifetimeManager; +using OpenShock.LiveControlGateway.PubSub; +var builder = OpenShockApplication.CreateDefaultBuilder(args, options => +{ #if DEBUG - serverOptions.ListenAnyIP(580); - serverOptions.ListenAnyIP(5443, options => { options.UseHttps("devcert.pfx"); }); + options.ListenAnyIP(580); + options.ListenAnyIP(5443, options => options.UseHttps("devcert.pfx")); #else - serverOptions.ListenAnyIP(80); + options.ListenAnyIP(80); #endif - serverOptions.Limits.RequestHeadersTimeout = TimeSpan.FromMilliseconds(3000); - }); - webBuilder.UseStartup(); +}); + +var config = builder.GetAndRegisterOpenShockConfig(); +var commonService = builder.Services.AddOpenShockServices(config); + +builder.Services.AddSignalR() + .AddOpenShockStackExchangeRedis(options => { options.Configuration = commonService.RedisConfig; }) + .AddJsonProtocol(options => + { + options.PayloadSerializerOptions.PropertyNameCaseInsensitive = true; + options.PayloadSerializerOptions.Converters.Add(new SemVersionJsonConverter()); }); -try -{ - await builder.Build().RunAsync(); -} -catch (Exception e) -{ - Console.WriteLine(e); -} \ No newline at end of file + +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddSwaggerExt("OpenShock.LiveControlGateway"); + +builder.Services.ConfigureOptions(); +//services.AddHealthChecks().AddCheck("database"); + +builder.Services.AddHostedService(); +builder.Services.AddHostedService(); + +builder.Services.AddSingleton(); + +var app = builder.Build(); + +app.UseCommonOpenShockMiddleware(); + +app.UseSwaggerExt(); + +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/LiveControlGateway/Properties/launchSettings.json b/LiveControlGateway/Properties/launchSettings.json index 2b883619..0d428b5e 100644 --- a/LiveControlGateway/Properties/launchSettings.json +++ b/LiveControlGateway/Properties/launchSettings.json @@ -4,13 +4,7 @@ "commandName": "Project", "dotnetRunMessages": true, "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "OPENSHOCK__DB": "Host=docker-node;Port=1337;Database=openshock;Username=root;Password=root", - "OPENSHOCK__FQDN": "luc-lcg.lucheart.ovh", - "OPENSHOCK__COUNTRYCODE": "DE", - "OPENSHOCK__REDIS__HOST":"docker-node", - //"OPENSHOCK__REDIS__HOST":"localhost", - "OPENSHOCK__REDIS__PASSWORD": "" + "ASPNETCORE_ENVIRONMENT": "Development" } } } diff --git a/LiveControlGateway/Startup.cs b/LiveControlGateway/Startup.cs deleted file mode 100644 index 80c69aba..00000000 --- a/LiveControlGateway/Startup.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.Text.Json; -using Asp.Versioning.ApiExplorer; -using Microsoft.OpenApi.Models; -using OpenShock.Common; -using OpenShock.Common.Constants; -using OpenShock.Common.JsonSerialization; -using OpenShock.Common.Services.Device; -using OpenShock.Common.Services.Ota; -using OpenShock.Common.Utils; -using OpenShock.LiveControlGateway.LifetimeManager; -using OpenShock.LiveControlGateway.PubSub; -using JsonSerializer = System.Text.Json.JsonSerializer; - -namespace OpenShock.LiveControlGateway; - -/// -/// Startup class for the LCG -/// -public sealed class Startup -{ - private LCGConfig _lcgConfig; - - /// - /// Setup the LCG, configure config and validate - /// - /// - /// - public Startup(IConfiguration configuration) - { -#if DEBUG - var root = (IConfigurationRoot)configuration; - var debugView = root.GetDebugView(); - Console.WriteLine(debugView); -#endif - _lcgConfig = configuration.GetChildren().First(x => x.Key.Equals("openshock", StringComparison.InvariantCultureIgnoreCase)) - .Get() ?? - throw new Exception("Couldn't bind config, check config file"); - - var validator = new ValidationContext(_lcgConfig); - Validator.ValidateObject(_lcgConfig, validator, true); - -#if DEBUG - Console.WriteLine(JsonSerializer.Serialize(_lcgConfig, - new JsonSerializerOptions { WriteIndented = true })); -#endif - } - - /// - /// Configures the services for the LCG - /// - /// - public void ConfigureServices(IServiceCollection services) - { - services.AddSingleton(_lcgConfig); - - var commonService = services.AddOpenShockServices(_lcgConfig); - - services.AddSignalR() - .AddOpenShockStackExchangeRedis(options => { options.Configuration = commonService.RedisConfig; }) - .AddJsonProtocol(options => - { - options.PayloadSerializerOptions.PropertyNameCaseInsensitive = true; - options.PayloadSerializerOptions.Converters.Add(new SemVersionJsonConverter()); - }); - - services.AddScoped(); - services.AddScoped(); - - services.AddSwaggerGen(options => - { - options.CustomOperationIds(e => - $"{e.ActionDescriptor.RouteValues["controller"]}_{e.ActionDescriptor.RouteValues["action"]}_{e.HttpMethod}"); - options.SchemaFilter(); - options.ParameterFilter(); - options.OperationFilter(); - options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "OpenShock.LiveControlGateway.xml")); - options.AddSecurityDefinition("OpenShockToken", new OpenApiSecurityScheme - { - Name = "OpenShockToken", - Type = SecuritySchemeType.ApiKey, - Scheme = "ApiKeyAuth", - In = ParameterLocation.Header, - Description = "API Token Authorization header." - }); - options.AddSecurityRequirement(new OpenApiSecurityRequirement - { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = AuthConstants.AuthTokenHeaderName - } - }, - Array.Empty() - } - }); - options.AddServer(new OpenApiServer { Url = "https://api.openshock.app" }); - options.AddServer(new OpenApiServer { Url = "https://staging-api.openshock.app" }); - options.AddServer(new OpenApiServer { Url = "https://localhost" }); - } - ); - - services.ConfigureOptions(); - //services.AddHealthChecks().AddCheck("database"); - - services.AddHostedService(); - services.AddHostedService(); - - services.AddSingleton(); - - } - - /// - /// Register middleware and co. - /// - /// - /// - /// - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) - { - ApplicationLogging.LoggerFactory = loggerFactory; - app.UseCommonOpenShockMiddleware(); - - app.UseSwagger(); - var provider = app.ApplicationServices.GetRequiredService(); - app.UseSwaggerUI(c => - { - foreach (var description in provider.ApiVersionDescriptions) - c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", - description.GroupName.ToUpperInvariant()); - }); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } -} \ No newline at end of file From 34652c455a9f45e5469d0ed05b5e1a0d5d8e620a Mon Sep 17 00:00:00 2001 From: hhvrc Date: Thu, 28 Nov 2024 10:57:35 +0100 Subject: [PATCH 11/22] Automatically resolve api versions --- API/Program.cs | 4 +- Common/Extensions/AssemblyExtensions.cs | 19 ++++ Common/Extensions/SwaggerGenExtensions.cs | 102 ++++++++++++++-------- LiveControlGateway/Program.cs | 4 +- 4 files changed, 85 insertions(+), 44 deletions(-) create mode 100644 Common/Extensions/AssemblyExtensions.cs diff --git a/API/Program.cs b/API/Program.cs index 1ed81d91..b24c5bc4 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -15,7 +15,6 @@ using OpenShock.Common.Services.LCGNodeProvisioner; using OpenShock.Common.Services.Ota; using OpenShock.Common.Services.Turnstile; -using OpenShock.Common.Utils; using Scalar.AspNetCore; using Serilog; @@ -43,7 +42,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddSwaggerExt("OpenShock.API"); +builder.Services.AddSwaggerExt(); builder.Services.AddSingleton(); @@ -79,7 +78,6 @@ throw new Exception("Unknown mail type"); } -builder.Services.ConfigureOptions(); //services.AddHealthChecks().AddCheck("database"); builder.Services.AddHostedService(); diff --git a/Common/Extensions/AssemblyExtensions.cs b/Common/Extensions/AssemblyExtensions.cs new file mode 100644 index 00000000..3fb01cb4 --- /dev/null +++ b/Common/Extensions/AssemblyExtensions.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc; +using System.Reflection; + +namespace OpenShock.Common.Extensions; + +public static class AssemblyExtensions +{ + public static IEnumerable GetAllControllers(this Assembly assembly) + { + return assembly + .GetTypes() + .Where(type => type.IsClass && typeof(ControllerBase).IsAssignableFrom(type)); + } + + public static IEnumerable GetAllControllerEndpointAttributes(this Assembly assembly) where TAttribute : Attribute + { + return GetAllControllers(assembly).SelectMany(type => type.GetCustomAttributes(true).Concat(type.GetMethods(BindingFlags.Instance).SelectMany(m => m.GetCustomAttributes()))); + } +} diff --git a/Common/Extensions/SwaggerGenExtensions.cs b/Common/Extensions/SwaggerGenExtensions.cs index 5f0c3d21..ab572c0c 100644 --- a/Common/Extensions/SwaggerGenExtensions.cs +++ b/Common/Extensions/SwaggerGenExtensions.cs @@ -4,56 +4,82 @@ using OpenShock.Common.Models; using Semver; using Asp.Versioning.ApiExplorer; +using OpenShock.Common.Utils; +using Asp.Versioning; +using Microsoft.AspNetCore.Mvc; +using System.Reflection; +using System.Linq; namespace OpenShock.Common.Extensions; public static class SwaggerGenExtensions { - public static IServiceCollection AddSwaggerExt(this IServiceCollection services, string assemblyName) + public static IServiceCollection AddSwaggerExt(this IServiceCollection services) where TProgram : class { - return services.AddSwaggerGen(options => + var assembly = typeof(TProgram).Assembly; + + string assemblyName = assembly + .GetName() + .Name ?? throw new NullReferenceException("Assembly name"); + + var versions = assembly.GetAllControllerEndpointAttributes() + .SelectMany(type => type.Versions) + .Select(v => v.ToString()) + .ToHashSet() + .OrderBy(v => v) + .ToList(); + + if (versions.Any(v => !int.TryParse(v, out _))) { - options.CustomOperationIds(e => - $"{e.ActionDescriptor.RouteValues["controller"]}_{e.ActionDescriptor.AttributeRouteInfo?.Name ?? e.ActionDescriptor.RouteValues["action"]}"); - options.SchemaFilter(); - options.ParameterFilter(); - options.OperationFilter(); - options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, assemblyName + ".xml"), true); - options.AddSecurityDefinition(AuthConstants.AuthTokenHeaderName, new OpenApiSecurityScheme - { - Name = AuthConstants.AuthTokenHeaderName, - Type = SecuritySchemeType.ApiKey, - Scheme = "ApiKeyAuth", - In = ParameterLocation.Header, - Description = "API Token Authorization header." - }); - options.AddSecurityRequirement(new OpenApiSecurityRequirement + throw new InvalidDataException($"Found invalid API versions: [{string.Join(", ", versions.Where(v => !int.TryParse(v, out _)))}]"); + } + + return services + .AddSwaggerGen(options => { + options.CustomOperationIds(e => + $"{e.ActionDescriptor.RouteValues["controller"]}_{e.ActionDescriptor.AttributeRouteInfo?.Name ?? e.ActionDescriptor.RouteValues["action"]}"); + options.SchemaFilter(); + options.ParameterFilter(); + options.OperationFilter(); + options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, assemblyName + ".xml"), true); + options.AddSecurityDefinition(AuthConstants.AuthTokenHeaderName, new OpenApiSecurityScheme { + Name = AuthConstants.AuthTokenHeaderName, + Type = SecuritySchemeType.ApiKey, + Scheme = "ApiKeyAuth", + In = ParameterLocation.Header, + Description = "API Token Authorization header." + }); + options.AddSecurityRequirement(new OpenApiSecurityRequirement { - new OpenApiSecurityScheme { - Reference = new OpenApiReference + new OpenApiSecurityScheme { - Type = ReferenceType.SecurityScheme, - Id = AuthConstants.AuthTokenHeaderName - } - }, - Array.Empty() - } - }); - options.AddServer(new OpenApiServer { Url = "https://api.openshock.app" }); - options.AddServer(new OpenApiServer { Url = "https://staging-api.openshock.app" }); -#if DEBUG - options.AddServer(new OpenApiServer { Url = "https://localhost" }); -#endif - options.SwaggerDoc("v1", new OpenApiInfo { Title = "OpenShock", Version = "1" }); - options.SwaggerDoc("v2", new OpenApiInfo { Title = "OpenShock", Version = "2" }); - options.MapType(() => OpenApiSchemas.SemVerSchema); - options.MapType(() => OpenApiSchemas.PauseReasonEnumSchema); + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = AuthConstants.AuthTokenHeaderName + } + }, + Array.Empty() + } + }); + options.AddServer(new OpenApiServer { Url = "https://api.openshock.app" }); + options.AddServer(new OpenApiServer { Url = "https://staging-api.openshock.app" }); + #if DEBUG + options.AddServer(new OpenApiServer { Url = "https://localhost" }); + #endif + foreach (var version in versions) + { + options.SwaggerDoc("v" + version, new OpenApiInfo { Title = "OpenShock", Version = version }); + } + options.MapType(() => OpenApiSchemas.SemVerSchema); + options.MapType(() => OpenApiSchemas.PauseReasonEnumSchema); - // Avoid nullable strings everywhere - options.SupportNonNullableReferenceTypes(); - }); + // Avoid nullable strings everywhere + options.SupportNonNullableReferenceTypes(); + }) + .ConfigureOptions(); } public static IApplicationBuilder UseSwaggerExt(this WebApplication app) diff --git a/LiveControlGateway/Program.cs b/LiveControlGateway/Program.cs index 6ddf3fb2..1a6b0313 100644 --- a/LiveControlGateway/Program.cs +++ b/LiveControlGateway/Program.cs @@ -3,7 +3,6 @@ using OpenShock.Common.JsonSerialization; using OpenShock.Common.Services.Device; using OpenShock.Common.Services.Ota; -using OpenShock.Common.Utils; using OpenShock.LiveControlGateway; using OpenShock.LiveControlGateway.LifetimeManager; using OpenShock.LiveControlGateway.PubSub; @@ -32,9 +31,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddSwaggerExt("OpenShock.LiveControlGateway"); +builder.Services.AddSwaggerExt(); -builder.Services.ConfigureOptions(); //services.AddHealthChecks().AddCheck("database"); builder.Services.AddHostedService(); From 26be413034d12176284e6c8ff1c6131e4e28cb41 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Thu, 28 Nov 2024 10:59:32 +0100 Subject: [PATCH 12/22] Fix missing config exception --- Common/Extensions/ConfigurationExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Common/Extensions/ConfigurationExtensions.cs b/Common/Extensions/ConfigurationExtensions.cs index 90509578..50f6c90e 100644 --- a/Common/Extensions/ConfigurationExtensions.cs +++ b/Common/Extensions/ConfigurationExtensions.cs @@ -15,8 +15,8 @@ public static T GetAndRegisterOpenShockConfig(this WebApplicationBuilder buil var config = builder.Configuration .GetChildren() - .First(x => x.Key.Equals("openshock", StringComparison.InvariantCultureIgnoreCase)) - .Get() ?? throw new Exception("Couldn't bind config, check config file"); + .FirstOrDefault(x => x.Key.Equals("openshock", StringComparison.InvariantCultureIgnoreCase)) + ?.Get() ?? throw new Exception("Couldn't bind config, check config file"); MiniValidation.MiniValidator.TryValidate(config, true, true, out var errors); if (errors.Count > 0) From 98faae2ce818a8cee27283d2d6ed396350fdf091 Mon Sep 17 00:00:00 2001 From: HeavenVR Date: Thu, 28 Nov 2024 23:18:30 +0100 Subject: [PATCH 13/22] Use ASP.NET authentication principles (#138) * Better auth documentation and cleanup * Change claimsidentity * Start using ASP.NET Policies * Fix OpenAPI schema * Fix security requirements selection * Fix missing auth response messages * Fix AuthenticationHandler errors * Fix API Hub endpoints * Exit early on invalid sharelink user auth --- .../Account/Authenticated/_ApiController.cs | 24 +-- API/Controller/Account/Logout.cs | 4 +- API/Controller/Account/_ApiController.cs | 11 +- API/Controller/Admin/_ApiController.cs | 9 +- API/Controller/Device/_ApiController.cs | 7 +- API/Controller/Devices/DeviceOtaController.cs | 6 +- API/Controller/Devices/_ApiController.cs | 5 +- API/Controller/Public/_ApiController.cs | 1 - API/Controller/Sessions/SessionSelf.cs | 3 +- API/Controller/Sessions/_ApiController.cs | 4 +- API/Controller/Shares/Links/_ApiController.cs | 5 +- API/Controller/Shares/_ApiController.cs | 5 +- API/Controller/Shockers/_ApiController.cs | 3 + API/Controller/Tokens/TokenController.cs | 12 +- API/Controller/Tokens/TokenSelfController.cs | 4 +- API/Controller/Tokens/_ApiController.cs | 5 +- API/Controller/Users/_ApiController.cs | 5 +- API/Controller/Version/_ApiController.cs | 1 - API/Program.cs | 1 + Common/AttributeFilter.cs | 31 ---- .../Attributes/RankAttribute.cs | 39 ----- .../Attributes/TokenOnlyAttribute.cs | 24 --- .../Attributes/UserSessionOnlyAttribute.cs | 24 --- .../{LinkUser.cs => AuthenticatedUser.cs} | 2 +- .../ApiTokenAuthentication.cs | 83 ++++++++++ .../HubAuthentication.cs} | 10 +- .../UserSessionAuthentication.cs | 93 +++++++++++ .../ApiTokenPermissionHandler.cs | 28 ++++ ...e.cs => AuthenticatedHubControllerBase.cs} | 4 +- .../AuthenticatedSessionControllerBase.cs | 19 +-- .../Handlers/LoginSessionAuthentication.cs | 149 ------------------ Common/Authentication/OpenShockAuthClaims.cs | 7 + .../Authentication/OpenShockAuthPolicies.cs | 12 ++ Common/Authentication/OpenShockAuthSchemas.cs | 8 +- .../ApiTokenPermissionRequirement.cs | 14 ++ Common/Constants/AuthConstants.cs | 6 +- Common/Errors/AuthResultError.cs | 3 +- Common/Hubs/ShareLinkHub.cs | 25 +-- Common/Hubs/UserHub.cs | 6 +- Common/IQueryableExtensions.cs | 4 +- Common/Models/ControlLogAdditionalItem.cs | 6 +- Common/OpenShockServiceHelper.cs | 55 +++++-- Common/Services/Session/SessionService.cs | 2 +- Common/Swagger/AttributeFilter.cs | 119 ++++++++++++++ .../SwaggerGenExtensions.cs | 66 +++++--- Common/Utils/AuthUtils.cs | 40 +---- Cron/DashboardAdminAuth.cs | 4 +- .../Controllers/HubControllerBase.cs | 2 + .../Controllers/HubV1Controller.cs | 2 +- .../Controllers/HubV2Controller.cs | 2 +- .../Controllers/LiveControlController.cs | 6 +- LiveControlGateway/Program.cs | 1 + 52 files changed, 576 insertions(+), 435 deletions(-) delete mode 100644 Common/AttributeFilter.cs delete mode 100644 Common/Authentication/Attributes/RankAttribute.cs delete mode 100644 Common/Authentication/Attributes/TokenOnlyAttribute.cs delete mode 100644 Common/Authentication/Attributes/UserSessionOnlyAttribute.cs rename Common/Authentication/{LinkUser.cs => AuthenticatedUser.cs} (95%) create mode 100644 Common/Authentication/AuthenticationHandlers/ApiTokenAuthentication.cs rename Common/Authentication/{Handlers/DeviceAuthentication.cs => AuthenticationHandlers/HubAuthentication.cs} (88%) create mode 100644 Common/Authentication/AuthenticationHandlers/UserSessionAuthentication.cs create mode 100644 Common/Authentication/AuthorizationHandlers/ApiTokenPermissionHandler.cs rename Common/Authentication/ControllerBase/{AuthenticatedDeviceControllerBase.cs => AuthenticatedHubControllerBase.cs} (80%) delete mode 100644 Common/Authentication/Handlers/LoginSessionAuthentication.cs create mode 100644 Common/Authentication/OpenShockAuthClaims.cs create mode 100644 Common/Authentication/OpenShockAuthPolicies.cs create mode 100644 Common/Authentication/Requirements/ApiTokenPermissionRequirement.cs create mode 100644 Common/Swagger/AttributeFilter.cs rename Common/{Extensions => Swagger}/SwaggerGenExtensions.cs (62%) diff --git a/API/Controller/Account/Authenticated/_ApiController.cs b/API/Controller/Account/Authenticated/_ApiController.cs index aa547a7f..80e53e29 100644 --- a/API/Controller/Account/Authenticated/_ApiController.cs +++ b/API/Controller/Account/Authenticated/_ApiController.cs @@ -1,11 +1,9 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using OpenShock.API.Services.Account; -using OpenShock.Common.Authentication.Attributes; +using OpenShock.Common.Authentication; using OpenShock.Common.Authentication.ControllerBase; -using OpenShock.Common.OpenShockDb; -using OpenShock.Common.Services.Session; -using Redis.OM.Contracts; namespace OpenShock.API.Controller.Account.Authenticated; @@ -13,28 +11,20 @@ namespace OpenShock.API.Controller.Account.Authenticated; /// User account management /// [ApiController] -[UserSessionOnly] [ApiVersion("1")] [Route("/{version:apiVersion}/account")] +[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionCookie)] public sealed partial class AuthenticatedAccountController : AuthenticatedSessionControllerBase { - private readonly OpenShockContext _db; - private readonly IRedisConnectionProvider _redis; - private readonly ILogger _logger; private readonly IAccountService _accountService; - private readonly ISessionService _sessionService; + private readonly ILogger _logger; public AuthenticatedAccountController( - OpenShockContext db, - IRedisConnectionProvider redis, - ILogger logger, IAccountService accountService, - ISessionService sessionService) + ILogger logger + ) { - _db = db; - _redis = redis; - _logger = logger; _accountService = accountService; - _sessionService = sessionService; + _logger = logger; } } \ No newline at end of file diff --git a/API/Controller/Account/Logout.cs b/API/Controller/Account/Logout.cs index 80cf5b57..bf998036 100644 --- a/API/Controller/Account/Logout.cs +++ b/API/Controller/Account/Logout.cs @@ -16,9 +16,9 @@ public async Task Logout( [FromServices] ApiConfig apiConfig) { // Remove session if valid - if (HttpContext.TryGetSessionKey(out var sessionKey)) + if (HttpContext.TryGetUserSessionCookie(out var sessionCookie)) { - await sessionService.DeleteSessionById(sessionKey); + await sessionService.DeleteSessionById(sessionCookie); } // Make sure cookie is removed, no matter if authenticated or not diff --git a/API/Controller/Account/_ApiController.cs b/API/Controller/Account/_ApiController.cs index 47952a8c..3b7f4a99 100644 --- a/API/Controller/Account/_ApiController.cs +++ b/API/Controller/Account/_ApiController.cs @@ -12,22 +12,17 @@ namespace OpenShock.API.Controller.Account; /// User account management /// [ApiController] -[AllowAnonymous] [ApiVersion("1")] [ApiVersion("2")] [Route("/{version:apiVersion}/account")] public sealed partial class AccountController : OpenShockControllerBase { - private readonly OpenShockContext _db; - private readonly IRedisConnectionProvider _redis; - private readonly ILogger _logger; private readonly IAccountService _accountService; + private readonly ILogger _logger; - public AccountController(OpenShockContext db, IRedisConnectionProvider redis, ILogger logger, IAccountService accountService) + public AccountController(IAccountService accountService, ILogger logger) { - _db = db; - _redis = redis; - _logger = logger; _accountService = accountService; + _logger = logger; } } \ No newline at end of file diff --git a/API/Controller/Admin/_ApiController.cs b/API/Controller/Admin/_ApiController.cs index 1dc4ce8b..ac6c33fe 100644 --- a/API/Controller/Admin/_ApiController.cs +++ b/API/Controller/Admin/_ApiController.cs @@ -1,16 +1,15 @@ -using Microsoft.AspNetCore.Mvc; -using OpenShock.Common.Authentication.Attributes; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using OpenShock.Common.Authentication; using OpenShock.Common.Authentication.ControllerBase; -using OpenShock.Common.Models; using OpenShock.Common.OpenShockDb; using Redis.OM.Contracts; namespace OpenShock.API.Controller.Admin; [ApiController] -[Rank(RankType.Admin)] -[UserSessionOnly] [Route("/{version:apiVersion}/admin")] +[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionCookie, Policy = OpenShockAuthPolicies.AdminAccess)] public sealed partial class AdminController : AuthenticatedSessionControllerBase { private readonly OpenShockContext _db; diff --git a/API/Controller/Device/_ApiController.cs b/API/Controller/Device/_ApiController.cs index a0634d9c..ab3baa1f 100644 --- a/API/Controller/Device/_ApiController.cs +++ b/API/Controller/Device/_ApiController.cs @@ -1,4 +1,6 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using OpenShock.Common.Authentication; using OpenShock.Common.Authentication.ControllerBase; using OpenShock.Common.OpenShockDb; using Redis.OM.Contracts; @@ -10,7 +12,8 @@ namespace OpenShock.API.Controller.Device; /// [ApiController] [Route("/{version:apiVersion}/device")] -public sealed partial class DeviceController : AuthenticatedDeviceControllerBase +[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.HubToken)] +public sealed partial class DeviceController : AuthenticatedHubControllerBase { private readonly OpenShockContext _db; private readonly IRedisConnectionProvider _redis; diff --git a/API/Controller/Devices/DeviceOtaController.cs b/API/Controller/Devices/DeviceOtaController.cs index 09a48af3..1d2c983c 100644 --- a/API/Controller/Devices/DeviceOtaController.cs +++ b/API/Controller/Devices/DeviceOtaController.cs @@ -1,8 +1,10 @@ using System.Net; using System.Net.Mime; using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using OpenShock.Common.Authentication; using OpenShock.Common.Authentication.Attributes; using OpenShock.Common.Errors; using OpenShock.Common.Models; @@ -22,10 +24,10 @@ public sealed partial class DevicesController /// OK /// Could not find device or you do not have access to it [HttpGet("{deviceId}/ota")] - [UserSessionOnly] + [MapToApiVersion("1")] + [Authorize(Policy = OpenShockAuthPolicies.UserAccess)] [ProducesResponseType>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // DeviceNotFound - [MapToApiVersion("1")] public async Task GetOtaUpdateHistory([FromRoute] Guid deviceId, [FromServices] IOtaService otaService) { // Check if user owns device or has a share diff --git a/API/Controller/Devices/_ApiController.cs b/API/Controller/Devices/_ApiController.cs index b002b88b..75137a4a 100644 --- a/API/Controller/Devices/_ApiController.cs +++ b/API/Controller/Devices/_ApiController.cs @@ -1,5 +1,7 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using OpenShock.Common.Authentication; using OpenShock.Common.Authentication.ControllerBase; using OpenShock.Common.OpenShockDb; using Redis.OM.Contracts; @@ -10,9 +12,10 @@ namespace OpenShock.API.Controller.Devices; /// Device management /// [ApiController] -[Route("/{version:apiVersion}/devices")] [ApiVersion("1")] [ApiVersion("2")] +[Route("/{version:apiVersion}/devices")] +[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionApiTokenCombo)] public sealed partial class DevicesController : AuthenticatedSessionControllerBase { private readonly OpenShockContext _db; diff --git a/API/Controller/Public/_ApiController.cs b/API/Controller/Public/_ApiController.cs index d7c11986..67d312b8 100644 --- a/API/Controller/Public/_ApiController.cs +++ b/API/Controller/Public/_ApiController.cs @@ -8,7 +8,6 @@ namespace OpenShock.API.Controller.Public; [ApiController] [Route("/{version:apiVersion}/public")] -[AllowAnonymous] public sealed partial class PublicController : OpenShockControllerBase { private readonly OpenShockContext _db; diff --git a/API/Controller/Sessions/SessionSelf.cs b/API/Controller/Sessions/SessionSelf.cs index e79d57be..1e621f81 100644 --- a/API/Controller/Sessions/SessionSelf.cs +++ b/API/Controller/Sessions/SessionSelf.cs @@ -1,6 +1,8 @@ using System.Net.Mime; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using OpenShock.API.Models.Response; +using OpenShock.Common.Authentication; using OpenShock.Common.Authentication.Attributes; using OpenShock.Common.Authentication.Services; using OpenShock.Common.Problems; @@ -16,7 +18,6 @@ public sealed partial class SessionsController /// /// [HttpGet("self")] - [UserSessionOnly] [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] public LoginSessionResponse GetSelfSession([FromServices] IUserReferenceService userReferenceService) { diff --git a/API/Controller/Sessions/_ApiController.cs b/API/Controller/Sessions/_ApiController.cs index af766dad..1d8cf3d0 100644 --- a/API/Controller/Sessions/_ApiController.cs +++ b/API/Controller/Sessions/_ApiController.cs @@ -1,5 +1,7 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using OpenShock.Common.Authentication; using OpenShock.Common.Authentication.Attributes; using OpenShock.Common.Authentication.ControllerBase; using OpenShock.Common.Services.Session; @@ -10,9 +12,9 @@ namespace OpenShock.API.Controller.Sessions; /// Session management /// [ApiController] -[UserSessionOnly] [ApiVersion("1")] [Route("/{version:apiVersion}/sessions")] +[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionCookie)] public sealed partial class SessionsController : AuthenticatedSessionControllerBase { private readonly ISessionService _sessionService; diff --git a/API/Controller/Shares/Links/_ApiController.cs b/API/Controller/Shares/Links/_ApiController.cs index a4865dd3..60ebed9f 100644 --- a/API/Controller/Shares/Links/_ApiController.cs +++ b/API/Controller/Shares/Links/_ApiController.cs @@ -1,4 +1,6 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using OpenShock.Common.Authentication; using OpenShock.Common.Authentication.ControllerBase; using OpenShock.Common.OpenShockDb; @@ -9,6 +11,7 @@ namespace OpenShock.API.Controller.Shares.Links; /// [ApiController] [Route("/{version:apiVersion}/shares/links")] +[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionApiTokenCombo)] public sealed partial class ShareLinksController : AuthenticatedSessionControllerBase { private readonly OpenShockContext _db; diff --git a/API/Controller/Shares/_ApiController.cs b/API/Controller/Shares/_ApiController.cs index 192e7aa0..5b515222 100644 --- a/API/Controller/Shares/_ApiController.cs +++ b/API/Controller/Shares/_ApiController.cs @@ -1,5 +1,7 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using OpenShock.Common.Authentication; using OpenShock.Common.Authentication.ControllerBase; using OpenShock.Common.OpenShockDb; @@ -9,9 +11,10 @@ namespace OpenShock.API.Controller.Shares; /// Shocker share management /// [ApiController] -[Route("/{version:apiVersion}/shares")] [ApiVersion("1")] [ApiVersion("2")] +[Route("/{version:apiVersion}/shares")] +[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionApiTokenCombo)] public sealed partial class SharesController : AuthenticatedSessionControllerBase { private readonly OpenShockContext _db; diff --git a/API/Controller/Shockers/_ApiController.cs b/API/Controller/Shockers/_ApiController.cs index 9ee4129c..c07746a8 100644 --- a/API/Controller/Shockers/_ApiController.cs +++ b/API/Controller/Shockers/_ApiController.cs @@ -1,5 +1,7 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using OpenShock.Common.Authentication; using OpenShock.Common.Authentication.ControllerBase; using OpenShock.Common.OpenShockDb; @@ -12,6 +14,7 @@ namespace OpenShock.API.Controller.Shockers; [ApiVersion("1")] [ApiVersion("2")] [Route("/{version:apiVersion}/shockers")] +[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionApiTokenCombo)] public sealed partial class ShockerController : AuthenticatedSessionControllerBase { private readonly OpenShockContext _db; diff --git a/API/Controller/Tokens/TokenController.cs b/API/Controller/Tokens/TokenController.cs index ace7625c..df433de5 100644 --- a/API/Controller/Tokens/TokenController.cs +++ b/API/Controller/Tokens/TokenController.cs @@ -1,11 +1,13 @@ using System.ComponentModel.DataAnnotations; using System.Net; using System.Net.Mime; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using OpenShock.API.Models.Response; using OpenShock.API.Utils; using OpenShock.Common; +using OpenShock.Common.Authentication; using OpenShock.Common.Authentication.Attributes; using OpenShock.Common.Constants; using OpenShock.Common.Errors; @@ -23,7 +25,7 @@ public sealed partial class TokensController /// /// All tokens for the current user [HttpGet] - [UserSessionOnly] + [Authorize(Policy = OpenShockAuthPolicies.UserAccess)] [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] public async Task> ListTokens() { @@ -50,7 +52,7 @@ public async Task> ListTokens() /// The token /// The token does not exist or you do not have access to it. [HttpGet("{tokenId}")] - [UserSessionOnly] + [Authorize(Policy = OpenShockAuthPolicies.UserAccess)] [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // ApiTokenNotFound public async Task GetTokenById([FromRoute] Guid tokenId) @@ -79,7 +81,7 @@ public async Task GetTokenById([FromRoute] Guid tokenId) /// Successfully deleted token /// The token does not exist or you do not have access to it. [HttpDelete("{tokenId}")] - [UserSessionOnly] + [Authorize(Policy = OpenShockAuthPolicies.UserAccess)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // ApiTokenNotFound public async Task DeleteToken([FromRoute] Guid tokenId) @@ -103,7 +105,7 @@ public async Task DeleteToken([FromRoute] Guid tokenId) /// /// The created token [HttpPost] - [UserSessionOnly] + [Authorize(Policy = OpenShockAuthPolicies.UserAccess)] [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] public async Task CreateToken([FromBody] CreateTokenRequest body) { @@ -137,7 +139,7 @@ public async Task CreateToken([FromBody] CreateTokenReques /// The edited token /// The token does not exist or you do not have access to it. [HttpPatch("{tokenId}")] - [UserSessionOnly] + [Authorize(Policy = OpenShockAuthPolicies.UserAccess)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // ApiTokenNotFound public async Task EditToken([FromRoute] Guid tokenId, [FromBody] EditTokenRequest body) diff --git a/API/Controller/Tokens/TokenSelfController.cs b/API/Controller/Tokens/TokenSelfController.cs index b0072cd8..587e3d8f 100644 --- a/API/Controller/Tokens/TokenSelfController.cs +++ b/API/Controller/Tokens/TokenSelfController.cs @@ -1,6 +1,8 @@ using System.Net.Mime; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using OpenShock.API.Models.Response; +using OpenShock.Common.Authentication; using OpenShock.Common.Authentication.Attributes; using OpenShock.Common.Authentication.Services; using OpenShock.Common.OpenShockDb; @@ -17,7 +19,7 @@ public sealed partial class TokensController /// /// [HttpGet("self")] - [TokenOnly] + [Authorize(Policy = OpenShockAuthPolicies.TokenSessionOnly)] [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] public TokenResponse GetSelfToken([FromServices] IUserReferenceService userReferenceService) { diff --git a/API/Controller/Tokens/_ApiController.cs b/API/Controller/Tokens/_ApiController.cs index 16d36701..c7b33cfe 100644 --- a/API/Controller/Tokens/_ApiController.cs +++ b/API/Controller/Tokens/_ApiController.cs @@ -1,4 +1,6 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using OpenShock.Common.Authentication; using OpenShock.Common.Authentication.ControllerBase; using OpenShock.Common.OpenShockDb; using Redis.OM.Contracts; @@ -7,6 +9,7 @@ namespace OpenShock.API.Controller.Tokens; [ApiController] [Route("/{version:apiVersion}/tokens")] +[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionApiTokenCombo)] public sealed partial class TokensController : AuthenticatedSessionControllerBase { private readonly OpenShockContext _db; diff --git a/API/Controller/Users/_ApiController.cs b/API/Controller/Users/_ApiController.cs index e0e179fd..c96aadfa 100644 --- a/API/Controller/Users/_ApiController.cs +++ b/API/Controller/Users/_ApiController.cs @@ -1,4 +1,6 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using OpenShock.Common.Authentication; using OpenShock.Common.Authentication.ControllerBase; using OpenShock.Common.OpenShockDb; using Redis.OM.Contracts; @@ -7,6 +9,7 @@ namespace OpenShock.API.Controller.Users; [ApiController] [Route("/{version:apiVersion}/users")] +[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionApiTokenCombo)] public sealed partial class UsersController : AuthenticatedSessionControllerBase { private readonly OpenShockContext _db; diff --git a/API/Controller/Version/_ApiController.cs b/API/Controller/Version/_ApiController.cs index 8210d3dc..f2305e68 100644 --- a/API/Controller/Version/_ApiController.cs +++ b/API/Controller/Version/_ApiController.cs @@ -15,7 +15,6 @@ namespace OpenShock.API.Controller.Version; /// Version stuff /// [ApiController] -[AllowAnonymous] [Route("/{version:apiVersion}")] public sealed partial class VersionController : OpenShockControllerBase { diff --git a/API/Program.cs b/API/Program.cs index b24c5bc4..bdb71b66 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -15,6 +15,7 @@ using OpenShock.Common.Services.LCGNodeProvisioner; using OpenShock.Common.Services.Ota; using OpenShock.Common.Services.Turnstile; +using OpenShock.Common.Swagger; using Scalar.AspNetCore; using Serilog; diff --git a/Common/AttributeFilter.cs b/Common/AttributeFilter.cs deleted file mode 100644 index 2a39b4cc..00000000 --- a/Common/AttributeFilter.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.OpenApi.Models; -using OpenShock.Common.DataAnnotations.Interfaces; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace OpenShock.Common; - -public sealed class AttributeFilter : ISchemaFilter, IParameterFilter, IOperationFilter -{ - public void Apply(OpenApiParameter parameter, ParameterFilterContext context) - { - foreach (var attribute in context.ParameterInfo?.GetCustomAttributes(true).OfType() ?? - Enumerable.Empty()) - attribute.Apply(parameter); - foreach (var attribute in context.PropertyInfo?.GetCustomAttributes(true).OfType() ?? - Enumerable.Empty()) - attribute.Apply(parameter); - } - - public void Apply(OpenApiSchema schema, SchemaFilterContext context) - { - foreach (var attribute in context.MemberInfo?.GetCustomAttributes(true).OfType() ?? - Enumerable.Empty()) attribute.Apply(schema); - } - - public void Apply(OpenApiOperation operation, OperationFilterContext context) - { - foreach (var attribute in context.MethodInfo?.GetCustomAttributes(true).OfType() ?? - Enumerable.Empty()) - attribute.Apply(operation); - } -} \ No newline at end of file diff --git a/Common/Authentication/Attributes/RankAttribute.cs b/Common/Authentication/Attributes/RankAttribute.cs deleted file mode 100644 index 46a62c71..00000000 --- a/Common/Authentication/Attributes/RankAttribute.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Net; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using OpenShock.Common.Authentication.Services; -using OpenShock.Common.Models; - -namespace OpenShock.Common.Authentication.Attributes; - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public sealed class RankAttribute : Attribute, IAuthorizationFilter -{ - private readonly RankType _requiredRank; - - public RankAttribute(RankType requiredRank) - { - _requiredRank = requiredRank; - } - - public void OnAuthorization(AuthorizationFilterContext context) - { - var user = context.HttpContext.RequestServices.GetService>()?.CurrentClient; - if (user == null) - { - context.Result = new JsonResult(new BaseResponse - { - Message = "Error while authorizing request", - }); - context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; - return; - } - if(user.DbUser.Rank.IsAllowed(_requiredRank)) return; - - context.Result = new JsonResult(new BaseResponse - { - Message = $"Required rank not met. Required rank is {_requiredRank} but you only have {user.DbUser.Rank}" - }); - context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; - } -} \ No newline at end of file diff --git a/Common/Authentication/Attributes/TokenOnlyAttribute.cs b/Common/Authentication/Attributes/TokenOnlyAttribute.cs deleted file mode 100644 index 97b4d033..00000000 --- a/Common/Authentication/Attributes/TokenOnlyAttribute.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.AspNetCore.Mvc.Filters; -using OpenShock.Common.Authentication.Services; -using OpenShock.Common.Errors; -using OpenShock.Common.OpenShockDb; - -namespace OpenShock.Common.Authentication.Attributes; - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public sealed class TokenOnlyAttribute : Attribute, IAuthorizationFilter -{ - public void OnAuthorization(AuthorizationFilterContext context) - { - var userReferenceService = context.HttpContext.RequestServices.GetRequiredService(); - - if (userReferenceService.AuthReference == null) - { - var error = AuthorizationError.UnknownError; - context.Result = error.ToObjectResult(context.HttpContext); - return; - } - - if (!userReferenceService.AuthReference.Value.IsT1) context.Result = AuthorizationError.TokenOnly.ToObjectResult(context.HttpContext); - } -} \ No newline at end of file diff --git a/Common/Authentication/Attributes/UserSessionOnlyAttribute.cs b/Common/Authentication/Attributes/UserSessionOnlyAttribute.cs deleted file mode 100644 index 822ed3ef..00000000 --- a/Common/Authentication/Attributes/UserSessionOnlyAttribute.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.AspNetCore.Mvc.Filters; -using OpenShock.Common.Authentication.Services; -using OpenShock.Common.Errors; -using OpenShock.Common.OpenShockDb; - -namespace OpenShock.Common.Authentication.Attributes; - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public sealed class UserSessionOnlyAttribute : Attribute, IAuthorizationFilter -{ - public void OnAuthorization(AuthorizationFilterContext context) - { - var userReferenceService = context.HttpContext.RequestServices.GetRequiredService(); - - if (userReferenceService.AuthReference == null) - { - var error = AuthorizationError.UnknownError; - context.Result = error.ToObjectResult(context.HttpContext); - return; - } - - if (!userReferenceService.AuthReference.Value.IsT0) context.Result = AuthorizationError.UserSessionOnly.ToObjectResult(context.HttpContext); - } -} \ No newline at end of file diff --git a/Common/Authentication/LinkUser.cs b/Common/Authentication/AuthenticatedUser.cs similarity index 95% rename from Common/Authentication/LinkUser.cs rename to Common/Authentication/AuthenticatedUser.cs index 365af977..f6df4041 100644 --- a/Common/Authentication/LinkUser.cs +++ b/Common/Authentication/AuthenticatedUser.cs @@ -4,7 +4,7 @@ namespace OpenShock.Common.Authentication; -public sealed class LinkUser +public sealed class AuthenticatedUser { public required User DbUser { get; set; } diff --git a/Common/Authentication/AuthenticationHandlers/ApiTokenAuthentication.cs b/Common/Authentication/AuthenticationHandlers/ApiTokenAuthentication.cs new file mode 100644 index 00000000..22d8c992 --- /dev/null +++ b/Common/Authentication/AuthenticationHandlers/ApiTokenAuthentication.cs @@ -0,0 +1,83 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using OpenShock.Common.Authentication.Services; +using OpenShock.Common.Errors; +using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; +using OpenShock.Common.Problems; +using OpenShock.Common.Services.BatchUpdate; +using OpenShock.Common.Utils; +using System.Net.Mime; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Text.Json; + +namespace OpenShock.Common.Authentication.AuthenticationHandlers; + +public sealed class ApiTokenAuthentication : AuthenticationHandler +{ + private readonly IClientAuthService _authService; + private readonly IUserReferenceService _userReferenceService; + private readonly IBatchUpdateService _batchUpdateService; + private readonly OpenShockContext _db; + private readonly JsonSerializerOptions _serializerOptions; + + public ApiTokenAuthentication( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + IClientAuthService clientAuth, + IUserReferenceService userReferenceService, + OpenShockContext db, + IOptions jsonOptions, IBatchUpdateService batchUpdateService) + : base(options, logger, encoder) + { + _authService = clientAuth; + _userReferenceService = userReferenceService; + _db = db; + _serializerOptions = jsonOptions.Value.SerializerOptions; + _batchUpdateService = batchUpdateService; + } + + protected override async Task HandleAuthenticateAsync() + { + if (!Context.TryGetApiTokenFromHeader(out var token)) + { + return AuthenticateResult.Fail(AuthResultError.HeaderMissingOrInvalid.Title!); + } + + 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 AuthenticateResult.Fail(AuthResultError.TokenInvalid.Title!); + + _batchUpdateService.UpdateTokenLastUsed(tokenDto.Id); + _authService.CurrentClient = new AuthenticatedUser + { + DbUser = tokenDto.User + }; + _userReferenceService.AuthReference = tokenDto; + + List claims = [ + new(ClaimTypes.AuthenticationMethod, OpenShockAuthSchemas.ApiToken), + new(ClaimTypes.NameIdentifier, tokenDto.User.Id.ToString()), + new(OpenShockAuthClaims.ApiTokenId, tokenDto.Id.ToString()) + ]; + + foreach (var perm in tokenDto.Permissions) + { + claims.Add(new(OpenShockAuthClaims.ApiTokenPermission, perm.ToString())); + } + + var ident = new ClaimsIdentity(claims, nameof(ApiTokenAuthentication)); + + Context.User = new ClaimsPrincipal(ident); + + var ticket = new AuthenticationTicket(new ClaimsPrincipal(ident), Scheme.Name); + + return AuthenticateResult.Success(ticket); + } +} \ No newline at end of file diff --git a/Common/Authentication/Handlers/DeviceAuthentication.cs b/Common/Authentication/AuthenticationHandlers/HubAuthentication.cs similarity index 88% rename from Common/Authentication/Handlers/DeviceAuthentication.cs rename to Common/Authentication/AuthenticationHandlers/HubAuthentication.cs index d519d77b..886d3ede 100644 --- a/Common/Authentication/Handlers/DeviceAuthentication.cs +++ b/Common/Authentication/AuthenticationHandlers/HubAuthentication.cs @@ -12,12 +12,12 @@ using OpenShock.Common.Problems; using OpenShock.Common.Utils; -namespace OpenShock.Common.Authentication.Handlers; +namespace OpenShock.Common.Authentication.AuthenticationHandlers; /// /// Device / Box / The Thing / ESP32 authentication with DeviceToken header /// -public sealed class DeviceAuthentication : AuthenticationHandler +public sealed class HubAuthentication : AuthenticationHandler { private readonly IClientAuthService _authService; private readonly OpenShockContext _db; @@ -26,7 +26,7 @@ public sealed class DeviceAuthentication : AuthenticationHandler options, ILoggerFactory logger, UrlEncoder encoder, @@ -44,7 +44,7 @@ protected override async Task HandleAuthenticateAsync() { if (!Context.TryGetDeviceTokenFromHeader(out string? sessionKey)) { - return Fail(AuthResultError.CookieOrHeaderMissingOrInvalid); + return Fail(AuthResultError.HeaderMissingOrInvalid); } var device = await _db.Devices.Where(x => x.Token == sessionKey).FirstOrDefaultAsync(); @@ -57,7 +57,7 @@ protected override async Task HandleAuthenticateAsync() { new Claim("id", _authService.CurrentClient.Id.ToString()), }; - var ident = new ClaimsIdentity(claims, nameof(LoginSessionAuthentication)); + var ident = new ClaimsIdentity(claims, nameof(HubAuthentication)); var ticket = new AuthenticationTicket(new ClaimsPrincipal(ident), Scheme.Name); return AuthenticateResult.Success(ticket); diff --git a/Common/Authentication/AuthenticationHandlers/UserSessionAuthentication.cs b/Common/Authentication/AuthenticationHandlers/UserSessionAuthentication.cs new file mode 100644 index 00000000..5523dcdb --- /dev/null +++ b/Common/Authentication/AuthenticationHandlers/UserSessionAuthentication.cs @@ -0,0 +1,93 @@ +using System.Net.Mime; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using OpenShock.Common.Authentication.Services; +using OpenShock.Common.Constants; +using OpenShock.Common.Errors; +using OpenShock.Common.OpenShockDb; +using OpenShock.Common.Problems; +using OpenShock.Common.Services.BatchUpdate; +using OpenShock.Common.Services.Session; +using OpenShock.Common.Utils; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Text.Json; + +namespace OpenShock.Common.Authentication.AuthenticationHandlers; + +public sealed class UserSessionAuthentication : AuthenticationHandler +{ + private readonly IClientAuthService _authService; + private readonly IUserReferenceService _userReferenceService; + private readonly IBatchUpdateService _batchUpdateService; + private readonly OpenShockContext _db; + private readonly ISessionService _sessionService; + private readonly JsonSerializerOptions _serializerOptions; + + public UserSessionAuthentication( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + IClientAuthService clientAuth, + IUserReferenceService userReferenceService, + OpenShockContext db, + ISessionService sessionService, + IOptions jsonOptions, IBatchUpdateService batchUpdateService) + : base(options, logger, encoder) + { + _authService = clientAuth; + _userReferenceService = userReferenceService; + _db = db; + _sessionService = sessionService; + _serializerOptions = jsonOptions.Value.SerializerOptions; + _batchUpdateService = batchUpdateService; + } + + protected override async Task HandleAuthenticateAsync() + { + if (!Context.TryGetUserSessionCookie(out var sessionKey)) + { + return AuthenticateResult.Fail(AuthResultError.CookieMissingOrInvalid.Type!); + } + + var session = await _sessionService.GetSessionById(sessionKey); + if (session == null) return AuthenticateResult.Fail(AuthResultError.SessionInvalid.Type!); + + if (session.Expires!.Value < DateTime.UtcNow.Subtract(Duration.LoginSessionExpansionAfter)) + { +#pragma warning disable CS4014 + LucTask.Run(async () => +#pragma warning restore CS4014 + { + session.Expires = DateTime.UtcNow.Add(Duration.LoginSessionLifetime); + await _sessionService.UpdateSession(session, Duration.LoginSessionLifetime); + }); + } + + _batchUpdateService.UpdateSessionLastUsed(sessionKey, DateTime.UtcNow); + + var retrievedUser = await _db.Users.FirstAsync(user => user.Id == session.UserId); + + _userReferenceService.AuthReference = session; + _authService.CurrentClient = new AuthenticatedUser + { + DbUser = retrievedUser + }; + + List claims = [ + new(ClaimTypes.AuthenticationMethod, OpenShockAuthSchemas.UserSessionCookie), + new(ClaimTypes.NameIdentifier, retrievedUser.Id.ToString()), + new(ClaimTypes.Role, retrievedUser.Rank.ToString()) + ]; + + var ident = new ClaimsIdentity(claims, nameof(UserSessionAuthentication)); + + Context.User = new ClaimsPrincipal(ident); + + var ticket = new AuthenticationTicket(new ClaimsPrincipal(ident), Scheme.Name); + + return AuthenticateResult.Success(ticket); + } +} \ No newline at end of file diff --git a/Common/Authentication/AuthorizationHandlers/ApiTokenPermissionHandler.cs b/Common/Authentication/AuthorizationHandlers/ApiTokenPermissionHandler.cs new file mode 100644 index 00000000..449f52cd --- /dev/null +++ b/Common/Authentication/AuthorizationHandlers/ApiTokenPermissionHandler.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Authorization; +using OpenShock.Common.Authentication.Requirements; +using OpenShock.Common.Models; + +namespace OpenShock.Common.Authentication.AuthorizationHandlers; + +public class ApiTokenPermissionHandler : AuthorizationHandler +{ + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ApiTokenPermissionRequirement requirement) + { + var perms = context + .User + .Identities + .SelectMany(ident => ident.Claims.Where(claim => claim.Type == OpenShockAuthClaims.ApiTokenPermission)) + .Select(claim => Enum.Parse(claim.Value)); + + if (!requirement.RequiredPermission.IsAllowed(perms)) + { + context.Fail(new AuthorizationFailureReason(this, $"You do not have the required permission to access this endpoint. Missing permission: {requirement.RequiredPermission}")); + + return Task.CompletedTask; + } + + context.Succeed(requirement); + + return Task.CompletedTask; + } +} diff --git a/Common/Authentication/ControllerBase/AuthenticatedDeviceControllerBase.cs b/Common/Authentication/ControllerBase/AuthenticatedHubControllerBase.cs similarity index 80% rename from Common/Authentication/ControllerBase/AuthenticatedDeviceControllerBase.cs rename to Common/Authentication/ControllerBase/AuthenticatedHubControllerBase.cs index b2ae8e16..3909c4a2 100644 --- a/Common/Authentication/ControllerBase/AuthenticatedDeviceControllerBase.cs +++ b/Common/Authentication/ControllerBase/AuthenticatedHubControllerBase.cs @@ -6,8 +6,8 @@ namespace OpenShock.Common.Authentication.ControllerBase; -[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.DeviceToken)] -public class AuthenticatedDeviceControllerBase : OpenShockControllerBase, IActionFilter +[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.HubToken)] +public class AuthenticatedHubControllerBase : OpenShockControllerBase, IActionFilter { public Device CurrentDevice = null!; diff --git a/Common/Authentication/ControllerBase/AuthenticatedSessionControllerBase.cs b/Common/Authentication/ControllerBase/AuthenticatedSessionControllerBase.cs index 5f820df0..2c28d2e6 100644 --- a/Common/Authentication/ControllerBase/AuthenticatedSessionControllerBase.cs +++ b/Common/Authentication/ControllerBase/AuthenticatedSessionControllerBase.cs @@ -1,37 +1,34 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using OpenShock.Common.Authentication.Services; using OpenShock.Common.Models; -using OpenShock.Common.OpenShockDb; namespace OpenShock.Common.Authentication.ControllerBase; -[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.SessionTokenCombo)] public class AuthenticatedSessionControllerBase : OpenShockControllerBase, IActionFilter { - public LinkUser CurrentUser = null!; + public AuthenticatedUser CurrentUser = null!; [NonAction] public void OnActionExecuting(ActionExecutingContext context) { - CurrentUser = ControllerContext.HttpContext.RequestServices.GetRequiredService>().CurrentClient; + CurrentUser = ControllerContext.HttpContext.RequestServices.GetRequiredService>().CurrentClient; } [NonAction] public void OnActionExecuted(ActionExecutedContext context) { } - + [NonAction] public bool IsAllowed(PermissionType requiredType) { var userReferenceService = HttpContext.RequestServices.GetRequiredService(); - - if(userReferenceService.AuthReference == null) throw new Exception("UserReferenceService.AuthReference is null, this should not happen"); - + + if (userReferenceService.AuthReference == null) throw new Exception("UserReferenceService.AuthReference is null, this should not happen"); + if (userReferenceService.AuthReference.Value.IsT0) return true; // We are in a session - + return requiredType.IsAllowed(userReferenceService.AuthReference.Value.AsT1.Permissions); } } \ No newline at end of file diff --git a/Common/Authentication/Handlers/LoginSessionAuthentication.cs b/Common/Authentication/Handlers/LoginSessionAuthentication.cs deleted file mode 100644 index 55c3037e..00000000 --- a/Common/Authentication/Handlers/LoginSessionAuthentication.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System.Net.Mime; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Http.Json; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; -using OpenShock.Common.Authentication.Services; -using OpenShock.Common.Constants; -using OpenShock.Common.Errors; -using OpenShock.Common.Models; -using OpenShock.Common.OpenShockDb; -using OpenShock.Common.Problems; -using OpenShock.Common.Redis; -using OpenShock.Common.Services.BatchUpdate; -using OpenShock.Common.Services.Session; -using OpenShock.Common.Utils; -using System.Security.Claims; -using System.Text.Encodings.Web; -using System.Text.Json; - -namespace OpenShock.Common.Authentication.Handlers; - -public sealed class LoginSessionAuthentication : AuthenticationHandler -{ - private readonly IClientAuthService _authService; - private readonly IUserReferenceService _userReferenceService; - private readonly IBatchUpdateService _batchUpdateService; - private readonly OpenShockContext _db; - private readonly ISessionService _sessionService; - private readonly JsonSerializerOptions _serializerOptions; - private OpenShockProblem? _authResultError = null; - - public LoginSessionAuthentication( - IOptionsMonitor options, - ILoggerFactory logger, - UrlEncoder encoder, - IClientAuthService clientAuth, - IUserReferenceService userReferenceService, - OpenShockContext db, - ISessionService sessionService, - IOptions jsonOptions, IBatchUpdateService batchUpdateService) - : base(options, logger, encoder) - { - _authService = clientAuth; - _userReferenceService = userReferenceService; - _db = db; - _sessionService = sessionService; - _serializerOptions = jsonOptions.Value.SerializerOptions; - _batchUpdateService = batchUpdateService; - } - - protected override Task HandleAuthenticateAsync() - { - if (Context.TryGetSessionKey(out var sessionKey)) - { - return SessionAuth(sessionKey); - } - - if (Context.TryGetAuthTokenFromHeader(out var token)) - { - return TokenAuth(token); - } - - return Task.FromResult(Fail(AuthResultError.CookieOrHeaderMissingOrInvalid)); - } - - private async Task TokenAuth(string 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); - - _batchUpdateService.UpdateTokenLastUsed(tokenDto.Id); - _authService.CurrentClient = new LinkUser - { - DbUser = tokenDto.User - }; - _userReferenceService.AuthReference = tokenDto; - - Context.Items["User"] = _authService.CurrentClient.DbUser.Id; - - var claims = new List - { - new(ClaimTypes.NameIdentifier, _authService.CurrentClient.DbUser.Id.ToString()), - new(ControlLogAdditionalItem.ApiTokenId, tokenDto.Id.ToString()) - }; - - var ident = new ClaimsIdentity(claims, nameof(LoginSessionAuthentication)); - var ticket = new AuthenticationTicket(new ClaimsPrincipal(ident), Scheme.Name); - - return AuthenticateResult.Success(ticket); - } - - private async Task SessionAuth(string sessionKey) - { - var session = await _sessionService.GetSessionById(sessionKey); - if (session == null) return Fail(AuthResultError.SessionInvalid); - - if (session.Expires!.Value < DateTime.UtcNow.Subtract(Duration.LoginSessionExpansionAfter)) - { -#pragma warning disable CS4014 - LucTask.Run(async () => -#pragma warning restore CS4014 - { - session.Expires = DateTime.UtcNow.Add(Duration.LoginSessionLifetime); - await _sessionService.UpdateSession(session, Duration.LoginSessionLifetime); - }); - } - - _batchUpdateService.UpdateSessionLastUsed(sessionKey, DateTime.UtcNow); - - var retrievedUser = await _db.Users.FirstAsync(user => user.Id == session.UserId); - - _userReferenceService.AuthReference = session; - _authService.CurrentClient = new LinkUser - { - DbUser = retrievedUser - }; - - Context.Items["User"] = _authService.CurrentClient.DbUser.Id; - - var claims = new List - { - new(ClaimTypes.NameIdentifier, _authService.CurrentClient.DbUser.Id.ToString()) - }; - - var ident = new ClaimsIdentity(claims, nameof(LoginSessionAuthentication)); - var ticket = new AuthenticationTicket(new ClaimsPrincipal(ident), Scheme.Name); - - return AuthenticateResult.Success(ticket); - } - - - private AuthenticateResult Fail(OpenShockProblem reason) - { - _authResultError = reason; - return AuthenticateResult.Fail(reason.Type!); - } - - /// - protected override Task HandleChallengeAsync(AuthenticationProperties properties) - { - _authResultError ??= AuthResultError.UnknownError; - Response.StatusCode = _authResultError.Status!.Value; - _authResultError.AddContext(Context); - return Context.Response.WriteAsJsonAsync(_authResultError, _serializerOptions, contentType: MediaTypeNames.Application.ProblemJson); - } -} \ No newline at end of file diff --git a/Common/Authentication/OpenShockAuthClaims.cs b/Common/Authentication/OpenShockAuthClaims.cs new file mode 100644 index 00000000..da7691e7 --- /dev/null +++ b/Common/Authentication/OpenShockAuthClaims.cs @@ -0,0 +1,7 @@ +namespace OpenShock.Common.Authentication; + +public class OpenShockAuthClaims +{ + public const string ApiTokenId = "apiTokenId"; + public const string ApiTokenPermission = "ApiTokenPermission"; +} diff --git a/Common/Authentication/OpenShockAuthPolicies.cs b/Common/Authentication/OpenShockAuthPolicies.cs new file mode 100644 index 00000000..eddc6a03 --- /dev/null +++ b/Common/Authentication/OpenShockAuthPolicies.cs @@ -0,0 +1,12 @@ +namespace OpenShock.Common.Authentication; + +public static class OpenShockAuthPolicies +{ + public const string UserAccess = "UserAccess"; + public const string SupportAccess = "SupportAccess"; + public const string StaffAccess = "StaffAccess"; + public const string AdminAccess = "AdminAccess"; + public const string SystemAccess = "SystemAccess"; + + public const string TokenSessionOnly = "ApiTokenOnly"; +} diff --git a/Common/Authentication/OpenShockAuthSchemas.cs b/Common/Authentication/OpenShockAuthSchemas.cs index 8328f9c6..51ce9db1 100644 --- a/Common/Authentication/OpenShockAuthSchemas.cs +++ b/Common/Authentication/OpenShockAuthSchemas.cs @@ -4,9 +4,9 @@ namespace OpenShock.Common.Authentication; public static class OpenShockAuthSchemas { - // TODO: What is this for? - public const string SessionTokenCombo = "session-token-combo"; + public const string UserSessionCookie = "UserSessionCookie"; + public const string ApiToken = "ApiToken"; + public const string HubToken = "HubToken"; - /// TODO: Replace this with ? - public const string DeviceToken = "device-token"; + public const string UserSessionApiTokenCombo = $"{UserSessionCookie},{ApiToken}"; } \ No newline at end of file diff --git a/Common/Authentication/Requirements/ApiTokenPermissionRequirement.cs b/Common/Authentication/Requirements/ApiTokenPermissionRequirement.cs new file mode 100644 index 00000000..275d565f --- /dev/null +++ b/Common/Authentication/Requirements/ApiTokenPermissionRequirement.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Authorization; +using OpenShock.Common.Models; + +namespace OpenShock.Common.Authentication.Requirements; + +public class ApiTokenPermissionRequirement : IAuthorizationRequirement +{ + public ApiTokenPermissionRequirement(PermissionType requiredPermission) + { + RequiredPermission = requiredPermission; + } + + public PermissionType RequiredPermission { get; init; } +} diff --git a/Common/Constants/AuthConstants.cs b/Common/Constants/AuthConstants.cs index 0215b079..653c7a6b 100644 --- a/Common/Constants/AuthConstants.cs +++ b/Common/Constants/AuthConstants.cs @@ -2,8 +2,8 @@ public static class AuthConstants { - public const string SessionCookieName = "openShockSession"; + public const string UserSessionCookieName = "openShockSession"; public const string SessionHeaderName = "OpenShockSession"; - public const string AuthTokenHeaderName = "OpenShockToken"; - public const string DeviceAuthTokenHeaderName = "DeviceToken"; + public const string ApiTokenHeaderName = "OpenShockToken"; + public const string HubTokenHeaderName = "DeviceToken"; } diff --git a/Common/Errors/AuthResultError.cs b/Common/Errors/AuthResultError.cs index 1be2dfa5..1c5d0fc5 100644 --- a/Common/Errors/AuthResultError.cs +++ b/Common/Errors/AuthResultError.cs @@ -6,7 +6,8 @@ namespace OpenShock.Common.Errors; public static class AuthResultError { public static OpenShockProblem UnknownError => new("Authentication.UnknownError", "An unknown error occurred.", HttpStatusCode.InternalServerError); - public static OpenShockProblem CookieOrHeaderMissingOrInvalid => new("Authentication.HeaderMissingOrInvalid", "Missing a required authentication cookie or header or it is invalid.", HttpStatusCode.Unauthorized); + public static OpenShockProblem CookieMissingOrInvalid => new("Authentication.CookieMissingOrInvalid", "Missing or invalid authentication cookie.", HttpStatusCode.Unauthorized); + public static OpenShockProblem HeaderMissingOrInvalid => new("Authentication.HeaderMissingOrInvalid", "Missing or invalid authentication header.", HttpStatusCode.Unauthorized); public static OpenShockProblem SessionInvalid => new("Authentication.SessionInvalid", "The session is invalid", HttpStatusCode.Unauthorized); public static OpenShockProblem TokenInvalid => new("Authentication.TokenInvalid", "The token is invalid", HttpStatusCode.Unauthorized); diff --git a/Common/Hubs/ShareLinkHub.cs b/Common/Hubs/ShareLinkHub.cs index 577c646a..0b975fea 100644 --- a/Common/Hubs/ShareLinkHub.cs +++ b/Common/Hubs/ShareLinkHub.cs @@ -35,8 +35,6 @@ public ShareLinkHub(OpenShockContext db, IHubContext userHub, public override async Task OnConnectedAsync() { - _tokenPermissions = _userReferenceService.AuthReference is not { IsT1: true } ? null : _userReferenceService.AuthReference.Value.AsT1.Permissions; - var httpContext = Context.GetHttpContext(); if (httpContext?.GetRouteValue("Id") is not string param || !Guid.TryParse(param, out var id)) { @@ -44,6 +42,21 @@ public override async Task OnConnectedAsync() Context.Abort(); return; } + + GenericIni? user = null; + + if (httpContext.TryGetUserSessionCookie(out var sessionCookie)) + { + user = await SessionAuth(sessionCookie); + if (user == null) + { + _logger.LogWarning("Connection tried authentication with invalid user session cookie, terminating connection..."); + Context.Abort(); + return; + } + } + + _tokenPermissions = _userReferenceService.AuthReference is not { IsT1: true } ? null : _userReferenceService.AuthReference.Value.AsT1.Permissions; var exists = await _db.ShockerSharesLinks.AnyAsync(x => x.Id == id && (x.ExpiresOn == null || x.ExpiresOn > DateTime.UtcNow)); if (!exists) @@ -52,13 +65,6 @@ public override async Task OnConnectedAsync() Context.Abort(); return; } - - GenericIni? user = null; - - if (httpContext.TryGetSessionKey(out var sessionKey)) - { - user = await SessionAuth(sessionKey); - } // TODO: Add token auth @@ -68,6 +74,7 @@ public override async Task OnConnectedAsync() { _logger.LogWarning("customName was not set nor was the user authenticated, terminating connection..."); Context.Abort(); + return; } var additionalItems = new Dictionary diff --git a/Common/Hubs/UserHub.cs b/Common/Hubs/UserHub.cs index ebec4364..4c982ab4 100644 --- a/Common/Hubs/UserHub.cs +++ b/Common/Hubs/UserHub.cs @@ -16,7 +16,7 @@ namespace OpenShock.Common.Hubs; -[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.SessionTokenCombo)] +[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionApiTokenCombo)] public sealed class UserHub : Hub { private readonly ILogger _logger; @@ -78,8 +78,8 @@ public async Task ControlV2(IEnumerable sh if (!_tokenPermissions.IsAllowedAllowOnNull(PermissionType.Shockers_Use)) return; var additionalItems = new Dictionary(); - var apiTokenId = Context.User?.FindFirst(ControlLogAdditionalItem.ApiTokenId); - if (apiTokenId != null) additionalItems[ControlLogAdditionalItem.ApiTokenId] = apiTokenId.Value; + var apiTokenId = Context.User?.FindFirst(OpenShockAuthClaims.ApiTokenId); + if (apiTokenId != null) additionalItems[OpenShockAuthClaims.ApiTokenId] = apiTokenId.Value; var sender = await _db.Users.Where(x => x.Id == UserId).Select(x => new ControlLogSender { diff --git a/Common/IQueryableExtensions.cs b/Common/IQueryableExtensions.cs index aae525ce..1e57c2f7 100644 --- a/Common/IQueryableExtensions.cs +++ b/Common/IQueryableExtensions.cs @@ -48,7 +48,7 @@ public static IQueryable WhereIsUserOrRank(this IQueryable WhereIsUserOrRank(this IQueryable source, Expression> navigationSelector, LinkUser user, RankType rank) + public static IQueryable WhereIsUserOrRank(this IQueryable source, Expression> navigationSelector, AuthenticatedUser user, RankType rank) { return WhereIsUserOrRank(source, navigationSelector, user.DbUser, rank); } @@ -58,7 +58,7 @@ public static IQueryable WhereIsUserOrAdmin(this IQueryable WhereIsUserOrAdmin(this IQueryable source, Expression> navigationSelector, LinkUser user) + public static IQueryable WhereIsUserOrAdmin(this IQueryable source, Expression> navigationSelector, AuthenticatedUser user) { return WhereIsUserOrRank(source, navigationSelector, user.DbUser, RankType.Admin); } diff --git a/Common/Models/ControlLogAdditionalItem.cs b/Common/Models/ControlLogAdditionalItem.cs index 90f8f947..abe50c9a 100644 --- a/Common/Models/ControlLogAdditionalItem.cs +++ b/Common/Models/ControlLogAdditionalItem.cs @@ -1,7 +1,9 @@ -namespace OpenShock.Common.Models; +using OpenShock.Common.Authentication; + +namespace OpenShock.Common.Models; public static class ControlLogAdditionalItem { - public const string ApiTokenId = "apiTokenId"; + public const string ApiTokenId = OpenShockAuthClaims.ApiTokenId; public const string ShareLinkId = "shareLinkId"; } \ No newline at end of file diff --git a/Common/OpenShockServiceHelper.cs b/Common/OpenShockServiceHelper.cs index cb641be3..1fb2d4a8 100644 --- a/Common/OpenShockServiceHelper.cs +++ b/Common/OpenShockServiceHelper.cs @@ -1,12 +1,15 @@ -using System.Text.Json; +using System.Security.Claims; +using System.Text.Json; using System.Text.Json.Serialization; using Asp.Versioning; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection.Extensions; using OpenShock.Common.Authentication; -using OpenShock.Common.Authentication.Handlers; +using OpenShock.Common.Authentication.AuthenticationHandlers; +using OpenShock.Common.Authentication.Requirements; using OpenShock.Common.Authentication.Services; using OpenShock.Common.Config; using OpenShock.Common.ExceptionHandle; @@ -26,6 +29,24 @@ namespace OpenShock.Common; public static class OpenShockServiceHelper { + // TODO: this is temporary while we still rely on enums for user ranks + static bool HandleRankCheck(AuthorizationHandlerContext context, RankType requiredRank) + { + var ranks = context.User.Identities.SelectMany(ident => ident.Claims.Where(claim => claim.Type == ident.RoleClaimType)).Select(claim => Enum.Parse(claim.Value)).ToList(); + + if (!ranks.Any()) + { + return false; + } + + if (ranks.Max() < requiredRank) + { + return false; + } + + return true; + } + /// /// Register all OpenShock services for PRODUCTION use /// @@ -39,18 +60,30 @@ public static ServicesResult AddOpenShockServices(this IServiceCollection servic // <---- ASP.NET ----> services.AddExceptionHandler(); - services.AddScoped, ClientAuthService>(); + services.AddScoped, ClientAuthService>(); services.AddScoped, ClientAuthService>(); services.AddScoped(); - - new AuthenticationBuilder(services) - .AddScheme( - OpenShockAuthSchemas.SessionTokenCombo, _ => { }) - .AddScheme( - OpenShockAuthSchemas.DeviceToken, _ => { }); - + services.AddAuthenticationCore(); - services.AddAuthorization(); + new AuthenticationBuilder(services) + .AddScheme( + OpenShockAuthSchemas.UserSessionCookie, _ => { }) + .AddScheme( + OpenShockAuthSchemas.ApiToken, _ => { }) + .AddScheme( + OpenShockAuthSchemas.HubToken, _ => { }); + + services.AddAuthorization(options => + { + options.AddPolicy(OpenShockAuthPolicies.SystemAccess, policy => policy.RequireRole(RankType.System.ToString())); + options.AddPolicy(OpenShockAuthPolicies.AdminAccess, policy => policy.RequireAssertion(context => HandleRankCheck(context, RankType.Admin))); + options.AddPolicy(OpenShockAuthPolicies.StaffAccess, policy => policy.RequireAssertion(context => HandleRankCheck(context, RankType.Staff))); + options.AddPolicy(OpenShockAuthPolicies.SupportAccess, policy => policy.RequireAssertion(context => HandleRankCheck(context, RankType.Support))); + options.AddPolicy(OpenShockAuthPolicies.UserAccess, policy => policy.RequireAssertion(context => HandleRankCheck(context, RankType.User))); + + options.AddPolicy(OpenShockAuthPolicies.TokenSessionOnly, policy => policy.RequireClaim(ClaimTypes.AuthenticationMethod, OpenShockAuthSchemas.ApiToken)); + // TODO: Add token permission policies + }); services.Configure(options => { diff --git a/Common/Services/Session/SessionService.cs b/Common/Services/Session/SessionService.cs index 9d70f508..5ca5cc78 100644 --- a/Common/Services/Session/SessionService.cs +++ b/Common/Services/Session/SessionService.cs @@ -1,4 +1,4 @@ -using OpenShock.Common.Authentication.Handlers; +using OpenShock.Common.Authentication.AuthenticationHandlers; using OpenShock.Common.Constants; using OpenShock.Common.Redis; using Redis.OM; diff --git a/Common/Swagger/AttributeFilter.cs b/Common/Swagger/AttributeFilter.cs new file mode 100644 index 00000000..6e606476 --- /dev/null +++ b/Common/Swagger/AttributeFilter.cs @@ -0,0 +1,119 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.OpenApi.Models; +using OpenShock.Common.Authentication; +using OpenShock.Common.DataAnnotations.Interfaces; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace OpenShock.Common.Swagger; + +public sealed class AttributeFilter : ISchemaFilter, IParameterFilter, IOperationFilter +{ + public void Apply(OpenApiParameter parameter, ParameterFilterContext context) + { + // Apply OpenShock Parameter Attributes + foreach (var attribute in context.ParameterInfo?.GetCustomAttributes(true).OfType() ?? []) + { + attribute.Apply(parameter); + } + + // Apply OpenShock Parameter Attributes + foreach (var attribute in context.PropertyInfo?.GetCustomAttributes(true).OfType() ?? []) + { + attribute.Apply(parameter); + } + } + + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + // Apply OpenShock Parameter Attributes + foreach (var attribute in context.MemberInfo?.GetCustomAttributes(true).OfType() ?? []) + { + attribute.Apply(schema); + } + } + + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + // Apply OpenShock Parameter Attributes + foreach (var attribute in context.MethodInfo?.GetCustomAttributes(true).OfType() ?? []) + { + attribute.Apply(operation); + } + + // Get Authorize attribute + var attributes = context.MethodInfo?.DeclaringType?.GetCustomAttributes(true) + .Union(context.MethodInfo.GetCustomAttributes(true)) + .OfType() + .ToList() ?? []; + + if (attributes.Count != 0) + { + if (attributes.Count(attr => !string.IsNullOrEmpty(attr.AuthenticationSchemes)) > 1) throw new Exception("Dunno what to apply to this method (multiple authentication attributes with schemes set)"); + + var scheme = attributes.Select(attr => attr.AuthenticationSchemes).SingleOrDefault(scheme => !string.IsNullOrEmpty(scheme)); + var roles = attributes.Select(attr => attr.Roles).Where(roles => !string.IsNullOrEmpty(roles)).SelectMany(roles => roles!.Split(',')).Select(role => role.Trim()).ToList(); + var policies = attributes.Select(attr => attr.Policy).Where(policies => !string.IsNullOrEmpty(policies)).SelectMany(policies => policies!.Split(',')).Select(policy => policy.Trim()).ToList(); + + // Add what should be show inside the security section + List securityInfos = []; + if (!string.IsNullOrEmpty(scheme)) securityInfos.Add($"{nameof(AuthorizeAttribute.AuthenticationSchemes)}:{scheme}"); + if (roles.Count > 0) securityInfos.Add($"{nameof(AuthorizeAttribute.Roles)}:{string.Join(',', roles)}"); + if (policies.Count > 0) securityInfos.Add($"{nameof(AuthorizeAttribute.Policy)}:{string.Join(',', policies)}"); + + List securityRequirements = []; + foreach (var authenticationScheme in scheme?.Split(',').Select(s => s.Trim()) ?? []) + { + securityRequirements.AddRange(authenticationScheme switch + { + OpenShockAuthSchemas.UserSessionCookie => [ + new OpenApiSecurityRequirement {{ + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Id = OpenShockAuthSchemas.UserSessionCookie, + Type = ReferenceType.SecurityScheme, + } + }, + securityInfos + }} + ], + OpenShockAuthSchemas.ApiToken => [ + new OpenApiSecurityRequirement {{ + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Id = OpenShockAuthSchemas.ApiToken, + Type = ReferenceType.SecurityScheme, + } + }, + securityInfos + }} + ], + OpenShockAuthSchemas.HubToken => [ + new OpenApiSecurityRequirement {{ + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Id = OpenShockAuthSchemas.HubToken, + Type = ReferenceType.SecurityScheme + } + }, + securityInfos + }} + ], + _ => [], + }); + } + + operation.Security = securityRequirements; + } + else + { + operation.Security.Clear(); + } + } +} \ No newline at end of file diff --git a/Common/Extensions/SwaggerGenExtensions.cs b/Common/Swagger/SwaggerGenExtensions.cs similarity index 62% rename from Common/Extensions/SwaggerGenExtensions.cs rename to Common/Swagger/SwaggerGenExtensions.cs index ab572c0c..d8e45ef4 100644 --- a/Common/Extensions/SwaggerGenExtensions.cs +++ b/Common/Swagger/SwaggerGenExtensions.cs @@ -9,19 +9,21 @@ using Microsoft.AspNetCore.Mvc; using System.Reflection; using System.Linq; +using OpenShock.Common.Extensions; +using OpenShock.Common.Authentication; -namespace OpenShock.Common.Extensions; +namespace OpenShock.Common.Swagger; public static class SwaggerGenExtensions { public static IServiceCollection AddSwaggerExt(this IServiceCollection services) where TProgram : class { var assembly = typeof(TProgram).Assembly; - + string assemblyName = assembly .GetName() .Name ?? throw new NullReferenceException("Assembly name"); - + var versions = assembly.GetAllControllerEndpointAttributes() .SelectMany(type => type.Versions) .Select(v => v.ToString()) @@ -35,40 +37,58 @@ public static IServiceCollection AddSwaggerExt(this IServiceCollection } return services - .AddSwaggerGen(options => { + .AddSwaggerGen(options => + { options.CustomOperationIds(e => $"{e.ActionDescriptor.RouteValues["controller"]}_{e.ActionDescriptor.AttributeRouteInfo?.Name ?? e.ActionDescriptor.RouteValues["action"]}"); options.SchemaFilter(); options.ParameterFilter(); options.OperationFilter(); options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, assemblyName + ".xml"), true); - options.AddSecurityDefinition(AuthConstants.AuthTokenHeaderName, new OpenApiSecurityScheme + options.AddSecurityDefinition(OpenShockAuthSchemas.UserSessionCookie, new OpenApiSecurityScheme { - Name = AuthConstants.AuthTokenHeaderName, + Name = AuthConstants.UserSessionCookieName, + Description = "Enter user session cookie", + In = ParameterLocation.Cookie, Type = SecuritySchemeType.ApiKey, - Scheme = "ApiKeyAuth", + Scheme = OpenShockAuthSchemas.UserSessionCookie, + Reference = new OpenApiReference + { + Id = OpenShockAuthSchemas.UserSessionCookie, + Type = ReferenceType.SecurityScheme, + } + }); + options.AddSecurityDefinition(OpenShockAuthSchemas.ApiToken, new OpenApiSecurityScheme + { + Name = AuthConstants.ApiTokenHeaderName, + Description = "Enter API Token", In = ParameterLocation.Header, - Description = "API Token Authorization header." + Type = SecuritySchemeType.ApiKey, + Scheme = OpenShockAuthSchemas.ApiToken, + Reference = new OpenApiReference + { + Id = OpenShockAuthSchemas.ApiToken, + Type = ReferenceType.SecurityScheme, + } }); - options.AddSecurityRequirement(new OpenApiSecurityRequirement + options.AddSecurityDefinition(OpenShockAuthSchemas.HubToken, new OpenApiSecurityScheme + { + Name = AuthConstants.HubTokenHeaderName, + Description = "Enter hub token", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey, + Scheme = OpenShockAuthSchemas.HubToken, + Reference = new OpenApiReference { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = AuthConstants.AuthTokenHeaderName - } - }, - Array.Empty() - } - }); + Id = OpenShockAuthSchemas.HubToken, + Type = ReferenceType.SecurityScheme, + } + }); options.AddServer(new OpenApiServer { Url = "https://api.openshock.app" }); options.AddServer(new OpenApiServer { Url = "https://staging-api.openshock.app" }); - #if DEBUG +#if DEBUG options.AddServer(new OpenApiServer { Url = "https://localhost" }); - #endif +#endif foreach (var version in versions) { options.SwaggerDoc("v" + version, new OpenApiInfo { Title = "OpenShock", Version = version }); diff --git a/Common/Utils/AuthUtils.cs b/Common/Utils/AuthUtils.cs index b7e74320..6a662a7f 100644 --- a/Common/Utils/AuthUtils.cs +++ b/Common/Utils/AuthUtils.cs @@ -6,18 +6,18 @@ namespace OpenShock.Common.Utils; public static class AuthUtils { private static readonly string[] TokenHeaderNames = [ - AuthConstants.AuthTokenHeaderName, + AuthConstants.ApiTokenHeaderName, "Open-Shock-Token", "ShockLinkToken" ]; private static readonly string[] DeviceTokenHeaderNames = [ - AuthConstants.DeviceAuthTokenHeaderName, + AuthConstants.HubTokenHeaderName, "Device-Token" ]; public static void SetSessionKeyCookie(this HttpContext context, string sessionKey, string domain) { - context.Response.Cookies.Append(AuthConstants.SessionCookieName, sessionKey, new CookieOptions + context.Response.Cookies.Append(AuthConstants.UserSessionCookieName, sessionKey, new CookieOptions { Expires = new DateTimeOffset(DateTime.UtcNow.Add(Duration.LoginSessionLifetime)), Secure = true, @@ -29,7 +29,7 @@ public static void SetSessionKeyCookie(this HttpContext context, string sessionK public static void RemoveSessionKeyCookie(this HttpContext context, string domain) { - context.Response.Cookies.Append(AuthConstants.SessionCookieName, string.Empty, new CookieOptions + context.Response.Cookies.Append(AuthConstants.UserSessionCookieName, string.Empty, new CookieOptions { Expires = DateTime.Now.AddDays(-1), Secure = true, @@ -39,43 +39,19 @@ public static void RemoveSessionKeyCookie(this HttpContext context, string domai }); } - public static bool TryGetSessionKeyFromCookie(this HttpContext context, [NotNullWhen(true)] out string? sessionKey) + public static bool TryGetUserSessionCookie(this HttpContext context, [NotNullWhen(true)] out string? sessionCookie) { - if (context.Request.Cookies.TryGetValue(AuthConstants.SessionCookieName, out sessionKey) && !string.IsNullOrEmpty(sessionKey)) + if (context.Request.Cookies.TryGetValue(AuthConstants.UserSessionCookieName, out sessionCookie) && !string.IsNullOrEmpty(sessionCookie)) { return true; } - sessionKey = null; + sessionCookie = null; return false; } - public static bool TryGetSessionAuthFromHeader(this HttpContext context, [NotNullWhen(true)] out string? sessionKey) - { - if (context.Request.Headers.TryGetValue(AuthConstants.SessionHeaderName, out var value) && !string.IsNullOrEmpty(value)) - { - sessionKey = value!; - - return true; - } - - sessionKey = null; - - return false; - } - - public static bool TryGetSessionKey(this HttpContext context, [NotNullWhen(true)] out string? sessionKey) - { - if (TryGetSessionKeyFromCookie(context, out sessionKey)) return true; - if (TryGetSessionAuthFromHeader(context, out sessionKey)) return true; - - sessionKey = null; - - return false; - } - - public static bool TryGetAuthTokenFromHeader(this HttpContext context, [NotNullWhen(true)] out string? token) + public static bool TryGetApiTokenFromHeader(this HttpContext context, [NotNullWhen(true)] out string? token) { foreach (string header in TokenHeaderNames) { diff --git a/Cron/DashboardAdminAuth.cs b/Cron/DashboardAdminAuth.cs index 6fa03c7b..48fb1a4e 100644 --- a/Cron/DashboardAdminAuth.cs +++ b/Cron/DashboardAdminAuth.cs @@ -19,9 +19,9 @@ public async Task AuthorizeAsync(DashboardContext context) var userSessions = redis.RedisCollection(false); var db = httpContext.RequestServices.GetRequiredService(); - if (httpContext.TryGetSessionKeyFromCookie(out var sessionKeyCookie)) + if (httpContext.TryGetUserSessionCookie(out var userSessionCookie)) { - if (await SessionAuthAdmin(sessionKeyCookie, userSessions, db)) + if (await SessionAuthAdmin(userSessionCookie, userSessions, db)) { return true; } diff --git a/LiveControlGateway/Controllers/HubControllerBase.cs b/LiveControlGateway/Controllers/HubControllerBase.cs index 8c64e751..988873cb 100644 --- a/LiveControlGateway/Controllers/HubControllerBase.cs +++ b/LiveControlGateway/Controllers/HubControllerBase.cs @@ -1,5 +1,6 @@ using System.Net.WebSockets; using FlatSharp; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.SignalR; @@ -7,6 +8,7 @@ using OneOf; using OneOf.Types; using OpenShock.Common; +using OpenShock.Common.Authentication; using OpenShock.Common.Authentication.Services; using OpenShock.Common.Constants; using OpenShock.Common.Errors; diff --git a/LiveControlGateway/Controllers/HubV1Controller.cs b/LiveControlGateway/Controllers/HubV1Controller.cs index e157e72e..ada6f041 100644 --- a/LiveControlGateway/Controllers/HubV1Controller.cs +++ b/LiveControlGateway/Controllers/HubV1Controller.cs @@ -19,9 +19,9 @@ namespace OpenShock.LiveControlGateway.Controllers; /// Communication with the hubs aka ESP-32 microcontrollers /// [ApiController] -[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.DeviceToken)] [ApiVersion("1")] [Route("/{version:apiVersion}/ws/device")] +[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.HubToken)] public sealed class HubV1Controller : HubControllerBase { private readonly IHubContext _userHubContext; diff --git a/LiveControlGateway/Controllers/HubV2Controller.cs b/LiveControlGateway/Controllers/HubV2Controller.cs index 7c8e9285..f603c7b4 100644 --- a/LiveControlGateway/Controllers/HubV2Controller.cs +++ b/LiveControlGateway/Controllers/HubV2Controller.cs @@ -21,9 +21,9 @@ namespace OpenShock.LiveControlGateway.Controllers; /// Communication with the hubs aka ESP-32 microcontrollers /// [ApiController] -[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.DeviceToken)] [ApiVersion("2")] [Route("/{version:apiVersion}/ws/hub")] +[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.HubToken)] public sealed class HubV2Controller : HubControllerBase { private readonly IHubContext _userHubContext; diff --git a/LiveControlGateway/Controllers/LiveControlController.cs b/LiveControlGateway/Controllers/LiveControlController.cs index a29a11e4..f89d6700 100644 --- a/LiveControlGateway/Controllers/LiveControlController.cs +++ b/LiveControlGateway/Controllers/LiveControlController.cs @@ -33,7 +33,7 @@ namespace OpenShock.LiveControlGateway.Controllers; [ApiController] [Route("/{version:apiVersion}/ws/live/{hubId:guid}")] [TokenPermission(PermissionType.Shockers_Use)] -[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.SessionTokenCombo)] +[Authorize(AuthenticationSchemes = OpenShockAuthSchemas.UserSessionApiTokenCombo)] public sealed class LiveControlController : WebsocketBaseController>, IActionFilter { private readonly OpenShockContext _db; @@ -51,7 +51,7 @@ public sealed class LiveControlController : WebsocketBaseController _sharedShockers = new(); @@ -187,7 +187,7 @@ public async Task UpdatePermissions(OpenShockContext db) [NonAction] public void OnActionExecuting(ActionExecutingContext context) { - _currentUser = ControllerContext.HttpContext.RequestServices.GetRequiredService>() + _currentUser = ControllerContext.HttpContext.RequestServices.GetRequiredService>() .CurrentClient; } diff --git a/LiveControlGateway/Program.cs b/LiveControlGateway/Program.cs index 1a6b0313..7138537a 100644 --- a/LiveControlGateway/Program.cs +++ b/LiveControlGateway/Program.cs @@ -3,6 +3,7 @@ using OpenShock.Common.JsonSerialization; using OpenShock.Common.Services.Device; using OpenShock.Common.Services.Ota; +using OpenShock.Common.Swagger; using OpenShock.LiveControlGateway; using OpenShock.LiveControlGateway.LifetimeManager; using OpenShock.LiveControlGateway.PubSub; From 10f8929de546fbfbfa5de601f1b8d4147507b260 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Fri, 29 Nov 2024 00:47:47 +0100 Subject: [PATCH 14/22] Better Turnstile response handling --- API/Controller/Account/LoginV2.cs | 9 +++- .../Turnstile/CloduflareTurnstileError.cs | 12 ++++++ .../Turnstile/CloudflareTurnstileService.cs | 43 +++++++++++++++---- .../Turnstile/ICloudflareTurnstileService.cs | 2 +- 4 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 Common/Services/Turnstile/CloduflareTurnstileError.cs diff --git a/API/Controller/Account/LoginV2.cs b/API/Controller/Account/LoginV2.cs index c1bd753c..144267ab 100644 --- a/API/Controller/Account/LoginV2.cs +++ b/API/Controller/Account/LoginV2.cs @@ -38,7 +38,14 @@ public async Task LoginV2( var remoteIP = HttpContext.GetRemoteIP(); var turnStile = await turnstileService.VerifyUserResponseToken(body.TurnstileResponse, remoteIP, cancellationToken); - if (!turnStile.IsT0) return Problem(TurnstileError.InvalidTurnstile); + if (!turnStile.IsT0) + { + var cfErrors = turnStile.AsT1.Value!; + if (cfErrors.All(err => err == CloduflareTurnstileError.InvalidResponse)) + return Problem(TurnstileError.InvalidTurnstile); + + return Problem(new OpenShockProblem("InternalServerError", "Internal Server Error", HttpStatusCode.InternalServerError)); + } var loginAction = await _accountService.Login(body.UsernameOrEmail, body.Password, new LoginContext { diff --git a/Common/Services/Turnstile/CloduflareTurnstileError.cs b/Common/Services/Turnstile/CloduflareTurnstileError.cs new file mode 100644 index 00000000..1acaf090 --- /dev/null +++ b/Common/Services/Turnstile/CloduflareTurnstileError.cs @@ -0,0 +1,12 @@ +namespace OpenShock.Common.Services.Turnstile; + +public enum CloduflareTurnstileError +{ + MissingSecret, + InvalidSecret, + MissingResponse, + InvalidResponse, + BadRequest, + TimeoutOrDuplicate, + InternalServerError, +} \ No newline at end of file diff --git a/Common/Services/Turnstile/CloudflareTurnstileService.cs b/Common/Services/Turnstile/CloudflareTurnstileService.cs index bd72e11b..4b114ea7 100644 --- a/Common/Services/Turnstile/CloudflareTurnstileService.cs +++ b/Common/Services/Turnstile/CloudflareTurnstileService.cs @@ -23,11 +23,31 @@ public CloudflareTurnstileService(HttpClient httpClient, IOptions CreateError(params ReadOnlySpan errors) + { + return new Error(errors.ToArray()); + } + + private static CloduflareTurnstileError MapCfError(string error) + { + return error switch + { + "missing-input-secret" => CloduflareTurnstileError.MissingSecret, + "invalid-input-secret" => CloduflareTurnstileError.InvalidSecret, + "missing-input-response" => CloduflareTurnstileError.MissingResponse, + "invalid-input-response" => CloduflareTurnstileError.InvalidResponse, + "bad-request" => CloduflareTurnstileError.BadRequest, + "timeout-or-duplicate" => CloduflareTurnstileError.TimeoutOrDuplicate, + "internal-error" => CloduflareTurnstileError.InternalServerError, + _ => throw new ArgumentOutOfRangeException(nameof(error), error, null) + }; + } + /// - public async Task>>> VerifyUserResponseToken( + public async Task>> VerifyUserResponseToken( string responseToken, IPAddress? remoteIpAddress, CancellationToken cancellationToken = default) { - if (string.IsNullOrEmpty(responseToken)) return new MissingInput(); + if (string.IsNullOrEmpty(responseToken)) return CreateError(CloduflareTurnstileError.MissingResponse); #if DEBUG if (responseToken == "dev-bypass") return new Success(); @@ -46,18 +66,23 @@ public async Task( - cancellationToken: cancellationToken); + var response = await httpResponse.Content.ReadFromJsonAsync(cancellationToken); if (response.Success) return new Success(); + + var errors = response.ErrorCodes.Select(MapCfError).ToArray(); + + if (errors.All(err => err != CloduflareTurnstileError.InvalidResponse)) + { + _logger.LogError("Turnstile error: {StatusCode} {ReasonPhrase}", httpResponse.StatusCode, string.Join(" ", errors.Select(err => err.ToString()))); + } - return new Error>(response.ErrorCodes); + return CreateError(errors); } } diff --git a/Common/Services/Turnstile/ICloudflareTurnstileService.cs b/Common/Services/Turnstile/ICloudflareTurnstileService.cs index 583c7a0f..fd91204f 100644 --- a/Common/Services/Turnstile/ICloudflareTurnstileService.cs +++ b/Common/Services/Turnstile/ICloudflareTurnstileService.cs @@ -12,6 +12,6 @@ public interface ICloudflareTurnstileService /// /// /// Success, No response token was supplied, internal error in cloudflare turnstile, business logic error on turnstile validation - public Task>>> VerifyUserResponseToken( + public Task>> VerifyUserResponseToken( string responseToken, IPAddress? remoteIpAddress, CancellationToken cancellationToken = default); } \ No newline at end of file From 6464d3e2c9819d4829493426500a34a10385848b Mon Sep 17 00:00:00 2001 From: hhvrc Date: Fri, 29 Nov 2024 01:06:37 +0100 Subject: [PATCH 15/22] Fail early on bad turnstile configuration --- API/Controller/Account/SignupV2.cs | 12 +++++++----- API/Program.cs | 7 +++++++ .../Services/Turnstile/CloudflareTurnstileOptions.cs | 1 + .../Services/Turnstile/CloudflareTurnstileService.cs | 9 +++++---- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/API/Controller/Account/SignupV2.cs b/API/Controller/Account/SignupV2.cs index ffb6daab..ae2a5105 100644 --- a/API/Controller/Account/SignupV2.cs +++ b/API/Controller/Account/SignupV2.cs @@ -19,7 +19,6 @@ public sealed partial class AccountController /// /// /// - /// /// /// User successfully signed up /// Username or email already exists @@ -31,13 +30,16 @@ public sealed partial class AccountController public async Task SignUpV2( [FromBody] SignUpV2 body, [FromServices] ICloudflareTurnstileService turnstileService, - [FromServices] ApiConfig apiConfig, CancellationToken cancellationToken) { - if (apiConfig.Turnstile.Enabled) + var turnStile = await turnstileService.VerifyUserResponseToken(body.TurnstileResponse, HttpContext.GetRemoteIP(), cancellationToken); + if (!turnStile.IsT0) { - var turnStile = await turnstileService.VerifyUserResponseToken(body.TurnstileResponse, HttpContext.GetRemoteIP(), cancellationToken); - if (!turnStile.IsT0) return Problem(TurnstileError.InvalidTurnstile); + var cfErrors = turnStile.AsT1.Value!; + if (cfErrors.All(err => err == CloduflareTurnstileError.InvalidResponse)) + return Problem(TurnstileError.InvalidTurnstile); + + return Problem(new OpenShockProblem("InternalServerError", "Internal Server Error", HttpStatusCode.InternalServerError)); } var creationAction = await _accountService.Signup(body.Email, body.Username, body.Password); diff --git a/API/Program.cs b/API/Program.cs index bdb71b66..047eca83 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -1,3 +1,4 @@ +using System.Configuration; using Microsoft.AspNetCore.Http.Connections; using Microsoft.EntityFrameworkCore; using OpenShock.API; @@ -49,8 +50,14 @@ builder.Services.AddSingleton(x => { + if (config.Turnstile.Enabled && (string.IsNullOrWhiteSpace(config.Turnstile.SecretKey) || string.IsNullOrWhiteSpace(config.Turnstile.SecretKey))) + { + throw new ConfigurationErrorsException("Turnstile is enabled in config, but secretkey and/or token is missing or empty"); + } + return new CloudflareTurnstileOptions { + Enabled = config.Turnstile.Enabled, SecretKey = config.Turnstile.SecretKey ?? string.Empty, SiteKey = config.Turnstile.SiteKey ?? string.Empty }; diff --git a/Common/Services/Turnstile/CloudflareTurnstileOptions.cs b/Common/Services/Turnstile/CloudflareTurnstileOptions.cs index e2aae0ab..6eab032f 100644 --- a/Common/Services/Turnstile/CloudflareTurnstileOptions.cs +++ b/Common/Services/Turnstile/CloudflareTurnstileOptions.cs @@ -2,6 +2,7 @@ public sealed class CloudflareTurnstileOptions { + public required bool Enabled { get; set; } public required string SiteKey { get; set; } public required string SecretKey { get; set; } } \ No newline at end of file diff --git a/Common/Services/Turnstile/CloudflareTurnstileService.cs b/Common/Services/Turnstile/CloudflareTurnstileService.cs index 4b114ea7..9fd9f08a 100644 --- a/Common/Services/Turnstile/CloudflareTurnstileService.cs +++ b/Common/Services/Turnstile/CloudflareTurnstileService.cs @@ -7,15 +7,14 @@ namespace OpenShock.Common.Services.Turnstile; public sealed class CloudflareTurnstileService : ICloudflareTurnstileService { - public const string BaseUrl = "https://challenges.cloudflare.com/turnstile/v0/"; + private const string BaseUrl = "https://challenges.cloudflare.com/turnstile/v0/"; private const string SiteVerifyEndpoint = "siteverify"; private readonly HttpClient _httpClient; private readonly CloudflareTurnstileOptions _options; private readonly ILogger _logger; - public CloudflareTurnstileService(HttpClient httpClient, IOptions options, - ILogger logger) + public CloudflareTurnstileService(HttpClient httpClient, IOptions options, ILogger logger) { _httpClient = httpClient; _httpClient.BaseAddress = new Uri(BaseUrl); @@ -47,12 +46,14 @@ private static CloduflareTurnstileError MapCfError(string error) public async Task>> VerifyUserResponseToken( string responseToken, IPAddress? remoteIpAddress, CancellationToken cancellationToken = default) { + if (!_options.Enabled) return new Success(); + if (string.IsNullOrEmpty(responseToken)) return CreateError(CloduflareTurnstileError.MissingResponse); #if DEBUG if (responseToken == "dev-bypass") return new Success(); #endif - + var formUrlValues = new Dictionary { { "secret", _options.SecretKey }, From 6ae98cb7dc3e56d7f041c2fa6079ea8af99d4b67 Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 29 Nov 2024 21:28:32 +0100 Subject: [PATCH 16/22] Fix error logging on config validation error --- Common/Extensions/ConfigurationExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Common/Extensions/ConfigurationExtensions.cs b/Common/Extensions/ConfigurationExtensions.cs index 50f6c90e..24088099 100644 --- a/Common/Extensions/ConfigurationExtensions.cs +++ b/Common/Extensions/ConfigurationExtensions.cs @@ -30,7 +30,7 @@ public static T GetAndRegisterOpenShockConfig(this WebApplicationBuilder buil sb.AppendLine($"Error on field [{error.Key}] reason: {string.Join(", ", error.Value)}"); } - Log.Error(sb.ToString()); + Console.WriteLine(sb.ToString()); Environment.Exit(-10); } From 0f33079a03ce1757c91694bd2766d7bc58dfae74 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 30 Nov 2024 00:30:43 +0100 Subject: [PATCH 17/22] Remove unused redis channels --- API/Realtime/RedisSubscriberService.cs | 5 ++--- Common/Services/RedisPubSub/RedisChannels.cs | 4 +--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/API/Realtime/RedisSubscriberService.cs b/API/Realtime/RedisSubscriberService.cs index 0408046f..bd5c5e0a 100644 --- a/API/Realtime/RedisSubscriberService.cs +++ b/API/Realtime/RedisSubscriberService.cs @@ -47,9 +47,8 @@ public RedisSubscriberService( /// public async Task StartAsync(CancellationToken cancellationToken) { - await _subscriber.SubscribeAsync(RedisChannels.KeyEventExpired, (_, message) => { LucTask.Run(() => HandleKeyDelOrExpired(message)); }); + await _subscriber.SubscribeAsync(RedisChannels.KeyEventExpired, (_, message) => { LucTask.Run(() => HandleKeyExpired(message)); }); await _subscriber.SubscribeAsync(RedisChannels.DeviceOnlineStatus, (_, message) => { LucTask.Run(() => HandleDeviceOnlineStatus(message)); }); - await _subscriber.SubscribeAsync(RedisChannels.KeyEventDel, (_, message) => { LucTask.Run(() => HandleKeyDelOrExpired(message)); }); } private async Task HandleDeviceOnlineStatus(RedisValue message) @@ -61,7 +60,7 @@ private async Task HandleDeviceOnlineStatus(RedisValue message) await LogicDeviceOnlineStatus(data.Id); } - private async Task HandleKeyDelOrExpired(RedisValue message) + private async Task HandleKeyExpired(RedisValue message) { if (!message.HasValue) return; var msg = message.ToString().Split(':'); diff --git a/Common/Services/RedisPubSub/RedisChannels.cs b/Common/Services/RedisPubSub/RedisChannels.cs index 4c2bc243..f96df5a5 100644 --- a/Common/Services/RedisPubSub/RedisChannels.cs +++ b/Common/Services/RedisPubSub/RedisChannels.cs @@ -5,9 +5,7 @@ namespace OpenShock.Common.Services.RedisPubSub; public static class RedisChannels { public static readonly RedisChannel KeyEventExpired = new("__keyevent@0__:expired", RedisChannel.PatternMode.Literal); - public static readonly RedisChannel KeyEventJsonSet = new("__keyevent@0__:json.set", RedisChannel.PatternMode.Literal); - public static readonly RedisChannel KeyEventDel = new("__keyevent@0__:del", RedisChannel.PatternMode.Literal); - + public static readonly RedisChannel DeviceControl = new("msg-device-control", RedisChannel.PatternMode.Literal); public static readonly RedisChannel DeviceCaptive = new("msg-device-control-captive", RedisChannel.PatternMode.Literal); public static readonly RedisChannel DeviceUpdate = new("msg-device-update", RedisChannel.PatternMode.Literal); From 9c55623c95d0d54737a9dafbb087285592a990a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 09:49:57 +0100 Subject: [PATCH 18/22] build(deps): Bump the nuget-dependencies group with 14 updates (#139) Bumps the nuget-dependencies group with 14 updates: | Package | From | To | | --- | --- | --- | | [Fluid.Core](https://github.com/sebastienros/fluid) | `2.13.1` | `2.14.0` | | [Scalar.AspNetCore](https://github.com/scalar/scalar) | `1.2.44` | `1.2.45` | | [StackExchange.Redis](https://github.com/StackExchange/StackExchange.Redis) | `2.8.16` | `2.8.22` | | [Swashbuckle.AspNetCore.Swagger](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) | `7.0.0` | `7.1.0` | | [Swashbuckle.AspNetCore.SwaggerGen](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) | `7.0.0` | `7.1.0` | | [Swashbuckle.AspNetCore.SwaggerUI](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) | `7.0.0` | `7.1.0` | | [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) | `7.0.0` | `7.1.0` | | [TUnit](https://github.com/thomhurst/TUnit) | `0.4.1` | `0.4.45` | | [OpenTelemetry.Instrumentation.Http](https://github.com/open-telemetry/opentelemetry-dotnet-contrib) | `1.9.0` | `1.10.0` | | [Swashbuckle.AspNetCore.Annotations](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) | `7.0.0` | `7.1.0` | | [Microsoft.EntityFrameworkCore](https://github.com/dotnet/efcore) | `9.0.0` | `9.0.0` | | [Microsoft.EntityFrameworkCore.Relational](https://github.com/dotnet/efcore) | `9.0.0` | `9.0.0` | | Z.EntityFramework.Plus.EFCore | `9.103.6.2` | `9.103.6.3` | | [Hangfire.AspNetCore](https://github.com/HangfireIO/Hangfire) | `1.8.15` | `1.8.16` | Updates `Fluid.Core` from 2.13.1 to 2.14.0 - [Release notes](https://github.com/sebastienros/fluid/releases) - [Commits](https://github.com/sebastienros/fluid/compare/v2.13.1...v2.14.0) Updates `Scalar.AspNetCore` from 1.2.44 to 1.2.45 - [Changelog](https://github.com/scalar/scalar/blob/main/RELEASE.md) - [Commits](https://github.com/scalar/scalar/commits) Updates `StackExchange.Redis` from 2.8.16 to 2.8.22 - [Release notes](https://github.com/StackExchange/StackExchange.Redis/releases) - [Changelog](https://github.com/StackExchange/StackExchange.Redis/blob/main/docs/ReleaseNotes.md) - [Commits](https://github.com/StackExchange/StackExchange.Redis/compare/2.8.16...2.8.22) Updates `Swashbuckle.AspNetCore.Swagger` from 7.0.0 to 7.1.0 - [Release notes](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/releases) - [Commits](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/compare/v7.0.0...v7.1.0) Updates `Swashbuckle.AspNetCore.SwaggerGen` from 7.0.0 to 7.1.0 - [Release notes](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/releases) - [Commits](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/compare/v7.0.0...v7.1.0) Updates `Swashbuckle.AspNetCore.SwaggerUI` from 7.0.0 to 7.1.0 - [Release notes](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/releases) - [Commits](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/compare/v7.0.0...v7.1.0) Updates `Swashbuckle.AspNetCore` from 7.0.0 to 7.1.0 - [Release notes](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/releases) - [Commits](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/compare/v7.0.0...v7.1.0) Updates `TUnit` from 0.4.1 to 0.4.45 - [Commits](https://github.com/thomhurst/TUnit/commits) Updates `OpenTelemetry.Instrumentation.Http` from 1.9.0 to 1.10.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/releases) - [Commits](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/compare/Exporter.Geneva-1.9.0...Exporter.Geneva-1.10.0) Updates `Swashbuckle.AspNetCore.Annotations` from 7.0.0 to 7.1.0 - [Release notes](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/releases) - [Commits](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/compare/v7.0.0...v7.1.0) Updates `Swashbuckle.AspNetCore.Swagger` from 7.0.0 to 7.1.0 - [Release notes](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/releases) - [Commits](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/compare/v7.0.0...v7.1.0) Updates `Swashbuckle.AspNetCore.SwaggerGen` from 7.0.0 to 7.1.0 - [Release notes](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/releases) - [Commits](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/compare/v7.0.0...v7.1.0) Updates `Microsoft.EntityFrameworkCore` from 9.0.0 to 9.0.0 - [Release notes](https://github.com/dotnet/efcore/releases) - [Commits](https://github.com/dotnet/efcore/compare/v9.0.0...v9.0.0) Updates `Microsoft.EntityFrameworkCore.Relational` from 9.0.0 to 9.0.0 - [Release notes](https://github.com/dotnet/efcore/releases) - [Commits](https://github.com/dotnet/efcore/compare/v9.0.0...v9.0.0) Updates `Z.EntityFramework.Plus.EFCore` from 9.103.6.2 to 9.103.6.3 Updates `Hangfire.AspNetCore` from 1.8.15 to 1.8.16 - [Release notes](https://github.com/HangfireIO/Hangfire/releases) - [Commits](https://github.com/HangfireIO/Hangfire/compare/v1.8.15...v1.8.16) --- updated-dependencies: - dependency-name: Fluid.Core dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-dependencies - dependency-name: Scalar.AspNetCore dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-dependencies - dependency-name: StackExchange.Redis dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-dependencies - dependency-name: Swashbuckle.AspNetCore.Swagger dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-dependencies - dependency-name: Swashbuckle.AspNetCore.SwaggerGen dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-dependencies - dependency-name: Swashbuckle.AspNetCore.SwaggerUI dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-dependencies - dependency-name: Swashbuckle.AspNetCore dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-dependencies - dependency-name: TUnit dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-dependencies - dependency-name: OpenTelemetry.Instrumentation.Http dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-dependencies - dependency-name: Swashbuckle.AspNetCore.Annotations dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-dependencies - dependency-name: Swashbuckle.AspNetCore.Swagger dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-dependencies - dependency-name: Swashbuckle.AspNetCore.SwaggerGen dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-dependencies - dependency-name: Microsoft.EntityFrameworkCore dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-dependencies - dependency-name: Microsoft.EntityFrameworkCore.Relational dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-dependencies - dependency-name: Z.EntityFramework.Plus.EFCore dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-dependencies - dependency-name: Hangfire.AspNetCore dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- API/API.csproj | 14 +++++++------- Common.Tests/Common.Tests.csproj | 2 +- Common/Common.csproj | 14 +++++++------- Cron/Cron.csproj | 2 +- LiveControlGateway/LiveControlGateway.csproj | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/API/API.csproj b/API/API.csproj index 57b7be35..764a3380 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -23,7 +23,7 @@ - + @@ -39,16 +39,16 @@ - + - - - - - + + + + + diff --git a/Common.Tests/Common.Tests.csproj b/Common.Tests/Common.Tests.csproj index f27d4e36..f85eb62a 100644 --- a/Common.Tests/Common.Tests.csproj +++ b/Common.Tests/Common.Tests.csproj @@ -13,7 +13,7 @@ - + diff --git a/Common/Common.csproj b/Common/Common.csproj index 1c1a5822..b49d2d27 100644 --- a/Common/Common.csproj +++ b/Common/Common.csproj @@ -37,7 +37,7 @@ - + @@ -47,12 +47,12 @@ - - - - - - + + + + + + diff --git a/Cron/Cron.csproj b/Cron/Cron.csproj index 28e33098..fdc92a17 100644 --- a/Cron/Cron.csproj +++ b/Cron/Cron.csproj @@ -12,7 +12,7 @@ - + diff --git a/LiveControlGateway/LiveControlGateway.csproj b/LiveControlGateway/LiveControlGateway.csproj index 186bfd2f..40dfaa63 100644 --- a/LiveControlGateway/LiveControlGateway.csproj +++ b/LiveControlGateway/LiveControlGateway.csproj @@ -21,7 +21,7 @@ - + From b85c8b6a39a35b6176abbb66314830efd90f04da Mon Sep 17 00:00:00 2001 From: hhvrc Date: Wed, 4 Dec 2024 22:18:44 +0100 Subject: [PATCH 19/22] Add logging and seperate out magic number for ClearOldPasswordREsets CRON job --- Common/Constants/Constants.cs | 2 ++ Cron/Jobs/ClearOldPasswordResetsJob.cs | 23 +++++++++++++---------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/Common/Constants/Constants.cs b/Common/Constants/Constants.cs index 745e9833..2d5d94ce 100644 --- a/Common/Constants/Constants.cs +++ b/Common/Constants/Constants.cs @@ -2,6 +2,8 @@ public static class Duration { + public static readonly TimeSpan AuditRetentionTime = TimeSpan.FromDays(90); + public static readonly TimeSpan PasswordResetRequestLifetime = TimeSpan.FromHours(1); public static readonly TimeSpan NameChangeCooldown = TimeSpan.FromDays(7); diff --git a/Cron/Jobs/ClearOldPasswordResetsJob.cs b/Cron/Jobs/ClearOldPasswordResetsJob.cs index 83a4e3ac..9c0a28dc 100644 --- a/Cron/Jobs/ClearOldPasswordResetsJob.cs +++ b/Cron/Jobs/ClearOldPasswordResetsJob.cs @@ -7,32 +7,35 @@ namespace OpenShock.Cron.Jobs; /// -/// Deletes old password requests if they have expired their lifetime and havent been used +/// Deletes old password requests if they have expired their lifetime and haven't been used /// [CronJob("0 0 * * *")] // Every day at midnight (https://crontab.guru/) public sealed class ClearOldPasswordResetsJob { private readonly OpenShockContext _db; + private readonly ILogger _logger; /// /// DI constructor /// - /// - public ClearOldPasswordResetsJob(OpenShockContext db) + /// + /// + public ClearOldPasswordResetsJob(OpenShockContext db, ILogger logger) { _db = db; + _logger = logger; } public async Task Execute() { - // Delete all password reset requests that have not been used and are older than the lifetime. - // Leave expired requests that have been used for 14 days for moderation purposes. - var earliestCreatedOn = DateTime.Now - (Duration.PasswordResetRequestLifetime + TimeSpan.FromDays(14)); - var earliestCreatedOnUtc = DateTime.SpecifyKind(earliestCreatedOn, DateTimeKind.Utc); + var expiredAtUtc = DateTime.UtcNow - Duration.PasswordResetRequestLifetime; + var earliestCreatedOnUtc = expiredAtUtc - Duration.AuditRetentionTime; // Run the delete query - await _db.PasswordResets - .Where(x => x.UsedOn == null && x.CreatedOn < earliestCreatedOnUtc) - .ExecuteDeleteAsync(); + int nDeleted = await _db.PasswordResets + .Where(x => x.UsedOn == null && x.CreatedOn < earliestCreatedOnUtc) + .ExecuteDeleteAsync(); + + _logger.LogInformation("Deleted {deletedCount} expired password resets since {earliestCreatedOnUtc}", nDeleted, earliestCreatedOnUtc); } } \ No newline at end of file From 0058871cfb0cefc2ea3ed8d3eaef3280f0fc0516 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Thu, 5 Dec 2024 16:24:09 +0100 Subject: [PATCH 20/22] Replace AuthenticatedUser with DB User --- .../Account/Authenticated/ChangePassword.cs | 4 +- .../Account/Authenticated/ChangeUsername.cs | 4 +- API/Controller/Devices/DeviceOtaController.cs | 2 +- API/Controller/Devices/DevicesController.cs | 22 +++++----- API/Controller/Devices/ShockersController.cs | 2 +- API/Controller/Sessions/ListSessions.cs | 2 +- API/Controller/Shares/LinkShareCode.cs | 8 ++-- API/Controller/Shares/Links/AddShocker.cs | 4 +- .../Shares/Links/CreateShareLink.cs | 2 +- .../Shares/Links/EditShockerShareLink.cs | 2 +- API/Controller/Shares/Links/ListShareLinks.cs | 2 +- .../Shares/Links/PauseShockerShareLink.cs | 2 +- API/Controller/Shares/V2CreateShareRequest.cs | 6 +-- API/Controller/Shares/V2GetShares.cs | 4 +- API/Controller/Shares/V2Requests.cs | 14 +++---- .../Shockers/ControlLogController.cs | 2 +- .../Shockers/ControlShockerController.cs | 6 +-- .../Shockers/CreateShockerController.cs | 4 +- .../Shockers/DeleteShockerController.cs | 2 +- .../Shockers/GetShockerController.cs | 2 +- .../Shockers/OwnShockerController.cs | 2 +- .../Shockers/PatchShockerController.cs | 8 ++-- .../Shockers/PauseShockerController.cs | 4 +- .../Shockers/ShareShockerController.cs | 18 ++++---- .../Shockers/SharedShockersController.cs | 2 +- API/Controller/Tokens/TokenController.cs | 8 ++-- API/Controller/Users/GetSelf.cs | 14 +++---- Common/Authentication/AuthenticatedUser.cs | 41 ------------------- .../ApiTokenAuthentication.cs | 12 ++---- .../UserSessionAuthentication.cs | 9 ++-- .../AuthenticatedSessionControllerBase.cs | 5 ++- Common/Extensions/UserExtensions.cs | 39 ++++++++++++++++++ Common/IQueryableExtensions.cs | 20 +++------ Common/OpenShockServiceHelper.cs | 2 +- .../Controllers/LiveControlController.cs | 16 ++++---- 35 files changed, 138 insertions(+), 158 deletions(-) delete mode 100644 Common/Authentication/AuthenticatedUser.cs create mode 100644 Common/Extensions/UserExtensions.cs diff --git a/API/Controller/Account/Authenticated/ChangePassword.cs b/API/Controller/Account/Authenticated/ChangePassword.cs index 60647d2e..8c8807b5 100644 --- a/API/Controller/Account/Authenticated/ChangePassword.cs +++ b/API/Controller/Account/Authenticated/ChangePassword.cs @@ -18,12 +18,12 @@ public sealed partial class AuthenticatedAccountController [ProducesResponseType(StatusCodes.Status200OK)] public async Task ChangePassword(ChangePasswordRequest data) { - if (!PasswordHashingUtils.VerifyPassword(data.OldPassword, CurrentUser.DbUser.PasswordHash).Verified) + if (!PasswordHashingUtils.VerifyPassword(data.OldPassword, CurrentUser.PasswordHash).Verified) { return Problem(AccountError.PasswordChangeInvalidPassword); } - var result = await _accountService.ChangePassword(CurrentUser.DbUser.Id, data.NewPassword); + var result = await _accountService.ChangePassword(CurrentUser.Id, data.NewPassword); return result.Match(success => Ok(), notFound => throw new Exception("Unexpected result, apparently our current user does not exist...")); diff --git a/API/Controller/Account/Authenticated/ChangeUsername.cs b/API/Controller/Account/Authenticated/ChangeUsername.cs index 61f1fe68..ac59b15a 100644 --- a/API/Controller/Account/Authenticated/ChangeUsername.cs +++ b/API/Controller/Account/Authenticated/ChangeUsername.cs @@ -23,8 +23,8 @@ public sealed partial class AuthenticatedAccountController [ProducesResponseType(StatusCodes.Status403Forbidden, MediaTypeNames.Application.ProblemJson)] // UsernameRecentlyChanged public async Task ChangeUsername(ChangeUsernameRequest data) { - var result = await _accountService.ChangeUsername(CurrentUser.DbUser.Id, data.Username, - CurrentUser.DbUser.Rank.IsAllowed(RankType.Staff)); + var result = await _accountService.ChangeUsername(CurrentUser.Id, data.Username, + CurrentUser.Rank.IsAllowed(RankType.Staff)); return result.Match( success => Ok(), diff --git a/API/Controller/Devices/DeviceOtaController.cs b/API/Controller/Devices/DeviceOtaController.cs index 1d2c983c..b38f08f0 100644 --- a/API/Controller/Devices/DeviceOtaController.cs +++ b/API/Controller/Devices/DeviceOtaController.cs @@ -32,7 +32,7 @@ public async Task GetOtaUpdateHistory([FromRoute] Guid deviceId, { // Check if user owns device or has a share var deviceExistsAndYouHaveAccess = await _db.Devices.AnyAsync(x => - x.Id == deviceId && x.Owner == CurrentUser.DbUser.Id); + x.Id == deviceId && x.Owner == CurrentUser.Id); if (!deviceExistsAndYouHaveAccess) return Problem(DeviceError.DeviceNotFound); return RespondSuccessLegacy(await otaService.GetUpdates(deviceId)); diff --git a/API/Controller/Devices/DevicesController.cs b/API/Controller/Devices/DevicesController.cs index eba1343b..ba39e629 100644 --- a/API/Controller/Devices/DevicesController.cs +++ b/API/Controller/Devices/DevicesController.cs @@ -27,7 +27,7 @@ public sealed partial class DevicesController [MapToApiVersion("1")] public async Task ListDevices() { - var devices = await _db.Devices.Where(x => x.Owner == CurrentUser.DbUser.Id) + var devices = await _db.Devices.Where(x => x.Owner == CurrentUser.Id) .Select(x => new Models.Response.ResponseDevice { Id = x.Id, @@ -52,7 +52,7 @@ public async Task GetDeviceById([FromRoute] Guid deviceId) var hasAuthPerms = IsAllowed(PermissionType.Devices_Auth); - var device = await _db.Devices.Where(x => x.Owner == CurrentUser.DbUser.Id && x.Id == deviceId) + var device = await _db.Devices.Where(x => x.Owner == CurrentUser.Id && x.Id == deviceId) .Select(x => new Models.Response.ResponseDeviceWithToken { Id = x.Id, @@ -80,14 +80,14 @@ public async Task GetDeviceById([FromRoute] Guid deviceId) [MapToApiVersion("1")] public async Task EditDevice([FromRoute] Guid deviceId, [FromBody] HubEditRequest body, [FromServices] IDeviceUpdateService updateService) { - var device = await _db.Devices.Where(x => x.Owner == CurrentUser.DbUser.Id && x.Id == deviceId) + var device = await _db.Devices.Where(x => x.Owner == CurrentUser.Id && x.Id == deviceId) .FirstOrDefaultAsync(); if (device == null) return Problem(DeviceError.DeviceNotFound); device.Name = body.Name; await _db.SaveChangesAsync(); - await updateService.UpdateDeviceForAllShared(CurrentUser.DbUser.Id, device.Id, DeviceUpdateType.Updated); + await updateService.UpdateDeviceForAllShared(CurrentUser.Id, device.Id, DeviceUpdateType.Updated); return Ok(); } @@ -106,7 +106,7 @@ public async Task EditDevice([FromRoute] Guid deviceId, [FromBody [MapToApiVersion("1")] public async Task RegenerateDeviceToken([FromRoute] Guid deviceId) { - var device = await _db.Devices.Where(x => x.Owner == CurrentUser.DbUser.Id && x.Id == deviceId) + var device = await _db.Devices.Where(x => x.Owner == CurrentUser.Id && x.Id == deviceId) .FirstOrDefaultAsync(); if (device == null) return Problem(DeviceError.DeviceNotFound); @@ -135,7 +135,7 @@ public async Task RemoveDevice([FromRoute] Guid deviceId, [FromSe var affected = await _db.Devices.Where(x => x.Id == deviceId).WhereIsUserOrAdmin(x => x.OwnerNavigation, CurrentUser).ExecuteDeleteAsync(); if (affected <= 0) return Problem(DeviceError.DeviceNotFound); - await updateService.UpdateDeviceForAllShared(CurrentUser.DbUser.Id, deviceId, DeviceUpdateType.Deleted); + await updateService.UpdateDeviceForAllShared(CurrentUser.Id, deviceId, DeviceUpdateType.Deleted); return Ok(); } @@ -168,14 +168,14 @@ public async Task CreateDeviceV2([FromBody] HubCreateRequest data, [FromSe var device = new Common.OpenShockDb.Device { Id = Guid.NewGuid(), - Owner = CurrentUser.DbUser.Id, + Owner = CurrentUser.Id, Name = data.Name, Token = CryptoUtils.RandomString(256) }; _db.Devices.Add(device); await _db.SaveChangesAsync(); - await updateService.UpdateDevice(CurrentUser.DbUser.Id, device.Id, DeviceUpdateType.Created); + await updateService.UpdateDevice(CurrentUser.Id, device.Id, DeviceUpdateType.Created); Response.StatusCode = (int)HttpStatusCode.Created; return device.Id; @@ -196,7 +196,7 @@ public async Task GetPairCode([FromRoute] Guid deviceId) { var devicePairs = _redis.RedisCollection(); - var deviceExists = await _db.Devices.AnyAsync(x => x.Id == deviceId && x.Owner == CurrentUser.DbUser.Id); + var deviceExists = await _db.Devices.AnyAsync(x => x.Id == deviceId && x.Owner == CurrentUser.Id); if (!deviceExists) Problem(DeviceError.DeviceNotFound); // replace with unlink? var existing = await devicePairs.FindByIdAsync(deviceId.ToString()); @@ -232,8 +232,8 @@ public async Task GetLiveControlGatewayInfo([FromRoute] Guid devi { // Check if user owns device or has a share var deviceExistsAndYouHaveAccess = await _db.Devices.AnyAsync(x => - x.Id == deviceId && (x.Owner == CurrentUser.DbUser.Id || x.Shockers.Any(y => y.ShockerShares.Any( - z => z.SharedWith == CurrentUser.DbUser.Id)))); + x.Id == deviceId && (x.Owner == CurrentUser.Id || x.Shockers.Any(y => y.ShockerShares.Any( + z => z.SharedWith == CurrentUser.Id)))); if (!deviceExistsAndYouHaveAccess) return Problem(DeviceError.DeviceNotFound); // Check if device is online diff --git a/API/Controller/Devices/ShockersController.cs b/API/Controller/Devices/ShockersController.cs index 6769fad0..819e8d38 100644 --- a/API/Controller/Devices/ShockersController.cs +++ b/API/Controller/Devices/ShockersController.cs @@ -24,7 +24,7 @@ public sealed partial class DevicesController [MapToApiVersion("1")] public async Task GetShockers([FromRoute] Guid deviceId) { - var deviceExists = await _db.Devices.AnyAsync(x => x.Owner == CurrentUser.DbUser.Id && x.Id == deviceId); + var deviceExists = await _db.Devices.AnyAsync(x => x.Owner == CurrentUser.Id && x.Id == deviceId); if (!deviceExists) return Problem(DeviceError.DeviceNotFound); var shockers = await _db.Shockers.Where(x => x.Device == deviceId).Select(x => new ShockerResponse { diff --git a/API/Controller/Sessions/ListSessions.cs b/API/Controller/Sessions/ListSessions.cs index 5b0a71fa..27468f5e 100644 --- a/API/Controller/Sessions/ListSessions.cs +++ b/API/Controller/Sessions/ListSessions.cs @@ -11,7 +11,7 @@ public sealed partial class SessionsController [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] public async Task> ListSessions() { - var sessions = await _sessionService.ListSessionsByUserId(CurrentUser.DbUser.Id); + var sessions = await _sessionService.ListSessionsByUserId(CurrentUser.Id); return sessions.Select(LoginSessionResponse.MapFrom); } diff --git a/API/Controller/Shares/LinkShareCode.cs b/API/Controller/Shares/LinkShareCode.cs index d59a9ab4..76ba84fb 100644 --- a/API/Controller/Shares/LinkShareCode.cs +++ b/API/Controller/Shares/LinkShareCode.cs @@ -37,13 +37,13 @@ [FromServices] IDeviceUpdateService deviceUpdateService Share = x, x.Shocker.DeviceNavigation.Owner, x.Shocker.Device }).FirstOrDefaultAsync(); if (shareCode == null) return Problem(ShareCodeError.ShareCodeNotFound); - if (shareCode.Owner == CurrentUser.DbUser.Id) return Problem(ShareCodeError.CantLinkOwnShareCode); - if (await _db.ShockerShares.AnyAsync(x => x.ShockerId == shareCodeId && x.SharedWith == CurrentUser.DbUser.Id)) + if (shareCode.Owner == CurrentUser.Id) return Problem(ShareCodeError.CantLinkOwnShareCode); + if (await _db.ShockerShares.AnyAsync(x => x.ShockerId == shareCodeId && x.SharedWith == CurrentUser.Id)) return Problem(ShareCodeError.ShockerAlreadyLinked); _db.ShockerShares.Add(new ShockerShare { - SharedWith = CurrentUser.DbUser.Id, + SharedWith = CurrentUser.Id, ShockerId = shareCode.Share.ShockerId, PermSound = shareCode.Share.PermSound, PermVibrate = shareCode.Share.PermVibrate, @@ -56,7 +56,7 @@ [FromServices] IDeviceUpdateService deviceUpdateService if (await _db.SaveChangesAsync() <= 1) throw new Exception("Error while linking share code to your account"); - await deviceUpdateService.UpdateDevice(shareCode.Owner, shareCode.Device, DeviceUpdateType.ShockerUpdated, CurrentUser.DbUser.Id); + await deviceUpdateService.UpdateDevice(shareCode.Owner, shareCode.Device, DeviceUpdateType.ShockerUpdated, CurrentUser.Id); return RespondSuccessLegacySimple("Successfully linked share code"); } diff --git a/API/Controller/Shares/Links/AddShocker.cs b/API/Controller/Shares/Links/AddShocker.cs index 6438feb6..aa6cc972 100644 --- a/API/Controller/Shares/Links/AddShocker.cs +++ b/API/Controller/Shares/Links/AddShocker.cs @@ -25,11 +25,11 @@ public sealed partial class ShareLinksController [ProducesResponseType(StatusCodes.Status409Conflict, MediaTypeNames.Application.ProblemJson)] // ShockerAlreadyInShareLink public async Task AddShocker([FromRoute] Guid shareLinkId, [FromRoute] Guid shockerId) { - var exists = await _db.ShockerSharesLinks.AnyAsync(x => x.OwnerId == CurrentUser.DbUser.Id && x.Id == shareLinkId); + var exists = await _db.ShockerSharesLinks.AnyAsync(x => x.OwnerId == CurrentUser.Id && x.Id == shareLinkId); if (!exists) return Problem(ShareLinkError.ShareLinkNotFound); var ownShocker = - await _db.Shockers.AnyAsync(x => x.Id == shockerId && x.DeviceNavigation.Owner == CurrentUser.DbUser.Id); + await _db.Shockers.AnyAsync(x => x.Id == shockerId && x.DeviceNavigation.Owner == CurrentUser.Id); if (!ownShocker) return Problem(ShockerError.ShockerNotFound); if (await _db.ShockerSharesLinksShockers.AnyAsync(x => x.ShareLinkId == shareLinkId && x.ShockerId == shockerId)) diff --git a/API/Controller/Shares/Links/CreateShareLink.cs b/API/Controller/Shares/Links/CreateShareLink.cs index 9ae0ba32..fe7bb69f 100644 --- a/API/Controller/Shares/Links/CreateShareLink.cs +++ b/API/Controller/Shares/Links/CreateShareLink.cs @@ -20,7 +20,7 @@ public async Task CreateShareLink([FromBody] ShareLinkCreate body var entity = new ShockerSharesLink { Id = Guid.NewGuid(), - Owner = CurrentUser.DbUser, + Owner = CurrentUser, ExpiresOn = body.ExpiresOn == null ? null : DateTime.SpecifyKind(body.ExpiresOn.Value, DateTimeKind.Utc), Name = body.Name }; diff --git a/API/Controller/Shares/Links/EditShockerShareLink.cs b/API/Controller/Shares/Links/EditShockerShareLink.cs index f4e10b12..b5480a59 100644 --- a/API/Controller/Shares/Links/EditShockerShareLink.cs +++ b/API/Controller/Shares/Links/EditShockerShareLink.cs @@ -26,7 +26,7 @@ public sealed partial class ShareLinksController [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // ShareLinkNotFound, ShockerNotInShareLink public async Task EditShocker([FromRoute] Guid shareLinkId, [FromRoute] Guid shockerId, [FromBody] ShareLinkEditShocker body) { - var exists = await _db.ShockerSharesLinks.AnyAsync(x => x.OwnerId == CurrentUser.DbUser.Id && x.Id == shareLinkId); + var exists = await _db.ShockerSharesLinks.AnyAsync(x => x.OwnerId == CurrentUser.Id && x.Id == shareLinkId); if (!exists) return Problem(ShareLinkError.ShareLinkNotFound); var shocker = diff --git a/API/Controller/Shares/Links/ListShareLinks.cs b/API/Controller/Shares/Links/ListShareLinks.cs index 5e85a479..2986ad48 100644 --- a/API/Controller/Shares/Links/ListShareLinks.cs +++ b/API/Controller/Shares/Links/ListShareLinks.cs @@ -17,7 +17,7 @@ public sealed partial class ShareLinksController [ProducesResponseType>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] public async Task List() { - var ownShareLinks = await _db.ShockerSharesLinks.Where(x => x.OwnerId == CurrentUser.DbUser.Id) + var ownShareLinks = await _db.ShockerSharesLinks.Where(x => x.OwnerId == CurrentUser.Id) .Select(x => ShareLinkResponse.GetFromEf(x)).ToListAsync(); return RespondSuccessLegacy(ownShareLinks); diff --git a/API/Controller/Shares/Links/PauseShockerShareLink.cs b/API/Controller/Shares/Links/PauseShockerShareLink.cs index 49120acc..35214e46 100644 --- a/API/Controller/Shares/Links/PauseShockerShareLink.cs +++ b/API/Controller/Shares/Links/PauseShockerShareLink.cs @@ -26,7 +26,7 @@ public sealed partial class ShareLinksController [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // ShareLinkNotFound, ShockerNotInShareLink public async Task PauseShocker([FromRoute] Guid shareLinkId, [FromRoute] Guid shockerId, [FromBody] PauseRequest body) { - var exists = await _db.ShockerSharesLinks.AnyAsync(x => x.OwnerId == CurrentUser.DbUser.Id && x.Id == shareLinkId); + var exists = await _db.ShockerSharesLinks.AnyAsync(x => x.OwnerId == CurrentUser.Id && x.Id == shareLinkId); if (!exists) return Problem(ShareLinkError.ShareLinkNotFound); var shocker = diff --git a/API/Controller/Shares/V2CreateShareRequest.cs b/API/Controller/Shares/V2CreateShareRequest.cs index 9d18e734..8dc7dc1f 100644 --- a/API/Controller/Shares/V2CreateShareRequest.cs +++ b/API/Controller/Shares/V2CreateShareRequest.cs @@ -20,14 +20,14 @@ public sealed partial class SharesController [ApiVersion("2")] public async Task CreateShare([FromBody] CreateShareRequest data) { - if (data.User == CurrentUser.DbUser.Id) + if (data.User == CurrentUser.Id) { return Problem(ShareError.ShareRequestCreateCannotShareWithSelf); } var providedShockerIds = data.Shockers.Select(x => x.Id).ToArray(); var belongsToUsFuture = _db.Shockers.AsNoTracking().Where(x => - x.DeviceNavigation.Owner == CurrentUser.DbUser.Id && providedShockerIds.Contains(x.Id)).Select(x => x.Id).Future(); + x.DeviceNavigation.Owner == CurrentUser.Id && providedShockerIds.Contains(x.Id)).Select(x => x.Id).Future(); if (data.User != null) { @@ -51,7 +51,7 @@ public async Task CreateShare([FromBody] CreateShareRequest data) var shareRequest = new ShareRequest { Id = Guid.NewGuid(), - Owner = CurrentUser.DbUser.Id, + Owner = CurrentUser.Id, User = data.User }; _db.ShareRequests.Add(shareRequest); diff --git a/API/Controller/Shares/V2GetShares.cs b/API/Controller/Shares/V2GetShares.cs index 54a8866f..fd3c2dc5 100644 --- a/API/Controller/Shares/V2GetShares.cs +++ b/API/Controller/Shares/V2GetShares.cs @@ -18,7 +18,7 @@ public sealed partial class SharesController [ApiVersion("2")] public async Task> GetSharesByUsers() { - var sharedToUsers = await _db.ShockerShares.Where(x => x.Shocker.DeviceNavigation.Owner == CurrentUser.DbUser.Id) + var sharedToUsers = await _db.ShockerShares.Where(x => x.Shocker.DeviceNavigation.Owner == CurrentUser.Id) .Select(x => new GenericIni { Id = x.SharedWithNavigation.Id, @@ -34,7 +34,7 @@ public async Task> GetSharesByUsers() [ApiVersion("2")] public async Task GetSharesToUser(Guid userId) { - var sharedWithUser = await _db.ShockerShares.Where(x => x.Shocker.DeviceNavigation.Owner == CurrentUser.DbUser.Id && x.SharedWith == userId) + var sharedWithUser = await _db.ShockerShares.Where(x => x.Shocker.DeviceNavigation.Owner == CurrentUser.Id && x.SharedWith == userId) .Select(x => new UserShareInfo { Id = x.Shocker.Id, diff --git a/API/Controller/Shares/V2Requests.cs b/API/Controller/Shares/V2Requests.cs index 6fe44ac0..86bea8c8 100644 --- a/API/Controller/Shares/V2Requests.cs +++ b/API/Controller/Shares/V2Requests.cs @@ -19,7 +19,7 @@ public sealed partial class SharesController [ApiVersion("2")] public async Task> GetOutstandingRequestsList() { - var outstandingShares = await _db.ShareRequests.Where(x => x.Owner == CurrentUser.DbUser.Id) + var outstandingShares = await _db.ShareRequests.Where(x => x.Owner == CurrentUser.Id) .Select(x => new ShareRequestBaseItem() { Id = x.Id, @@ -52,7 +52,7 @@ public async Task> GetOutstandingRequestsList( [ApiVersion("2")] public async Task> GetIncomingRequestsList() { - var outstandingShares = await _db.ShareRequests.Where(x => x.User == CurrentUser.DbUser.Id) + var outstandingShares = await _db.ShareRequests.Where(x => x.User == CurrentUser.Id) .Select(x => new ShareRequestBaseItem { Id = x.Id, @@ -86,7 +86,7 @@ public async Task> GetIncomingRequestsList() [ApiVersion("2")] public async Task GetRequest(Guid id) { - var outstandingShare = await _db.ShareRequests.Where(x => x.Id == id && (x.Owner == CurrentUser.DbUser.Id || x.User == CurrentUser.DbUser.Id)) + var outstandingShare = await _db.ShareRequests.Where(x => x.Id == id && (x.Owner == CurrentUser.Id || x.User == CurrentUser.Id)) .Select(x => new ShareRequestBaseDetails() { Id = x.Id, @@ -135,7 +135,7 @@ public async Task GetRequest(Guid id) public async Task DeleteRequest(Guid id) { var deletedShareRequest = await _db.ShareRequests - .Where(x => x.Id == id && x.Owner == CurrentUser.DbUser.Id).ExecuteDeleteAsync(); + .Where(x => x.Id == id && x.Owner == CurrentUser.Id).ExecuteDeleteAsync(); if (deletedShareRequest <= 0) return Problem(ShareError.ShareRequestNotFound); @@ -149,7 +149,7 @@ public async Task DeleteRequest(Guid id) public async Task DenyRequest(Guid id) { var deletedShareRequest = await _db.ShareRequests - .Where(x => x.Id == id && x.User == CurrentUser.DbUser.Id).ExecuteDeleteAsync(); + .Where(x => x.Id == id && x.User == CurrentUser.Id).ExecuteDeleteAsync(); if (deletedShareRequest <= 0) return Problem(ShareError.ShareRequestNotFound); @@ -163,11 +163,11 @@ public async Task DenyRequest(Guid id) // public async Task RedeemRequest(Guid id) // { // var shareRequest = await _db.ShareRequests - // .Where(x => x.Id == id && (x.User == null || x.User == CurrentUser.DbUser.Id)).Include(x => x.ShareRequestsShockers).FirstOrDefaultAsync(); + // .Where(x => x.Id == id && (x.User == null || x.User == CurrentUser.Id)).Include(x => x.ShareRequestsShockers).FirstOrDefaultAsync(); // // if (shareRequest == null) return Problem(ShareError.ShareRequestNotFound); // - // var alreadySharedShockers = await _db.ShockerShares.Where(x => x.Shocker.DeviceNavigation.OwnerNavigation.Id == shareRequest.Owner && x.SharedWith == CurrentUser.DbUser.Id).Select(x => x.ShockerId).ToArrayAsync(); + // var alreadySharedShockers = await _db.ShockerShares.Where(x => x.Shocker.DeviceNavigation.OwnerNavigation.Id == shareRequest.Owner && x.SharedWith == CurrentUser.Id).Select(x => x.ShockerId).ToArrayAsync(); // // foreach (var shareRequestShareRequestsShocker in shareRequest.ShareRequestsShockers) // { diff --git a/API/Controller/Shockers/ControlLogController.cs b/API/Controller/Shockers/ControlLogController.cs index f3d268ae..f6fdefc0 100644 --- a/API/Controller/Shockers/ControlLogController.cs +++ b/API/Controller/Shockers/ControlLogController.cs @@ -29,7 +29,7 @@ public sealed partial class ShockerController public async Task GetShockerLogs([FromRoute] Guid shockerId, [FromQuery] uint offset = 0, [FromQuery] [Range(1, 500)] uint limit = 100) { - var exists = await _db.Shockers.AnyAsync(x => x.DeviceNavigation.Owner == CurrentUser.DbUser.Id && x.Id == shockerId); + var exists = await _db.Shockers.AnyAsync(x => x.DeviceNavigation.Owner == CurrentUser.Id && x.Id == shockerId); if (!exists) return Problem(ShockerError.ShockerNotFound); var logs = await _db.ShockerControlLogs.Where(x => x.ShockerId == shockerId) diff --git a/API/Controller/Shockers/ControlShockerController.cs b/API/Controller/Shockers/ControlShockerController.cs index 4c52934a..83f0e7f9 100644 --- a/API/Controller/Shockers/ControlShockerController.cs +++ b/API/Controller/Shockers/ControlShockerController.cs @@ -36,9 +36,9 @@ public async Task SendControl( { var sender = new ControlLogSender { - Id = CurrentUser.DbUser.Id, - Name = CurrentUser.DbUser.Name, - Image = GravatarUtils.GetImageUrl(CurrentUser.DbUser.Email), + Id = CurrentUser.Id, + Name = CurrentUser.Name, + Image = GravatarUtils.GetImageUrl(CurrentUser.Email), ConnectionId = HttpContext.Connection.Id, AdditionalItems = EmptyDic, CustomName = body.CustomName diff --git a/API/Controller/Shockers/CreateShockerController.cs b/API/Controller/Shockers/CreateShockerController.cs index 0aca5492..8f666c8e 100644 --- a/API/Controller/Shockers/CreateShockerController.cs +++ b/API/Controller/Shockers/CreateShockerController.cs @@ -31,7 +31,7 @@ public async Task RegisterShocker( [FromBody] NewShocker body, [FromServices] IDeviceUpdateService deviceUpdateService) { - var device = await _db.Devices.Where(x => x.Owner == CurrentUser.DbUser.Id && x.Id == body.Device) + var device = await _db.Devices.Where(x => x.Owner == CurrentUser.Id && x.Id == body.Device) .Select(x => x.Id).FirstOrDefaultAsync(); if (device == Guid.Empty) return Problem(DeviceError.DeviceNotFound); var shockerCount = await _db.Shockers.CountAsync(x => x.Device == body.Device); @@ -49,7 +49,7 @@ public async Task RegisterShocker( _db.Shockers.Add(shocker); await _db.SaveChangesAsync(); - await deviceUpdateService.UpdateDeviceForAllShared(CurrentUser.DbUser.Id, device, + await deviceUpdateService.UpdateDeviceForAllShared(CurrentUser.Id, device, DeviceUpdateType.ShockerUpdated); return RespondSuccessLegacy(shocker.Id, statusCode: HttpStatusCode.Created); diff --git a/API/Controller/Shockers/DeleteShockerController.cs b/API/Controller/Shockers/DeleteShockerController.cs index 6dac199f..ee2171d8 100644 --- a/API/Controller/Shockers/DeleteShockerController.cs +++ b/API/Controller/Shockers/DeleteShockerController.cs @@ -43,7 +43,7 @@ public async Task RemoveShocker( _db.Shockers.Remove(affected); await _db.SaveChangesAsync(); - await deviceUpdateService.UpdateDeviceForAllShared(CurrentUser.DbUser.Id, affected.Device, DeviceUpdateType.ShockerUpdated); + await deviceUpdateService.UpdateDeviceForAllShared(CurrentUser.Id, affected.Device, DeviceUpdateType.ShockerUpdated); return RespondSuccessLegacySimple("Shocker removed successfully"); } diff --git a/API/Controller/Shockers/GetShockerController.cs b/API/Controller/Shockers/GetShockerController.cs index 2a758ae7..eb94b85c 100644 --- a/API/Controller/Shockers/GetShockerController.cs +++ b/API/Controller/Shockers/GetShockerController.cs @@ -24,7 +24,7 @@ public sealed partial class ShockerController [MapToApiVersion("1")] public async Task GetShockerById([FromRoute] Guid shockerId) { - var shocker = await _db.Shockers.Where(x => x.DeviceNavigation.Owner == CurrentUser.DbUser.Id && x.Id == shockerId).Select(x => new ShockerWithDevice + var shocker = await _db.Shockers.Where(x => x.DeviceNavigation.Owner == CurrentUser.Id && x.Id == shockerId).Select(x => new ShockerWithDevice { Id = x.Id, Name = x.Name, diff --git a/API/Controller/Shockers/OwnShockerController.cs b/API/Controller/Shockers/OwnShockerController.cs index 6e3de1e2..f2fe191f 100644 --- a/API/Controller/Shockers/OwnShockerController.cs +++ b/API/Controller/Shockers/OwnShockerController.cs @@ -19,7 +19,7 @@ public sealed partial class ShockerController [MapToApiVersion("1")] public async Task ListShockers() { - var shockers = await _db.Devices.Where(x => x.Owner == CurrentUser.DbUser.Id).OrderBy(x => x.CreatedOn).Select( + var shockers = await _db.Devices.Where(x => x.Owner == CurrentUser.Id).OrderBy(x => x.CreatedOn).Select( x => new ResponseDeviceWithShockers { Id = x.Id, diff --git a/API/Controller/Shockers/PatchShockerController.cs b/API/Controller/Shockers/PatchShockerController.cs index b8bc50d5..cb0e6ea3 100644 --- a/API/Controller/Shockers/PatchShockerController.cs +++ b/API/Controller/Shockers/PatchShockerController.cs @@ -32,10 +32,10 @@ public async Task EditShocker( [FromBody] NewShocker body, [FromServices] IDeviceUpdateService deviceUpdateService) { - var device = await _db.Devices.AnyAsync(x => x.Owner == CurrentUser.DbUser.Id && x.Id == body.Device); + var device = await _db.Devices.AnyAsync(x => x.Owner == CurrentUser.Id && x.Id == body.Device); if (!device) return Problem(DeviceError.DeviceNotFound); - var shocker = await _db.Shockers.Where(x => x.DeviceNavigation.Owner == CurrentUser.DbUser.Id && x.Id == shockerId) + var shocker = await _db.Shockers.Where(x => x.DeviceNavigation.Owner == CurrentUser.Id && x.Id == shockerId) .FirstOrDefaultAsync(); if (shocker == null) return Problem(ShockerError.ShockerNotFound); var oldDevice = shocker.Device; @@ -48,9 +48,9 @@ public async Task EditShocker( await _db.SaveChangesAsync(); if (oldDevice != body.Device) - await deviceUpdateService.UpdateDeviceForAllShared(CurrentUser.DbUser.Id, oldDevice, DeviceUpdateType.ShockerUpdated); + await deviceUpdateService.UpdateDeviceForAllShared(CurrentUser.Id, oldDevice, DeviceUpdateType.ShockerUpdated); - await deviceUpdateService.UpdateDeviceForAllShared(CurrentUser.DbUser.Id, body.Device, DeviceUpdateType.ShockerUpdated); + await deviceUpdateService.UpdateDeviceForAllShared(CurrentUser.Id, body.Device, DeviceUpdateType.ShockerUpdated); return RespondSuccessLegacySimple("Shocker updated successfully"); } diff --git a/API/Controller/Shockers/PauseShockerController.cs b/API/Controller/Shockers/PauseShockerController.cs index 5ec9da17..5c653004 100644 --- a/API/Controller/Shockers/PauseShockerController.cs +++ b/API/Controller/Shockers/PauseShockerController.cs @@ -30,13 +30,13 @@ public sealed partial class ShockerController public async Task PauseShocker([FromRoute] Guid shockerId, [FromBody] PauseRequest body, [FromServices] IDeviceUpdateService deviceUpdateService) { - var shocker = await _db.Shockers.Where(x => x.Id == shockerId && x.DeviceNavigation.Owner == CurrentUser.DbUser.Id) + var shocker = await _db.Shockers.Where(x => x.Id == shockerId && x.DeviceNavigation.Owner == CurrentUser.Id) .FirstOrDefaultAsync(); if (shocker == null) return Problem(ShockerError.ShockerNotFound); shocker.Paused = body.Pause; await _db.SaveChangesAsync(); - await deviceUpdateService.UpdateDeviceForAllShared(CurrentUser.DbUser.Id, shocker.Device, DeviceUpdateType.ShockerUpdated); + await deviceUpdateService.UpdateDeviceForAllShared(CurrentUser.Id, shocker.Device, DeviceUpdateType.ShockerUpdated); return RespondSuccessLegacy(body.Pause); } diff --git a/API/Controller/Shockers/ShareShockerController.cs b/API/Controller/Shockers/ShareShockerController.cs index c0d223af..2f78b9bd 100644 --- a/API/Controller/Shockers/ShareShockerController.cs +++ b/API/Controller/Shockers/ShareShockerController.cs @@ -29,10 +29,10 @@ public sealed partial class ShockerController [MapToApiVersion("1")] public async Task GetShockerShares([FromRoute] Guid shockerId) { - var owns = await _db.Shockers.AnyAsync(x => x.DeviceNavigation.Owner == CurrentUser.DbUser.Id && x.Id == shockerId); + var owns = await _db.Shockers.AnyAsync(x => x.DeviceNavigation.Owner == CurrentUser.Id && x.Id == shockerId); if (!owns) return Problem(ShockerError.ShockerNotFound); var shares = await _db.ShockerShares - .Where(x => x.ShockerId == shockerId && x.Shocker.DeviceNavigation.Owner == CurrentUser.DbUser.Id).Select(x => + .Where(x => x.ShockerId == shockerId && x.Shocker.DeviceNavigation.Owner == CurrentUser.Id).Select(x => new ShareInfo { Paused = x.Paused, @@ -72,10 +72,10 @@ public async Task GetShockerShares([FromRoute] Guid shockerId) [MapToApiVersion("1")] public async Task ShockerShareCodeList([FromRoute] Guid shockerId) { - var owns = await _db.Shockers.AnyAsync(x => x.DeviceNavigation.Owner == CurrentUser.DbUser.Id && x.Id == shockerId); + var owns = await _db.Shockers.AnyAsync(x => x.DeviceNavigation.Owner == CurrentUser.Id && x.Id == shockerId); if (!owns) return Problem(ShockerError.ShockerNotFound); var shares = await _db.ShockerShareCodes - .Where(x => x.ShockerId == shockerId && x.Shocker.DeviceNavigation.Owner == CurrentUser.DbUser.Id).Select(x => + .Where(x => x.ShockerId == shockerId && x.Shocker.DeviceNavigation.Owner == CurrentUser.Id).Select(x => new ShareCodeInfo { CreatedOn = x.CreatedOn, @@ -112,7 +112,7 @@ public async Task ShockerShareCodeCreate( [FromServices] IDeviceUpdateService deviceUpdateService ) { - var device = await _db.Shockers.Where(x => x.DeviceNavigation.Owner == CurrentUser.DbUser.Id && x.Id == shockerId) + var device = await _db.Shockers.Where(x => x.DeviceNavigation.Owner == CurrentUser.Id && x.Id == shockerId) .Select(x => x.Device).FirstOrDefaultAsync(); if (device == Guid.Empty) return Problem(ShockerError.ShockerNotFound); @@ -129,7 +129,7 @@ [FromServices] IDeviceUpdateService deviceUpdateService _db.ShockerShareCodes.Add(newCode); await _db.SaveChangesAsync(); - await deviceUpdateService.UpdateDeviceForAllShared(CurrentUser.DbUser.Id, device, DeviceUpdateType.ShockerUpdated); + await deviceUpdateService.UpdateDeviceForAllShared(CurrentUser.Id, device, DeviceUpdateType.ShockerUpdated); return RespondSuccessLegacy(newCode.Id); } @@ -154,7 +154,7 @@ public async Task ShockerShareCodeRemove( { var affected = await _db.ShockerShares.Where(x => x.ShockerId == shockerId && x.SharedWith == sharedWithUserId && - (x.Shocker.DeviceNavigation.Owner == CurrentUser.DbUser.Id || x.SharedWith == CurrentUser.DbUser.Id)) + (x.Shocker.DeviceNavigation.Owner == CurrentUser.Id || x.SharedWith == CurrentUser.Id)) .ExecuteDeleteAsync(); if (affected <= 0) return Problem(ShockerError.ShockerNotFound); @@ -188,7 +188,7 @@ public async Task ShockerShareCodeUpdate( { var affected = await _db.ShockerShares.Where(x => x.ShockerId == shockerId && x.SharedWith == sharedWithUserId && - x.Shocker.DeviceNavigation.Owner == CurrentUser.DbUser.Id).Select(x => + x.Shocker.DeviceNavigation.Owner == CurrentUser.Id).Select(x => new { Share = x, DeviceId = x.Shocker.Device, Owner = x.Shocker.DeviceNavigation.Owner }) .FirstOrDefaultAsync(); if (affected == null) return Problem(ShockerError.ShockerNotFound); @@ -231,7 +231,7 @@ public async Task ShockerShareCodePause( { var affected = await _db.ShockerShares.Where(x => x.ShockerId == shockerId && x.SharedWith == sharedWithUserId && - x.Shocker.DeviceNavigation.Owner == CurrentUser.DbUser.Id).Select(x => + x.Shocker.DeviceNavigation.Owner == CurrentUser.Id).Select(x => new { Share = x, DeviceId = x.Shocker.Device, Owner = x.Shocker.DeviceNavigation.Owner }).FirstOrDefaultAsync(); if (affected == null) return Problem(ShockerError.ShockerNotFound); diff --git a/API/Controller/Shockers/SharedShockersController.cs b/API/Controller/Shockers/SharedShockersController.cs index 65439b17..efcd664d 100644 --- a/API/Controller/Shockers/SharedShockersController.cs +++ b/API/Controller/Shockers/SharedShockersController.cs @@ -20,7 +20,7 @@ public sealed partial class ShockerController [MapToApiVersion("1")] public async Task ListSharedShockers() { - var sharedShockersRaw = await _db.ShockerShares.Where(x => x.SharedWith == CurrentUser.DbUser.Id).Select(x => + var sharedShockersRaw = await _db.ShockerShares.Where(x => x.SharedWith == CurrentUser.Id).Select(x => new { OwnerId = x.Shocker.DeviceNavigation.OwnerNavigation.Id, diff --git a/API/Controller/Tokens/TokenController.cs b/API/Controller/Tokens/TokenController.cs index df433de5..67d6ec79 100644 --- a/API/Controller/Tokens/TokenController.cs +++ b/API/Controller/Tokens/TokenController.cs @@ -30,7 +30,7 @@ public sealed partial class TokensController public async Task> ListTokens() { var apiTokens = await _db.ApiTokens - .Where(x => x.UserId == CurrentUser.DbUser.Id && (x.ValidUntil == null || x.ValidUntil > DateTime.UtcNow)) + .Where(x => x.UserId == CurrentUser.Id && (x.ValidUntil == null || x.ValidUntil > DateTime.UtcNow)) .OrderBy(x => x.CreatedOn) .Select(x => new TokenResponse { @@ -58,7 +58,7 @@ public async Task> ListTokens() public async Task GetTokenById([FromRoute] Guid tokenId) { var apiToken = await _db.ApiTokens - .Where(x => x.UserId == CurrentUser.DbUser.Id && x.Id == tokenId && (x.ValidUntil == null || x.ValidUntil > DateTime.UtcNow)) + .Where(x => x.UserId == CurrentUser.Id && x.Id == tokenId && (x.ValidUntil == null || x.ValidUntil > DateTime.UtcNow)) .Select(x => new TokenResponse { CreatedOn = x.CreatedOn, @@ -113,7 +113,7 @@ public async Task CreateToken([FromBody] CreateTokenReques var tokenDto = new ApiToken { - UserId = CurrentUser.DbUser.Id, + UserId = CurrentUser.Id, TokenHash = HashingUtils.HashSha256(token), CreatedByIp = HttpContext.GetRemoteIP(), Permissions = body.Permissions.Distinct().ToList(), @@ -145,7 +145,7 @@ public async Task CreateToken([FromBody] CreateTokenReques public async Task EditToken([FromRoute] Guid tokenId, [FromBody] EditTokenRequest body) { var token = await _db.ApiTokens - .FirstOrDefaultAsync(x => x.UserId == CurrentUser.DbUser.Id && x.Id == tokenId && (x.ValidUntil == null || x.ValidUntil > DateTime.UtcNow)); + .FirstOrDefaultAsync(x => x.UserId == CurrentUser.Id && x.Id == tokenId && (x.ValidUntil == null || x.ValidUntil > DateTime.UtcNow)); if (token == null) return Problem(ApiTokenError.ApiTokenNotFound); token.Name = body.Name; diff --git a/API/Controller/Users/GetSelf.cs b/API/Controller/Users/GetSelf.cs index cfabe9e2..1e801b05 100644 --- a/API/Controller/Users/GetSelf.cs +++ b/API/Controller/Users/GetSelf.cs @@ -1,7 +1,7 @@ -using System.Net.Mime; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; +using OpenShock.Common.Extensions; using OpenShock.Common.Models; -using OpenShock.Common.Problems; +using System.Net.Mime; namespace OpenShock.API.Controller.Users; @@ -17,11 +17,11 @@ public sealed partial class UsersController { Data = new SelfResponse { - Id = CurrentUser.DbUser.Id, - Name = CurrentUser.DbUser.Name, - Email = CurrentUser.DbUser.Email, + Id = CurrentUser.Id, + Name = CurrentUser.Name, + Email = CurrentUser.Email, Image = CurrentUser.GetImageLink(), - Rank = CurrentUser.DbUser.Rank + Rank = CurrentUser.Rank } }; public sealed class SelfResponse diff --git a/Common/Authentication/AuthenticatedUser.cs b/Common/Authentication/AuthenticatedUser.cs deleted file mode 100644 index f6df4041..00000000 --- a/Common/Authentication/AuthenticatedUser.cs +++ /dev/null @@ -1,41 +0,0 @@ -using OpenShock.Common.Models; -using OpenShock.Common.OpenShockDb; -using OpenShock.Common.Utils; - -namespace OpenShock.Common.Authentication; - -public sealed class AuthenticatedUser -{ - public required User DbUser { get; set; } - - public Uri GetImageLink() => GravatarUtils.GetImageUrl(DbUser.Email); - - public bool IsUser(Guid userId) - { - return DbUser.Id == userId; - } - - public bool IsUser(User user) - { - return DbUser == user || DbUser.Id == user.Id; - } - - public bool IsRank(RankType rank) - { - return DbUser.Rank >= rank; - } - - public bool IsUserOrRank(User user, RankType rank) - { - if (IsUser(user)) return true; - - return DbUser.Rank >= rank; - } - - public bool IsUserOrRank(Guid userId, RankType rank) - { - if (IsUser(userId)) return true; - - return DbUser.Rank >= rank; - } -} \ No newline at end of file diff --git a/Common/Authentication/AuthenticationHandlers/ApiTokenAuthentication.cs b/Common/Authentication/AuthenticationHandlers/ApiTokenAuthentication.cs index 22d8c992..fd691a78 100644 --- a/Common/Authentication/AuthenticationHandlers/ApiTokenAuthentication.cs +++ b/Common/Authentication/AuthenticationHandlers/ApiTokenAuthentication.cs @@ -4,12 +4,9 @@ using Microsoft.Extensions.Options; using OpenShock.Common.Authentication.Services; using OpenShock.Common.Errors; -using OpenShock.Common.Models; using OpenShock.Common.OpenShockDb; -using OpenShock.Common.Problems; using OpenShock.Common.Services.BatchUpdate; using OpenShock.Common.Utils; -using System.Net.Mime; using System.Security.Claims; using System.Text.Encodings.Web; using System.Text.Json; @@ -18,7 +15,7 @@ namespace OpenShock.Common.Authentication.AuthenticationHandlers; public sealed class ApiTokenAuthentication : AuthenticationHandler { - private readonly IClientAuthService _authService; + private readonly IClientAuthService _authService; private readonly IUserReferenceService _userReferenceService; private readonly IBatchUpdateService _batchUpdateService; private readonly OpenShockContext _db; @@ -28,7 +25,7 @@ public ApiTokenAuthentication( IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, - IClientAuthService clientAuth, + IClientAuthService clientAuth, IUserReferenceService userReferenceService, OpenShockContext db, IOptions jsonOptions, IBatchUpdateService batchUpdateService) @@ -55,10 +52,7 @@ protected override async Task HandleAuthenticateAsync() if (tokenDto == null) return AuthenticateResult.Fail(AuthResultError.TokenInvalid.Title!); _batchUpdateService.UpdateTokenLastUsed(tokenDto.Id); - _authService.CurrentClient = new AuthenticatedUser - { - DbUser = tokenDto.User - }; + _authService.CurrentClient = tokenDto.User; _userReferenceService.AuthReference = tokenDto; List claims = [ diff --git a/Common/Authentication/AuthenticationHandlers/UserSessionAuthentication.cs b/Common/Authentication/AuthenticationHandlers/UserSessionAuthentication.cs index 5523dcdb..ec69aeb9 100644 --- a/Common/Authentication/AuthenticationHandlers/UserSessionAuthentication.cs +++ b/Common/Authentication/AuthenticationHandlers/UserSessionAuthentication.cs @@ -19,7 +19,7 @@ namespace OpenShock.Common.Authentication.AuthenticationHandlers; public sealed class UserSessionAuthentication : AuthenticationHandler { - private readonly IClientAuthService _authService; + private readonly IClientAuthService _authService; private readonly IUserReferenceService _userReferenceService; private readonly IBatchUpdateService _batchUpdateService; private readonly OpenShockContext _db; @@ -30,7 +30,7 @@ public UserSessionAuthentication( IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, - IClientAuthService clientAuth, + IClientAuthService clientAuth, IUserReferenceService userReferenceService, OpenShockContext db, ISessionService sessionService, @@ -70,11 +70,8 @@ protected override async Task HandleAuthenticateAsync() var retrievedUser = await _db.Users.FirstAsync(user => user.Id == session.UserId); + _authService.CurrentClient = retrievedUser; _userReferenceService.AuthReference = session; - _authService.CurrentClient = new AuthenticatedUser - { - DbUser = retrievedUser - }; List claims = [ new(ClaimTypes.AuthenticationMethod, OpenShockAuthSchemas.UserSessionCookie), diff --git a/Common/Authentication/ControllerBase/AuthenticatedSessionControllerBase.cs b/Common/Authentication/ControllerBase/AuthenticatedSessionControllerBase.cs index 2c28d2e6..0546b09b 100644 --- a/Common/Authentication/ControllerBase/AuthenticatedSessionControllerBase.cs +++ b/Common/Authentication/ControllerBase/AuthenticatedSessionControllerBase.cs @@ -2,17 +2,18 @@ using Microsoft.AspNetCore.Mvc.Filters; using OpenShock.Common.Authentication.Services; using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; namespace OpenShock.Common.Authentication.ControllerBase; public class AuthenticatedSessionControllerBase : OpenShockControllerBase, IActionFilter { - public AuthenticatedUser CurrentUser = null!; + public User CurrentUser = null!; [NonAction] public void OnActionExecuting(ActionExecutingContext context) { - CurrentUser = ControllerContext.HttpContext.RequestServices.GetRequiredService>().CurrentClient; + CurrentUser = ControllerContext.HttpContext.RequestServices.GetRequiredService>().CurrentClient; } [NonAction] diff --git a/Common/Extensions/UserExtensions.cs b/Common/Extensions/UserExtensions.cs new file mode 100644 index 00000000..c4ac0cd0 --- /dev/null +++ b/Common/Extensions/UserExtensions.cs @@ -0,0 +1,39 @@ +using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; +using OpenShock.Common.Utils; + +namespace OpenShock.Common.Extensions; + +public static class UserExtensions +{ + public static Uri GetImageLink(this User user) => GravatarUtils.GetImageUrl(user.Email); + + public static bool IsUser(this User user, Guid otherUserId) + { + return user.Id == otherUserId; + } + + public static bool IsUser(this User user, User otherUser) + { + return user == otherUser || user.Id == otherUser.Id; + } + + public static bool IsRank(this User user, RankType rank) + { + return user.Rank >= rank; + } + + public static bool IsUserOrRank(this User user, User otherUser, RankType rank) + { + if (user.IsUser(otherUser)) return true; + + return user.Rank >= rank; + } + + public static bool IsUserOrRank(this User user, Guid otherUserId, RankType rank) + { + if (user.IsUser(otherUserId)) return true; + + return user.Rank >= rank; + } +} \ No newline at end of file diff --git a/Common/IQueryableExtensions.cs b/Common/IQueryableExtensions.cs index 1e57c2f7..a1a05f2b 100644 --- a/Common/IQueryableExtensions.cs +++ b/Common/IQueryableExtensions.cs @@ -1,5 +1,5 @@ -using OpenShock.Common.Authentication; -using OpenShock.Common.Models; +using OpenShock.Common.Models; +using OpenShock.Common.OpenShockDb; using System.Linq.Expressions; namespace OpenShock.Common; @@ -23,7 +23,7 @@ private static Expression> EntityPropExpr(Expressio private static BinaryExpression UserIdMatchesExpr(InvocationExpression userExpression, Guid userId) { return Expression.Equal( - Expression.Property(userExpression, nameof(OpenShockDb.User.Id)), + Expression.Property(userExpression, nameof(User.Id)), Expression.Constant(userId) ); } @@ -38,7 +38,7 @@ public static IQueryable WhereIsUser(this IQueryable return source.Where(IsUserMatchExpr(navigationSelector, userId)); } - public static IQueryable WhereIsUserOrRank(this IQueryable source, Expression> navigationSelector, OpenShockDb.User user, RankType rank) + public static IQueryable WhereIsUserOrRank(this IQueryable source, Expression> navigationSelector, User user, RankType rank) { if (user.Rank >= rank) { @@ -48,18 +48,8 @@ public static IQueryable WhereIsUserOrRank(this IQueryable WhereIsUserOrRank(this IQueryable source, Expression> navigationSelector, AuthenticatedUser user, RankType rank) - { - return WhereIsUserOrRank(source, navigationSelector, user.DbUser, rank); - } - - public static IQueryable WhereIsUserOrAdmin(this IQueryable source, Expression> navigationSelector, OpenShockDb.User user) + public static IQueryable WhereIsUserOrAdmin(this IQueryable source, Expression> navigationSelector, User user) { return WhereIsUserOrRank(source, navigationSelector, user, RankType.Admin); } - - public static IQueryable WhereIsUserOrAdmin(this IQueryable source, Expression> navigationSelector, AuthenticatedUser user) - { - return WhereIsUserOrRank(source, navigationSelector, user.DbUser, RankType.Admin); - } } diff --git a/Common/OpenShockServiceHelper.cs b/Common/OpenShockServiceHelper.cs index 1fb2d4a8..ca92de51 100644 --- a/Common/OpenShockServiceHelper.cs +++ b/Common/OpenShockServiceHelper.cs @@ -60,7 +60,7 @@ public static ServicesResult AddOpenShockServices(this IServiceCollection servic // <---- ASP.NET ----> services.AddExceptionHandler(); - services.AddScoped, ClientAuthService>(); + services.AddScoped, ClientAuthService>(); services.AddScoped, ClientAuthService>(); services.AddScoped(); diff --git a/LiveControlGateway/Controllers/LiveControlController.cs b/LiveControlGateway/Controllers/LiveControlController.cs index f89d6700..12e1354e 100644 --- a/LiveControlGateway/Controllers/LiveControlController.cs +++ b/LiveControlGateway/Controllers/LiveControlController.cs @@ -51,7 +51,7 @@ public sealed class LiveControlController : WebsocketBaseController _sharedShockers = new(); @@ -106,9 +106,9 @@ protected override Task UnregisterConnection() public async Task UpdatePermissions(OpenShockContext db) { Logger.LogDebug("Updating shared permissions for hub [{HubId}] for user [{User}]", Id, - _currentUser.DbUser.Id); + _currentUser.Id); - if (_device!.Owner == _currentUser.DbUser.Id) + if (_device!.Owner == _currentUser.Id) { Logger.LogTrace("User is owner of hub"); _sharedShockers = await db.Shockers.Where(x => x.Device == Id).ToDictionaryAsync(x => x.Id, x => new LiveShockerPermission() @@ -120,7 +120,7 @@ public async Task UpdatePermissions(OpenShockContext db) } _sharedShockers = await db.ShockerShares - .Where(x => x.Shocker.Device == Id && x.SharedWith == _currentUser.DbUser.Id).Select(x => new + .Where(x => x.Shocker.Device == Id && x.SharedWith == _currentUser.Id).Select(x => new { x.ShockerId, Lsp = new LiveShockerPermission @@ -153,8 +153,8 @@ public async Task UpdatePermissions(OpenShockContext db) _hubId = id; var hubExistsAndYouHaveAccess = await _db.Devices.AnyAsync(x => - x.Id == _hubId && (x.Owner == _currentUser.DbUser.Id || x.Shockers.Any(y => y.ShockerShares.Any( - z => z.SharedWith == _currentUser.DbUser.Id && z.PermLive)))); + x.Id == _hubId && (x.Owner == _currentUser.Id || x.Shockers.Any(y => y.ShockerShares.Any( + z => z.SharedWith == _currentUser.Id && z.PermLive)))); if (!hubExistsAndYouHaveAccess) { @@ -187,7 +187,7 @@ public async Task UpdatePermissions(OpenShockContext db) [NonAction] public void OnActionExecuting(ActionExecutingContext context) { - _currentUser = ControllerContext.HttpContext.RequestServices.GetRequiredService>() + _currentUser = ControllerContext.HttpContext.RequestServices.GetRequiredService>() .CurrentClient; } @@ -550,7 +550,7 @@ private async Task SendPing() if (WebSocket is not { State: WebSocketState.Open }) return; if (Logger.IsEnabled(LogLevel.Debug)) - Logger.LogDebug("Sending ping to live control user [{UserId}] for hub [{HubId}]", _currentUser.DbUser.Id, + Logger.LogDebug("Sending ping to live control user [{UserId}] for hub [{HubId}]", _currentUser.Id, Id); _pingTimestamp = Stopwatch.GetTimestamp(); From 6b268d2972d0ac353ebf779474b232a7a3648aa9 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Thu, 5 Dec 2024 16:29:14 +0100 Subject: [PATCH 21/22] Add missing using extensions namespace --- API/Controller/Sessions/DeleteSessions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/API/Controller/Sessions/DeleteSessions.cs b/API/Controller/Sessions/DeleteSessions.cs index a2e23686..9446e4c8 100644 --- a/API/Controller/Sessions/DeleteSessions.cs +++ b/API/Controller/Sessions/DeleteSessions.cs @@ -1,9 +1,9 @@ -using System.Net; -using System.Net.Mime; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using OpenShock.Common.Errors; +using OpenShock.Common.Extensions; using OpenShock.Common.Models; using OpenShock.Common.Problems; +using System.Net.Mime; namespace OpenShock.API.Controller.Sessions; From e1ae55099317e51dcf66c80ca2c4487d757b7e8c Mon Sep 17 00:00:00 2001 From: Luca Date: Thu, 21 Nov 2024 19:51:15 +0100 Subject: [PATCH 22/22] Debug errors on websocket closure --- Common/Websocket/WebsockBaseController.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Common/Websocket/WebsockBaseController.cs b/Common/Websocket/WebsockBaseController.cs index c73ac080..610a1854 100644 --- a/Common/Websocket/WebsockBaseController.cs +++ b/Common/Websocket/WebsockBaseController.cs @@ -1,5 +1,6 @@ using System.Net.Mime; using System.Net.WebSockets; +using System.Text.Json; using System.Threading.Channels; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -166,7 +167,7 @@ private async Task MessageLoop() } catch (Exception e) { - Logger.LogError(e, "Error while sending message to client"); + Logger.LogError(e, "Error while sending message to client - {Msg}", JsonSerializer.Serialize(msg)); throw; } }