diff options
23 files changed, 541 insertions, 210 deletions
@@ -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> |
