summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorivar <i@oiee.no>2025-10-26 22:57:28 +0100
committerivar <i@oiee.no>2025-10-26 22:57:28 +0100
commit842502e82c4ddfea05a5f77c361aaa27f75afc42 (patch)
tree70fe96cd88600224257b80569015008626e3d317
parent5c59225ee10949cc58fccce4d968d1ede58be9b6 (diff)
downloadwhat-842502e82c4ddfea05a5f77c361aaa27f75afc42.tar.xz
what-842502e82c4ddfea05a5f77c361aaa27f75afc42.zip
Refactor db schema and add audits
-rw-r--r--api/WhatApi/Database.cs112
-rw-r--r--api/WhatApi/Endpoints/BaseEndpoint.cs1
-rw-r--r--api/WhatApi/Endpoints/DownloadContentEndpoint.cs2
-rw-r--r--api/WhatApi/Endpoints/GetPlacesEndpoint.cs4
-rw-r--r--api/WhatApi/Endpoints/UploadContentEndpoint.cs5
-rw-r--r--api/WhatApi/Migrations/20251013213511_Initial.Designer.cs93
-rw-r--r--api/WhatApi/Migrations/20251013213511_Initial.cs75
-rw-r--r--api/WhatApi/Migrations/20251026215643_Initial.Designer.cs214
-rw-r--r--api/WhatApi/Migrations/20251026215643_Initial.cs143
-rw-r--r--api/WhatApi/Migrations/DatabaseModelSnapshot.cs129
-rw-r--r--api/WhatApi/Program.cs14
-rw-r--r--api/WhatApi/Seed.cs1
-rw-r--r--api/WhatApi/Tables/AuditTrail.cs39
-rw-r--r--api/WhatApi/Tables/Content.cs20
-rw-r--r--api/WhatApi/Tables/IAuditableEntity.cs9
-rw-r--r--api/WhatApi/Tables/Place.cs22
-rw-r--r--api/WhatApi/Tables/Session.cs10
-rw-r--r--api/WhatApi/Tables/User.cs29
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