feat(cv): qualify templates and reliable local import processing
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user