diff --git a/Gordon360/Extensions/MiddlewareExtensions.cs b/Gordon360/Extensions/MiddlewareExtensions.cs new file mode 100644 index 000000000..1cb32a614 --- /dev/null +++ b/Gordon360/Extensions/MiddlewareExtensions.cs @@ -0,0 +1,15 @@ + +using Gordon360.Utilities.Logger; +using Microsoft.AspNetCore.Builder; +// See https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/extensibility?view=aspnetcore-6.0 +// for background on how this works. +namespace Gordon360.Extensions +{ + public static class MiddlewareExtensions + { + public static IApplicationBuilder UseFactoryActivatedMiddleware( + this IApplicationBuilder app) + => app.UseMiddleware(); + } + +} diff --git a/Gordon360/Models/CCT/Context/CCTContext.cs b/Gordon360/Models/CCT/Context/CCTContext.cs index 83d831692..bc59f0c44 100644 --- a/Gordon360/Models/CCT/Context/CCTContext.cs +++ b/Gordon360/Models/CCT/Context/CCTContext.cs @@ -80,6 +80,7 @@ public CCTContext(DbContextOptions options) public virtual DbSet Police { get; set; } public virtual DbSet PrivType { get; set; } public virtual DbSet REQUEST { get; set; } + public virtual DbSet RequestResponseLog { get; set; } public virtual DbSet RequestView { get; set; } public virtual DbSet RoleType { get; set; } public virtual DbSet RoomAssign { get; set; } @@ -399,8 +400,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.PART_CDE).IsFixedLength(); entity.Property(e => e.SESS_CDE).IsFixedLength(); - - entity.Property(e => e.USER_NAME).IsFixedLength(); }); modelBuilder.Entity(entity => @@ -509,9 +508,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity(entity => { - entity.HasKey(e => e.Username) - .HasName("PK__Particip__536C85E53B50E910"); - entity.Property(e => e.ID).ValueGeneratedOnAdd(); }); @@ -525,6 +521,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.HasOne(d => d.ParticipantUsernameNavigation) .WithMany(p => p.ParticipantActivity) .HasForeignKey(d => d.ParticipantUsername) + .OnDelete(DeleteBehavior.ClientSetNull) .HasConstraintName("FK_ParticipantActivity_Participant"); entity.HasOne(d => d.PrivType) @@ -540,7 +537,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .WithMany(p => p.ParticipantNotification) .HasForeignKey(d => d.ParticipantUsername) .OnDelete(DeleteBehavior.ClientSetNull) - .HasConstraintName("FK_PartipantNotification_Participant"); + .HasConstraintName("FK_ParticipantNotification_Participant"); }); modelBuilder.Entity(entity => @@ -563,6 +560,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.HasOne(d => d.ParticipantUsernameNavigation) .WithMany(p => p.ParticipantTeam) .HasForeignKey(d => d.ParticipantUsername) + .OnDelete(DeleteBehavior.ClientSetNull) .HasConstraintName("FK_ParticipantTeam_Participant"); entity.HasOne(d => d.RoleType) diff --git a/Gordon360/Models/CCT/Context/efpt.CCT.config.json b/Gordon360/Models/CCT/Context/efpt.CCT.config.json index 78dc7de30..2b3cf6a9c 100644 --- a/Gordon360/Models/CCT/Context/efpt.CCT.config.json +++ b/Gordon360/Models/CCT/Context/efpt.CCT.config.json @@ -94,6 +94,10 @@ "Name": "[dbo].[REQUEST]", "ObjectType": 0 }, + { + "Name": "[dbo].[RequestResponseLog]", + "ObjectType": 0 + }, { "Name": "[dbo].[Rooms]", "ObjectType": 0 @@ -799,7 +803,7 @@ "ObjectType": 1 } ], - "UiHint": "SQLTrain1.CCT", + "UiHint": "sqltrain1.CCT.dbo", "UseBoolPropertiesWithoutDefaultSql": false, "UseDatabaseNames": true, "UseDbContextSplitting": false, diff --git a/Gordon360/Models/CCT/dbo/RequestResponseLog.cs b/Gordon360/Models/CCT/dbo/RequestResponseLog.cs new file mode 100644 index 000000000..288372ca6 --- /dev/null +++ b/Gordon360/Models/CCT/dbo/RequestResponseLog.cs @@ -0,0 +1,48 @@ +// This file has been auto generated by EF Core Power Tools. +#nullable disable +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace Gordon360.Models.CCT +{ + [Table("RequestResponseLog", Schema = "dbo")] + public partial class RequestResponseLog + { + [Key] + [StringLength(36)] + [Unicode(false)] + public string LogID { get; set; } + [Required] + [StringLength(15)] + [Unicode(false)] + public string ClientIP { get; set; } + [Column(TypeName = "datetime")] + public DateTime RequestDateTime { get; set; } + [Required] + [StringLength(510)] + [Unicode(false)] + public string UserAgent { get; set; } + [Required] + [StringLength(63)] + [Unicode(false)] + public string RequestHost { get; set; } + [Required] + [StringLength(7)] + [Unicode(false)] + public string RequestMethod { get; set; } + [Required] + [StringLength(100)] + [Unicode(false)] + public string RequestPath { get; set; } + [StringLength(510)] + [Unicode(false)] + public string RequestQuery { get; set; } + [Unicode(false)] + public string RequestBody { get; set; } + public int ResponseStatus { get; set; } + public int? ResponseContentLength { get; set; } + } +} \ No newline at end of file diff --git a/Gordon360/Program.cs b/Gordon360/Program.cs index 41baa575c..d146e47b8 100644 --- a/Gordon360/Program.cs +++ b/Gordon360/Program.cs @@ -16,6 +16,8 @@ using Microsoft.OpenApi.Models; using System.Collections.Generic; using System; +using Gordon360.Utilities.Logger; +using Gordon360.Extensions; var builder = WebApplication.CreateBuilder(args); @@ -75,7 +77,7 @@ corsBuilder.WithOrigins(builder.Configuration.GetValue("AllowedOrigin")).AllowAnyMethod().AllowAnyHeader(); })); -builder.Services.AddDbContext(options => +builder.Services.AddDbContextFactory(options => options.UseSqlServer(builder.Configuration.GetConnectionString("CCT")) ).AddDbContext(options => options.UseSqlServer(builder.Configuration.GetConnectionString("MyGordon")) @@ -106,6 +108,12 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +/* Logging Options */ +builder.Services.AddOptions().Bind +(builder.Configuration.GetSection("RequestResponseLogger")).ValidateDataAnnotations(); + +builder.Services.AddTransient(); + builder.Services.AddMemoryCache(); var app = builder.Build(); @@ -136,4 +144,6 @@ app.MapControllers(); +app.UseFactoryActivatedMiddleware(); + app.Run(); diff --git a/Gordon360/Utilities/Logger/LoggerMiddleware.cs b/Gordon360/Utilities/Logger/LoggerMiddleware.cs new file mode 100644 index 000000000..226345762 --- /dev/null +++ b/Gordon360/Utilities/Logger/LoggerMiddleware.cs @@ -0,0 +1,118 @@ +using Gordon360.Models.CCT; +using Gordon360.Models.CCT.Context; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Gordon360.Utilities.Logger +{ + public class RequestResponseLoggerMiddleware : IMiddleware + { + private readonly RequestResponseLoggerOptionModel _options; + private readonly CCTContext _context; + public RequestResponseLoggerMiddleware + (IOptions options, CCTContext context) + { + _options = options.Value; + _context = context; + } + + /// + /// Invoke Async is called on all HTTP requests and are intercepted by httpContext pipelining + /// + /// + /// + /// + public async Task InvokeAsync(HttpContext httpContext, RequestDelegate next) + { + RequestResponseLog log = new RequestResponseLog(); + // Middleware is enabled only when the + // EnableRequestResponseLogging config value is set. + if (_options == null || !_options.IsEnabled) + { + await next(httpContext); + return; + } + log.RequestDateTime = DateTime.UtcNow; + HttpRequest request = httpContext.Request; + + /*log*/ + log.LogID = Guid.NewGuid().ToString(); + var ip = request.HttpContext.Connection.RemoteIpAddress; + log.ClientIP = ip == null ? "" : ip.ToString(); + + /*request*/ + log.RequestMethod = request.Method; + log.RequestPath = request.Path; + log.RequestQuery = request.QueryString.ToString(); + log.UserAgent = request.Headers.UserAgent; + log.RequestBody = await ReadBodyFromRequest(request); + log.RequestHost = request.Host.ToString(); + + // Temporarily replace the HttpResponseStream, + // which is a write-only stream, with a MemoryStream to capture + // its value in-flight. + HttpResponse response = httpContext.Response; + var originalResponseBody = response.Body; + using var newResponseBody = new MemoryStream(); + response.Body = newResponseBody; + + // Call the next middleware in the pipeline + try + { + await next(httpContext); + } + catch + { + + } + + newResponseBody.Seek(0, SeekOrigin.Begin); + var responseBodyText = + await new StreamReader(response.Body).ReadToEndAsync(); + + newResponseBody.Seek(0, SeekOrigin.Begin); + await newResponseBody.CopyToAsync(originalResponseBody); + + log.ResponseStatus = response.StatusCode; + log.ResponseContentLength = GetResponseContentLength(response.Headers); + _context.RequestResponseLog.Add(log); + _context.SaveChanges(); + } + + /// + /// Returns length of response content, defaults to 0 + /// + /// + /// + private int GetResponseContentLength(IHeaderDictionary headers) + { + if (headers.ContentLength is long contentLenth) + return (int)contentLenth; + return 0; + } + + private async Task ReadBodyFromRequest(HttpRequest request) + { + // Ensure the request's body can be read multiple times + // (for the next middlewares in the pipeline). + request.EnableBuffering(); + using var streamReader = new StreamReader(request.Body, leaveOpen: true); + var requestBody = await streamReader.ReadToEndAsync(); + // Reset the request's body stream position for + // next middleware in the pipeline. + request.Body.Position = 0; + return requestBody; + } + } +} \ No newline at end of file diff --git a/Gordon360/Utilities/Logger/RequestResponseLoggerOptionModel.cs b/Gordon360/Utilities/Logger/RequestResponseLoggerOptionModel.cs new file mode 100644 index 000000000..1ddfe4420 --- /dev/null +++ b/Gordon360/Utilities/Logger/RequestResponseLoggerOptionModel.cs @@ -0,0 +1,9 @@ +namespace Gordon360.Utilities.Logger +{ + public class RequestResponseLoggerOptionModel + { + public bool IsEnabled { get; set; } + public string Name { get; set; } + public string DateTimeFormat { get; set; } + } +}