diff options
| author | ivar <i@oiee.no> | 2025-10-26 22:57:28 +0100 |
|---|---|---|
| committer | ivar <i@oiee.no> | 2025-10-26 22:57:28 +0100 |
| commit | 842502e82c4ddfea05a5f77c361aaa27f75afc42 (patch) | |
| tree | 70fe96cd88600224257b80569015008626e3d317 /api | |
| parent | 5c59225ee10949cc58fccce4d968d1ede58be9b6 (diff) | |
| download | what-842502e82c4ddfea05a5f77c361aaa27f75afc42.tar.xz what-842502e82c4ddfea05a5f77c361aaa27f75afc42.zip | |
Refactor db schema and add audits
Diffstat (limited to 'api')
| -rw-r--r-- | api/WhatApi/Database.cs | 112 | ||||
| -rw-r--r-- | api/WhatApi/Endpoints/BaseEndpoint.cs | 1 | ||||
| -rw-r--r-- | api/WhatApi/Endpoints/DownloadContentEndpoint.cs | 2 | ||||
| -rw-r--r-- | api/WhatApi/Endpoints/GetPlacesEndpoint.cs | 4 | ||||
| -rw-r--r-- | api/WhatApi/Endpoints/UploadContentEndpoint.cs | 5 | ||||
| -rw-r--r-- | api/WhatApi/Migrations/20251013213511_Initial.Designer.cs | 93 | ||||
| -rw-r--r-- | api/WhatApi/Migrations/20251013213511_Initial.cs | 75 | ||||
| -rw-r--r-- | api/WhatApi/Migrations/20251026215643_Initial.Designer.cs | 214 | ||||
| -rw-r--r-- | api/WhatApi/Migrations/20251026215643_Initial.cs | 143 | ||||
| -rw-r--r-- | api/WhatApi/Migrations/DatabaseModelSnapshot.cs | 129 | ||||
| -rw-r--r-- | api/WhatApi/Program.cs | 14 | ||||
| -rw-r--r-- | api/WhatApi/Seed.cs | 1 | ||||
| -rw-r--r-- | api/WhatApi/Tables/AuditTrail.cs | 39 | ||||
| -rw-r--r-- | api/WhatApi/Tables/Content.cs | 20 | ||||
| -rw-r--r-- | api/WhatApi/Tables/IAuditableEntity.cs | 9 | ||||
| -rw-r--r-- | api/WhatApi/Tables/Place.cs | 22 | ||||
| -rw-r--r-- | api/WhatApi/Tables/Session.cs | 10 | ||||
| -rw-r--r-- | api/WhatApi/Tables/User.cs | 29 |
18 files changed, 698 insertions, 224 deletions
diff --git a/api/WhatApi/Database.cs b/api/WhatApi/Database.cs index 39de79a..8e8ed07 100644 --- a/api/WhatApi/Database.cs +++ b/api/WhatApi/Database.cs @@ -1,19 +1,111 @@ -using Microsoft.EntityFrameworkCore; +using System.Security.Claims; +using WhatApi.Tables; namespace WhatApi; -public class Database(DbContextOptions<Database> options) : DbContext(options) +public class Database(DbContextOptions<Database> options, IHttpContextAccessor httpContextAccessor) : DbContext(options) { - public DbSet<Tables.Content> Content { get; set; } - public DbSet<Tables.Place> Places { get; set; } + public DbSet<Content> Content => Set<Content>(); + public DbSet<Place> Places => Set<Place>(); + public DbSet<User> Users => Set<User>(); + public DbSet<AuditTrail> AuditTrails => Set<AuditTrail>(); + protected override void OnModelCreating(ModelBuilder b) { b.HasPostgresExtension("postgis"); - b.Entity<Tables.Place>(e => { - e.Property(x => x.Location).HasColumnType($"geometry(point,{Constants.Wgs84SpatialReferenceId})"); - e.HasIndex(x => x.Location).HasMethod("gist"); - e.ToTable("Place"); - }); - b.Entity<Tables.Content>(); + b.ApplyConfiguration(new AuditTrailConfiguration()); + b.ApplyConfiguration(new PlaceConfiguration()); + b.ApplyConfiguration(new UserConfiguration()); + b.ApplyConfiguration(new ContentConfiguration()); base.OnModelCreating(b); } + + public override int SaveChanges() { + SetAuditableProperties(); + var auditEntries = HandleAuditingBeforeSaveChanges(); + if (auditEntries.Count != 0) + AuditTrails.AddRange(auditEntries); + return base.SaveChanges(); + } + + public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) { + SetAuditableProperties(); + var auditEntries = HandleAuditingBeforeSaveChanges(); + if (auditEntries.Count != 0) + await AuditTrails.AddRangeAsync(auditEntries, cancellationToken); + return await base.SaveChangesAsync(cancellationToken); + } + + private List<AuditTrail> HandleAuditingBeforeSaveChanges() { + var userId = GetUserId(); + var entries = ChangeTracker.Entries<IAuditableEntity>() + .Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted); + + var auditTrails = new List<AuditTrail>(); + + foreach (var entry in entries) { + var audit = new AuditTrail { + Id = Guid.NewGuid(), + UserId = userId, + EntityName = entry.Entity.GetType().Name, + DateUtc = DateTimeOffset.UtcNow + }; + + foreach (var prop in entry.Properties) { + if (prop.Metadata.IsPrimaryKey()) { + audit.PrimaryKey = prop.CurrentValue?.ToString(); + continue; + } + + if (prop.Metadata.Name.Equals("Password")) continue; + + var name = prop.Metadata.Name; + + switch (entry.State) { + case EntityState.Added: + audit.TrailType = TrailType.Create; + audit.NewValues[name] = prop.CurrentValue; + break; + case EntityState.Deleted: + audit.TrailType = TrailType.Delete; + audit.OldValues[name] = prop.OriginalValue; + break; + case EntityState.Modified: + if (!Equals(prop.OriginalValue, prop.CurrentValue)) { + audit.TrailType = TrailType.Update; + audit.ChangedColumns.Add(name); + audit.OldValues[name] = prop.OriginalValue; + audit.NewValues[name] = prop.CurrentValue; + } + break; + } + } + + if (audit.TrailType != TrailType.None) + auditTrails.Add(audit); + } + + return auditTrails; + } + + private Guid GetUserId() { + var system = new Guid("e87ab078-55bc-4655-86d9-c5b2ecad7162"); + var userIdString = httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier); + return string.IsNullOrWhiteSpace(userIdString) ? system : new Guid(userIdString); + } + + private void SetAuditableProperties() { + var actor = GetUserId(); + foreach (var entry in ChangeTracker.Entries<IAuditableEntity>()) { + switch (entry.State) { + case EntityState.Added: + entry.Entity.CreatedAtUtc = DateTimeOffset.UtcNow; + entry.Entity.CreatedBy = actor; + break; + case EntityState.Modified: + entry.Entity.UpdatedAtUtc = DateTimeOffset.UtcNow; + entry.Entity.UpdatedBy = actor; + break; + } + } + } }
\ No newline at end of file diff --git a/api/WhatApi/Endpoints/BaseEndpoint.cs b/api/WhatApi/Endpoints/BaseEndpoint.cs index 4d8f9ad..1fdf14f 100644 --- a/api/WhatApi/Endpoints/BaseEndpoint.cs +++ b/api/WhatApi/Endpoints/BaseEndpoint.cs @@ -1,5 +1,4 @@ using System.Net; -using Microsoft.AspNetCore.Mvc; namespace WhatApi.Endpoints; diff --git a/api/WhatApi/Endpoints/DownloadContentEndpoint.cs b/api/WhatApi/Endpoints/DownloadContentEndpoint.cs index dbe6bff..34e51e8 100644 --- a/api/WhatApi/Endpoints/DownloadContentEndpoint.cs +++ b/api/WhatApi/Endpoints/DownloadContentEndpoint.cs @@ -1,5 +1,3 @@ -using Microsoft.AspNetCore.Mvc; - namespace WhatApi.Endpoints; public class DownloadContentEndpoint : BaseEndpoint diff --git a/api/WhatApi/Endpoints/GetPlacesEndpoint.cs b/api/WhatApi/Endpoints/GetPlacesEndpoint.cs index a3fc7e3..8dbdff6 100644 --- a/api/WhatApi/Endpoints/GetPlacesEndpoint.cs +++ b/api/WhatApi/Endpoints/GetPlacesEndpoint.cs @@ -1,8 +1,4 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using NetTopologySuite; using NetTopologySuite.Features; -using NetTopologySuite.Geometries; using WhatApi.Tables; namespace WhatApi.Endpoints; diff --git a/api/WhatApi/Endpoints/UploadContentEndpoint.cs b/api/WhatApi/Endpoints/UploadContentEndpoint.cs index 82fb71b..b114310 100644 --- a/api/WhatApi/Endpoints/UploadContentEndpoint.cs +++ b/api/WhatApi/Endpoints/UploadContentEndpoint.cs @@ -1,7 +1,3 @@ -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.AspNetCore.Mvc; -using NetTopologySuite; -using NetTopologySuite.Geometries; namespace WhatApi.Endpoints; @@ -32,7 +28,6 @@ public class UploadContentEndpoint(Database db) : BaseEndpoint var content = new Tables.Content() { Id = contentId, Mime = request.File.ContentType, - Created = DateTime.UtcNow, BlobId = blobId, Ip = GetIp() }; diff --git a/api/WhatApi/Migrations/20251013213511_Initial.Designer.cs b/api/WhatApi/Migrations/20251013213511_Initial.Designer.cs deleted file mode 100644 index 5ddcc9f..0000000 --- a/api/WhatApi/Migrations/20251013213511_Initial.Designer.cs +++ /dev/null @@ -1,93 +0,0 @@ -// <auto-generated /> -using System; -using System.Net; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using NetTopologySuite.Geometries; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using WhatApi; - -#nullable disable - -namespace WhatApi.Migrations -{ - [DbContext(typeof(Database))] - [Migration("20251013213511_Initial")] - partial class Initial - { - /// <inheritdoc /> - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.9") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("WhatApi.Tables.Content", b => - { - b.Property<Guid>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property<Guid>("BlobId") - .HasColumnType("uuid"); - - b.Property<DateTime>("Created") - .HasColumnType("timestamp with time zone"); - - b.Property<IPAddress>("Ip") - .IsRequired() - .HasColumnType("inet"); - - b.Property<string>("Mime") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Content"); - }); - - modelBuilder.Entity("WhatApi.Tables.Place", b => - { - b.Property<Guid>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property<Guid>("ContentId") - .HasColumnType("uuid"); - - b.Property<Point>("Location") - .IsRequired() - .HasColumnType("geometry(point,4326)"); - - b.HasKey("Id"); - - b.HasIndex("ContentId"); - - b.HasIndex("Location"); - - NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Location"), "gist"); - - b.ToTable("Place", (string)null); - }); - - modelBuilder.Entity("WhatApi.Tables.Place", b => - { - b.HasOne("WhatApi.Tables.Content", "Content") - .WithMany() - .HasForeignKey("ContentId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Content"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/api/WhatApi/Migrations/20251013213511_Initial.cs b/api/WhatApi/Migrations/20251013213511_Initial.cs deleted file mode 100644 index 1fa8bbf..0000000 --- a/api/WhatApi/Migrations/20251013213511_Initial.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Net; -using Microsoft.EntityFrameworkCore.Migrations; -using NetTopologySuite.Geometries; - -#nullable disable - -namespace WhatApi.Migrations -{ - /// <inheritdoc /> - public partial class Initial : Migration - { - /// <inheritdoc /> - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterDatabase() - .Annotation("Npgsql:PostgresExtension:postgis", ",,"); - - migrationBuilder.CreateTable( - name: "Content", - columns: table => new - { - Id = table.Column<Guid>(type: "uuid", nullable: false), - Mime = table.Column<string>(type: "text", nullable: false), - Created = table.Column<DateTime>(type: "timestamp with time zone", nullable: false), - BlobId = table.Column<Guid>(type: "uuid", nullable: false), - Ip = table.Column<IPAddress>(type: "inet", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Content", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Place", - columns: table => new - { - Id = table.Column<Guid>(type: "uuid", nullable: false), - ContentId = table.Column<Guid>(type: "uuid", nullable: false), - Location = table.Column<Point>(type: "geometry(point,4326)", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Place", x => x.Id); - table.ForeignKey( - name: "FK_Place_Content_ContentId", - column: x => x.ContentId, - principalTable: "Content", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Place_ContentId", - table: "Place", - column: "ContentId"); - - migrationBuilder.CreateIndex( - name: "IX_Place_Location", - table: "Place", - column: "Location") - .Annotation("Npgsql:IndexMethod", "gist"); - } - - /// <inheritdoc /> - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Place"); - - migrationBuilder.DropTable( - name: "Content"); - } - } -} diff --git a/api/WhatApi/Migrations/20251026215643_Initial.Designer.cs b/api/WhatApi/Migrations/20251026215643_Initial.Designer.cs new file mode 100644 index 0000000..c430ed1 --- /dev/null +++ b/api/WhatApi/Migrations/20251026215643_Initial.Designer.cs @@ -0,0 +1,214 @@ +// <auto-generated /> +using System; +using System.Collections.Generic; +using System.Net; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using WhatApi; + +#nullable disable + +namespace WhatApi.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20251026215643_Initial")] + partial class Initial + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("WhatApi.Tables.AuditTrail", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.PrimitiveCollection<List<string>>("ChangedColumns") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property<DateTimeOffset>("DateUtc") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("EntityName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<Dictionary<string, object>>("NewValues") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property<Dictionary<string, object>>("OldValues") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property<string>("PrimaryKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<string>("TrailType") + .IsRequired() + .HasColumnType("text"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("EntityName"); + + b.ToTable("audit_trails", (string)null); + }); + + modelBuilder.Entity("WhatApi.Tables.Content", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property<Guid>("BlobId") + .HasColumnType("uuid"); + + b.Property<DateTimeOffset>("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("CreatedBy") + .HasColumnType("uuid"); + + b.Property<IPAddress>("Ip") + .IsRequired() + .HasColumnType("inet"); + + b.Property<string>("Mime") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<DateTimeOffset?>("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid?>("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("content", (string)null); + }); + + modelBuilder.Entity("WhatApi.Tables.Place", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property<Guid>("ContentId") + .HasColumnType("uuid"); + + b.Property<DateTimeOffset>("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("CreatedBy") + .HasColumnType("uuid"); + + b.Property<Point>("Location") + .IsRequired() + .HasColumnType("geometry(point,4326)"); + + b.Property<DateTimeOffset?>("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid?>("UpdatedBy") + .HasColumnType("uuid"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ContentId"); + + b.HasIndex("Location"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Location"), "gist"); + + b.HasIndex("UserId"); + + b.ToTable("place", (string)null); + }); + + modelBuilder.Entity("WhatApi.Tables.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property<DateTimeOffset>("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("CreatedBy") + .HasColumnType("uuid"); + + b.Property<string>("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<DateTimeOffset?>("LastSeen") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<string>("Password") + .IsRequired() + .HasColumnType("text"); + + b.Property<DateTimeOffset?>("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid?>("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("user", (string)null); + }); + + modelBuilder.Entity("WhatApi.Tables.Place", b => + { + b.HasOne("WhatApi.Tables.Content", "Content") + .WithMany() + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("WhatApi.Tables.User", null) + .WithMany("Places") + .HasForeignKey("UserId"); + + b.Navigation("Content"); + }); + + modelBuilder.Entity("WhatApi.Tables.User", b => + { + b.Navigation("Places"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api/WhatApi/Migrations/20251026215643_Initial.cs b/api/WhatApi/Migrations/20251026215643_Initial.cs new file mode 100644 index 0000000..b8f0444 --- /dev/null +++ b/api/WhatApi/Migrations/20251026215643_Initial.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Net; +using Microsoft.EntityFrameworkCore.Migrations; +using NetTopologySuite.Geometries; + +#nullable disable + +namespace WhatApi.Migrations +{ + /// <inheritdoc /> + public partial class Initial : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:PostgresExtension:postgis", ",,"); + + migrationBuilder.CreateTable( + name: "audit_trails", + columns: table => new + { + Id = table.Column<Guid>(type: "uuid", nullable: false), + UserId = table.Column<Guid>(type: "uuid", nullable: true), + EntityName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false), + PrimaryKey = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true), + TrailType = table.Column<string>(type: "text", nullable: false), + DateUtc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false), + OldValues = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: false), + NewValues = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: false), + ChangedColumns = table.Column<List<string>>(type: "jsonb", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_audit_trails", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "content", + columns: table => new + { + Id = table.Column<Guid>(type: "uuid", nullable: false), + Mime = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false), + BlobId = table.Column<Guid>(type: "uuid", nullable: false), + Ip = table.Column<IPAddress>(type: "inet", nullable: false), + CreatedAtUtc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false), + UpdatedAtUtc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column<Guid>(type: "uuid", nullable: false), + UpdatedBy = table.Column<Guid>(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_content", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "user", + columns: table => new + { + Id = table.Column<Guid>(type: "uuid", nullable: false), + Name = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false), + Email = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false), + Password = table.Column<string>(type: "text", nullable: false), + LastSeen = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), + CreatedAtUtc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false), + UpdatedAtUtc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column<Guid>(type: "uuid", nullable: false), + UpdatedBy = table.Column<Guid>(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_user", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "place", + columns: table => new + { + Id = table.Column<Guid>(type: "uuid", nullable: false), + ContentId = table.Column<Guid>(type: "uuid", nullable: false), + Location = table.Column<Point>(type: "geometry(point,4326)", nullable: false), + CreatedAtUtc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false), + UpdatedAtUtc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column<Guid>(type: "uuid", nullable: false), + UpdatedBy = table.Column<Guid>(type: "uuid", nullable: true), + UserId = table.Column<Guid>(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_place", x => x.Id); + table.ForeignKey( + name: "FK_place_content_ContentId", + column: x => x.ContentId, + principalTable: "content", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_place_user_UserId", + column: x => x.UserId, + principalTable: "user", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_audit_trails_EntityName", + table: "audit_trails", + column: "EntityName"); + + migrationBuilder.CreateIndex( + name: "IX_place_ContentId", + table: "place", + column: "ContentId"); + + migrationBuilder.CreateIndex( + name: "IX_place_Location", + table: "place", + column: "Location") + .Annotation("Npgsql:IndexMethod", "gist"); + + migrationBuilder.CreateIndex( + name: "IX_place_UserId", + table: "place", + column: "UserId"); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "audit_trails"); + + migrationBuilder.DropTable( + name: "place"); + + migrationBuilder.DropTable( + name: "content"); + + migrationBuilder.DropTable( + name: "user"); + } + } +} diff --git a/api/WhatApi/Migrations/DatabaseModelSnapshot.cs b/api/WhatApi/Migrations/DatabaseModelSnapshot.cs index f1e5fcb..1d81e04 100644 --- a/api/WhatApi/Migrations/DatabaseModelSnapshot.cs +++ b/api/WhatApi/Migrations/DatabaseModelSnapshot.cs @@ -1,5 +1,6 @@ // <auto-generated /> using System; +using System.Collections.Generic; using System.Net; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -25,6 +26,50 @@ namespace WhatApi.Migrations NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("WhatApi.Tables.AuditTrail", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.PrimitiveCollection<List<string>>("ChangedColumns") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property<DateTimeOffset>("DateUtc") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("EntityName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<Dictionary<string, object>>("NewValues") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property<Dictionary<string, object>>("OldValues") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property<string>("PrimaryKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<string>("TrailType") + .IsRequired() + .HasColumnType("text"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("EntityName"); + + b.ToTable("audit_trails", (string)null); + }); + modelBuilder.Entity("WhatApi.Tables.Content", b => { b.Property<Guid>("Id") @@ -34,20 +79,30 @@ namespace WhatApi.Migrations b.Property<Guid>("BlobId") .HasColumnType("uuid"); - b.Property<DateTime>("Created") + b.Property<DateTimeOffset>("CreatedAtUtc") .HasColumnType("timestamp with time zone"); + b.Property<Guid>("CreatedBy") + .HasColumnType("uuid"); + b.Property<IPAddress>("Ip") .IsRequired() .HasColumnType("inet"); b.Property<string>("Mime") .IsRequired() - .HasColumnType("text"); + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<DateTimeOffset?>("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid?>("UpdatedBy") + .HasColumnType("uuid"); b.HasKey("Id"); - b.ToTable("Content"); + b.ToTable("content", (string)null); }); modelBuilder.Entity("WhatApi.Tables.Place", b => @@ -59,10 +114,25 @@ namespace WhatApi.Migrations b.Property<Guid>("ContentId") .HasColumnType("uuid"); + b.Property<DateTimeOffset>("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("CreatedBy") + .HasColumnType("uuid"); + b.Property<Point>("Location") .IsRequired() .HasColumnType("geometry(point,4326)"); + b.Property<DateTimeOffset?>("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid?>("UpdatedBy") + .HasColumnType("uuid"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid"); + b.HasKey("Id"); b.HasIndex("ContentId"); @@ -71,7 +141,49 @@ namespace WhatApi.Migrations NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Location"), "gist"); - b.ToTable("Place", (string)null); + b.HasIndex("UserId"); + + b.ToTable("place", (string)null); + }); + + modelBuilder.Entity("WhatApi.Tables.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property<DateTimeOffset>("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("CreatedBy") + .HasColumnType("uuid"); + + b.Property<string>("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<DateTimeOffset?>("LastSeen") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property<string>("Password") + .IsRequired() + .HasColumnType("text"); + + b.Property<DateTimeOffset?>("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid?>("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("user", (string)null); }); modelBuilder.Entity("WhatApi.Tables.Place", b => @@ -82,8 +194,17 @@ namespace WhatApi.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("WhatApi.Tables.User", null) + .WithMany("Places") + .HasForeignKey("UserId"); + b.Navigation("Content"); }); + + modelBuilder.Entity("WhatApi.Tables.User", b => + { + b.Navigation("Places"); + }); #pragma warning restore 612, 618 } } diff --git a/api/WhatApi/Program.cs b/api/WhatApi/Program.cs index 80d06e8..884240d 100644 --- a/api/WhatApi/Program.cs +++ b/api/WhatApi/Program.cs @@ -1,7 +1,12 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.EntityFrameworkCore; -using NetTopologySuite.IO.Converters; +global using System.Text.Json; +global using System.Text.Json.Serialization; +global using Microsoft.EntityFrameworkCore; +global using NetTopologySuite.IO.Converters; +global using Microsoft.EntityFrameworkCore.Metadata.Builders; +global using NetTopologySuite.Geometries; +global using Microsoft.AspNetCore.Http.Extensions; +global using Microsoft.AspNetCore.Mvc; +global using NetTopologySuite; namespace WhatApi; @@ -10,6 +15,7 @@ public partial class Program public static int Main(string[] args) { var builder = WebApplication.CreateBuilder(args); + builder.Services.AddHttpContextAccessor(); builder.Services.AddDbContextPool<Database>(b => { b.EnableSensitiveDataLogging(); b.UseNpgsql(builder.Configuration.GetConnectionString("Master"), o => o.UseNetTopologySuite()); diff --git a/api/WhatApi/Seed.cs b/api/WhatApi/Seed.cs index c3127cc..13bd4aa 100644 --- a/api/WhatApi/Seed.cs +++ b/api/WhatApi/Seed.cs @@ -1,6 +1,5 @@ using Bogus; using Bogus.Locations; -using NetTopologySuite.Geometries; using WhatApi.Tables; namespace WhatApi; diff --git a/api/WhatApi/Tables/AuditTrail.cs b/api/WhatApi/Tables/AuditTrail.cs new file mode 100644 index 0000000..9613af4 --- /dev/null +++ b/api/WhatApi/Tables/AuditTrail.cs @@ -0,0 +1,39 @@ +namespace WhatApi.Tables; + +public class AuditTrail +{ + public Guid Id { get; init; } + public Guid? UserId { get; init; } + public required string EntityName { get; init; } + public string? PrimaryKey { get; set; } + public TrailType TrailType { get; set; } + public DateTimeOffset DateUtc { get; init; } + + public Dictionary<string, object?> OldValues { get; init; } = []; + public Dictionary<string, object?> NewValues { get; init; } = []; + public List<string> ChangedColumns { get; init; } = []; +} + +public class AuditTrailConfiguration : IEntityTypeConfiguration<AuditTrail> +{ + public void Configure(EntityTypeBuilder<AuditTrail> builder) + { + builder.ToTable("audit_trails"); + builder.HasIndex(e => e.EntityName); + builder.Property(e => e.EntityName).HasMaxLength(100).IsRequired(); + builder.Property(e => e.PrimaryKey).HasMaxLength(100); + builder.Property(e => e.DateUtc).IsRequired(); + builder.Property(e => e.TrailType).HasConversion<string>(); + builder.Property(e => e.OldValues).HasColumnType("jsonb"); + builder.Property(e => e.NewValues).HasColumnType("jsonb"); + builder.Property(e => e.ChangedColumns).HasColumnType("jsonb"); + } +} + +public enum TrailType : byte +{ + None = 0, + Create = 1, + Update = 2, + Delete = 3 +}
\ No newline at end of file diff --git a/api/WhatApi/Tables/Content.cs b/api/WhatApi/Tables/Content.cs index 79f2579..0b26b82 100644 --- a/api/WhatApi/Tables/Content.cs +++ b/api/WhatApi/Tables/Content.cs @@ -2,11 +2,23 @@ using System.Net; namespace WhatApi.Tables; -public class Content +public class Content : IAuditableEntity { public Guid Id { get; set; } - public string Mime { get; set; } - public DateTime Created { get; set; } + public required string Mime { get; set; } public Guid BlobId { get; set; } - public IPAddress Ip { get; set; } + public required IPAddress Ip { get; set; } + public DateTimeOffset CreatedAtUtc { get; set; } + public DateTimeOffset? UpdatedAtUtc { get; set; } + public Guid CreatedBy { get; set; } + public Guid? UpdatedBy { get; set; } +} + +public class ContentConfiguration : IEntityTypeConfiguration<Content> +{ + public void Configure(EntityTypeBuilder<Content> builder) { + builder.HasKey(x => x.Id); + builder.Property(x => x.Mime).HasMaxLength(100).IsRequired(); + builder.ToTable("content"); + } }
\ No newline at end of file diff --git a/api/WhatApi/Tables/IAuditableEntity.cs b/api/WhatApi/Tables/IAuditableEntity.cs new file mode 100644 index 0000000..a11e7f2 --- /dev/null +++ b/api/WhatApi/Tables/IAuditableEntity.cs @@ -0,0 +1,9 @@ +namespace WhatApi.Tables; + +public interface IAuditableEntity +{ + public DateTimeOffset CreatedAtUtc { get; set; } + public DateTimeOffset? UpdatedAtUtc { get; set; } + public Guid CreatedBy { get; set; } + public Guid? UpdatedBy { get; set; } +}
\ No newline at end of file diff --git a/api/WhatApi/Tables/Place.cs b/api/WhatApi/Tables/Place.cs index ff95c96..abbd71a 100644 --- a/api/WhatApi/Tables/Place.cs +++ b/api/WhatApi/Tables/Place.cs @@ -1,11 +1,25 @@ -using NetTopologySuite.Geometries; - namespace WhatApi.Tables; -public class Place +public class Place : IAuditableEntity { public Guid Id { get; set; } public Guid ContentId { get; set; } - public Content Content { get; set; } + public Content Content { get; set; } = null!; public required Point Location { get; set; } + public DateTimeOffset CreatedAtUtc { get; set; } + public DateTimeOffset? UpdatedAtUtc { get; set; } + public Guid CreatedBy { get; set; } + public Guid? UpdatedBy { get; set; } +} + +public class PlaceConfiguration : IEntityTypeConfiguration<Place> +{ + public void Configure(EntityTypeBuilder<Place> builder) { + builder.ToTable("place"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Location).IsRequired(); + builder.HasOne(x => x.Content); + builder.Property(x => x.Location).HasColumnType($"geometry(point,{Constants.Wgs84SpatialReferenceId})"); + builder.HasIndex(x => x.Location).HasMethod("gist"); + } }
\ No newline at end of file diff --git a/api/WhatApi/Tables/Session.cs b/api/WhatApi/Tables/Session.cs deleted file mode 100644 index a0affa8..0000000 --- a/api/WhatApi/Tables/Session.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace WhatApi.Tables; - -public class Session -{ - public Guid Id { get; set; } - public string Token { get; set; } - public DateTime Created { get; set; } - public DateTime? Expires { get; set; } - public User User { get; set; } -}
\ No newline at end of file diff --git a/api/WhatApi/Tables/User.cs b/api/WhatApi/Tables/User.cs index 0ebe9b6..5c068b2 100644 --- a/api/WhatApi/Tables/User.cs +++ b/api/WhatApi/Tables/User.cs @@ -1,12 +1,27 @@ namespace WhatApi.Tables; -public class User +public class User : IAuditableEntity { public Guid Id { get; set; } - public string Name { get; set; } - public string Email { get; set; } - public string Password { get; set; } - public DateTime Created { get; set; } - public DateTime? LastSeen { get; set; } - public IEnumerable<Place> Places { get; set; } + public required string Name { get; set; } + public required string Email { get; set; } + public required string Password { get; set; } + public DateTimeOffset? LastSeen { get; set; } + public IEnumerable<Place> Places { get; set; } = null!; + public DateTimeOffset CreatedAtUtc { get; set; } + public DateTimeOffset? UpdatedAtUtc { get; set; } + public Guid CreatedBy { get; set; } + public Guid? UpdatedBy { get; set; } +} + +public class UserConfiguration : IEntityTypeConfiguration<User> +{ + + public void Configure(EntityTypeBuilder<User> builder) { + builder.HasKey(x => x.Id); + builder.Property(x => x.Name).HasMaxLength(50); + builder.Property(x => x.Email).HasMaxLength(100); + builder.HasMany(x => x.Places); + builder.ToTable("user"); + } }
\ No newline at end of file |
