From cd70f54266d708867a1eb35870bc755bc5b2df32 Mon Sep 17 00:00:00 2001 From: ivar Date: Wed, 3 Dec 2025 21:49:20 +0100 Subject: Refactor db --- .../.idea.WhatApi/.idea/data_source_mapping.xml | 6 + api/.idea/.idea.WhatApi/.idea/indexLayout.xml | 4 +- api/.idea/.idea.WhatApi/.idea/sqldialects.xml | 6 + api/WhatApi/Constants.cs | 9 + api/WhatApi/Database.cs | 115 ----------- api/WhatApi/Database/AppDatabase.cs | 113 +++++++++++ api/WhatApi/Database/Tables/AuditTrail.cs | 39 ++++ api/WhatApi/Database/Tables/BaseAuditableEntity.cs | 18 ++ api/WhatApi/Database/Tables/Content.cs | 20 ++ api/WhatApi/Database/Tables/IAuditableEntity.cs | 9 + api/WhatApi/Database/Tables/Place.cs | 21 ++ api/WhatApi/Database/Tables/User.cs | 27 +++ api/WhatApi/Dockerfile | 4 +- api/WhatApi/Endpoints/BaseEndpoint.cs | 1 + api/WhatApi/Endpoints/CreateUserEndpoint.cs | 30 +++ api/WhatApi/Endpoints/GetPlacesEndpoint.cs | 3 +- api/WhatApi/Endpoints/LoginEndpoint.cs | 52 +++++ api/WhatApi/Endpoints/UploadContentEndpoint.cs | 6 +- api/WhatApi/Extras/IConfigurationExtensions.cs | 9 + api/WhatApi/Extras/PasswordHasher.cs | 23 +++ api/WhatApi/Middleware/AuthenticationMiddleware.cs | 2 +- api/WhatApi/Middleware/AuthorizationMiddleware.cs | 2 +- api/WhatApi/Middleware/UserLastSeenMiddleware.cs | 2 +- .../Migrations/20251026215643_Initial.Designer.cs | 214 -------------------- api/WhatApi/Migrations/20251026215643_Initial.cs | 143 -------------- .../20251202203059_PasswordMaxLength.Designer.cs | 215 --------------------- .../Migrations/20251202203059_PasswordMaxLength.cs | 36 ---- .../Migrations/20251203204812_Initial.Designer.cs | 215 +++++++++++++++++++++ api/WhatApi/Migrations/20251203204812_Initial.cs | 143 ++++++++++++++ api/WhatApi/Migrations/AppDatabaseModelSnapshot.cs | 212 ++++++++++++++++++++ api/WhatApi/Migrations/DatabaseModelSnapshot.cs | 212 -------------------- api/WhatApi/Program.cs | 47 ++++- api/WhatApi/Seed.cs | 5 +- api/WhatApi/Tables/AuditTrail.cs | 39 ---- api/WhatApi/Tables/Content.cs | 24 --- api/WhatApi/Tables/IAuditableEntity.cs | 9 - api/WhatApi/Tables/Place.cs | 25 --- api/WhatApi/Tables/User.cs | 31 --- api/WhatApi/WhatApi.csproj | 54 +++--- api/http/http-client.env.json | 5 + api/http/login.http | 17 ++ 41 files changed, 1061 insertions(+), 1106 deletions(-) create mode 100644 api/.idea/.idea.WhatApi/.idea/data_source_mapping.xml create mode 100644 api/.idea/.idea.WhatApi/.idea/sqldialects.xml delete mode 100644 api/WhatApi/Database.cs create mode 100644 api/WhatApi/Database/AppDatabase.cs create mode 100644 api/WhatApi/Database/Tables/AuditTrail.cs create mode 100644 api/WhatApi/Database/Tables/BaseAuditableEntity.cs create mode 100644 api/WhatApi/Database/Tables/Content.cs create mode 100644 api/WhatApi/Database/Tables/IAuditableEntity.cs create mode 100644 api/WhatApi/Database/Tables/Place.cs create mode 100644 api/WhatApi/Database/Tables/User.cs create mode 100644 api/WhatApi/Endpoints/CreateUserEndpoint.cs create mode 100644 api/WhatApi/Endpoints/LoginEndpoint.cs create mode 100644 api/WhatApi/Extras/IConfigurationExtensions.cs create mode 100644 api/WhatApi/Extras/PasswordHasher.cs delete mode 100644 api/WhatApi/Migrations/20251026215643_Initial.Designer.cs delete mode 100644 api/WhatApi/Migrations/20251026215643_Initial.cs delete mode 100644 api/WhatApi/Migrations/20251202203059_PasswordMaxLength.Designer.cs delete mode 100644 api/WhatApi/Migrations/20251202203059_PasswordMaxLength.cs create mode 100644 api/WhatApi/Migrations/20251203204812_Initial.Designer.cs create mode 100644 api/WhatApi/Migrations/20251203204812_Initial.cs create mode 100644 api/WhatApi/Migrations/AppDatabaseModelSnapshot.cs delete mode 100644 api/WhatApi/Migrations/DatabaseModelSnapshot.cs delete mode 100644 api/WhatApi/Tables/AuditTrail.cs delete mode 100644 api/WhatApi/Tables/Content.cs delete mode 100644 api/WhatApi/Tables/IAuditableEntity.cs delete mode 100644 api/WhatApi/Tables/Place.cs delete mode 100644 api/WhatApi/Tables/User.cs create mode 100644 api/http/http-client.env.json create mode 100644 api/http/login.http (limited to 'api') diff --git a/api/.idea/.idea.WhatApi/.idea/data_source_mapping.xml b/api/.idea/.idea.WhatApi/.idea/data_source_mapping.xml new file mode 100644 index 0000000..16f7743 --- /dev/null +++ b/api/.idea/.idea.WhatApi/.idea/data_source_mapping.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/api/.idea/.idea.WhatApi/.idea/indexLayout.xml b/api/.idea/.idea.WhatApi/.idea/indexLayout.xml index 7b08163..323a9b7 100644 --- a/api/.idea/.idea.WhatApi/.idea/indexLayout.xml +++ b/api/.idea/.idea.WhatApi/.idea/indexLayout.xml @@ -1,7 +1,9 @@ - + + . + diff --git a/api/.idea/.idea.WhatApi/.idea/sqldialects.xml b/api/.idea/.idea.WhatApi/.idea/sqldialects.xml new file mode 100644 index 0000000..f21172d --- /dev/null +++ b/api/.idea/.idea.WhatApi/.idea/sqldialects.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/api/WhatApi/Constants.cs b/api/WhatApi/Constants.cs index 01385c1..dab7ab2 100644 --- a/api/WhatApi/Constants.cs +++ b/api/WhatApi/Constants.cs @@ -3,4 +3,13 @@ namespace WhatApi; public static class Constants { public const int Wgs84SpatialReferenceId = 4326; + public static readonly Guid SystemUid = new("8c3b23aa-e759-4cf0-b405-c13979d15586"); + + public static class Env + { + public const string MasterDbConnectionString = "MasterDbConnectionString"; + public const string TokenEntropy = "TokenEntropy"; + public const string TokenIssuer = "TokenIssuer"; + public const string TokenAudience = "TokenAudience"; + } } \ No newline at end of file diff --git a/api/WhatApi/Database.cs b/api/WhatApi/Database.cs deleted file mode 100644 index 8aeffed..0000000 --- a/api/WhatApi/Database.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.Security.Claims; -using WhatApi.Extras; -using WhatApi.Tables; - -namespace WhatApi; - -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 AuditTrails => Set(); - - protected override void OnModelCreating(ModelBuilder b) { - b.HasPostgresExtension("postgis"); - b.ApplyConfiguration(new AuditTrailConfiguration()); - b.ApplyConfiguration(new PlaceConfiguration()); - b.ApplyConfiguration(new UserConfiguration()); - b.ApplyConfiguration(new ContentConfiguration()); - base.OnModelCreating(b); - } - - public override int SaveChanges() { - if (configuration.GetValue("DISABLE_AUDIT_TRAILS")) return base.SaveChanges(); - SetAuditableProperties(); - 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 = GetActiveAuditTrails(); - if (auditEntries.Count != 0) - await AuditTrails.AddRangeAsync(auditEntries, cancellationToken); - return await base.SaveChangesAsync(cancellationToken); - } - - private List GetActiveAuditTrails() { - var userId = GetUserId(); - var entries = ChangeTracker.Entries() - .Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted); - - var auditTrails = new List(); - - 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.PropertyInfo?.CustomAttributes.FirstOrDefault(c => c.AttributeType == typeof(AuditTrailIgnoreAttribute)) != null) - 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()) { - 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/Database/AppDatabase.cs b/api/WhatApi/Database/AppDatabase.cs new file mode 100644 index 0000000..64c138f --- /dev/null +++ b/api/WhatApi/Database/AppDatabase.cs @@ -0,0 +1,113 @@ +using System.Security.Claims; + +namespace WhatApi.Database; + +public class AppDatabase(DbContextOptions options, IHttpContextAccessor httpContextAccessor, IConfiguration configuration) : DbContext(options) +{ + public DbSet Content => Set(); + public DbSet Places => Set(); + public DbSet Users => Set(); + public DbSet AuditTrails => Set(); + + protected override void OnModelCreating(ModelBuilder b) { + b.HasPostgresExtension("postgis"); + b.ApplyConfiguration(new AuditTrailConfiguration()); + b.ApplyConfiguration(new PlaceConfiguration()); + b.ApplyConfiguration(new UserConfiguration()); + b.ApplyConfiguration(new ContentConfiguration()); + base.OnModelCreating(b); + } + + public override int SaveChanges() { + if (configuration.GetValue("DISABLE_AUDIT_TRAILS")) return base.SaveChanges(); + SetAuditableProperties(); + 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 = GetActiveAuditTrails(); + if (auditEntries.Count != 0) + await AuditTrails.AddRangeAsync(auditEntries, cancellationToken); + return await base.SaveChangesAsync(cancellationToken); + } + + private List GetActiveAuditTrails() { + var userId = GetUserId(); + var entries = ChangeTracker.Entries() + .Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted); + + var auditTrails = new List(); + + 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.PropertyInfo?.CustomAttributes.FirstOrDefault(c => c.AttributeType == typeof(AuditTrailIgnoreAttribute)) != null) + 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()) { + 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/Database/Tables/AuditTrail.cs b/api/WhatApi/Database/Tables/AuditTrail.cs new file mode 100644 index 0000000..4ded46c --- /dev/null +++ b/api/WhatApi/Database/Tables/AuditTrail.cs @@ -0,0 +1,39 @@ +namespace WhatApi.Database.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 OldValues { get; init; } = []; + public Dictionary NewValues { get; init; } = []; + public List ChangedColumns { get; init; } = []; +} + +public class AuditTrailConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder 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(); + 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/Database/Tables/BaseAuditableEntity.cs b/api/WhatApi/Database/Tables/BaseAuditableEntity.cs new file mode 100644 index 0000000..25fb3fa --- /dev/null +++ b/api/WhatApi/Database/Tables/BaseAuditableEntity.cs @@ -0,0 +1,18 @@ +namespace WhatApi.Database.Tables; + +public class BaseAuditableEntity : IAuditableEntity +{ + public DateTimeOffset CreatedAtUtc { get; set; } + public DateTimeOffset? UpdatedAtUtc { get; set; } + public Guid CreatedBy { get; set; } + public Guid? UpdatedBy { get; set; } + + public void SetCreated(Guid createdBy) { + CreatedBy = createdBy; + CreatedAtUtc = DateTimeOffset.UtcNow; + } + public void SetUpdated(Guid updatedBy) { + UpdatedBy = updatedBy; + UpdatedAtUtc = DateTimeOffset.UtcNow; + } +} \ No newline at end of file diff --git a/api/WhatApi/Database/Tables/Content.cs b/api/WhatApi/Database/Tables/Content.cs new file mode 100644 index 0000000..8148f81 --- /dev/null +++ b/api/WhatApi/Database/Tables/Content.cs @@ -0,0 +1,20 @@ +using System.Net; + +namespace WhatApi.Database.Tables; + +public class Content : BaseAuditableEntity +{ + public Guid Id { get; set; } + public required string Mime { get; set; } + public Guid BlobId { get; set; } + public required IPAddress Ip { get; set; } +} + +public class ContentConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder 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/Database/Tables/IAuditableEntity.cs b/api/WhatApi/Database/Tables/IAuditableEntity.cs new file mode 100644 index 0000000..61d64fd --- /dev/null +++ b/api/WhatApi/Database/Tables/IAuditableEntity.cs @@ -0,0 +1,9 @@ +namespace WhatApi.Database.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/Database/Tables/Place.cs b/api/WhatApi/Database/Tables/Place.cs new file mode 100644 index 0000000..2914aa7 --- /dev/null +++ b/api/WhatApi/Database/Tables/Place.cs @@ -0,0 +1,21 @@ +namespace WhatApi.Database.Tables; + +public class Place : BaseAuditableEntity +{ + public Guid Id { get; set; } + public Guid ContentId { get; set; } + public Content Content { get; set; } = null!; + public required Point Location { get; set; } +} + +public class PlaceConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder 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"); + } +} diff --git a/api/WhatApi/Database/Tables/User.cs b/api/WhatApi/Database/Tables/User.cs new file mode 100644 index 0000000..bfcdb50 --- /dev/null +++ b/api/WhatApi/Database/Tables/User.cs @@ -0,0 +1,27 @@ +namespace WhatApi.Database.Tables; + +public class User : BaseAuditableEntity +{ + public Guid Id { get; set; } + public required string Name { get; set; } + public required string Email { get; set; } + public required string PasswordHash { get; set; } + public DateTimeOffset? LastSeen { get; set; } + public IEnumerable Places { get; set; } = null!; + + public void SetLastSeen() { + LastSeen = DateTimeOffset.UtcNow; + } +} + +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.PasswordHash).HasMaxLength(100); + builder.HasMany(x => x.Places); + builder.ToTable("user"); + } +} \ No newline at end of file diff --git a/api/WhatApi/Dockerfile b/api/WhatApi/Dockerfile index d559b98..2655750 100644 --- a/api/WhatApi/Dockerfile +++ b/api/WhatApi/Dockerfile @@ -1,10 +1,10 @@ -FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base USER $APP_UID WORKDIR /app EXPOSE 8080 EXPOSE 8081 -FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build ARG BUILD_CONFIGURATION=Release WORKDIR /src COPY ["WhatApi/WhatApi.csproj", "WhatApi/"] diff --git a/api/WhatApi/Endpoints/BaseEndpoint.cs b/api/WhatApi/Endpoints/BaseEndpoint.cs index 1fdf14f..0820ac1 100644 --- a/api/WhatApi/Endpoints/BaseEndpoint.cs +++ b/api/WhatApi/Endpoints/BaseEndpoint.cs @@ -2,6 +2,7 @@ using System.Net; namespace WhatApi.Endpoints; +[Authorize] [ApiController] public class BaseEndpoint : ControllerBase { diff --git a/api/WhatApi/Endpoints/CreateUserEndpoint.cs b/api/WhatApi/Endpoints/CreateUserEndpoint.cs new file mode 100644 index 0000000..dc89c16 --- /dev/null +++ b/api/WhatApi/Endpoints/CreateUserEndpoint.cs @@ -0,0 +1,30 @@ +namespace WhatApi.Endpoints; + +public class CreateUserEndpoint(AppDatabase db, IConfiguration configuration) : BaseEndpoint +{ + public class CreateUserRequest + { + public required string Email { get; set; } + public required string Password { get; set; } + public required string Name { get; set; } + } + + [AllowAnonymous] + [HttpPost("~/create-user")] + public async Task HandleAsync(CreateUserRequest req, CancellationToken ct = default) { + var userList = await db.Users.Select(c => new { + c.Name + }).ToListAsync(ct); + if (userList.Count == 0 && !configuration.IsDevelopment) return Unauthorized(); + if (userList.Any(c => c.Name.Equals(req.Name, StringComparison.InvariantCultureIgnoreCase))) return BadRequest("Username taken"); + var user = new User { + Name = req.Name, + Email = req.Email, + PasswordHash = PasswordHasher.HashPassword(req.Password) + }; + user.SetCreated(Constants.SystemUid); + db.Users.Add(user); + await db.SaveChangesAsync(ct); + return Ok(); + } +} \ No newline at end of file diff --git a/api/WhatApi/Endpoints/GetPlacesEndpoint.cs b/api/WhatApi/Endpoints/GetPlacesEndpoint.cs index 08068c8..28b1613 100644 --- a/api/WhatApi/Endpoints/GetPlacesEndpoint.cs +++ b/api/WhatApi/Endpoints/GetPlacesEndpoint.cs @@ -1,9 +1,8 @@ using NetTopologySuite.Features; -using WhatApi.Tables; namespace WhatApi.Endpoints; -public class GetPlacesEndpoint(Database db) : BaseEndpoint +public class GetPlacesEndpoint(AppDatabase db) : BaseEndpoint { [HttpGet("~/places")] public async Task HandleAsync(double w, double s, double e, double n, CancellationToken ct = default) { diff --git a/api/WhatApi/Endpoints/LoginEndpoint.cs b/api/WhatApi/Endpoints/LoginEndpoint.cs new file mode 100644 index 0000000..ee697ef --- /dev/null +++ b/api/WhatApi/Endpoints/LoginEndpoint.cs @@ -0,0 +1,52 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using Microsoft.AspNetCore.Identity; +using Microsoft.IdentityModel.Tokens; + +namespace WhatApi.Endpoints; + +public class LoginEndpoint(AppDatabase db, IConfiguration configuration) : BaseEndpoint +{ + public class LoginRequest + { + public required string Username { get; set; } + public required string Password { get; set; } + } + + [HttpPost("~/login")] + public async Task HandleAsync(LoginRequest login, CancellationToken ct = default) { + var user = await db.Users.FirstOrDefaultAsync(c => c.Name == login.Username, ct); + if (user?.PasswordHash is null) return Unauthorized(); + + var verificationResult = PasswordHasher.VerifyHashedPassword(user.PasswordHash, login.Password); + if (verificationResult == PasswordVerificationResult.Failed) return Unauthorized(); + + var tokenEntropy = configuration.GetValue(Constants.Env.TokenEntropy); + + ArgumentException.ThrowIfNullOrWhiteSpace(tokenEntropy); + + var key = Encoding.ASCII.GetBytes(tokenEntropy); + var tokenIssuer = configuration.GetValue(Constants.Env.TokenIssuer); + var tokenAudience = configuration.GetValue(Constants.Env.TokenAudience); + var tokenHandler = new JwtSecurityTokenHandler(); + + var tokenDescriptor = new SecurityTokenDescriptor { + Subject = new ClaimsIdentity([ + new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), + new Claim(ClaimTypes.Name, user.Name) + ]), + Expires = DateTime.UtcNow.AddMinutes(60), + Issuer = tokenIssuer, + Audience = tokenAudience, + SigningCredentials = new SigningCredentials( + new SymmetricSecurityKey(key), + SecurityAlgorithms.HmacSha256Signature) + }; + + var token = tokenHandler.CreateToken(tokenDescriptor); + var tokenString = tokenHandler.WriteToken(token); + user.SetLastSeen(); + await db.SaveChangesAsync(ct); + return Ok(tokenString); + } +} \ No newline at end of file diff --git a/api/WhatApi/Endpoints/UploadContentEndpoint.cs b/api/WhatApi/Endpoints/UploadContentEndpoint.cs index 2c84252..26dbdba 100644 --- a/api/WhatApi/Endpoints/UploadContentEndpoint.cs +++ b/api/WhatApi/Endpoints/UploadContentEndpoint.cs @@ -1,6 +1,6 @@ namespace WhatApi.Endpoints; -public class UploadContentEndpoint(Database db) : BaseEndpoint +public class UploadContentEndpoint(AppDatabase db) : BaseEndpoint { public record UploadContent(IFormFile File, string LatLong); @@ -19,12 +19,12 @@ public class UploadContentEndpoint(Database db) : BaseEndpoint var gf = NtsGeometryServices.Instance.CreateGeometryFactory(srid: Constants.Wgs84SpatialReferenceId); var point = gf.CreatePoint(new Coordinate(double.Parse(longitude), double.Parse(latitude))); - var place = new Tables.Place() { + var place = new Place() { ContentId = contentId, Location = point }; - var content = new Tables.Content() { + var content = new Content() { Id = contentId, Mime = request.File.ContentType, BlobId = blobId, diff --git a/api/WhatApi/Extras/IConfigurationExtensions.cs b/api/WhatApi/Extras/IConfigurationExtensions.cs new file mode 100644 index 0000000..5453699 --- /dev/null +++ b/api/WhatApi/Extras/IConfigurationExtensions.cs @@ -0,0 +1,9 @@ +namespace WhatApi.Extras; + +public static class IConfigurationExtensions +{ + extension(IConfiguration configuration) + { + public bool IsDevelopment => configuration.GetValue("ASPNETCORE_ENVIRONMENT") == "Development"; + } +} \ No newline at end of file diff --git a/api/WhatApi/Extras/PasswordHasher.cs b/api/WhatApi/Extras/PasswordHasher.cs new file mode 100644 index 0000000..88e69a7 --- /dev/null +++ b/api/WhatApi/Extras/PasswordHasher.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Identity; + +namespace WhatApi.Extras; + +public class VendorPasswordHasher : PasswordHasher; + +public static class PasswordHasher +{ + private static readonly VendorPasswordHasher _ = new(); + private static readonly User User = new() { + Name = "", + Email = "", + PasswordHash = "" + }; + + public static string HashPassword(string password) { + return _.HashPassword(User, password); + } + + public static PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword) { + return _.VerifyHashedPassword(User, hashedPassword, providedPassword); + } +} \ No newline at end of file diff --git a/api/WhatApi/Middleware/AuthenticationMiddleware.cs b/api/WhatApi/Middleware/AuthenticationMiddleware.cs index 8cdce53..a691d3c 100644 --- a/api/WhatApi/Middleware/AuthenticationMiddleware.cs +++ b/api/WhatApi/Middleware/AuthenticationMiddleware.cs @@ -1,6 +1,6 @@ namespace WhatApi.Middleware; -public class AuthenticationMiddleware(RequestDelegate next,Database db) +public class AuthenticationMiddleware(RequestDelegate next) { public async Task InvokeAsync(HttpContext context) { await next(context); diff --git a/api/WhatApi/Middleware/AuthorizationMiddleware.cs b/api/WhatApi/Middleware/AuthorizationMiddleware.cs index 26b3f4a..f8db6c4 100644 --- a/api/WhatApi/Middleware/AuthorizationMiddleware.cs +++ b/api/WhatApi/Middleware/AuthorizationMiddleware.cs @@ -1,6 +1,6 @@ namespace WhatApi.Middleware; -public class AuthorizationMiddleware(RequestDelegate next, Database db) +public class AuthorizationMiddleware(RequestDelegate next) { public async Task InvokeAsync(HttpContext context) { await next(context); diff --git a/api/WhatApi/Middleware/UserLastSeenMiddleware.cs b/api/WhatApi/Middleware/UserLastSeenMiddleware.cs index 5c7f1e7..b3fcd89 100644 --- a/api/WhatApi/Middleware/UserLastSeenMiddleware.cs +++ b/api/WhatApi/Middleware/UserLastSeenMiddleware.cs @@ -4,7 +4,7 @@ namespace WhatApi.Middleware; public class UserLastSeenMiddleware(RequestDelegate next) { - public async Task InvokeAsync(HttpContext context,Database db) { + public async Task InvokeAsync(HttpContext context, AppDatabase 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/20251026215643_Initial.Designer.cs b/api/WhatApi/Migrations/20251026215643_Initial.Designer.cs deleted file mode 100644 index c430ed1..0000000 --- a/api/WhatApi/Migrations/20251026215643_Initial.Designer.cs +++ /dev/null @@ -1,214 +0,0 @@ -// -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 - { - /// - 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("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() - .HasColumnType("text"); - - 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/20251026215643_Initial.cs b/api/WhatApi/Migrations/20251026215643_Initial.cs deleted file mode 100644 index b8f0444..0000000 --- a/api/WhatApi/Migrations/20251026215643_Initial.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using Microsoft.EntityFrameworkCore.Migrations; -using NetTopologySuite.Geometries; - -#nullable disable - -namespace WhatApi.Migrations -{ - /// - public partial class Initial : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterDatabase() - .Annotation("Npgsql:PostgresExtension:postgis", ",,"); - - migrationBuilder.CreateTable( - name: "audit_trails", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: true), - EntityName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - PrimaryKey = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - TrailType = table.Column(type: "text", nullable: false), - DateUtc = table.Column(type: "timestamp with time zone", nullable: false), - OldValues = table.Column>(type: "jsonb", nullable: false), - NewValues = table.Column>(type: "jsonb", nullable: false), - ChangedColumns = table.Column>(type: "jsonb", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_audit_trails", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "content", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Mime = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - BlobId = table.Column(type: "uuid", nullable: false), - Ip = table.Column(type: "inet", nullable: false), - CreatedAtUtc = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAtUtc = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: false), - UpdatedBy = table.Column(type: "uuid", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_content", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "user", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - Email = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - Password = table.Column(type: "text", nullable: false), - LastSeen = table.Column(type: "timestamp with time zone", nullable: true), - CreatedAtUtc = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAtUtc = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: false), - UpdatedBy = table.Column(type: "uuid", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_user", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "place", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - ContentId = table.Column(type: "uuid", nullable: false), - Location = table.Column(type: "geometry(point,4326)", nullable: false), - CreatedAtUtc = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAtUtc = table.Column(type: "timestamp with time zone", nullable: true), - CreatedBy = table.Column(type: "uuid", nullable: false), - UpdatedBy = table.Column(type: "uuid", nullable: true), - UserId = table.Column(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"); - } - - /// - 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/20251202203059_PasswordMaxLength.Designer.cs b/api/WhatApi/Migrations/20251202203059_PasswordMaxLength.Designer.cs deleted file mode 100644 index f23a067..0000000 --- a/api/WhatApi/Migrations/20251202203059_PasswordMaxLength.Designer.cs +++ /dev/null @@ -1,215 +0,0 @@ -// -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 deleted file mode 100644 index 8129df8..0000000 --- a/api/WhatApi/Migrations/20251202203059_PasswordMaxLength.cs +++ /dev/null @@ -1,36 +0,0 @@ -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/20251203204812_Initial.Designer.cs b/api/WhatApi/Migrations/20251203204812_Initial.Designer.cs new file mode 100644 index 0000000..39847ba --- /dev/null +++ b/api/WhatApi/Migrations/20251203204812_Initial.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.Database; + +#nullable disable + +namespace WhatApi.Migrations +{ + [DbContext(typeof(AppDatabase))] + [Migration("20251203204812_Initial")] + partial class Initial + { + /// + 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.Database.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.Database.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.Database.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.Database.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("PasswordHash") + .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.Database.Tables.Place", b => + { + b.HasOne("WhatApi.Database.Tables.Content", "Content") + .WithMany() + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("WhatApi.Database.Tables.User", null) + .WithMany("Places") + .HasForeignKey("UserId"); + + b.Navigation("Content"); + }); + + modelBuilder.Entity("WhatApi.Database.Tables.User", b => + { + b.Navigation("Places"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api/WhatApi/Migrations/20251203204812_Initial.cs b/api/WhatApi/Migrations/20251203204812_Initial.cs new file mode 100644 index 0000000..f49a9f3 --- /dev/null +++ b/api/WhatApi/Migrations/20251203204812_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 +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:PostgresExtension:postgis", ",,"); + + migrationBuilder.CreateTable( + name: "audit_trails", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: true), + EntityName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + PrimaryKey = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + TrailType = table.Column(type: "text", nullable: false), + DateUtc = table.Column(type: "timestamp with time zone", nullable: false), + OldValues = table.Column>(type: "jsonb", nullable: false), + NewValues = table.Column>(type: "jsonb", nullable: false), + ChangedColumns = table.Column(type: "jsonb", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_audit_trails", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "content", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Mime = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + BlobId = table.Column(type: "uuid", nullable: false), + Ip = table.Column(type: "inet", nullable: false), + CreatedAtUtc = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAtUtc = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: false), + UpdatedBy = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_content", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "user", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + Email = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + PasswordHash = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + LastSeen = table.Column(type: "timestamp with time zone", nullable: true), + CreatedAtUtc = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAtUtc = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: false), + UpdatedBy = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_user", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "place", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ContentId = table.Column(type: "uuid", nullable: false), + Location = table.Column(type: "geometry(point,4326)", nullable: false), + UserId = table.Column(type: "uuid", nullable: true), + CreatedAtUtc = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAtUtc = table.Column(type: "timestamp with time zone", nullable: true), + CreatedBy = table.Column(type: "uuid", nullable: false), + UpdatedBy = table.Column(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"); + } + + /// + 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/AppDatabaseModelSnapshot.cs b/api/WhatApi/Migrations/AppDatabaseModelSnapshot.cs new file mode 100644 index 0000000..24973b0 --- /dev/null +++ b/api/WhatApi/Migrations/AppDatabaseModelSnapshot.cs @@ -0,0 +1,212 @@ +// +using System; +using System.Collections.Generic; +using System.Net; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using WhatApi.Database; + +#nullable disable + +namespace WhatApi.Migrations +{ + [DbContext(typeof(AppDatabase))] + partial class AppDatabaseModelSnapshot : ModelSnapshot + { + protected override void BuildModel(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.Database.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.Database.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.Database.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.Database.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("PasswordHash") + .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.Database.Tables.Place", b => + { + b.HasOne("WhatApi.Database.Tables.Content", "Content") + .WithMany() + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("WhatApi.Database.Tables.User", null) + .WithMany("Places") + .HasForeignKey("UserId"); + + b.Navigation("Content"); + }); + + modelBuilder.Entity("WhatApi.Database.Tables.User", b => + { + b.Navigation("Places"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api/WhatApi/Migrations/DatabaseModelSnapshot.cs b/api/WhatApi/Migrations/DatabaseModelSnapshot.cs deleted file mode 100644 index babdf01..0000000 --- a/api/WhatApi/Migrations/DatabaseModelSnapshot.cs +++ /dev/null @@ -1,212 +0,0 @@ -// -using System; -using System.Collections.Generic; -using System.Net; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using NetTopologySuite.Geometries; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using WhatApi; - -#nullable disable - -namespace WhatApi.Migrations -{ - [DbContext(typeof(Database))] - partial class DatabaseModelSnapshot : ModelSnapshot - { - protected override void BuildModel(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/Program.cs b/api/WhatApi/Program.cs index ac7e825..fa50661 100644 --- a/api/WhatApi/Program.cs +++ b/api/WhatApi/Program.cs @@ -7,15 +7,24 @@ global using NetTopologySuite.Geometries; global using Microsoft.AspNetCore.Http.Extensions; global using Microsoft.AspNetCore.Mvc; global using NetTopologySuite; +global using WhatApi.Database.Tables; +global using WhatApi.Database; +global using System.Text; +global using WhatApi.Extras; +global using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; using Npgsql; using WhatApi; using WhatApi.Middleware; 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")); +builder.Services.AddDbContextPool(b => { + var connectionString = builder.Configuration.GetValue(Constants.Env.MasterDbConnectionString); + var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString); dataSourceBuilder.EnableDynamicJson(); if (dev) { b.EnableSensitiveDataLogging(); @@ -28,9 +37,35 @@ builder.Services.AddDbContextPool(b => { o.UseNetTopologySuite(); }); }); + if (dev) builder.Configuration["DISABLE_AUDIT_TRAILS"] = "true"; + builder.Services.AddCors(o => o.AddDefaultPolicy(p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader())); +var tokenEntropy = builder.Configuration.GetValue(Constants.Env.TokenEntropy); +ArgumentException.ThrowIfNullOrEmpty(tokenEntropy); +var tokenIssuer = builder.Configuration.GetValue(Constants.Env.TokenIssuer); +var tokenAudience = builder.Configuration.GetValue(Constants.Env.TokenAudience); + +builder.Services.AddAuthentication(options => { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => { + options.RequireHttpsMetadata = false; + options.SaveToken = true; + options.TokenValidationParameters = new TokenValidationParameters { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = tokenIssuer, + ValidAudience = tokenAudience, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(tokenEntropy)), + ClockSkew = TimeSpan.Zero + }; + }); +builder.Services.AddAuthorization(); builder.Services.AddControllers() .AddJsonOptions(o => { o.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; @@ -39,19 +74,25 @@ builder.Services.AddControllers() o.JsonSerializerOptions.Converters.Add(new GeoJsonConverterFactory()); }); + var app = builder.Build(); + if (dev) { using var scope = app.Services.CreateScope(); - var db = scope.ServiceProvider.GetRequiredService(); + var db = scope.ServiceProvider.GetRequiredService(); Seed.Full(db, opt => { opt.ClearTables = false; }); } + app.UseRouting(); app.UseForwardedHeaders(); app.UseCors(); app.MapStaticAssets(); app.UseMiddleware(); +app.UseAuthentication(); app.MapControllers(); +app.MapGet("/", () => Results.Redirect("/map")); app.Run(); + return 0; \ No newline at end of file diff --git a/api/WhatApi/Seed.cs b/api/WhatApi/Seed.cs index 0e8ab59..4b6361e 100644 --- a/api/WhatApi/Seed.cs +++ b/api/WhatApi/Seed.cs @@ -1,18 +1,17 @@ using Bogus; using Bogus.Locations; -using WhatApi.Tables; namespace WhatApi; public static class Seed { - public static void Full(Database db, Action seedOptions) { + public static void Full(AppDatabase db, Action seedOptions) { var opt = new SeedOptions(); seedOptions.Invoke(opt); Content(db, opt); } - public static void Content(Database db, SeedOptions opt) { + public static void Content(AppDatabase db, SeedOptions opt) { var any = db.Places.Any(); if (any) { if (!opt.ClearTables) return; diff --git a/api/WhatApi/Tables/AuditTrail.cs b/api/WhatApi/Tables/AuditTrail.cs deleted file mode 100644 index 9613af4..0000000 --- a/api/WhatApi/Tables/AuditTrail.cs +++ /dev/null @@ -1,39 +0,0 @@ -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 OldValues { get; init; } = []; - public Dictionary NewValues { get; init; } = []; - public List ChangedColumns { get; init; } = []; -} - -public class AuditTrailConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder 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(); - 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 deleted file mode 100644 index 0b26b82..0000000 --- a/api/WhatApi/Tables/Content.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Net; - -namespace WhatApi.Tables; - -public class Content : IAuditableEntity -{ - public Guid Id { get; set; } - public required string Mime { get; set; } - public Guid BlobId { 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 -{ - public void Configure(EntityTypeBuilder 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 deleted file mode 100644 index a11e7f2..0000000 --- a/api/WhatApi/Tables/IAuditableEntity.cs +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 969007b..0000000 --- a/api/WhatApi/Tables/Place.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace WhatApi.Tables; - -public class Place : IAuditableEntity -{ - public Guid Id { get; set; } - public Guid ContentId { 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 -{ - public void Configure(EntityTypeBuilder 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"); - } -} diff --git a/api/WhatApi/Tables/User.cs b/api/WhatApi/Tables/User.cs deleted file mode 100644 index 9044439..0000000 --- a/api/WhatApi/Tables/User.cs +++ /dev/null @@ -1,31 +0,0 @@ -using WhatApi.Extras; - -namespace WhatApi.Tables; - -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!; - public DateTimeOffset CreatedAtUtc { get; set; } - public DateTimeOffset? UpdatedAtUtc { get; set; } - public Guid CreatedBy { get; set; } - public Guid? UpdatedBy { get; set; } -} - -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"); - } -} \ No newline at end of file diff --git a/api/WhatApi/WhatApi.csproj b/api/WhatApi/WhatApi.csproj index c97c55e..91f3d3f 100644 --- a/api/WhatApi/WhatApi.csproj +++ b/api/WhatApi/WhatApi.csproj @@ -5,43 +5,45 @@ enable enable Linux - 5506c159-f534-4090-b80b-2703e1eb7f6c - + 5506c159-f534-4090-b80b-2703e1eb7f6c + true + - - .dockerignore - + + .dockerignore + - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + - + - - - Always - - - - Always - + + + Always + + + + Always + diff --git a/api/http/http-client.env.json b/api/http/http-client.env.json new file mode 100644 index 0000000..af19690 --- /dev/null +++ b/api/http/http-client.env.json @@ -0,0 +1,5 @@ +{ + "dev": { + "canonical": "http://localhost:5281" + } +} \ No newline at end of file diff --git a/api/http/login.http b/api/http/login.http new file mode 100644 index 0000000..8ebba0c --- /dev/null +++ b/api/http/login.http @@ -0,0 +1,17 @@ +### GET request to example server +POST {{canonical}}/login + +{ + "username": "", + "password": "" +} + +### Create user +POST {{canonical}}/create-user +Content-Type: application/json + +{ + "name": "ivarsu", + "email": "ivarsu@oiee.no", + "password": "ivargangertre" +} -- cgit v1.3