feat(cv): qualify templates and reliable local import processing

This commit is contained in:
cesnimda
2026-10-28 14:36:12 +01:00
parent 73ea888375
commit 3f4db8ca81
69 changed files with 2597 additions and 180 deletions
+44 -11
View File
@@ -16,6 +16,7 @@ namespace JobTrackerApi.Controllers;
[Route("api/auth")]
public sealed class AuthController : ControllerBase
{
private static readonly SemaphoreSlim AvatarProcessingSlots = new(2, 2);
private readonly IConfiguration _cfg;
private readonly UserManager<ApplicationUser> _users;
private readonly ITokenService _tokens;
@@ -933,9 +934,16 @@ public sealed class AuthController : ControllerBase
}
await using var stream = file.OpenReadStream();
using var memory = new MemoryStream();
await stream.CopyToAsync(memory);
var bytes = memory.ToArray();
var bytes = new byte[MaxAvatarBytes + 1];
var length = 0;
while (length < bytes.Length)
{
var read = await stream.ReadAsync(bytes.AsMemory(length), HttpContext.RequestAborted);
if (read == 0) break;
length += read;
}
if (length > MaxAvatarBytes) return BadRequest("Avatar image is too large.");
Array.Resize(ref bytes, length);
var detectedContentType = DetectAvatarContentType(bytes);
if (detectedContentType is null)
@@ -943,15 +951,40 @@ public sealed class AuthController : ControllerBase
return BadRequest("Only PNG, JPEG, or WebP images are supported.");
}
user.AvatarImageDataUrl = await AvatarStorage.StoreAsync(_avatarDataRoot, user.Id, bytes, detectedContentType, HttpContext.RequestAborted);
var result = await _users.UpdateAsync(user);
if (!result.Succeeded)
if (!await AvatarProcessingSlots.WaitAsync(0, HttpContext.RequestAborted))
return StatusCode(503, new { code = "avatar_busy", message = "Image processing is busy. Try again shortly." });
try
{
return BadRequest(string.Join("; ", result.Errors.Select(e => e.Description)));
byte[]? normalized;
try { normalized = AvatarImageProcessor.Normalize(bytes); }
catch (Exception exception) when (exception is DllNotFoundException or TypeInitializationException or EntryPointNotFoundException or BadImageFormatException)
{
return StatusCode(503, new { code = "avatar_decoder_unavailable", message = "Image processing is unavailable. Your previous photo has been preserved." });
}
if (normalized is null) return BadRequest("Select a valid, still PNG, JPEG or WebP image up to 2048 pixels per side and 4 megapixels. Crop or resize larger images.");
var previous = user.AvatarImageDataUrl;
var staged = await AvatarStorage.StageAsync(_avatarDataRoot, user.Id, normalized, HttpContext.RequestAborted);
try
{
user.AvatarImageDataUrl = staged;
var result = await _users.UpdateAsync(user);
if (!result.Succeeded)
{
user.AvatarImageDataUrl = previous;
AvatarStorage.TryDelete(staged);
return BadRequest("The photo could not be saved. Your previous photo has been preserved.");
}
}
catch
{
user.AvatarImageDataUrl = previous;
// An acknowledgement failure may follow a committed identity write; retention checks references.
throw;
}
AvatarStorage.TryDelete(previous);
return Ok(new { avatarImageDataUrl = $"data:image/png;base64,{Convert.ToBase64String(normalized)}" });
}
return Ok(new { avatarImageDataUrl = AvatarStorage.Resolve(user.AvatarImageDataUrl) });
finally { AvatarProcessingSlots.Release(); }
}
[HttpDelete("avatar")]
@@ -972,7 +1005,7 @@ public sealed class AuthController : ControllerBase
return BadRequest(string.Join("; ", result.Errors.Select(e => e.Description)));
}
AvatarStorage.Delete(storedAvatar);
AvatarStorage.TryDelete(storedAvatar);
return NoContent();
}