Initial commit carried over from private repo. This is V2.
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m3s
Build and Push Docker Image / docker (push) Successful in 43s

This commit is contained in:
2025-07-04 21:24:12 +02:00
parent 7715816029
commit 4393977389
96 changed files with 3223 additions and 0 deletions

View File

@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "8.0.10",
"commands": [
"dotnet-ef"
],
"rollForward": false
}
}
}

View File

@ -0,0 +1,7 @@
namespace MessengerApi
{
public class Constants
{
public const string USERFILE_FILENAME = "/app/users.conf";
}
}

View File

@ -0,0 +1,9 @@
using MessengerApi.Db;
namespace MessengerApi.Contracts.Factories
{
public interface IDbContextFactory
{
MessengerDbContext CreateDbContext();
}
}

View File

@ -0,0 +1,15 @@
using MessengerApi.Db.Contracts.Repositories;
namespace MessengerApi.Contracts.Models.Scoped
{
public interface IUnitOfWork
{
IUserRepository Users { get; }
IUserRouteRepository UserRoutes { get; }
IMessageRepository Messages { get; }
Task SaveChanges(CancellationToken ct = default);
}
}

View File

@ -0,0 +1,8 @@
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
USER app
WORKDIR /app
EXPOSE 8080
COPY ./publish .
ENTRYPOINT ["dotnet", "MessengerApi.dll"]

View File

@ -0,0 +1,34 @@
using MessengerApi.Configuration.Model;
using MessengerApi.Configuration.Model.Persistence;
using MessengerApi.Contracts.Factories;
using MessengerApi.Db;
using MessengerApi.Db.Npg;
using MessengerApi.Db.Sql;
using Microsoft.EntityFrameworkCore;
namespace MessengerApi.Factories
{
public class DbContextFactory : IDbContextFactory, IDbContextFactory<MessengerDbContext>
{
private readonly MessengerConfiguration configuration;
public DbContextFactory(MessengerConfiguration configuration)
{
this.configuration = configuration;
}
public MessengerDbContext CreateDbContext()
{
if (this.configuration.PersistenceConfiguration.PersistenceType == Configuration.Enums.PersistenceTypes.Sql)
{
return new MessengerSqlDbContext((configuration.PersistenceConfiguration as SqlPersistenceConfiguration).ConnectionString);
}
else if (this.configuration.PersistenceConfiguration.PersistenceType == Configuration.Enums.PersistenceTypes.PostgreSql)
{
return new MessengerNpgDbContext((configuration.PersistenceConfiguration as NpgPersistenceConfiguration).ConnectionString);
}
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,32 @@
using MessengerApi.Configuration.Model;
namespace MessengerApi.Factories
{
public class LoggerFactory : IServiceProvider
{
private readonly MessengerConfiguration _configuration;
public LoggerFactory(MessengerConfiguration configuration)
{
_configuration = configuration;
}
public ILogger CreateLogger()
{
var logger = new ConsoleLogger()
{
IsDebugOutputEnabled = (this._configuration.Verbosity == Configuration.Enums.LoggingVerbosity.Debug || this._configuration.Verbosity == Configuration.Enums.LoggingVerbosity.Trace)
? true : false,
IsTraceOutputEnabled = this._configuration.Verbosity == Configuration.Enums.LoggingVerbosity.Trace
? true : false
};
return logger;
}
public object GetService(Type serviceType)
{
return this.CreateLogger();
}
}
}

View File

@ -0,0 +1,2 @@
global using portaloggy;
global using ILogger = portaloggy.ILogger;

View File

@ -0,0 +1,81 @@
using MessengerApi.Contracts.Models.Scoped;
using MessengerApi.Models;
using MessengerApi.Models.Scoped;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using System.Security.Claims;
using System.Text.Encodings.Web;
namespace MessengerApi.Handlers
{
/// <summary>
/// Validates our permananet API keys sent over as Bearer tokens.
/// </summary>
public class CustomBearerAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly IMemoryCache memoryCache;
public CustomBearerAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory loggerFactory,
UrlEncoder encoder,
IMemoryCache memoryCache)
: base(options, loggerFactory, encoder)
{
this.memoryCache = memoryCache;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
const string HEADER = "Authorization";
const string PREFIX = "Bearer ";
Context.RequestServices.GetRequiredService<Timing>(); // creates the object in scope.
if (!Request.Headers.TryGetValue(HEADER, out var authHeader) ||
!authHeader.ToString().StartsWith(PREFIX))
{
return Task.FromResult(AuthenticateResult.NoResult());
}
var token = authHeader.ToString().Substring(PREFIX.Length).Trim();
if(this.memoryCache.TryGetValue(token, out CachedIdentity oldCache))
{
var identity = Context.RequestServices.GetRequiredService<Identity>();
identity.User = oldCache.User;
identity.UserRoutes = oldCache.UserRoutes;
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(oldCache.ClaimsPrincipal, Scheme.Name)));
}
else
{
var unitOfWork = Context.RequestServices.GetRequiredService<IUnitOfWork>();
var user = unitOfWork.Users.SingleByApiKeyAndEnabled(Guid.Parse(token), true);
var routes = unitOfWork.UserRoutes.GetAllByUser(user).ToArray();
var principal = new ClaimsPrincipal(
new ClaimsIdentity(
new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Name),
new Claim(ClaimTypes.Name, user.Name)
}, Scheme.Name));
var cache = new CachedIdentity
{
ClaimsPrincipal = principal,
User = user,
UserRoutes = routes
};
this.memoryCache.Set(token, cache, TimeSpan.FromMinutes(5));
var identity = Context.RequestServices.GetRequiredService<Identity>();
identity.User = cache.User;
identity.UserRoutes = cache.UserRoutes;
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(cache.ClaimsPrincipal, Scheme.Name)));
}
}
}
}

View File

@ -0,0 +1,40 @@
using MessengerApi.Contracts.Models.Scoped;
using MessengerApi.Models.Scoped;
namespace MessengerApi.Handlers.Endpoint
{
public class AckEndpointHandler
{
private readonly ILogger logger;
private readonly IUnitOfWork unitOfWork;
private readonly Identity identity;
public AckEndpointHandler(
ILogger logger,
IUnitOfWork unitOfWork,
Identity identity)
{
this.logger = logger;
this.unitOfWork = unitOfWork;
this.identity = identity;
}
public async Task AckMessage(Guid messageId)
{
var message = unitOfWork.Messages.GetById(messageId);
// Authorize.
if (message.ToId != this.identity.User.Id)
{
throw new InvalidOperationException("It's not your message to ack.");
}
else if(!message.IsDelivered)
{
throw new InvalidOperationException("Can't ack undelivered message.");
}
// Act.
message.IsAcknowledged = true;
}
}
}

View File

@ -0,0 +1,35 @@
using MessengerApi.Contracts.Models.Scoped;
using MessengerApi.Models.Scoped;
namespace MessengerApi.Handlers.Endpoint
{
public class PeekEndpointHandler
{
private readonly ILogger logger;
private readonly Timing timing;
private readonly Identity identity;
private readonly IUnitOfWork unitOfWork;
public PeekEndpointHandler(
ILogger logger,
Timing timing,
Identity identity,
IUnitOfWork unitOfWork)
{
this.logger = logger;
this.timing = timing;
this.identity = identity;
this.unitOfWork = unitOfWork;
}
public Task<int> Peek()
{
var pendingMessages = this.unitOfWork.Messages.GetPendingMessages(this.identity.User);
this.logger.Debug($"[{this.timing.Timestamp:s}] User {this.identity.User.Name} is receiving {pendingMessages.Count()}.");
return Task.FromResult(pendingMessages.Count());
}
}
}

View File

@ -0,0 +1,43 @@
using MessengerApi.Contracts.Models.Scoped;
using MessengerApi.Db.Entities;
using MessengerApi.Models.Scoped;
namespace MessengerApi.Handlers.Endpoint
{
public class ReceiveEndpointHandler
{
private readonly ILogger logger;
private readonly Timing timing;
private readonly Identity identity;
private readonly IUnitOfWork unitOfWork;
public ReceiveEndpointHandler(
ILogger logger,
Timing timing,
Identity identity,
IUnitOfWork unitOfWork)
{
this.logger = logger;
this.timing = timing;
this.identity = identity;
this.unitOfWork = unitOfWork;
}
public Task<Message[]> ReceiveMessages()
{
var pendingMessages = this.unitOfWork.Messages.GetPendingMessages(this.identity.User);
this.logger.Debug($"[{this.timing.Timestamp:s}] User {this.identity.User.Name} is receiving {pendingMessages.Count()}.");
if (!pendingMessages.Any())
{
return Task.FromResult(new Message[0]);
}
var messages = pendingMessages.ToList();
messages.ForEach(x => x.IsDelivered = true);
return Task.FromResult(messages.ToArray());
}
}
}

View File

@ -0,0 +1,60 @@
using MessengerApi.Configuration.Model;
using MessengerApi.Contracts.Models.Scoped;
using MessengerApi.Db.Entities;
using MessengerApi.Models.Scoped;
namespace MessengerApi.Handlers.Endpoint
{
public class SendEndpointHandler
{
private readonly MessengerConfiguration configuration;
private readonly ILogger logger;
private readonly Timing timing;
private readonly Identity identity;
private readonly IUnitOfWork unitOfWork;
public SendEndpointHandler(
MessengerConfiguration configuration,
ILogger logger,
IUnitOfWork unitOfWork,
Timing timing,
Identity identity)
{
this.configuration = configuration;
this.logger = logger;
this.unitOfWork = unitOfWork;
this.timing = timing;
this.identity = identity;
}
public Task<Message> SendMessage(
Guid? toUserId,
string payload,
string payloadType,
int? payloadLifespanInSeconds)
{
// Authorize.
var targetRecipientId = toUserId.HasValue
? this.identity.UserRoutes.Single(x => x.From.Id == this.identity.User.Id && x.To.Id == toUserId.Value).To.Id
: this.identity.UserRoutes.Single().To.Id;
this.logger.Debug($"[{this.timing.Timestamp:s}] User {this.identity.User.Name} is authorized to send message to {targetRecipientId}.");
// Act.
var message = new Message
{
Id = Guid.NewGuid(),
CreatedUtc = this.timing.Timestamp,
FromId = this.identity.User.Id,
ToId = targetRecipientId,
Payload = payload,
PayloadType = payloadType,
PayloadLifespanInSeconds = payloadLifespanInSeconds ?? (this.configuration.DefaultMessageLifetimeInMinutes * 60)
};
this.unitOfWork.Messages.Add(message);
return Task.FromResult(message);
}
}
}

View File

@ -0,0 +1,47 @@
using MessengerApi.Configuration.Model;
using MessengerApi.Contracts.Factories;
using Microsoft.EntityFrameworkCore;
namespace MessengerApi.Handlers
{
public class HousekeepingHandler
{
private readonly ILogger logger;
private readonly MessengerConfiguration configuration;
private readonly IDbContextFactory dbContextFactory;
public HousekeepingHandler(
ILogger logger,
IDbContextFactory dbContextFactory,
MessengerConfiguration configuration)
{
this.logger = logger;
this.dbContextFactory = dbContextFactory;
this.configuration = configuration;
}
public async Task RemoveOldMessages()
{
this.logger.Trace($"Executing {nameof(this.RemoveOldMessages)}.");
var timestamp = DateTime.UtcNow;
var cutoff = timestamp.AddMinutes(-this.configuration.HousekeepingMessageAgeInMinutes);
using var ctx = this.dbContextFactory.CreateDbContext();
await ctx.Messages.Where(x => x.CreatedUtc < cutoff).ExecuteDeleteAsync();
if (this.configuration.HousekeepingMessageState != Configuration.Enums.HousekeepingMessageStates.None)
{
this.logger.Trace($"Executing additional message state cleaning in {nameof(this.RemoveOldMessages)}.");
if (this.configuration.HousekeepingMessageState == Configuration.Enums.HousekeepingMessageStates.Delivered)
{
await ctx.Messages.Where(x => x.IsDelivered).ExecuteDeleteAsync();
}
else if (this.configuration.HousekeepingMessageState == Configuration.Enums.HousekeepingMessageStates.Acknowledged)
{
await ctx.Messages.Where(x => x.IsAcknowledged).ExecuteDeleteAsync();
}
}
}
}
}

View File

@ -0,0 +1,99 @@
using MessengerApi.Configuration.Model;
using MessengerApi.Contracts.Factories;
using MessengerApi.Db;
using MessengerApi.Models;
using System.Text;
namespace MessengerApi.Handlers
{
// TODO: This needs to be redone, because at every run, it wipes users and creates new ones. This makes
// all existing DB messages unassignable.
public class UserSetupHandler
{
private readonly MessengerConfiguration configuration;
private readonly ILogger logger;
private readonly IDbContextFactory dbContextFactory;
public UserSetupHandler(
MessengerConfiguration configuration,
ILogger logger,
IDbContextFactory dbContextFactory)
{
this.configuration = configuration;
this.logger = logger;
this.dbContextFactory = dbContextFactory;
}
public async Task UpdateFromFile(FileInfo file)
{
if(file.Exists)
{
var lines = await File.ReadAllLinesAsync(file.FullName, Encoding.UTF8);
var items = await this.ReadLines(lines);
await this.SynchronizeUsers(items);
}
}
private async Task<UserSetupItem[]> ReadLines(string[] lines)
{
var items = new List<UserSetupItem>();
foreach (var line in lines)
{
var values = line.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var item = new UserSetupItem
{
UserName = values[0],
ApiKey = values[1],
};
if(values.Length > 2)
{
item.CanSendToUserNames = values[2].Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
}
items.Add(item);
}
if (items.GroupBy(x => x.UserName).Any(x => x.Count() > 1))
{
throw new InvalidOperationException("Usernames are not unique. One username per line.");
}
else if(items.GroupBy(x=>x.ApiKey).Any(x=>x.Count() > 1))
{
throw new InvalidOperationException("API keys are not unique. One API key per line.");
}
return items.ToArray();
}
private Task SynchronizeUsers(IEnumerable<UserSetupItem> users)
{
using var db = this.dbContextFactory.CreateDbContext();
db.RemoveRange(db.Users);
db.RemoveRange(db.UserRoutes);
var dbUsers = users.Select(x => new Db.Entities.User
{
Id = new Guid(),
Name = x.UserName,
ApiKey = Guid.Parse(x.ApiKey),
IsEnabled = true
});
var dbRoutes = users.SelectMany(x => x.CanSendToUserNames.Select(cs => new Db.Entities.UserRoute
{
Id = new Guid(),
From = dbUsers.Single(dbu => dbu.Name == x.UserName),
To = dbUsers.Single(dbu => dbu.Name == x.UserName)
}));
db.AddRange(dbUsers);
db.AddRange(dbRoutes);
db.SaveChanges();
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>85c81e87-1274-45ce-8b91-6d6619ffdfa2</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<BaseOutputPath>..\out\</BaseOutputPath>
<StartupObject>MessengerApi.Api.Program</StartupObject>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="portaloggy" />
<PackageReference Include="Swashbuckle.AspNetCore" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MessengerApi.Configuration\MessengerApi.Configuration.csproj" />
<ProjectReference Include="..\MessengerApi.Contracts\MessengerApi.Contracts.csproj" />
<ProjectReference Include="..\MessengerApi.Db.Npg\MessengerApi.Db.Npg.csproj" />
<ProjectReference Include="..\MessengerApi.Db.Sql\MessengerApi.Db.Sql.csproj" />
<ProjectReference Include="..\MessengerApi.Db\MessengerApi.Db.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,13 @@
using System.Security.Claims;
namespace MessengerApi.Models
{
public class CachedIdentity
{
public Db.Entities.User User { get; set; }
public Db.Entities.UserRoute[] UserRoutes { get; set; }
public ClaimsPrincipal ClaimsPrincipal { get; set; }
}
}

View File

@ -0,0 +1,7 @@
namespace MessengerApi.Models.Http
{
public class AckRequest
{
public Guid MessageId { get; set; }
}
}

View File

@ -0,0 +1,13 @@
namespace MessengerApi.Models.Http
{
public class SendRequest
{
public Guid? ToUserId { get; set; }
public string Payload { get; set; }
public string PayloadType { get; set; }
public int? PayloadLifetimeInSeconds { get; set; }
}
}

View File

@ -0,0 +1,7 @@
namespace MessengerApi.Models.Http
{
public class VerifyRequest
{
public Guid MessageId { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace MessengerApi.Models.Scoped
{
public class Identity
{
public Db.Entities.User User { get; set; }
public Db.Entities.UserRoute[] UserRoutes { get; set; }
}
}

View File

@ -0,0 +1,7 @@
namespace MessengerApi.Models.Scoped
{
public class Timing
{
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
}
}

View File

@ -0,0 +1,33 @@
using MessengerApi.Contracts.Factories;
using MessengerApi.Contracts.Models.Scoped;
using MessengerApi.Db;
using MessengerApi.Db.Contracts.Repositories;
using MessengerApi.Db.Repositories;
namespace MessengerApi.Models.Scoped
{
public class UnitOfWork : IUnitOfWork
{
private MessengerDbContext context;
public IUserRepository Users { get; }
public IUserRouteRepository UserRoutes { get; }
public IMessageRepository Messages { get; }
public UnitOfWork(
IDbContextFactory dbContextFactory)
{
this.context = dbContextFactory.CreateDbContext();
this.Users = new UserRepository(this.context.Users);
this.UserRoutes = new UserRouteRepository(this.context.UserRoutes);
this.Messages = new MessageRepository(this.context.Messages);
}
public Task SaveChanges(CancellationToken ct = default)
{
return this.context.SaveChangesAsync(ct);
}
}
}

View File

@ -0,0 +1,11 @@
namespace MessengerApi.Models
{
public class UserSetupItem
{
public string UserName { get; set; }
public string ApiKey { get; set; }
public string[] CanSendToUserNames { get; set; }
}
}

View File

@ -0,0 +1,313 @@
using MessengerApi.Configuration.Model;
using MessengerApi.Configuration.Model.Persistence;
using MessengerApi.Configuration.Sources.Environment;
using MessengerApi.Contracts.Factories;
using MessengerApi.Contracts.Models.Scoped;
using MessengerApi.Db;
using MessengerApi.Factories;
using MessengerApi.Handlers;
using MessengerApi.Handlers.Endpoint;
using MessengerApi.Models.Http;
using MessengerApi.Models.Scoped;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Net;
using System.Threading.RateLimiting;
namespace MessengerApi.Api
{
public class Program
{
public static void Main(string[] args)
{
MessengerConfiguration configuration = null;
try
{
configuration = new MessengerConfiguration(new EnvironmentConfigurationSource());
}
catch (Exception ex)
{
Console.WriteLine("Can't load settings.", ex);
throw;
}
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddEnvironmentVariables();
builder.Services.AddMemoryCache();
builder.Services.AddSingleton<MessengerConfiguration>(configuration);
builder.Services.AddSingleton<ILogger>(new Factories.LoggerFactory(configuration).CreateLogger());
builder.Services.AddSingleton<SendEndpointHandler>();
builder.Services.AddSingleton<HousekeepingHandler>();
builder.Services.AddSingleton<UserSetupHandler>();
builder.Services.AddSingleton<IDbContextFactory, DbContextFactory>();
builder.Services.AddScoped<Timing>();
builder.Services.AddScoped<Identity>();
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddScoped<SendEndpointHandler>();
builder.Services.AddScoped<ReceiveEndpointHandler>();
builder.Services.AddScoped<AckEndpointHandler>();
builder.Services.AddScoped<PeekEndpointHandler>();
// Authentication.
builder.Services
.AddAuthentication("Bearer")
.AddScheme<AuthenticationSchemeOptions, CustomBearerAuthenticationHandler>("Bearer", null);
// CORS.
builder.Services
.AddCors(opt => opt.AddPolicy("originpolicy", builder =>
{
builder
.WithOrigins(configuration.Origins.ToArray())
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
}));
// Ratelimiting
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
var key = httpContext.Request.Headers["Authorization"].FirstOrDefault()
?? "anonymous";
return RateLimitPartition.GetFixedWindowLimiter(key, _ => new FixedWindowRateLimiterOptions
{
PermitLimit = configuration.RateLimitPerMinute,
Window = TimeSpan.FromMinutes(1),
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 0
});
});
options.RejectionStatusCode = 429;
});
// Proxy registration to forward real client IPs.
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
foreach (var proxy in configuration.Proxies)
{
options.KnownProxies.Add(IPAddress.Parse(proxy));
}
});
var app = builder.Build();
app.UseDeveloperExceptionPage();
// DB Migrations
using (var ctx = app.Services.GetRequiredService<IDbContextFactory>().CreateDbContext())
{
var migrationLogger = app.Services.GetRequiredService<ILogger>();
try
{
if (ctx.Database.GetPendingMigrations().Any())
{
migrationLogger.Info("Applying migrations.");
ctx.Database.Migrate();
}
else
{
migrationLogger.Info("No migrations pending.");
}
}
catch (Exception ex)
{
migrationLogger.Error("Can't run migrations successfully.", ex);
throw;
}
}
// Housekeeping.
if (configuration.HousekeepingEnabled)
{
_ = Task.Run(async () =>
{
while (true)
{
await app.Services.GetService<HousekeepingHandler>().RemoveOldMessages();
await Task.Delay(TimeSpan.FromMinutes(1));
}
});
}
// User synchronization
var userSetupHandler = app.Services.GetRequiredService<UserSetupHandler>();
userSetupHandler.UpdateFromFile(new FileInfo(Constants.USERFILE_FILENAME)).GetAwaiter().GetResult();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseCors("originpolicy");
app.UseForwardedHeaders();
// Ray id logging.
app.Use(async (context, next) =>
{
var stamp = DateTime.UtcNow;
var logger = context.RequestServices.GetRequiredService<ILogger>();
var ipa = context?.Connection?.RemoteIpAddress?.ToString() ?? "unknown";
var uid = context?.User?.Identity?.Name ?? "unknown";
var una = context?.User?.Claims?.SingleOrDefault(x => x.Type == "UserName")?.Value ?? "unknown";
var rid = context?.TraceIdentifier ?? "unknown";
var endpoint = context?.GetEndpoint()?.DisplayName ?? "unknown";
logger.Info($"{endpoint} call {rid}; ip {ipa}; u {una}/{uid}");
await next();
});
app.UseRateLimiter();
// Endpoint registration.
app.MapPost("/send", async (
ILogger logger,
IUnitOfWork unitOfWork,
SendEndpointHandler handler,
[FromBody] SendRequest request) =>
{
try
{
var response = await handler.SendMessage(request.ToUserId, request.Payload, request.PayloadType, request.PayloadLifetimeInSeconds);
await unitOfWork.SaveChanges();
return Results.Json(response.Id);
}
catch (Exception ex)
{
logger.Error("Can't send.", ex);
return Results.InternalServerError();
}
});
app.MapGet("/receive", async (
ILogger logger,
IUnitOfWork unitOfWork,
ReceiveEndpointHandler handler) =>
{
try
{
var messages = await handler.ReceiveMessages();
if (messages?.Any() != true)
{
return Results.NoContent();
}
else
{
await unitOfWork.SaveChanges();
return Results.Json(new
{
Messages = messages.Select(x => new
{
Id = x.Id,
TimestampUtc = x.CreatedUtc,
Payload = x.Payload,
PayloadType = x.PayloadType,
Sender = x.FromId
})
});
}
}
catch (Exception ex)
{
logger.Error("Can't send.", ex);
return Results.InternalServerError();
}
});
app.MapPost("/ack", async (
ILogger logger,
IUnitOfWork unitOfWork,
AckEndpointHandler handler,
AckRequest request) =>
{
try
{
await handler.AckMessage(request.MessageId);
await unitOfWork.SaveChanges();
return Results.Ok();
}
catch (Exception ex)
{
logger.Error("Can't send.", ex);
return Results.InternalServerError();
}
});
app.MapGet("/yellowpages", (
ILogger logger,
IUnitOfWork unitOfWork,
Identity identity) =>
{
try
{
var routes = unitOfWork.UserRoutes.GetByFrom(identity.User).ToList();
return Results.Json(new
{
Users = routes.Select(x => new
{
Id = x.To.Id,
Name = x.To.Name
})
});
}
catch (Exception ex)
{
logger.Error("Can't yellowpages.", ex);
return Results.InternalServerError();
}
});
app.MapGet("/peek", async (
ILogger logger,
PeekEndpointHandler handler) =>
{
try
{
var pending = await handler.Peek();
return Results.Json(pending);
}
catch (Exception ex)
{
logger.Error("Can't peek.", ex);
return Results.InternalServerError();
}
});
app.MapGet("/verify", (
ILogger logger,
IUnitOfWork unitOfWork,
Guid messageId) =>
{
try
{
var message = unitOfWork.Messages.GetById(messageId);
return Results.Json(new
{
message.IsDelivered,
message.IsAcknowledged
});
}
catch (Exception ex)
{
logger.Error("Can't verify.", ex);
return Results.InternalServerError();
}
});
app.Run();
}
}
}

View File

@ -0,0 +1,28 @@
{
"profiles": {
"http": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"PERSISTENCE_TYPE": "Sql",
"CORS_ORIGINS": "",
"PROXIES": "",
"QUERY_RATE_PER_MINUTE": "100",
"DEFAULT_MESSAGE_LIFETIME_IN_MINUTES": "60",
"HOUSEKEEPING_ENABLED": "False",
"LOGGING_VERBOSITY": "Trace"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:5259"
}
},
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:55327",
"sslPort": 44348
}
}
}