- new projects WebDrop.FileApi (file service), WebDrop.FileProtocol (shared tickets and receipts) and WebDrop.Sync (WebDAV) - file content of any size goes to the file service; chat, avatars and space structure stay on the main backend - tickets issued by the main backend, receipts by the file service, deletion queue and daily reconciliation - daily database snapshot and file sync to external storage (OpenList/WebDAV) with a 30 day retention - reorganize Core, Storage and Api into feature folders (namespaces unchanged) - web client uploads file frames to the file service and reads them with tickets
26 lines
1.3 KiB
C#
26 lines
1.3 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Security.Cryptography;
|
|
using WebDrop.Core;
|
|
|
|
namespace WebDrop.Api;
|
|
|
|
public sealed record PasskeyCeremony(string Username, string Purpose, string State, string PrfSalt, string? SessionHash, DateTimeOffset Expires);
|
|
public sealed class PasskeyCeremonies
|
|
{
|
|
private readonly ConcurrentDictionary<string, PasskeyCeremony> _pending = new();
|
|
public string Put(PasskeyCeremony ceremony)
|
|
{
|
|
foreach (var entry in _pending) if (entry.Value.Expires <= DateTimeOffset.UtcNow) _pending.TryRemove(entry.Key, out _);
|
|
if (_pending.Count >= 1000) throw new WebDropException(ErrorCode.RateLimited);
|
|
var id = Codec.Encode(RandomNumberGenerator.GetBytes(32));
|
|
_pending[id] = ceremony; return id;
|
|
}
|
|
public PasskeyCeremony Take(string id, string username, string purpose, string? token)
|
|
{
|
|
if (string.IsNullOrEmpty(id) || id.Length > 100 || !_pending.TryRemove(id, out var ceremony) || ceremony.Expires <= DateTimeOffset.UtcNow
|
|
|| ceremony.Username != username || ceremony.Purpose != purpose || ceremony.SessionHash != Hash(token)) throw new WebDropException(ErrorCode.BadRequest);
|
|
return ceremony;
|
|
}
|
|
public static string? Hash(string? token) => token is null ? null : Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(token)));
|
|
}
|