Initial commit carried over from private repo. This is V2.
This commit is contained in:
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
code/MessengerApi/Handlers/Endpoint/AckEndpointHandler.cs
Normal file
40
code/MessengerApi/Handlers/Endpoint/AckEndpointHandler.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
35
code/MessengerApi/Handlers/Endpoint/PeekEndpointHandler.cs
Normal file
35
code/MessengerApi/Handlers/Endpoint/PeekEndpointHandler.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
60
code/MessengerApi/Handlers/Endpoint/SendEndpointHandler.cs
Normal file
60
code/MessengerApi/Handlers/Endpoint/SendEndpointHandler.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
code/MessengerApi/Handlers/HousekeepingHandler.cs
Normal file
47
code/MessengerApi/Handlers/HousekeepingHandler.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
99
code/MessengerApi/Handlers/UserSetupHandler.cs
Normal file
99
code/MessengerApi/Handlers/UserSetupHandler.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user