summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorivar <i@oiee.no>2025-12-02 22:38:23 +0100
committerivar <i@oiee.no>2025-12-02 22:38:23 +0100
commit68ffad06a6cfd2cd2015ab03fb82bf69629dd7ec (patch)
treeefab90e59deae00953704059efdec14cb3ad81c7
parentd2089c0038460504869b27203143e40441a86eff (diff)
downloadwhat-68ffad06a6cfd2cd2015ab03fb82bf69629dd7ec.tar.xz
what-68ffad06a6cfd2cd2015ab03fb82bf69629dd7ec.zip
Move off razor pages
-rw-r--r--.gitignore1
-rw-r--r--api/.idea/.idea.WhatApi/.idea/dataSources.xml12
-rw-r--r--api/WhatApi/Database.cs16
-rw-r--r--api/WhatApi/Endpoints/GetMapPageEndpoint.cs12
-rw-r--r--api/WhatApi/Endpoints/GetPlacesEndpoint.cs4
-rw-r--r--api/WhatApi/Endpoints/GetUploadPageEndpoint.cs12
-rw-r--r--api/WhatApi/Endpoints/UploadContentEndpoint.cs1
-rw-r--r--api/WhatApi/Extras/AuditTrailIgnoreAttribute.cs5
-rw-r--r--api/WhatApi/Middleware/UserLastSeenMiddleware.cs4
-rw-r--r--api/WhatApi/Migrations/20251202203059_PasswordMaxLength.Designer.cs215
-rw-r--r--api/WhatApi/Migrations/20251202203059_PasswordMaxLength.cs36
-rw-r--r--api/WhatApi/Migrations/DatabaseModelSnapshot.cs7
-rw-r--r--api/WhatApi/Pages/Map.cshtml92
-rw-r--r--api/WhatApi/Pages/Map.cshtml.cs10
-rw-r--r--api/WhatApi/Pages/Upload.cshtml.cs10
-rw-r--r--api/WhatApi/Program.cs84
-rw-r--r--api/WhatApi/Seed.cs44
-rw-r--r--api/WhatApi/Tables/Place.cs2
-rw-r--r--api/WhatApi/Tables/User.cs6
-rw-r--r--api/WhatApi/Templates/TemplateFulfiller.cs23
-rw-r--r--api/WhatApi/Templates/web_map.liquid85
-rw-r--r--api/WhatApi/Templates/web_upload.liquid (renamed from api/WhatApi/Pages/Upload.cshtml)43
-rw-r--r--api/WhatApi/WhatApi.csproj27
23 files changed, 541 insertions, 210 deletions
diff --git a/.gitignore b/.gitignore
index f2e5fe9..b7e80ea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -118,3 +118,4 @@ Bogus.Premium.LicenseKey
**/files/*
**/.DS_Store
**/*.user
+**/*.local
diff --git a/api/.idea/.idea.WhatApi/.idea/dataSources.xml b/api/.idea/.idea.WhatApi/.idea/dataSources.xml
new file mode 100644
index 0000000..f4ad2c2
--- /dev/null
+++ b/api/.idea/.idea.WhatApi/.idea/dataSources.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="DataSourceManagerImpl" format="xml" multifile-model="true">
+ <data-source source="LOCAL" name="postgres@localhost" uuid="4705593e-7cb4-468e-aaf4-fd47704f2409">
+ <driver-ref>postgresql</driver-ref>
+ <synchronize>true</synchronize>
+ <jdbc-driver>org.postgresql.Driver</jdbc-driver>
+ <jdbc-url>jdbc:postgresql://localhost:54322/postgres</jdbc-url>
+ <working-dir>$ProjectFileDir$</working-dir>
+ </data-source>
+ </component>
+</project> \ No newline at end of file
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<Database> options, IHttpContextAccessor httpContextAccessor) : DbContext(options)
+public class Database(DbContextOptions<Database> options, IHttpContextAccessor httpContextAccessor, IConfiguration configuration) : DbContext(options)
{
public DbSet<Content> Content => Set<Content>();
public DbSet<Place> Places => Set<Place>();
- public DbSet<User> Users => Set<User>();
+ public DbSet<User> Users => Set<User>();
public DbSet<AuditTrail> AuditTrails => Set<AuditTrail>();
protected override void OnModelCreating(ModelBuilder b) {
@@ -20,22 +21,24 @@ public class Database(DbContextOptions<Database> options, IHttpContextAccessor h
}
public override int SaveChanges() {
+ if (configuration.GetValue<bool>("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<int> SaveChangesAsync(CancellationToken cancellationToken = default) {
+ if (configuration.GetValue<bool>("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<AuditTrail> HandleAuditingBeforeSaveChanges() {
+ private List<AuditTrail> GetActiveAuditTrails() {
var userId = GetUserId();
var entries = ChangeTracker.Entries<IAuditableEntity>()
.Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted);
@@ -56,7 +59,8 @@ public class Database(DbContextOptions<Database> 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 @@
+// <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("20251202203059_PasswordMaxLength")]
+ partial class PasswordMaxLength
+ {
+ /// <inheritdoc />
+ 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<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.PrimitiveCollection<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()
+ .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("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
+{
+ /// <inheritdoc />
+ public partial class PasswordMaxLength : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterColumn<string>(
+ name: "Password",
+ table: "user",
+ type: "character varying(100)",
+ maxLength: 100,
+ nullable: false,
+ oldClrType: typeof(string),
+ oldType: "text");
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterColumn<string>(
+ 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<List<string>>("ChangedColumns")
+ b.PrimitiveCollection<string>("ChangedColumns")
.IsRequired()
.HasColumnType("jsonb");
@@ -173,7 +173,8 @@ namespace WhatApi.Migrations
b.Property<string>("Password")
.IsRequired()
- .HasColumnType("text");
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
b.Property<DateTimeOffset?>("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;
-}
-
-<!DOCTYPE html>
-
-<html>
-<head>
- <link href="https://unpkg.com/maplibre-gl@@^5.7.2/dist/maplibre-gl.css"/>
- <style>
- body {
- margin: 0;
- padding: 0;
- }
-
- html, body, #map {
- height: 90%;
- }
- </style>
- <title></title>
-</head>
-<body>
-<div id="map"></div>
-<script src="https://unpkg.com/maplibre-gl@@^5.7.2/dist/maplibre-gl.js"></script>
-<script>
- const map = new maplibregl.Map({
- container: "map",
- style: "https://tiles.openfreemap.org/styles/bright",
- center: [10.253494570441944, 59.937419399772125],
- zoom: 7
- });
-
- const markers = new Map();
-
- let t = null;
-
- map.on("moveend", () => {
- clearTimeout(t);
- t = setTimeout(updateData, 150);
- });
-
- map.on("load", () => {
- map.loadImage("/pin.png").then(image => map.addImage("custom-marker", image.data));
- map.addSource("places", {
- type: "geojson",
- data: {type: "FeatureCollection", features: []},
- // cluster: true,
- // clusterRadius: 40,
- // clusterMaxZoom: 14
- });
-
- map.addLayer({
- id: "places-layer",
- type: "symbol",
- source: "places",
- layout: {
- "icon-image": "custom-marker"
- }
- });
-
- updateData();
- });
-
- let aborter = new AbortController();
-
- async function updateData() {
- const b = map.getBounds();
- const south = b.getSouth(), west = b.getWest(), north = b.getNorth(), east = b.getEast();
-
- if (aborter) {
- aborter.abort();
- }
-
- aborter = new AbortController();
-
- const res = await fetch(`/places?w=${west}&s=${south}&e=${east}&n=${north}`, {
- signal: aborter.signal
- });
-
- if (!res.ok) {
- return;
- }
-
- const data = await res.json().finally(() => aborter = null);
- map.getSource("places").setData(data);
- }
-</script>
-</body>
-</html> \ 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.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<Database>(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<Database>(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<Database>();
- Seed(db);
-#endif
- app.UseRouting();
- app.UseForwardedHeaders();
- app.UseCors();
- app.MapStaticAssets();
- app.UseMiddleware<UserLastSeenMiddleware>();
- 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<Database>();
+ Seed.Full(db, opt => {
+ opt.ClearTables = false;
+ });
+}
+app.UseRouting();
+app.UseForwardedHeaders();
+app.UseCors();
+app.MapStaticAssets();
+app.UseMiddleware<UserLastSeenMiddleware>();
+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<Place>();
- var location = new Faker().Location();
- for (var i = 0; i < 1000; i++) {
+ public static void Full(Database db, Action<SeedOptions> 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<Place>
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<Place> Places { get; set; } = null!;
@@ -16,11 +20,11 @@ public class User : IAuditableEntity
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.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 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+ <link href="https://unpkg.com/maplibre-gl@^5.7.2/dist/maplibre-gl.css"/>
+ <style>
+ body {
+ margin: 0;
+ padding: 0;
+ }
+
+ html, body, #map {
+ height: 90%;
+ }
+ </style>
+ <title></title>
+</head>
+<body>
+<div id="map"></div>
+<script src="https://unpkg.com/maplibre-gl@^5.7.2/dist/maplibre-gl.js"></script>
+<script>
+ const map = new maplibregl.Map({
+ container: "map",
+ style: "https://tiles.openfreemap.org/styles/bright",
+ center: [10.253494570441944, 59.937419399772125],
+ zoom: 7
+ });
+
+ const markers = new Map();
+
+ let t = null;
+
+ map.on("moveend", () => {
+ clearTimeout(t);
+ t = setTimeout(updateData, 150);
+ });
+
+ map.on("load", () => {
+ map.loadImage("/pin.png").then(image => map.addImage("custom-marker", image.data));
+ map.addSource("places", {
+ type: "geojson",
+ data: {type: "FeatureCollection", features: []},
+ // cluster: true,
+ // clusterRadius: 40,
+ // clusterMaxZoom: 14
+ });
+
+ map.addLayer({
+ id: "places-layer",
+ type: "symbol",
+ source: "places",
+ layout: {
+ "icon-image": "custom-marker"
+ }
+ });
+
+ updateData();
+ });
+
+ let aborter = new AbortController();
+
+ async function updateData() {
+ const b = map.getBounds();
+ const south = b.getSouth(), west = b.getWest(), north = b.getNorth(), east = b.getEast();
+
+ if (aborter) {
+ aborter.abort();
+ }
+
+ aborter = new AbortController();
+
+ const res = await fetch(`/places?w=${west}&s=${south}&e=${east}&n=${north}`, {
+ signal: aborter.signal
+ });
+
+ if (!res.ok) {
+ return;
+ }
+
+ const data = await res.json().finally(() => aborter = null);
+ map.getSource("places").setData(data);
+ }
+</script>
+</body>
+</html> \ No newline at end of file
diff --git a/api/WhatApi/Pages/Upload.cshtml b/api/WhatApi/Templates/web_upload.liquid
index 4c87b11..5688a01 100644
--- a/api/WhatApi/Pages/Upload.cshtml
+++ b/api/WhatApi/Templates/web_upload.liquid
@@ -1,10 +1,3 @@
-@page
-@model WhatApi.Pages.Upload
-
-@{
- Layout = null;
-}
-
<!DOCTYPE html>
<html>
@@ -56,27 +49,27 @@
class="coordinates"></pre>
<script src='https://unpkg.com/maplibre-gl@5.9.0/dist/maplibre-gl.js'></script>
<script>
- const latlongInput = document.querySelector("[name=LatLong]");
- const coordinates = document.getElementById("coordinates");
- const map = new maplibregl.Map({
- container: "map",
- style: "https://tiles.openfreemap.org/styles/bright",
- center: [10.253494, 59.937419],
- zoom: 7
- });
+ const latlongInput = document.querySelector("[name=LatLong]");
+ const coordinates = document.getElementById("coordinates");
+ const map = new maplibregl.Map({
+ container: "map",
+ style: "https://tiles.openfreemap.org/styles/bright",
+ center: [10.253494, 59.937419],
+ zoom: 7
+ });
- const center = map.getCenter();
- const marker = new maplibregl.Marker({draggable: true}).setLngLat([center.lng, center.lat]).addTo(map);
+ const center = map.getCenter();
+ const marker = new maplibregl.Marker({draggable: true}).setLngLat([center.lng, center.lat]).addTo(map);
- function onDragEnd() {
- const {lat: lat, lng: lng} = marker.getLngLat();
- coordinates.style.display = "block";
- latlongInput.value = lat + "," + lng;
- coordinates.innerHTML =
- `Longitude: ${lng}<br />Latitude: ${lat}`;
- }
+ function onDragEnd() {
+ const {lat: lat, lng: lng} = marker.getLngLat();
+ coordinates.style.display = "block";
+ latlongInput.value = lat + "," + lng;
+ coordinates.innerHTML =
+ `Longitude: ${lng}<br />Latitude: ${lat}`;
+ }
- marker.on("dragend", onDragEnd);
+ marker.on("dragend", onDragEnd);
</script>
</body>
</html> \ 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 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
- <TargetFramework>net9.0</TargetFramework>
+ <TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
@@ -15,20 +15,33 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Bogus" Version="35.6.3" />
- <PackageReference Include="Bogus.Locations" Version="35.6.3" />
- <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.9" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.9">
+ <PackageReference Include="Bogus" Version="35.6.5" />
+ <PackageReference Include="Bogus.Locations" Version="35.6.5" />
+ <PackageReference Include="Fluid.Core" Version="3.0.0-beta.2" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NetTopologySuite.IO.GeoJSON4STJ" Version="4.0.0" />
- <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
- <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="9.0.4" />
+ <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
+ <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="10.0.0" />
+ <PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="files\" />
</ItemGroup>
+ <ItemGroup>
+ <None Remove="Templates\web_login.liquid" />
+ <Resource Include="Templates\web_map.liquid">
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ </Resource>
+ <None Remove="Templates\web_upload.liquid" />
+ <Resource Include="Templates\web_upload.liquid">
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ </Resource>
+ </ItemGroup>
+
</Project>