diff --git a/Controllers/AdminController.cs b/Controllers/AdminController.cs index bd0e2a8..52377a4 100644 --- a/Controllers/AdminController.cs +++ b/Controllers/AdminController.cs @@ -6,6 +6,7 @@ using Swashbuckle.AspNetCore.Annotations; using AutoMapper; using garge_api.Models.Admin; +using Microsoft.EntityFrameworkCore; namespace garge_api.Controllers { @@ -200,21 +201,119 @@ public async Task RemoveRole([FromRoute] string roleName, [FromQu } /// - /// Gets all users. + /// Gets all users with their roles. /// /// A list of all users. [HttpGet("/api/users")] [SwaggerOperation(Summary = "Gets all users.")] [SwaggerResponse(200, "Users retrieved successfully.", typeof(IEnumerable))] - public IActionResult GetUsers() + public async Task GetUsers() { _logger.LogInformation("GetUsers called by {@LogData}", new { User = User.Identity?.Name }); var users = _userManager.Users.ToList(); - var dtos = _mapper.Map>(users); + var dtos = new List(); + foreach (var user in users) + { + var dto = _mapper.Map(user); + dto.Roles = await _userManager.GetRolesAsync(user); + dtos.Add(dto); + } return Ok(dtos); } + /// + /// Gets aggregate platform stats. + /// + [HttpGet("/api/admin/stats")] + [SwaggerOperation(Summary = "Gets aggregate platform stats.")] + [SwaggerResponse(200, "Stats retrieved successfully.", typeof(AdminStatsDto))] + public async Task GetStats() + { + _logger.LogInformation("GetStats called by {@LogData}", new { User = User.Identity?.Name }); + + var stats = new AdminStatsDto + { + TotalUsers = await _userManager.Users.CountAsync(), + TotalSensors = await _context.Sensors.CountAsync(), + TotalSwitches = await _context.Switches.CountAsync(), + ActiveAutomations = await _context.AutomationRules.CountAsync(r => r.IsEnabled), + }; + return Ok(stats); + } + + /// + /// Gets all discovered MQTT devices. + /// + [HttpGet("/api/admin/devices")] + [SwaggerOperation(Summary = "Gets all discovered MQTT devices.")] + public async Task GetDevices() + { + _logger.LogInformation("GetDevices called by {@LogData}", new { User = User.Identity?.Name }); + + var devices = await _context.DiscoveredDevices + .OrderByDescending(d => d.Timestamp) + .ToListAsync(); + return Ok(devices); + } + + /// + /// Gets cumulative daily stats over time for charting. + /// + [HttpGet("/api/admin/stats/history")] + [SwaggerOperation(Summary = "Gets cumulative daily stats over time.")] + public async Task GetStatsHistory() + { + _logger.LogInformation("GetStatsHistory called by {@LogData}", new { User = User.Identity?.Name }); + + var userDates = await _userManager.Users + .Select(u => u.CreatedAt.Date) + .ToListAsync(); + + var sensorDates = await _context.Sensors + .Select(s => s.CreatedAt.Date) + .ToListAsync(); + + var switchDates = await _context.Switches + .Select(s => s.CreatedAt.Date) + .ToListAsync(); + + var automationDates = await _context.AutomationRules + .Select(a => a.CreatedAt.Date) + .ToListAsync(); + + var sanityFloor = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var allDates = userDates.Concat(sensorDates).Concat(switchDates).Concat(automationDates) + .Where(d => d >= sanityFloor) + .ToList(); + if (!allDates.Any()) return Ok(new List()); + + var start = allDates.Min(); + var today = DateTime.UtcNow.Date; + + var result = new List(); + int users = 0, sensors = 0, switches = 0, automations = 0; + + for (var date = start; date <= today; date = date.AddDays(1)) + { + users += userDates.Count(d => d == date); + sensors += sensorDates.Count(d => d == date); + switches += switchDates.Count(d => d == date); + automations += automationDates.Count(d => d == date); + + result.Add(new + { + date = date.ToString("yyyy-MM-dd"), + totalUsers = users, + totalSensors = sensors, + totalSwitches = switches, + totalAutomations = automations, + }); + } + + return Ok(result); + } + /// /// Deletes a user by their ID. /// diff --git a/Dtos/Admin/AdminStatsDto.cs b/Dtos/Admin/AdminStatsDto.cs new file mode 100644 index 0000000..d926e0f --- /dev/null +++ b/Dtos/Admin/AdminStatsDto.cs @@ -0,0 +1,10 @@ +namespace garge_api.Dtos.Admin +{ + public class AdminStatsDto + { + public int TotalUsers { get; set; } + public int TotalSensors { get; set; } + public int TotalSwitches { get; set; } + public int ActiveAutomations { get; set; } + } +} diff --git a/Dtos/Admin/UserDto.cs b/Dtos/Admin/UserDto.cs index 9a63cbd..72606b9 100644 --- a/Dtos/Admin/UserDto.cs +++ b/Dtos/Admin/UserDto.cs @@ -7,5 +7,6 @@ public class UserDto public required string FirstName { get; set; } public required string LastName { get; set; } public required string Email { get; set; } + public IList Roles { get; set; } = []; } } diff --git a/Migrations/20260426194359_AddSensorActivity.Designer.cs b/Migrations/20260426194359_AddSensorActivity.Designer.cs new file mode 100644 index 0000000..cfc4883 --- /dev/null +++ b/Migrations/20260426194359_AddSensorActivity.Designer.cs @@ -0,0 +1,1149 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using garge_api.Models; + +#nullable disable + +namespace garge_api.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260426194359_AddSensorActivity")] + partial class AddSensorActivity + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.14") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("User", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("EmailVerificationCode") + .HasColumnType("text"); + + b.Property("EmailVerificationCodeExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PasswordResetCodeExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordResetCodeHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("UserProfile", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PriceZone") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.HasKey("Id"); + + b.ToTable("UserProfiles"); + }); + + modelBuilder.Entity("garge_api.Models.Admin.RolePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Permission") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoleName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleName", "Permission") + .IsUnique(); + + b.ToTable("RolePermissions"); + }); + + modelBuilder.Entity("garge_api.Models.Auth.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Expires") + .HasColumnType("timestamp with time zone"); + + b.Property("Revoked") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("garge_api.Models.Automation.AutomationRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("Condition") + .IsRequired() + .HasColumnType("text"); + + b.Property("ElectricityPriceArea") + .HasColumnType("text"); + + b.Property("ElectricityPriceCondition") + .HasColumnType("text"); + + b.Property("ElectricityPriceOperator") + .HasColumnType("text"); + + b.Property("ElectricityPriceThreshold") + .HasColumnType("double precision"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("LastTriggeredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SensorId") + .HasColumnType("integer"); + + b.Property("SensorType") + .IsRequired() + .HasColumnType("text"); + + b.Property("TargetId") + .HasColumnType("integer"); + + b.Property("TargetType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Threshold") + .HasColumnType("double precision"); + + b.Property("TimerActivatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TimerDurationHours") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.HasIndex("TargetType", "TargetId", "SensorType", "SensorId", "Condition", "Threshold", "Action") + .IsUnique(); + + b.ToTable("AutomationRules"); + }); + + modelBuilder.Entity("garge_api.Models.Electricity.StoredElectricityPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Area") + .IsRequired() + .HasColumnType("text"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeliveryEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryStart") + .HasColumnType("timestamp with time zone"); + + b.Property("FetchedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Resolution") + .IsRequired() + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.HasIndex("Area", "Resolution", "DeliveryStart") + .IsUnique(); + + b.ToTable("StoredElectricityPrices"); + }); + + modelBuilder.Entity("garge_api.Models.Group.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Icon") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("garge_api.Models.Group.GroupSensor", b => + { + b.Property("GroupId") + .HasColumnType("integer"); + + b.Property("SensorId") + .HasColumnType("integer"); + + b.HasKey("GroupId", "SensorId"); + + b.HasIndex("SensorId"); + + b.ToTable("GroupSensors"); + }); + + modelBuilder.Entity("garge_api.Models.Group.GroupSwitch", b => + { + b.Property("GroupId") + .HasColumnType("integer"); + + b.Property("SwitchId") + .HasColumnType("integer"); + + b.HasKey("GroupId", "SwitchId"); + + b.HasIndex("SwitchId"); + + b.ToTable("GroupSwitches"); + }); + + modelBuilder.Entity("garge_api.Models.Mqtt.DiscoveredDevice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DiscoveredBy") + .IsRequired() + .HasColumnType("text"); + + b.Property("Target") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DiscoveredBy", "Target", "Type") + .IsUnique(); + + b.ToTable("DiscoveredDevices"); + }); + + modelBuilder.Entity("garge_api.Models.Mqtt.EMQXMqttAcl", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("Permission") + .IsRequired() + .HasColumnType("text"); + + b.Property("Qos") + .HasColumnType("smallint"); + + b.Property("Retain") + .HasColumnType("smallint"); + + b.Property("Topic") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Username", "Permission", "Action", "Topic", "Qos", "Retain") + .IsUnique(); + + b.ToTable("EMQXMqttAcls"); + }); + + modelBuilder.Entity("garge_api.Models.Mqtt.EMQXMqttUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IsSuperuser") + .HasColumnType("boolean"); + + b.Property("PasswordHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Salt") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Username") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("EMQXMqttUsers"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.BatteryHealth", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Baseline") + .HasColumnType("real"); + + b.Property("ChargesRecorded") + .HasColumnType("integer"); + + b.Property("DropPct") + .HasColumnType("real"); + + b.Property("LastCharge") + .HasColumnType("real"); + + b.Property("LastChargedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SensorId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SensorId"); + + b.HasIndex("Timestamp"); + + b.ToTable("BatteryHealthData"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.Sensor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DefaultName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ParentName") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegistrationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Sensors"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.SensorActivity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivityDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("SensorId") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ActivityDate"); + + b.HasIndex("SensorId"); + + b.ToTable("SensorActivities"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.SensorData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SensorId") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SensorId"); + + b.HasIndex("Timestamp"); + + b.HasIndex("SensorId", "Timestamp") + .HasDatabaseName("IX_SensorData_SensorId_Timestamp"); + + b.ToTable("SensorData"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.UserSensor", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("SensorId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "SensorId"); + + b.HasIndex("SensorId"); + + b.ToTable("UserSensors"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.UserSensorCustomName", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("SensorId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("UserId", "SensorId"); + + b.HasIndex("SensorId"); + + b.ToTable("UserSensorCustomNames"); + }); + + modelBuilder.Entity("garge_api.Models.Switch.Switch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RegistrationCode") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.ToTable("Switches"); + }); + + modelBuilder.Entity("garge_api.Models.Switch.SwitchData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SwitchId") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SwitchId"); + + b.ToTable("SwitchData", (string)null); + }); + + modelBuilder.Entity("garge_api.Models.Switch.UserSwitch", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("SwitchId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "SwitchId"); + + b.HasIndex("SwitchId"); + + b.ToTable("UserSwitches"); + }); + + modelBuilder.Entity("garge_api.Models.Switch.UserSwitchCustomName", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("SwitchId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("UserId", "SwitchId"); + + b.HasIndex("SwitchId"); + + b.ToTable("UserSwitchCustomNames"); + }); + + modelBuilder.Entity("garge_api.Models.Webhook.WebhookSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WebhookUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.HasKey("Id"); + + b.ToTable("WebhookSubscriptions"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("UserProfile", b => + { + b.HasOne("User", "User") + .WithOne() + .HasForeignKey("UserProfile", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("garge_api.Models.Auth.RefreshToken", b => + { + b.HasOne("User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("garge_api.Models.Group.GroupSensor", b => + { + b.HasOne("garge_api.Models.Group.Group", "Group") + .WithMany("GroupSensors") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("garge_api.Models.Sensor.Sensor", "Sensor") + .WithMany() + .HasForeignKey("SensorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("Sensor"); + }); + + modelBuilder.Entity("garge_api.Models.Group.GroupSwitch", b => + { + b.HasOne("garge_api.Models.Group.Group", "Group") + .WithMany("GroupSwitches") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("garge_api.Models.Switch.Switch", "Switch") + .WithMany() + .HasForeignKey("SwitchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("Switch"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.BatteryHealth", b => + { + b.HasOne("garge_api.Models.Sensor.Sensor", "Sensor") + .WithMany() + .HasForeignKey("SensorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sensor"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.SensorActivity", b => + { + b.HasOne("garge_api.Models.Sensor.Sensor", "Sensor") + .WithMany() + .HasForeignKey("SensorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sensor"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.SensorData", b => + { + b.HasOne("garge_api.Models.Sensor.Sensor", "Sensor") + .WithMany() + .HasForeignKey("SensorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sensor"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.UserSensor", b => + { + b.HasOne("garge_api.Models.Sensor.Sensor", "Sensor") + .WithMany() + .HasForeignKey("SensorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sensor"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.UserSensorCustomName", b => + { + b.HasOne("garge_api.Models.Sensor.Sensor", "Sensor") + .WithMany() + .HasForeignKey("SensorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sensor"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("garge_api.Models.Switch.SwitchData", b => + { + b.HasOne("garge_api.Models.Switch.Switch", "Switch") + .WithMany() + .HasForeignKey("SwitchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Switch"); + }); + + modelBuilder.Entity("garge_api.Models.Switch.UserSwitch", b => + { + b.HasOne("garge_api.Models.Switch.Switch", "Switch") + .WithMany() + .HasForeignKey("SwitchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Switch"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("garge_api.Models.Switch.UserSwitchCustomName", b => + { + b.HasOne("garge_api.Models.Switch.Switch", "Switch") + .WithMany() + .HasForeignKey("SwitchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Switch"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("garge_api.Models.Group.Group", b => + { + b.Navigation("GroupSensors"); + + b.Navigation("GroupSwitches"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20260426194359_AddSensorActivity.cs b/Migrations/20260426194359_AddSensorActivity.cs new file mode 100644 index 0000000..80d5227 --- /dev/null +++ b/Migrations/20260426194359_AddSensorActivity.cs @@ -0,0 +1,58 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace garge_api.Migrations +{ + /// + public partial class AddSensorActivity : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "SensorActivities", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + SensorId = table.Column(type: "integer", nullable: false), + UserId = table.Column(type: "text", nullable: false), + Title = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + Notes = table.Column(type: "text", nullable: true), + ActivityDate = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_SensorActivities", x => x.Id); + table.ForeignKey( + name: "FK_SensorActivities_Sensors_SensorId", + column: x => x.SensorId, + principalTable: "Sensors", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_SensorActivities_ActivityDate", + table: "SensorActivities", + column: "ActivityDate"); + + migrationBuilder.CreateIndex( + name: "IX_SensorActivities_SensorId", + table: "SensorActivities", + column: "SensorId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SensorActivities"); + } + } +} diff --git a/Migrations/20260427143229_AddOdometerToSensorActivity.Designer.cs b/Migrations/20260427143229_AddOdometerToSensorActivity.Designer.cs new file mode 100644 index 0000000..d0db901 --- /dev/null +++ b/Migrations/20260427143229_AddOdometerToSensorActivity.Designer.cs @@ -0,0 +1,1152 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using garge_api.Models; + +#nullable disable + +namespace garge_api.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260427143229_AddOdometerToSensorActivity")] + partial class AddOdometerToSensorActivity + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.14") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("User", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("EmailVerificationCode") + .HasColumnType("text"); + + b.Property("EmailVerificationCodeExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PasswordResetCodeExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordResetCodeHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("UserProfile", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PriceZone") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.HasKey("Id"); + + b.ToTable("UserProfiles"); + }); + + modelBuilder.Entity("garge_api.Models.Admin.RolePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Permission") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoleName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleName", "Permission") + .IsUnique(); + + b.ToTable("RolePermissions"); + }); + + modelBuilder.Entity("garge_api.Models.Auth.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Expires") + .HasColumnType("timestamp with time zone"); + + b.Property("Revoked") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("garge_api.Models.Automation.AutomationRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("Condition") + .IsRequired() + .HasColumnType("text"); + + b.Property("ElectricityPriceArea") + .HasColumnType("text"); + + b.Property("ElectricityPriceCondition") + .HasColumnType("text"); + + b.Property("ElectricityPriceOperator") + .HasColumnType("text"); + + b.Property("ElectricityPriceThreshold") + .HasColumnType("double precision"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("LastTriggeredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SensorId") + .HasColumnType("integer"); + + b.Property("SensorType") + .IsRequired() + .HasColumnType("text"); + + b.Property("TargetId") + .HasColumnType("integer"); + + b.Property("TargetType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Threshold") + .HasColumnType("double precision"); + + b.Property("TimerActivatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TimerDurationHours") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.HasIndex("TargetType", "TargetId", "SensorType", "SensorId", "Condition", "Threshold", "Action") + .IsUnique(); + + b.ToTable("AutomationRules"); + }); + + modelBuilder.Entity("garge_api.Models.Electricity.StoredElectricityPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Area") + .IsRequired() + .HasColumnType("text"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeliveryEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryStart") + .HasColumnType("timestamp with time zone"); + + b.Property("FetchedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Resolution") + .IsRequired() + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.HasIndex("Area", "Resolution", "DeliveryStart") + .IsUnique(); + + b.ToTable("StoredElectricityPrices"); + }); + + modelBuilder.Entity("garge_api.Models.Group.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Icon") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("garge_api.Models.Group.GroupSensor", b => + { + b.Property("GroupId") + .HasColumnType("integer"); + + b.Property("SensorId") + .HasColumnType("integer"); + + b.HasKey("GroupId", "SensorId"); + + b.HasIndex("SensorId"); + + b.ToTable("GroupSensors"); + }); + + modelBuilder.Entity("garge_api.Models.Group.GroupSwitch", b => + { + b.Property("GroupId") + .HasColumnType("integer"); + + b.Property("SwitchId") + .HasColumnType("integer"); + + b.HasKey("GroupId", "SwitchId"); + + b.HasIndex("SwitchId"); + + b.ToTable("GroupSwitches"); + }); + + modelBuilder.Entity("garge_api.Models.Mqtt.DiscoveredDevice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DiscoveredBy") + .IsRequired() + .HasColumnType("text"); + + b.Property("Target") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DiscoveredBy", "Target", "Type") + .IsUnique(); + + b.ToTable("DiscoveredDevices"); + }); + + modelBuilder.Entity("garge_api.Models.Mqtt.EMQXMqttAcl", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("Permission") + .IsRequired() + .HasColumnType("text"); + + b.Property("Qos") + .HasColumnType("smallint"); + + b.Property("Retain") + .HasColumnType("smallint"); + + b.Property("Topic") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Username", "Permission", "Action", "Topic", "Qos", "Retain") + .IsUnique(); + + b.ToTable("EMQXMqttAcls"); + }); + + modelBuilder.Entity("garge_api.Models.Mqtt.EMQXMqttUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IsSuperuser") + .HasColumnType("boolean"); + + b.Property("PasswordHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Salt") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Username") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("EMQXMqttUsers"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.BatteryHealth", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Baseline") + .HasColumnType("real"); + + b.Property("ChargesRecorded") + .HasColumnType("integer"); + + b.Property("DropPct") + .HasColumnType("real"); + + b.Property("LastCharge") + .HasColumnType("real"); + + b.Property("LastChargedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SensorId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SensorId"); + + b.HasIndex("Timestamp"); + + b.ToTable("BatteryHealthData"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.Sensor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DefaultName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ParentName") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegistrationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Sensors"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.SensorActivity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivityDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OdometerKm") + .HasColumnType("integer"); + + b.Property("SensorId") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ActivityDate"); + + b.HasIndex("SensorId"); + + b.ToTable("SensorActivities"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.SensorData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SensorId") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SensorId"); + + b.HasIndex("Timestamp"); + + b.HasIndex("SensorId", "Timestamp") + .HasDatabaseName("IX_SensorData_SensorId_Timestamp"); + + b.ToTable("SensorData"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.UserSensor", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("SensorId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "SensorId"); + + b.HasIndex("SensorId"); + + b.ToTable("UserSensors"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.UserSensorCustomName", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("SensorId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("UserId", "SensorId"); + + b.HasIndex("SensorId"); + + b.ToTable("UserSensorCustomNames"); + }); + + modelBuilder.Entity("garge_api.Models.Switch.Switch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RegistrationCode") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.ToTable("Switches"); + }); + + modelBuilder.Entity("garge_api.Models.Switch.SwitchData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SwitchId") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SwitchId"); + + b.ToTable("SwitchData", (string)null); + }); + + modelBuilder.Entity("garge_api.Models.Switch.UserSwitch", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("SwitchId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "SwitchId"); + + b.HasIndex("SwitchId"); + + b.ToTable("UserSwitches"); + }); + + modelBuilder.Entity("garge_api.Models.Switch.UserSwitchCustomName", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("SwitchId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("UserId", "SwitchId"); + + b.HasIndex("SwitchId"); + + b.ToTable("UserSwitchCustomNames"); + }); + + modelBuilder.Entity("garge_api.Models.Webhook.WebhookSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WebhookUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.HasKey("Id"); + + b.ToTable("WebhookSubscriptions"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("UserProfile", b => + { + b.HasOne("User", "User") + .WithOne() + .HasForeignKey("UserProfile", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("garge_api.Models.Auth.RefreshToken", b => + { + b.HasOne("User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("garge_api.Models.Group.GroupSensor", b => + { + b.HasOne("garge_api.Models.Group.Group", "Group") + .WithMany("GroupSensors") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("garge_api.Models.Sensor.Sensor", "Sensor") + .WithMany() + .HasForeignKey("SensorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("Sensor"); + }); + + modelBuilder.Entity("garge_api.Models.Group.GroupSwitch", b => + { + b.HasOne("garge_api.Models.Group.Group", "Group") + .WithMany("GroupSwitches") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("garge_api.Models.Switch.Switch", "Switch") + .WithMany() + .HasForeignKey("SwitchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("Switch"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.BatteryHealth", b => + { + b.HasOne("garge_api.Models.Sensor.Sensor", "Sensor") + .WithMany() + .HasForeignKey("SensorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sensor"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.SensorActivity", b => + { + b.HasOne("garge_api.Models.Sensor.Sensor", "Sensor") + .WithMany() + .HasForeignKey("SensorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sensor"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.SensorData", b => + { + b.HasOne("garge_api.Models.Sensor.Sensor", "Sensor") + .WithMany() + .HasForeignKey("SensorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sensor"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.UserSensor", b => + { + b.HasOne("garge_api.Models.Sensor.Sensor", "Sensor") + .WithMany() + .HasForeignKey("SensorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sensor"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.UserSensorCustomName", b => + { + b.HasOne("garge_api.Models.Sensor.Sensor", "Sensor") + .WithMany() + .HasForeignKey("SensorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sensor"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("garge_api.Models.Switch.SwitchData", b => + { + b.HasOne("garge_api.Models.Switch.Switch", "Switch") + .WithMany() + .HasForeignKey("SwitchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Switch"); + }); + + modelBuilder.Entity("garge_api.Models.Switch.UserSwitch", b => + { + b.HasOne("garge_api.Models.Switch.Switch", "Switch") + .WithMany() + .HasForeignKey("SwitchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Switch"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("garge_api.Models.Switch.UserSwitchCustomName", b => + { + b.HasOne("garge_api.Models.Switch.Switch", "Switch") + .WithMany() + .HasForeignKey("SwitchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Switch"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("garge_api.Models.Group.Group", b => + { + b.Navigation("GroupSensors"); + + b.Navigation("GroupSwitches"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20260427143229_AddOdometerToSensorActivity.cs b/Migrations/20260427143229_AddOdometerToSensorActivity.cs new file mode 100644 index 0000000..7bf1e56 --- /dev/null +++ b/Migrations/20260427143229_AddOdometerToSensorActivity.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace garge_api.Migrations +{ + /// + public partial class AddOdometerToSensorActivity : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "OdometerKm", + table: "SensorActivities", + type: "integer", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "OdometerKm", + table: "SensorActivities"); + } + } +} diff --git a/Migrations/20260427161221_AddCreatedAt.Designer.cs b/Migrations/20260427161221_AddCreatedAt.Designer.cs new file mode 100644 index 0000000..84a553c --- /dev/null +++ b/Migrations/20260427161221_AddCreatedAt.Designer.cs @@ -0,0 +1,1164 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using garge_api.Models; + +#nullable disable + +namespace garge_api.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260427161221_AddCreatedAt")] + partial class AddCreatedAt + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.14") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("User", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("EmailVerificationCode") + .HasColumnType("text"); + + b.Property("EmailVerificationCodeExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PasswordResetCodeExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordResetCodeHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("UserProfile", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PriceZone") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.HasKey("Id"); + + b.ToTable("UserProfiles"); + }); + + modelBuilder.Entity("garge_api.Models.Admin.RolePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Permission") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoleName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleName", "Permission") + .IsUnique(); + + b.ToTable("RolePermissions"); + }); + + modelBuilder.Entity("garge_api.Models.Auth.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Expires") + .HasColumnType("timestamp with time zone"); + + b.Property("Revoked") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("garge_api.Models.Automation.AutomationRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("Condition") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ElectricityPriceArea") + .HasColumnType("text"); + + b.Property("ElectricityPriceCondition") + .HasColumnType("text"); + + b.Property("ElectricityPriceOperator") + .HasColumnType("text"); + + b.Property("ElectricityPriceThreshold") + .HasColumnType("double precision"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("LastTriggeredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SensorId") + .HasColumnType("integer"); + + b.Property("SensorType") + .IsRequired() + .HasColumnType("text"); + + b.Property("TargetId") + .HasColumnType("integer"); + + b.Property("TargetType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Threshold") + .HasColumnType("double precision"); + + b.Property("TimerActivatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TimerDurationHours") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.HasIndex("TargetType", "TargetId", "SensorType", "SensorId", "Condition", "Threshold", "Action") + .IsUnique(); + + b.ToTable("AutomationRules"); + }); + + modelBuilder.Entity("garge_api.Models.Electricity.StoredElectricityPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Area") + .IsRequired() + .HasColumnType("text"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeliveryEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryStart") + .HasColumnType("timestamp with time zone"); + + b.Property("FetchedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Resolution") + .IsRequired() + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.HasIndex("Area", "Resolution", "DeliveryStart") + .IsUnique(); + + b.ToTable("StoredElectricityPrices"); + }); + + modelBuilder.Entity("garge_api.Models.Group.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Icon") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("garge_api.Models.Group.GroupSensor", b => + { + b.Property("GroupId") + .HasColumnType("integer"); + + b.Property("SensorId") + .HasColumnType("integer"); + + b.HasKey("GroupId", "SensorId"); + + b.HasIndex("SensorId"); + + b.ToTable("GroupSensors"); + }); + + modelBuilder.Entity("garge_api.Models.Group.GroupSwitch", b => + { + b.Property("GroupId") + .HasColumnType("integer"); + + b.Property("SwitchId") + .HasColumnType("integer"); + + b.HasKey("GroupId", "SwitchId"); + + b.HasIndex("SwitchId"); + + b.ToTable("GroupSwitches"); + }); + + modelBuilder.Entity("garge_api.Models.Mqtt.DiscoveredDevice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DiscoveredBy") + .IsRequired() + .HasColumnType("text"); + + b.Property("Target") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DiscoveredBy", "Target", "Type") + .IsUnique(); + + b.ToTable("DiscoveredDevices"); + }); + + modelBuilder.Entity("garge_api.Models.Mqtt.EMQXMqttAcl", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("Permission") + .IsRequired() + .HasColumnType("text"); + + b.Property("Qos") + .HasColumnType("smallint"); + + b.Property("Retain") + .HasColumnType("smallint"); + + b.Property("Topic") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Username", "Permission", "Action", "Topic", "Qos", "Retain") + .IsUnique(); + + b.ToTable("EMQXMqttAcls"); + }); + + modelBuilder.Entity("garge_api.Models.Mqtt.EMQXMqttUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IsSuperuser") + .HasColumnType("boolean"); + + b.Property("PasswordHash") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Salt") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Username") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("EMQXMqttUsers"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.BatteryHealth", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Baseline") + .HasColumnType("real"); + + b.Property("ChargesRecorded") + .HasColumnType("integer"); + + b.Property("DropPct") + .HasColumnType("real"); + + b.Property("LastCharge") + .HasColumnType("real"); + + b.Property("LastChargedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SensorId") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SensorId"); + + b.HasIndex("Timestamp"); + + b.ToTable("BatteryHealthData"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.Sensor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ParentName") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegistrationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Sensors"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.SensorActivity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivityDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OdometerKm") + .HasColumnType("integer"); + + b.Property("SensorId") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ActivityDate"); + + b.HasIndex("SensorId"); + + b.ToTable("SensorActivities"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.SensorData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SensorId") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SensorId"); + + b.HasIndex("Timestamp"); + + b.HasIndex("SensorId", "Timestamp") + .HasDatabaseName("IX_SensorData_SensorId_Timestamp"); + + b.ToTable("SensorData"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.UserSensor", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("SensorId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "SensorId"); + + b.HasIndex("SensorId"); + + b.ToTable("UserSensors"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.UserSensorCustomName", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("SensorId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("UserId", "SensorId"); + + b.HasIndex("SensorId"); + + b.ToTable("UserSensorCustomNames"); + }); + + modelBuilder.Entity("garge_api.Models.Switch.Switch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RegistrationCode") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.ToTable("Switches"); + }); + + modelBuilder.Entity("garge_api.Models.Switch.SwitchData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("SwitchId") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SwitchId"); + + b.ToTable("SwitchData", (string)null); + }); + + modelBuilder.Entity("garge_api.Models.Switch.UserSwitch", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("SwitchId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "SwitchId"); + + b.HasIndex("SwitchId"); + + b.ToTable("UserSwitches"); + }); + + modelBuilder.Entity("garge_api.Models.Switch.UserSwitchCustomName", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("SwitchId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("UserId", "SwitchId"); + + b.HasIndex("SwitchId"); + + b.ToTable("UserSwitchCustomNames"); + }); + + modelBuilder.Entity("garge_api.Models.Webhook.WebhookSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WebhookUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.HasKey("Id"); + + b.ToTable("WebhookSubscriptions"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("UserProfile", b => + { + b.HasOne("User", "User") + .WithOne() + .HasForeignKey("UserProfile", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("garge_api.Models.Auth.RefreshToken", b => + { + b.HasOne("User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("garge_api.Models.Group.GroupSensor", b => + { + b.HasOne("garge_api.Models.Group.Group", "Group") + .WithMany("GroupSensors") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("garge_api.Models.Sensor.Sensor", "Sensor") + .WithMany() + .HasForeignKey("SensorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("Sensor"); + }); + + modelBuilder.Entity("garge_api.Models.Group.GroupSwitch", b => + { + b.HasOne("garge_api.Models.Group.Group", "Group") + .WithMany("GroupSwitches") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("garge_api.Models.Switch.Switch", "Switch") + .WithMany() + .HasForeignKey("SwitchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("Switch"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.BatteryHealth", b => + { + b.HasOne("garge_api.Models.Sensor.Sensor", "Sensor") + .WithMany() + .HasForeignKey("SensorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sensor"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.SensorActivity", b => + { + b.HasOne("garge_api.Models.Sensor.Sensor", "Sensor") + .WithMany() + .HasForeignKey("SensorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sensor"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.SensorData", b => + { + b.HasOne("garge_api.Models.Sensor.Sensor", "Sensor") + .WithMany() + .HasForeignKey("SensorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sensor"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.UserSensor", b => + { + b.HasOne("garge_api.Models.Sensor.Sensor", "Sensor") + .WithMany() + .HasForeignKey("SensorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sensor"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("garge_api.Models.Sensor.UserSensorCustomName", b => + { + b.HasOne("garge_api.Models.Sensor.Sensor", "Sensor") + .WithMany() + .HasForeignKey("SensorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sensor"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("garge_api.Models.Switch.SwitchData", b => + { + b.HasOne("garge_api.Models.Switch.Switch", "Switch") + .WithMany() + .HasForeignKey("SwitchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Switch"); + }); + + modelBuilder.Entity("garge_api.Models.Switch.UserSwitch", b => + { + b.HasOne("garge_api.Models.Switch.Switch", "Switch") + .WithMany() + .HasForeignKey("SwitchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Switch"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("garge_api.Models.Switch.UserSwitchCustomName", b => + { + b.HasOne("garge_api.Models.Switch.Switch", "Switch") + .WithMany() + .HasForeignKey("SwitchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Switch"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("garge_api.Models.Group.Group", b => + { + b.Navigation("GroupSensors"); + + b.Navigation("GroupSwitches"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20260427161221_AddCreatedAt.cs b/Migrations/20260427161221_AddCreatedAt.cs new file mode 100644 index 0000000..81dff2f --- /dev/null +++ b/Migrations/20260427161221_AddCreatedAt.cs @@ -0,0 +1,63 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace garge_api.Migrations +{ + /// + public partial class AddCreatedAt : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CreatedAt", + table: "Switches", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "CreatedAt", + table: "Sensors", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "CreatedAt", + table: "AutomationRules", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "CreatedAt", + table: "AspNetUsers", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CreatedAt", + table: "Switches"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + table: "Sensors"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + table: "AutomationRules"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + table: "AspNetUsers"); + } + } +} diff --git a/Migrations/ApplicationDbContextModelSnapshot.cs b/Migrations/ApplicationDbContextModelSnapshot.cs index 11a82d9..683c4aa 100644 --- a/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Migrations/ApplicationDbContextModelSnapshot.cs @@ -166,6 +166,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsConcurrencyToken() .HasColumnType("text"); + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + b.Property("Email") .HasMaxLength(256) .HasColumnType("character varying(256)"); @@ -342,6 +345,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + b.Property("ElectricityPriceArea") .HasColumnType("text"); @@ -638,6 +644,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + b.Property("DefaultName") .IsRequired() .HasMaxLength(50) @@ -674,6 +683,50 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Sensors"); }); + modelBuilder.Entity("garge_api.Models.Sensor.SensorActivity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivityDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OdometerKm") + .HasColumnType("integer"); + + b.Property("SensorId") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ActivityDate"); + + b.HasIndex("SensorId"); + + b.ToTable("SensorActivities"); + }); + modelBuilder.Entity("garge_api.Models.Sensor.SensorData", b => { b.Property("Id") @@ -789,6 +842,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + b.Property("Name") .IsRequired() .HasMaxLength(50) @@ -1021,6 +1077,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Sensor"); }); + modelBuilder.Entity("garge_api.Models.Sensor.SensorActivity", b => + { + b.HasOne("garge_api.Models.Sensor.Sensor", "Sensor") + .WithMany() + .HasForeignKey("SensorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sensor"); + }); + modelBuilder.Entity("garge_api.Models.Sensor.SensorData", b => { b.HasOne("garge_api.Models.Sensor.Sensor", "Sensor") diff --git a/Models/Admin/User.cs b/Models/Admin/User.cs index 6f1c88b..af4eeb0 100644 --- a/Models/Admin/User.cs +++ b/Models/Admin/User.cs @@ -13,4 +13,5 @@ public class User : IdentityUser public DateTime? EmailVerificationCodeExpiration { get; set; } public string? PasswordResetCodeHash { get; set; } public DateTime? PasswordResetCodeExpiration { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; } diff --git a/Models/Automation/AutomationRule.cs b/Models/Automation/AutomationRule.cs index 7800df3..d85f38a 100644 --- a/Models/Automation/AutomationRule.cs +++ b/Models/Automation/AutomationRule.cs @@ -40,5 +40,7 @@ public class AutomationRule // Optional timed action: keep socket ON for this many hours then auto-off public double? TimerDurationHours { get; set; } public DateTime? TimerActivatedAt { get; set; } + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; } } diff --git a/Models/Sensor/Sensor.cs b/Models/Sensor/Sensor.cs index c47e2d3..c6b20ba 100644 --- a/Models/Sensor/Sensor.cs +++ b/Models/Sensor/Sensor.cs @@ -32,5 +32,7 @@ public class Sensor [Required] public required string ParentName { get; set; } + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; } } diff --git a/Models/Switch/Switch.cs b/Models/Switch/Switch.cs index 936c0f9..b08d863 100644 --- a/Models/Switch/Switch.cs +++ b/Models/Switch/Switch.cs @@ -25,5 +25,7 @@ public class Switch [MaxLength(20)] public string? RegistrationCode { get; set; } + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; } }