Add initial implementation of API, database, and user management components.
This commit is contained in:
188
MikrocopApi/Middleware/ApiRequestLoggingMiddleware.cs
Normal file
188
MikrocopApi/Middleware/ApiRequestLoggingMiddleware.cs
Normal file
@@ -0,0 +1,188 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using MikrocopApi.Exceptions;
|
||||
|
||||
namespace MikrocopApi.Middleware;
|
||||
|
||||
public sealed class ApiRequestLoggingMiddleware
|
||||
{
|
||||
private static readonly HashSet<string> SensitiveFields = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"password",
|
||||
"passwordhash",
|
||||
"apikey",
|
||||
"x-api-key"
|
||||
};
|
||||
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<ApiRequestLoggingMiddleware> _logger;
|
||||
|
||||
public ApiRequestLoggingMiddleware(RequestDelegate next, ILogger<ApiRequestLoggingMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var requestBody = await ExtractBodyAsync(context.Request);
|
||||
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
var requestParameters = BuildRequestParameters(context, requestBody);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Time={Time}; ClientIp={ClientIp}; ClientName={ClientName}; Host={Host}; ApiMethod={ApiMethod}; RequestParameters={RequestParameters}; Message={Message}",
|
||||
DateTimeOffset.UtcNow,
|
||||
GetClientIp(context),
|
||||
GetClientName(context),
|
||||
Environment.MachineName,
|
||||
GetApiMethod(context),
|
||||
requestParameters,
|
||||
"Request completed.");
|
||||
}
|
||||
catch (AppException ex)
|
||||
{
|
||||
var requestParameters = BuildRequestParameters(context, requestBody);
|
||||
_logger.LogInformation(
|
||||
ex,
|
||||
"Time={Time}; ClientIp={ClientIp}; ClientName={ClientName}; Host={Host}; ApiMethod={ApiMethod}; RequestParameters={RequestParameters}; Message={Message}",
|
||||
DateTimeOffset.UtcNow,
|
||||
GetClientIp(context),
|
||||
GetClientName(context),
|
||||
Environment.MachineName,
|
||||
GetApiMethod(context),
|
||||
requestParameters,
|
||||
"Request handled with business exception.");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var requestParameters = BuildRequestParameters(context, requestBody);
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Time={Time}; ClientIp={ClientIp}; ClientName={ClientName}; Host={Host}; ApiMethod={ApiMethod}; RequestParameters={RequestParameters}; Message={Message}",
|
||||
DateTimeOffset.UtcNow,
|
||||
GetClientIp(context),
|
||||
GetClientName(context),
|
||||
Environment.MachineName,
|
||||
GetApiMethod(context),
|
||||
requestParameters,
|
||||
"Request failed.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetClientIp(HttpContext context)
|
||||
{
|
||||
return context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
||||
}
|
||||
|
||||
private static string GetClientName(HttpContext context)
|
||||
{
|
||||
if (context.User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
return context.User.Identity.Name ?? "authenticated-user";
|
||||
}
|
||||
|
||||
return "anonymous";
|
||||
}
|
||||
|
||||
private static string GetApiMethod(HttpContext context)
|
||||
{
|
||||
return context.GetEndpoint()?.DisplayName ?? $"{context.Request.Method} {context.Request.Path}";
|
||||
}
|
||||
|
||||
private static string BuildRequestParameters(HttpContext context, string? body)
|
||||
{
|
||||
var query = context.Request.Query.ToDictionary(kvp => kvp.Key, kvp => SanitizeValue(kvp.Key, kvp.Value.ToString()));
|
||||
var route = context.Request.RouteValues.ToDictionary(kvp => kvp.Key, kvp => SanitizeValue(kvp.Key, kvp.Value?.ToString() ?? string.Empty));
|
||||
|
||||
var payload = new
|
||||
{
|
||||
query,
|
||||
route,
|
||||
body
|
||||
};
|
||||
|
||||
return JsonSerializer.Serialize(payload);
|
||||
}
|
||||
|
||||
private static async Task<string?> ExtractBodyAsync(HttpRequest request)
|
||||
{
|
||||
if (request.ContentLength is not > 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
request.EnableBuffering();
|
||||
request.Body.Position = 0;
|
||||
using var reader = new StreamReader(request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true);
|
||||
var rawBody = await reader.ReadToEndAsync();
|
||||
request.Body.Position = 0;
|
||||
|
||||
return SanitizeBody(rawBody);
|
||||
}
|
||||
|
||||
private static string SanitizeBody(string body)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
return body;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var node = JsonNode.Parse(body);
|
||||
if (node is null)
|
||||
{
|
||||
return body;
|
||||
}
|
||||
|
||||
SanitizeNode(node);
|
||||
return node.ToJsonString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SanitizeNode(JsonNode node)
|
||||
{
|
||||
if (node is JsonObject jsonObject)
|
||||
{
|
||||
foreach (var kvp in jsonObject.ToList())
|
||||
{
|
||||
if (SensitiveFields.Contains(kvp.Key))
|
||||
{
|
||||
jsonObject[kvp.Key] = "***";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (kvp.Value is not null)
|
||||
{
|
||||
SanitizeNode(kvp.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node is JsonArray jsonArray)
|
||||
{
|
||||
foreach (var item in jsonArray)
|
||||
{
|
||||
if (item is not null)
|
||||
{
|
||||
SanitizeNode(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string SanitizeValue(string key, string value)
|
||||
{
|
||||
return SensitiveFields.Contains(key) ? "***" : value;
|
||||
}
|
||||
}
|
||||
42
MikrocopApi/Middleware/ExceptionHandlingMiddleware.cs
Normal file
42
MikrocopApi/Middleware/ExceptionHandlingMiddleware.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MikrocopApi.Exceptions;
|
||||
|
||||
namespace MikrocopApi.Middleware;
|
||||
|
||||
public sealed class ExceptionHandlingMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
public ExceptionHandlingMiddleware(RequestDelegate next)
|
||||
{
|
||||
_next = next;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
}
|
||||
catch (AppException ex)
|
||||
{
|
||||
context.Response.StatusCode = ex.StatusCode;
|
||||
await context.Response.WriteAsJsonAsync(new ProblemDetails
|
||||
{
|
||||
Title = ex.Title,
|
||||
Detail = ex.Message,
|
||||
Status = ex.StatusCode
|
||||
});
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||
await context.Response.WriteAsJsonAsync(new ProblemDetails
|
||||
{
|
||||
Title = "Internal Server Error",
|
||||
Detail = "An unexpected error occurred.",
|
||||
Status = StatusCodes.Status500InternalServerError
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user