feat(dashboard): add owner-scoped layouts with atomic revisions
This commit is contained in:
@@ -544,6 +544,54 @@ public sealed class AuthController : ControllerBase
|
||||
|
||||
public sealed record UpdateLanguagePreferenceRequest(string? Language);
|
||||
|
||||
[HttpGet("preferences/dashboard")]
|
||||
[Authorize(AuthenticationSchemes = "local")]
|
||||
public async Task<IActionResult> GetDashboardPreference(CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = LocalAuthIdentity.GetRequiredUserId(User);
|
||||
if (userId is null) return Unauthorized();
|
||||
var user = await _db.Users.AsNoTracking().Where(x => x.Id == userId)
|
||||
.Select(x => new { x.DashboardLayoutJson, x.DashboardLayoutRevision }).SingleOrDefaultAsync(cancellationToken);
|
||||
return user is null ? Unauthorized() : Ok(DashboardLayout.Read(user.DashboardLayoutJson, user.DashboardLayoutRevision));
|
||||
}
|
||||
|
||||
[HttpPut("preferences/dashboard")]
|
||||
[Authorize(AuthenticationSchemes = "local")]
|
||||
public async Task<IActionResult> UpdateDashboardPreference(CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = LocalAuthIdentity.GetRequiredUserId(User);
|
||||
if (userId is null) return Unauthorized();
|
||||
var existing = await _db.Users.AsNoTracking().Where(x => x.Id == userId)
|
||||
.Select(x => new { x.DashboardLayoutJson }).SingleOrDefaultAsync(cancellationToken);
|
||||
if (existing is null) return Unauthorized();
|
||||
if (DashboardLayout.Read(existing.DashboardLayoutJson, 0).Version > 1)
|
||||
return Conflict(new { error = "dashboard_version_unsupported" });
|
||||
if (Request.ContentLength > DashboardLayout.MaxBytes) return StatusCode(StatusCodes.Status413PayloadTooLarge);
|
||||
// Bound actual bytes as well as Content-Length, including chunked bodies.
|
||||
var bytes = new byte[DashboardLayout.MaxBytes + 1];
|
||||
var length = 0;
|
||||
while (length < bytes.Length)
|
||||
{
|
||||
var count = await Request.Body.ReadAsync(bytes.AsMemory(length), cancellationToken);
|
||||
if (count == 0) break;
|
||||
length += count;
|
||||
}
|
||||
if (length > DashboardLayout.MaxBytes) return StatusCode(StatusCodes.Status413PayloadTooLarge);
|
||||
DashboardLayout? layout;
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(bytes.AsMemory(0, length));
|
||||
layout = DashboardLayout.Parse(document.RootElement);
|
||||
}
|
||||
catch (JsonException) { return BadRequest("Invalid dashboard layout JSON."); }
|
||||
if (layout is null) return BadRequest("Invalid dashboard layout version, revision or widgets.");
|
||||
var json = JsonSerializer.Serialize(layout, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
var updated = await _db.Users.Where(x => x.Id == userId && x.DashboardLayoutRevision == layout.Revision)
|
||||
.ExecuteUpdateAsync(set => set.SetProperty(x => x.DashboardLayoutJson, json)
|
||||
.SetProperty(x => x.DashboardLayoutRevision, x => x.DashboardLayoutRevision + 1), cancellationToken);
|
||||
return updated == 0 ? Conflict(new { error = "dashboard_revision_conflict" }) : Ok(layout with { Revision = layout.Revision + 1 });
|
||||
}
|
||||
|
||||
[HttpPut("preferences/language")]
|
||||
[Authorize(AuthenticationSchemes = "local")]
|
||||
public async Task<IActionResult> UpdateLanguagePreference([FromBody] UpdateLanguagePreferenceRequest request)
|
||||
|
||||
Reference in New Issue
Block a user