feat(api): bind encrypted workspaces to accounts and devices
This commit is contained in:
1 parent
019b15041a
commit
c7d8ef15bc
49 files changed
+1904
-278
No files matched your search
@@ -0,0 +1,91 @@
|
||||
using WebDrop.Core;
|
||||
using WebDrop.Storage;
|
||||
|
||||
namespace WebDrop.Api;
|
||||
|
||||
public static partial class Endpoints
|
||||
{
|
||||
private static void MapAccounts(RouteGroupBuilder api, SqliteAccountStore accounts)
|
||||
{
|
||||
api.MapPost("/accounts", async (HttpContext ctx, CancellationToken ct) =>
|
||||
{
|
||||
var req = await ReadJson(ctx.Request, ApiJson.Default.AccountRegisterRequest, ct);
|
||||
var username = NormalizeUsername(req.Username);
|
||||
var salt = DecodeBase64(req.Salt, 16, 16);
|
||||
var verifier = DecodeBase64(req.Verifier, 32, 32);
|
||||
var vault = DecodeBase64(req.Vault, 28, 1_300_000);
|
||||
ValidatePublicKey(req.PublicKey);
|
||||
if (!accounts.Register(username, salt, verifier, vault, req.PublicKey)) return Json(new ErrorBody { Error = "username_unavailable" }, ApiJson.Default.ErrorBody, StatusCodes.Status409Conflict);
|
||||
return Results.NoContent();
|
||||
}).RequireRateLimiting(CreatePolicy);
|
||||
|
||||
api.MapPost("/accounts/salt", async (HttpContext ctx, CancellationToken ct) =>
|
||||
{
|
||||
var req = await ReadJson(ctx.Request, ApiJson.Default.AccountSaltRequest, ct);
|
||||
var username = NormalizeUsername(req.Username);
|
||||
var salt = accounts.LoginSalt(username);
|
||||
return Json(new AccountSaltBody { Salt = Convert.ToBase64String(salt) }, ApiJson.Default.AccountSaltBody);
|
||||
}).RequireRateLimiting(CreatePolicy);
|
||||
|
||||
api.MapPost("/accounts/session", async (HttpContext ctx, CancellationToken ct) =>
|
||||
{
|
||||
var req = await ReadJson(ctx.Request, ApiJson.Default.AccountSessionRequest, ct);
|
||||
var username = NormalizeUsername(req.Username);
|
||||
var verifier = DecodeBase64(req.Verifier, 32, 32);
|
||||
var token = accounts.Authenticate(username, verifier, req.DeviceId, req.DeviceName);
|
||||
if (token is null) return Json(new ErrorBody { Error = "account_invalid" }, ApiJson.Default.ErrorBody, StatusCodes.Status401Unauthorized);
|
||||
return Json(new AccountSessionBody { Token = token, Username = username }, ApiJson.Default.AccountSessionBody);
|
||||
}).RequireRateLimiting(CreatePolicy);
|
||||
|
||||
api.MapGet("/accounts/vault", (HttpContext ctx) =>
|
||||
{
|
||||
if (!TryAccount(ctx, accounts, out var username)) return Json(new ErrorBody { Error = "account_invalid" }, ApiJson.Default.ErrorBody, StatusCodes.Status401Unauthorized);
|
||||
var vault = accounts.GetVault(username);
|
||||
return vault is null ? Results.NotFound() : Json(new AccountVaultBody { Revision = vault.Value.Revision, Vault = Convert.ToBase64String(vault.Value.Vault) }, ApiJson.Default.AccountVaultBody);
|
||||
});
|
||||
|
||||
api.MapGet("/accounts/devices", (HttpContext ctx) =>
|
||||
{
|
||||
if (!TryAccount(ctx, accounts, out var username)) throw new WebDropException(ErrorCode.AccountRequired);
|
||||
var current = accounts.SessionDevice(ctx.Request.Headers["X-WD-Account"].ToString());
|
||||
var devices = accounts.Devices(username, current).Select(d => new AccountDeviceBody
|
||||
{ Id = d.Id, Name = d.Name, CreatedAt = d.CreatedAt, LastSeen = d.LastSeen, Current = d.Current }).ToList();
|
||||
return Json(devices, ApiJson.Default.ListAccountDeviceBody);
|
||||
});
|
||||
api.MapPatch("/accounts/devices/{id}", async (string id, HttpContext ctx, CancellationToken ct) =>
|
||||
{
|
||||
if (!TryAccount(ctx, accounts, out var username)) throw new WebDropException(ErrorCode.AccountRequired);
|
||||
var req = await ReadJson(ctx.Request, ApiJson.Default.AccountDeviceNameRequest, ct);
|
||||
if (req.Name is null || !accounts.RenameDevice(username, id, req.Name.Trim())) throw new WebDropException(ErrorCode.BadRequest);
|
||||
return Results.NoContent();
|
||||
});
|
||||
api.MapDelete("/accounts/devices/{id}", (string id, HttpContext ctx, ChannelService svc) =>
|
||||
{
|
||||
if (!TryAccount(ctx, accounts, out var username)) throw new WebDropException(ErrorCode.AccountRequired);
|
||||
accounts.RevokeDevice(username, id);
|
||||
svc.ReleaseAccountDevice(username, id);
|
||||
return Results.NoContent();
|
||||
});
|
||||
|
||||
api.MapPut("/accounts/vault", async (HttpContext ctx, CancellationToken ct) =>
|
||||
{
|
||||
if (!TryAccount(ctx, accounts, out var username)) return Json(new ErrorBody { Error = "account_invalid" }, ApiJson.Default.ErrorBody, StatusCodes.Status401Unauthorized);
|
||||
var req = await ReadJson(ctx.Request, ApiJson.Default.AccountVaultRequest, ct);
|
||||
if (req.Revision < 0) return Results.BadRequest();
|
||||
var vault = DecodeBase64(req.Vault, 28, 1_300_000);
|
||||
var next = accounts.PutVault(username, vault, req.Revision);
|
||||
return next is null ? Json(new ErrorBody { Error = "vault_conflict" }, ApiJson.Default.ErrorBody, StatusCodes.Status409Conflict) : Json(new AccountVaultBody { Revision = next.Value, Vault = req.Vault }, ApiJson.Default.AccountVaultBody);
|
||||
});
|
||||
|
||||
api.MapDelete("/accounts/session", (HttpContext ctx) =>
|
||||
{
|
||||
var accountToken = ctx.Request.Headers["X-WD-Account"].ToString();
|
||||
if (accountToken.Length > 0) { accounts.RevokeSession(accountToken); return Results.NoContent(); }
|
||||
var header = ctx.Request.Headers["Authorization"].ToString();
|
||||
if (!header.StartsWith("Bearer ", StringComparison.Ordinal)) return Results.NoContent();
|
||||
accounts.RevokeSession(header[7..]);
|
||||
return Results.NoContent();
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user