From 68ffad06a6cfd2cd2015ab03fb82bf69629dd7ec Mon Sep 17 00:00:00 2001 From: ivar Date: Tue, 2 Dec 2025 22:38:23 +0100 Subject: Move off razor pages --- api/WhatApi/Database.cs | 16 +- api/WhatApi/Endpoints/GetMapPageEndpoint.cs | 12 ++ api/WhatApi/Endpoints/GetPlacesEndpoint.cs | 4 +- api/WhatApi/Endpoints/GetUploadPageEndpoint.cs | 12 ++ api/WhatApi/Endpoints/UploadContentEndpoint.cs | 1 - api/WhatApi/Extras/AuditTrailIgnoreAttribute.cs | 5 + api/WhatApi/Middleware/UserLastSeenMiddleware.cs | 4 +- .../20251202203059_PasswordMaxLength.Designer.cs | 215 +++++++++++++++++++++ .../Migrations/20251202203059_PasswordMaxLength.cs | 36 ++++ api/WhatApi/Migrations/DatabaseModelSnapshot.cs | 7 +- api/WhatApi/Pages/Map.cshtml | 92 --------- api/WhatApi/Pages/Map.cshtml.cs | 10 - api/WhatApi/Pages/Upload.cshtml | 82 -------- api/WhatApi/Pages/Upload.cshtml.cs | 10 - api/WhatApi/Program.cs | 84 ++++---- api/WhatApi/Seed.cs | 44 ++++- api/WhatApi/Tables/Place.cs | 2 +- api/WhatApi/Tables/User.cs | 6 +- api/WhatApi/Templates/TemplateFulfiller.cs | 23 +++ api/WhatApi/Templates/web_map.liquid | 85 ++++++++ api/WhatApi/Templates/web_upload.liquid | 75 +++++++ api/WhatApi/WhatApi.csproj | 27 ++- 22 files changed, 585 insertions(+), 267 deletions(-) create mode 100644 api/WhatApi/Endpoints/GetMapPageEndpoint.cs create mode 100644 api/WhatApi/Endpoints/GetUploadPageEndpoint.cs create mode 100644 api/WhatApi/Extras/AuditTrailIgnoreAttribute.cs create mode 100644 api/WhatApi/Migrations/20251202203059_PasswordMaxLength.Designer.cs create mode 100644 api/WhatApi/Migrations/20251202203059_PasswordMaxLength.cs delete mode 100644 api/WhatApi/Pages/Map.cshtml delete mode 100644 api/WhatApi/Pages/Map.cshtml.cs delete mode 100644 api/WhatApi/Pages/Upload.cshtml delete mode 100644 api/WhatApi/Pages/Upload.cshtml.cs create mode 100644 api/WhatApi/Templates/TemplateFulfiller.cs create mode 100644 api/WhatApi/Templates/web_map.liquid create mode 100644 api/WhatApi/Templates/web_upload.liquid (limited to 'api/WhatApi') diff --git a/api/WhatApi/Database.cs b/api/WhatApi/Database.cs index 8e8ed07..8aeffed 100644 --- a/api/WhatApi/Database.cs +++ b/api/WhatApi/Database.cs @@ -1,13 +1,14 @@ using System.Security.Claims; +using WhatApi.Extras; using WhatApi.Tables; namespace WhatApi; -public class Database(DbContextOptions options, IHttpContextAccessor httpContextAccessor) : DbContext(options) +public class Database(DbContextOptions options, IHttpContextAccessor httpContextAccessor, IConfiguration configuration) : DbContext(options) { public DbSet Content => Set(); public DbSet Places => Set(); - public DbSet Users => Set(); + public DbSet Users => Set(); public DbSet AuditTrails => Set(); protected override void OnModelCreating(ModelBuilder b) { @@ -20,22 +21,24 @@ public class Database(DbContextOptions options, IHttpContextAccessor h } public override int SaveChanges() { + if (configuration.GetValue("DISABLE_AUDIT_TRAILS")) return base.SaveChanges(); SetAuditableProperties(); - var auditEntries = HandleAuditingBeforeSaveChanges(); + var auditEntries = GetActiveAuditTrails(); if (auditEntries.Count != 0) AuditTrails.AddRange(auditEntries); return base.SaveChanges(); } public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) { + if (configuration.GetValue("DISABLE_AUDIT_TRAILS")) return await base.SaveChangesAsync(cancellationToken); SetAuditableProperties(); - var auditEntries = HandleAuditingBeforeSaveChanges(); + var auditEntries = GetActiveAuditTrails(); if (auditEntries.Count != 0) await AuditTrails.AddRangeAsync(auditEntries, cancellationToken); return await base.SaveChangesAsync(cancellationToken); } - private List HandleAuditingBeforeSaveChanges() { + private List GetActiveAuditTrails() { var userId = GetUserId(); var entries = ChangeTracker.Entries() .Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted); @@ -56,7 +59,8 @@ public class Database(DbContextOptions options, IHttpContextAccessor h continue; } - if (prop.Metadata.Name.Equals("Password")) continue; + if (prop.Metadata.PropertyInfo?.CustomAttributes.FirstOrDefault(c => c.AttributeType == typeof(AuditTrailIgnoreAttribute)) != null) + continue; var name = prop.Metadata.Name; diff --git a/api/WhatApi/Endpoints/GetMapPageEndpoint.cs b/api/WhatApi/Endpoints/GetMapPageEndpoint.cs new file mode 100644 index 0000000..833a98c --- /dev/null +++ b/api/WhatApi/Endpoints/GetMapPageEndpoint.cs @@ -0,0 +1,12 @@ +using WhatApi.Templates; + +namespace WhatApi.Endpoints; + +public class GetMapPageEndpoint : BaseEndpoint +{ + + [HttpGet("~/map")] + public ActionResult GetMapPage() { + return Content(TemplateFulfiller.WebMapPage(), "text/html"); + } +} \ No newline at end of file diff --git a/api/WhatApi/Endpoints/GetPlacesEndpoint.cs b/api/WhatApi/Endpoints/GetPlacesEndpoint.cs index 8dbdff6..08068c8 100644 --- a/api/WhatApi/Endpoints/GetPlacesEndpoint.cs +++ b/api/WhatApi/Endpoints/GetPlacesEndpoint.cs @@ -12,7 +12,7 @@ public class GetPlacesEndpoint(Database db) : BaseEndpoint if (w > e) { resultingQuery = db.Places .FromSqlInterpolated($""" - SELECT * FROM "Place" + SELECT * FROM "place" WHERE ST_Intersects( "Location", ST_MakeEnvelope({w}, {s}, 180, {n}, {Constants.Wgs84SpatialReferenceId}) || ST_MakeEnvelope(-180, {n}, {e}, {n}, {Constants.Wgs84SpatialReferenceId}) @@ -21,7 +21,7 @@ public class GetPlacesEndpoint(Database db) : BaseEndpoint } else { resultingQuery = db.Places .FromSqlInterpolated($""" - SELECT * FROM "Place" + SELECT * FROM "place" WHERE ST_Intersects( "Location", ST_MakeEnvelope({w}, {s}, {e}, {n}, {Constants.Wgs84SpatialReferenceId}) diff --git a/api/WhatApi/Endpoints/GetUploadPageEndpoint.cs b/api/WhatApi/Endpoints/GetUploadPageEndpoint.cs new file mode 100644 index 0000000..ea14819 --- /dev/null +++ b/api/WhatApi/Endpoints/GetUploadPageEndpoint.cs @@ -0,0 +1,12 @@ +using WhatApi.Templates; + +namespace WhatApi.Endpoints; + +public class GetUploadPageEndpoint : BaseEndpoint +{ + + [HttpGet("~/upload")] + public ActionResult GetMapPage() { + return Content(TemplateFulfiller.WebUploadPage(), "text/html"); + } +} \ No newline at end of file diff --git a/api/WhatApi/Endpoints/UploadContentEndpoint.cs b/api/WhatApi/Endpoints/UploadContentEndpoint.cs index b114310..2c84252 100644 --- a/api/WhatApi/Endpoints/UploadContentEndpoint.cs +++ b/api/WhatApi/Endpoints/UploadContentEndpoint.cs @@ -1,4 +1,3 @@ - namespace WhatApi.Endpoints; public class UploadContentEndpoint(Database db) : BaseEndpoint diff --git a/api/WhatApi/Extras/AuditTrailIgnoreAttribute.cs b/api/WhatApi/Extras/AuditTrailIgnoreAttribute.cs new file mode 100644 index 0000000..839efe6 --- /dev/null +++ b/api/WhatApi/Extras/AuditTrailIgnoreAttribute.cs @@ -0,0 +1,5 @@ +namespace WhatApi.Extras; + +[Serializable] +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +public class AuditTrailIgnoreAttribute : Attribute; \ No newline at end of file diff --git a/api/WhatApi/Middleware/UserLastSeenMiddleware.cs b/api/WhatApi/Middleware/UserLastSeenMiddleware.cs index e52f890..5c7f1e7 100644 --- a/api/WhatApi/Middleware/UserLastSeenMiddleware.cs +++ b/api/WhatApi/Middleware/UserLastSeenMiddleware.cs @@ -2,9 +2,9 @@ using System.Security.Claims; namespace WhatApi.Middleware; -public class UserLastSeenMiddleware(RequestDelegate next, Database db) +public class UserLastSeenMiddleware(RequestDelegate next) { - public async Task InvokeAsync(HttpContext context) { + public async Task InvokeAsync(HttpContext context,Database db) { var userIdString = context.User.FindFirstValue(ClaimTypes.NameIdentifier); if (Guid.TryParse(userIdString, out var userId)) { var user = await db.Users.FirstOrDefaultAsync(c => c.Id == userId); diff --git a/api/WhatApi/Migrations/20251202203059_PasswordMaxLength.Designer.cs b/api/WhatApi/Migrations/20251202203059_PasswordMaxLength.Designer.cs new file mode 100644 index 0000000..f23a067 --- /dev/null +++ b/api/WhatApi/Migrations/20251202203059_PasswordMaxLength.Designer.cs @@ -0,0 +1,215 @@ +// +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("20251202203059_PasswordMaxLength")] + partial class PasswordMaxLength + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("WhatApi.Tables.AuditTrail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.PrimitiveCollection("ChangedColumns") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DateUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property>("NewValues") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property>("OldValues") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("PrimaryKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TrailType") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("EntityName"); + + b.ToTable("audit_trails", (string)null); + }); + + modelBuilder.Entity("WhatApi.Tables.Content", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BlobId") + .HasColumnType("uuid"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Ip") + .IsRequired() + .HasColumnType("inet"); + + b.Property("Mime") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("content", (string)null); + }); + + modelBuilder.Entity("WhatApi.Tables.Place", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentId") + .HasColumnType("uuid"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Location") + .IsRequired() + .HasColumnType("geometry(point,4326)"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("uuid"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LastSeen") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("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/20251202203059_PasswordMaxLength.cs b/api/WhatApi/Migrations/20251202203059_PasswordMaxLength.cs new file mode 100644 index 0000000..8129df8 --- /dev/null +++ b/api/WhatApi/Migrations/20251202203059_PasswordMaxLength.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace WhatApi.Migrations +{ + /// + public partial class PasswordMaxLength : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Password", + table: "user", + type: "character varying(100)", + maxLength: 100, + nullable: false, + oldClrType: typeof(string), + oldType: "text"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Password", + table: "user", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(100)", + oldMaxLength: 100); + } + } +} diff --git a/api/WhatApi/Migrations/DatabaseModelSnapshot.cs b/api/WhatApi/Migrations/DatabaseModelSnapshot.cs index 1d81e04..babdf01 100644 --- a/api/WhatApi/Migrations/DatabaseModelSnapshot.cs +++ b/api/WhatApi/Migrations/DatabaseModelSnapshot.cs @@ -20,7 +20,7 @@ namespace WhatApi.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "9.0.9") + .HasAnnotation("ProductVersion", "10.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); @@ -32,7 +32,7 @@ namespace WhatApi.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.PrimitiveCollection>("ChangedColumns") + b.PrimitiveCollection("ChangedColumns") .IsRequired() .HasColumnType("jsonb"); @@ -173,7 +173,8 @@ namespace WhatApi.Migrations b.Property("Password") .IsRequired() - .HasColumnType("text"); + .HasMaxLength(100) + .HasColumnType("character varying(100)"); b.Property("UpdatedAtUtc") .HasColumnType("timestamp with time zone"); diff --git a/api/WhatApi/Pages/Map.cshtml b/api/WhatApi/Pages/Map.cshtml deleted file mode 100644 index 31adf86..0000000 --- a/api/WhatApi/Pages/Map.cshtml +++ /dev/null @@ -1,92 +0,0 @@ -@page -@model WhatApi.Pages.Map - -@{ - Layout = null; -} - - - - - - - - - - -
- - - - \ No newline at end of file diff --git a/api/WhatApi/Pages/Map.cshtml.cs b/api/WhatApi/Pages/Map.cshtml.cs deleted file mode 100644 index 786180b..0000000 --- a/api/WhatApi/Pages/Map.cshtml.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace WhatApi.Pages; - -public class Map : PageModel -{ - public void OnGet() { - - } -} \ No newline at end of file diff --git a/api/WhatApi/Pages/Upload.cshtml b/api/WhatApi/Pages/Upload.cshtml deleted file mode 100644 index 4c87b11..0000000 --- a/api/WhatApi/Pages/Upload.cshtml +++ /dev/null @@ -1,82 +0,0 @@ -@page -@model WhatApi.Pages.Upload - -@{ - Layout = null; -} - - - - - - - - - - -
-
- - - -
-
-
-

-
-
-
-
\ No newline at end of file
diff --git a/api/WhatApi/Pages/Upload.cshtml.cs b/api/WhatApi/Pages/Upload.cshtml.cs
deleted file mode 100644
index 2fe362e..0000000
--- a/api/WhatApi/Pages/Upload.cshtml.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using Microsoft.AspNetCore.Mvc.RazorPages;
-
-namespace WhatApi.Pages;
-
-public class Upload : PageModel
-{
-    public void OnGet() {
-
-    }
-}
\ No newline at end of file
diff --git a/api/WhatApi/Program.cs b/api/WhatApi/Program.cs
index 823402d..ac7e825 100644
--- a/api/WhatApi/Program.cs
+++ b/api/WhatApi/Program.cs
@@ -7,47 +7,51 @@ global using NetTopologySuite.Geometries;
 global using Microsoft.AspNetCore.Http.Extensions;
 global using Microsoft.AspNetCore.Mvc;
 global using NetTopologySuite;
+using Npgsql;
+using WhatApi;
 using WhatApi.Middleware;
 
-namespace WhatApi;
-
-public partial class Program
-{
-    public static int Main(string[] args) {
-        var builder = WebApplication.CreateBuilder(args);
-
-        builder.Services.AddHttpContextAccessor();
-        builder.Services.AddDbContextPool(b => {
-            if (builder.Environment.IsDevelopment())
-                b.EnableSensitiveDataLogging();
-            b.UseNpgsql(builder.Configuration.GetConnectionString("Master"), o => o.UseNetTopologySuite());
-        });
-
-        builder.Services.AddRazorPages();
-        builder.Services.AddCors(o => o.AddDefaultPolicy(p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()));
-        builder.Services.AddControllers()
-            .AddJsonOptions(o => {
-                o.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
-                o.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals;
-                o.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
-                o.JsonSerializerOptions.Converters.Add(new GeoJsonConverterFactory());
-            });
+var builder = WebApplication.CreateBuilder(args);
+var dev = builder.Environment.IsDevelopment();
+builder.Services.AddHttpContextAccessor();
+builder.Services.AddDbContextPool(b => {
+    var dataSourceBuilder = new NpgsqlDataSourceBuilder(builder.Configuration.GetConnectionString("Master"));
+    dataSourceBuilder.EnableDynamicJson();
+    if (dev) {
+        b.EnableSensitiveDataLogging();
+        dataSourceBuilder.EnableParameterLogging();
+        dataSourceBuilder.UseNetTopologySuite();
+    }
+    b.UseNpgsql(dataSourceBuilder.Build(), o => {
+        o.EnableRetryOnFailure();
+        o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
+        o.UseNetTopologySuite();
+    });
+});
+if (dev) builder.Configuration["DISABLE_AUDIT_TRAILS"] = "true";
+builder.Services.AddCors(o => o.AddDefaultPolicy(p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()));
 
-        var app = builder.Build();
+builder.Services.AddControllers()
+    .AddJsonOptions(o => {
+        o.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+        o.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals;
+        o.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
+        o.JsonSerializerOptions.Converters.Add(new GeoJsonConverterFactory());
+    });
 
-#if DEBUG
-        using var scope = app.Services.CreateScope();
-        var db = scope.ServiceProvider.GetRequiredService();
-        Seed(db);
-#endif
-        app.UseRouting();
-        app.UseForwardedHeaders();
-        app.UseCors();
-        app.MapStaticAssets();
-        app.UseMiddleware();
-        app.MapRazorPages();
-        app.MapControllers();
-        app.Run();
-        return 0;
-    }
-}
\ No newline at end of file
+var app = builder.Build();
+if (dev) {
+    using var scope = app.Services.CreateScope();
+    var db = scope.ServiceProvider.GetRequiredService();
+    Seed.Full(db, opt => {
+        opt.ClearTables = false;
+    });
+}
+app.UseRouting();
+app.UseForwardedHeaders();
+app.UseCors();
+app.MapStaticAssets();
+app.UseMiddleware();
+app.MapControllers();
+app.Run();
+return 0;
\ No newline at end of file
diff --git a/api/WhatApi/Seed.cs b/api/WhatApi/Seed.cs
index 13bd4aa..0e8ab59 100644
--- a/api/WhatApi/Seed.cs
+++ b/api/WhatApi/Seed.cs
@@ -4,20 +4,44 @@ using WhatApi.Tables;
 
 namespace WhatApi;
 
-public partial class Program
+public static class Seed
 {
-    private static void Seed(Database db) {
-        if (db.Places.Any() || true) return;
-        var places = new List();
-        var location = new Faker().Location();
-        for (var i = 0; i < 1000; i++) {
+    public static void Full(Database db, Action seedOptions) {
+        var opt = new SeedOptions();
+        seedOptions.Invoke(opt);
+        Content(db, opt);
+    }
+
+    public static void Content(Database db, SeedOptions opt) {
+        var any = db.Places.Any();
+        if (any) {
+            if (!opt.ClearTables) return;
+            db.Places.RemoveRange(db.Places.ToList());
+            db.SaveChanges();
+        }
+        var faker = new Faker();
+        for (var i = 0; i < 30; i++) {
+            var ip = faker.Internet.IpAddress();
+            var content = new Content() {
+                Id = Guid.NewGuid(),
+                Ip = ip,
+                Mime = "image/jpeg",
+                BlobId = new Guid("3ae0ea2d-851d-4e27-ad89-205822126395")
+            };
+            var location = faker.Location();
             var point = location.AreaCircle(59.91838, 10.73861, 30000);
-            places.Add(new Place() {
+            var place = new Place() {
                 Location = new Point(new Coordinate(point.Longitude, point.Latitude)),
-                ContentId = new Guid("1337710a-8cdb-4d50-815f-772c0e9f1482")
-            });
+                Content = content
+            };
+            db.Places.Add(place);
         }
-        db.Places.AddRange(places);
+
         db.SaveChanges();
     }
+}
+
+public class SeedOptions
+{
+    public bool ClearTables { get; set; }
 }
\ No newline at end of file
diff --git a/api/WhatApi/Tables/Place.cs b/api/WhatApi/Tables/Place.cs
index abbd71a..969007b 100644
--- a/api/WhatApi/Tables/Place.cs
+++ b/api/WhatApi/Tables/Place.cs
@@ -22,4 +22,4 @@ public class PlaceConfiguration : IEntityTypeConfiguration
         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/User.cs b/api/WhatApi/Tables/User.cs
index 5c068b2..9044439 100644
--- a/api/WhatApi/Tables/User.cs
+++ b/api/WhatApi/Tables/User.cs
@@ -1,3 +1,5 @@
+using WhatApi.Extras;
+
 namespace WhatApi.Tables;
 
 public class User : IAuditableEntity
@@ -5,6 +7,8 @@ public class User : IAuditableEntity
     public Guid Id { get; set; }
     public required string Name { get; set; }
     public required string Email { get; set; }
+
+    [AuditTrailIgnore]
     public required string Password { get; set; }
     public DateTimeOffset? LastSeen { get; set; }
     public IEnumerable Places { get; set; } = null!;
@@ -16,11 +20,11 @@ public class User : IAuditableEntity
 
 public class UserConfiguration : IEntityTypeConfiguration
 {
-
     public void Configure(EntityTypeBuilder builder) {
         builder.HasKey(x => x.Id);
         builder.Property(x => x.Name).HasMaxLength(50);
         builder.Property(x => x.Email).HasMaxLength(100);
+        builder.Property(x => x.Password).HasMaxLength(100);
         builder.HasMany(x => x.Places);
         builder.ToTable("user");
     }
diff --git a/api/WhatApi/Templates/TemplateFulfiller.cs b/api/WhatApi/Templates/TemplateFulfiller.cs
new file mode 100644
index 0000000..19d3bde
--- /dev/null
+++ b/api/WhatApi/Templates/TemplateFulfiller.cs
@@ -0,0 +1,23 @@
+using Fluid;
+
+namespace WhatApi.Templates;
+
+public class TemplateFulfiller
+{
+    private static readonly FluidParser Parser = new();
+    private static readonly string TemplateDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Templates");
+    private static string WebMapTemplate => File.ReadAllText(Path.Combine(TemplateDirectory, "web_map.liquid"));
+    private static string WebUploadTemplate => File.ReadAllText(Path.Combine(TemplateDirectory, "web_upload.liquid"));
+
+    public static string WebMapPage(object? data = null) {
+        Parser.TryParse(WebMapTemplate, out var template);
+        var context = data is null ? new TemplateContext() : new TemplateContext(data);
+        return template.Render(context);
+    }
+
+    public static string WebUploadPage(object? data = null) {
+        Parser.TryParse(WebUploadTemplate, out var template);
+        var context = data is null ? new TemplateContext() : new TemplateContext(data);
+        return template.Render(context);
+    }
+}
\ No newline at end of file
diff --git a/api/WhatApi/Templates/web_map.liquid b/api/WhatApi/Templates/web_map.liquid
new file mode 100644
index 0000000..d0a70e7
--- /dev/null
+++ b/api/WhatApi/Templates/web_map.liquid
@@ -0,0 +1,85 @@
+
+
+
+
+    
+    
+    
+
+
+
+ + + + \ No newline at end of file diff --git a/api/WhatApi/Templates/web_upload.liquid b/api/WhatApi/Templates/web_upload.liquid new file mode 100644 index 0000000..5688a01 --- /dev/null +++ b/api/WhatApi/Templates/web_upload.liquid @@ -0,0 +1,75 @@ + + + + + + + + + +
+
+ + + +
+
+
+

+
+
+
+
\ No newline at end of file
diff --git a/api/WhatApi/WhatApi.csproj b/api/WhatApi/WhatApi.csproj
index 84dac71..c97c55e 100644
--- a/api/WhatApi/WhatApi.csproj
+++ b/api/WhatApi/WhatApi.csproj
@@ -1,7 +1,7 @@
 
 
     
-        net9.0
+        net10.0
         enable
         enable
         Linux
@@ -15,20 +15,33 @@
     
 
     
-      
-      
-      
-      
+      
+      
+      
+      
+      
         all
         runtime; build; native; contentfiles; analyzers; buildtransitive
       
       
-      
-      
+      
+      
+      
     
 
     
       
     
 
+    
+      
+      
+        Always
+      
+      
+      
+        Always
+      
+    
+
 
-- 
cgit v1.3