Polish UI, harden company creation, and add error pages

This commit is contained in:
cesnimda
2026-03-23 19:34:29 +01:00
parent 8f5eab2fe4
commit fcafda6f52
38 changed files with 2293 additions and 1269 deletions
@@ -57,6 +57,7 @@ public sealed class AuthController : ControllerBase
string? LastName,
string? DisplayName,
string? ProfileCvText,
string? AvatarImageDataUrl,
IList<string> Roles,
GoogleLinkDto? GoogleLink);
public sealed record UpdateProfileRequest(string? Email, string? UserName, string? FirstName, string? LastName, string? DisplayName, string? ProfileCvText);
@@ -172,6 +173,7 @@ public sealed class AuthController : ControllerBase
LastName: User.FindFirstValue("family_name"),
DisplayName: User.FindFirstValue("name"),
ProfileCvText: null,
AvatarImageDataUrl: null,
Roles: Array.Empty<string>(),
GoogleLink: provider == "google" ? new GoogleLinkDto(false, email, null) : null));
}
@@ -277,6 +279,70 @@ public sealed class AuthController : ControllerBase
return NoContent();
}
[HttpPost("avatar")]
[Authorize(AuthenticationSchemes = "local")]
[RequestSizeLimit(5_000_000)]
public async Task<IActionResult> UploadAvatar([FromForm] IFormFile? file)
{
var user = await _users.GetUserAsync(User);
if (user is null)
{
return Unauthorized();
}
if (file is null || file.Length == 0)
{
return BadRequest("Image file is required.");
}
if (!string.Equals(file.ContentType, "image/png", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(file.ContentType, "image/jpeg", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(file.ContentType, "image/webp", StringComparison.OrdinalIgnoreCase))
{
return BadRequest("Only PNG, JPEG, or WebP images are supported.");
}
if (file.Length > 5_000_000)
{
return BadRequest("Avatar image is too large.");
}
await using var stream = file.OpenReadStream();
using var memory = new MemoryStream();
await stream.CopyToAsync(memory);
var bytes = memory.ToArray();
var base64 = Convert.ToBase64String(bytes);
user.AvatarImageDataUrl = $"data:{file.ContentType};base64,{base64}";
var result = await _users.UpdateAsync(user);
if (!result.Succeeded)
{
return BadRequest(string.Join("; ", result.Errors.Select(e => e.Description)));
}
return Ok(new { avatarImageDataUrl = user.AvatarImageDataUrl });
}
[HttpDelete("avatar")]
[Authorize(AuthenticationSchemes = "local")]
public async Task<IActionResult> DeleteAvatar()
{
var user = await _users.GetUserAsync(User);
if (user is null)
{
return Unauthorized();
}
user.AvatarImageDataUrl = null;
var result = await _users.UpdateAsync(user);
if (!result.Succeeded)
{
return BadRequest(string.Join("; ", result.Errors.Select(e => e.Description)));
}
return NoContent();
}
public sealed record ChangePasswordRequest(string CurrentPassword, string NewPassword);
[HttpPost("change-password")]
@@ -374,6 +440,7 @@ public sealed class AuthController : ControllerBase
LastName: user.LastName,
DisplayName: user.DisplayName,
ProfileCvText: user.ProfileCvText,
AvatarImageDataUrl: user.AvatarImageDataUrl,
Roles: roles,
GoogleLink: new GoogleLinkDto(
Linked: !string.IsNullOrWhiteSpace(user.GoogleSubject),