mirror of
https://github.com/claunia/marechai.git
synced 2026-07-08 17:57:08 +00:00
Refactor error handling in services and controllers to use ExtractDetail method for consistent error messages
- Updated SoundSynthVideosController to return ProblemDetails for bad requests and conflicts. - Modified UsersController to return ProblemDetails for user not found and account purge failures. - Enhanced BooksService, DocumentsService, GpusService, InstructionSetExtensionsService, MachineFamiliesService, MagazinesService, ResolutionsService, ScreensService, SoftwareFamiliesService, SoftwarePlatformsService, SoftwareVersionsService, SoundSynthsService, and WwpcImportsService to utilize ExtractDetail for error messages from ApiException. - Introduced ExtractDetail method to standardize error message extraction from ApiException across multiple services.
This commit is contained in:
@@ -349,7 +349,7 @@ public class AuthController
|
||||
ApplicationUser user = await userManager.FindByEmailAsync(request.Email);
|
||||
|
||||
// Generic message: do NOT differentiate between unknown user and bad/expired token.
|
||||
if(user is null) return BadRequest("Invalid token or email.");
|
||||
if(user is null) return Problem(detail: "Invalid token or email.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
IdentityResult result = await userManager.ResetPasswordAsync(user, request.Token, request.NewPassword);
|
||||
|
||||
@@ -932,19 +932,19 @@ public class AuthController
|
||||
if(string.IsNullOrEmpty(userId)) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0)
|
||||
return BadRequest("No file provided.");
|
||||
return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(file.Length > 50 * 1024 * 1024)
|
||||
return BadRequest("File exceeds 50 MB limit.");
|
||||
return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
|
||||
if(!_allowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP, TIFF, BMP.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP, TIFF, BMP.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!string.IsNullOrEmpty(file.ContentType) &&
|
||||
!_allowedContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
return BadRequest("Unsupported content type.");
|
||||
return Problem(detail: "Unsupported content type.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
ApplicationUser user = await userManager.FindByIdAsync(userId);
|
||||
|
||||
@@ -1256,7 +1256,7 @@ public class AuthController
|
||||
_ => null
|
||||
};
|
||||
|
||||
if(identityProvider is null) return BadRequest("Unknown two-factor provider.");
|
||||
if(identityProvider is null) return Problem(detail: "Unknown two-factor provider.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool ok = await userManager.VerifyTwoFactorTokenAsync(user, identityProvider, request.Code);
|
||||
|
||||
@@ -1337,7 +1337,7 @@ public class AuthController
|
||||
ApplicationUser user = await userManager.FindByIdAsync(userId);
|
||||
if(user is null) return Unauthorized();
|
||||
|
||||
if(!user.TwoFactorViaEmail) return BadRequest("Email two-factor is not enabled for this account.");
|
||||
if(!user.TwoFactorViaEmail) return Problem(detail: "Email two-factor is not enabled for this account.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string code = await userManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultEmailProvider);
|
||||
await SendLocalizedLoginCodeAsync(user.Email!, code);
|
||||
@@ -1419,7 +1419,7 @@ public class AuthController
|
||||
bool ok = await userManager.VerifyTwoFactorTokenAsync(user, TokenOptions.DefaultAuthenticatorProvider,
|
||||
request.Code);
|
||||
|
||||
if(!ok) return BadRequest("Invalid verification code.");
|
||||
if(!ok) return Problem(detail: "Invalid verification code.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool wasFirstMethod = !user.TwoFactorEnabled;
|
||||
|
||||
@@ -1454,7 +1454,7 @@ public class AuthController
|
||||
ApplicationUser user = await userManager.FindByIdAsync(userId);
|
||||
if(user is null) return Unauthorized();
|
||||
|
||||
if(!user.EmailConfirmed) return BadRequest("Email is not confirmed.");
|
||||
if(!user.EmailConfirmed) return Problem(detail: "Email is not confirmed.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string code = await userManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultEmailProvider);
|
||||
await SendLocalizedLoginCodeAsync(user.Email!, code);
|
||||
@@ -1480,13 +1480,13 @@ public class AuthController
|
||||
ApplicationUser user = await userManager.FindByIdAsync(userId);
|
||||
if(user is null) return Unauthorized();
|
||||
|
||||
if(!user.EmailConfirmed) return BadRequest("Email is not confirmed.");
|
||||
if(!user.EmailConfirmed) return Problem(detail: "Email is not confirmed.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!await userManager.CheckPasswordAsync(user, request.Password))
|
||||
return BadRequest("Invalid password.");
|
||||
return Problem(detail: "Invalid password.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool ok = await userManager.VerifyTwoFactorTokenAsync(user, TokenOptions.DefaultEmailProvider, request.Code);
|
||||
if(!ok) return BadRequest("Invalid verification code.");
|
||||
if(!ok) return Problem(detail: "Invalid verification code.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool wasFirstMethod = !user.TwoFactorEnabled;
|
||||
|
||||
@@ -1522,13 +1522,13 @@ public class AuthController
|
||||
ApplicationUser user = await userManager.FindByIdAsync(userId);
|
||||
if(user is null) return Unauthorized();
|
||||
|
||||
if(!user.TwoFactorViaAuthenticator) return BadRequest("Authenticator is not enabled.");
|
||||
if(!user.TwoFactorViaAuthenticator) return Problem(detail: "Authenticator is not enabled.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!await userManager.CheckPasswordAsync(user, request.Password))
|
||||
return BadRequest("Invalid password.");
|
||||
return Problem(detail: "Invalid password.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!await VerifyOwnTwoFactorCodeAsync(user, request.Provider, request.Code))
|
||||
return BadRequest("Invalid verification code.");
|
||||
return Problem(detail: "Invalid verification code.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
await userManager.ResetAuthenticatorKeyAsync(user);
|
||||
user.TwoFactorViaAuthenticator = false;
|
||||
@@ -1562,13 +1562,13 @@ public class AuthController
|
||||
ApplicationUser user = await userManager.FindByIdAsync(userId);
|
||||
if(user is null) return Unauthorized();
|
||||
|
||||
if(!user.TwoFactorViaEmail) return BadRequest("Email two-factor is not enabled.");
|
||||
if(!user.TwoFactorViaEmail) return Problem(detail: "Email two-factor is not enabled.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!await userManager.CheckPasswordAsync(user, request.Password))
|
||||
return BadRequest("Invalid password.");
|
||||
return Problem(detail: "Invalid password.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!await VerifyOwnTwoFactorCodeAsync(user, request.Provider, request.Code))
|
||||
return BadRequest("Invalid verification code.");
|
||||
return Problem(detail: "Invalid verification code.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
user.TwoFactorViaEmail = false;
|
||||
|
||||
@@ -1603,13 +1603,13 @@ public class AuthController
|
||||
ApplicationUser user = await userManager.FindByIdAsync(userId);
|
||||
if(user is null) return Unauthorized();
|
||||
|
||||
if(!user.TwoFactorEnabled) return BadRequest("Two-factor authentication is not enabled.");
|
||||
if(!user.TwoFactorEnabled) return Problem(detail: "Two-factor authentication is not enabled.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!await userManager.CheckPasswordAsync(user, request.Password))
|
||||
return BadRequest("Invalid password.");
|
||||
return Problem(detail: "Invalid password.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!await VerifyOwnTwoFactorCodeAsync(user, request.Provider, request.Code))
|
||||
return BadRequest("Invalid verification code.");
|
||||
return Problem(detail: "Invalid verification code.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
IEnumerable<string> codes = await userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
|
||||
await userManager.ResetAccessFailedCountAsync(user);
|
||||
|
||||
@@ -923,19 +923,19 @@ public class BooksController(
|
||||
if(userId is null) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0)
|
||||
return BadRequest("No file provided.");
|
||||
return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(file.Length > 50 * 1024 * 1024)
|
||||
return BadRequest("File exceeds 50 MB limit.");
|
||||
return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
|
||||
if(!_allowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP, TIFF, BMP.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP, TIFF, BMP.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!string.IsNullOrEmpty(file.ContentType) &&
|
||||
!_allowedContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
return BadRequest("Unsupported content type.");
|
||||
return Problem(detail: "Unsupported content type.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
Book book = await context.Books.FindAsync(id);
|
||||
|
||||
@@ -1053,18 +1053,18 @@ public class BooksController(
|
||||
if(userId is null) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0)
|
||||
return BadRequest("No file provided.");
|
||||
return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(file.Length > 50 * 1024 * 1024)
|
||||
return BadRequest("File exceeds 50 MB limit.");
|
||||
return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
if(!_pendingAllowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!string.IsNullOrEmpty(file.ContentType) &&
|
||||
!_pendingAllowedContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
return BadRequest("Unsupported content type.");
|
||||
return Problem(detail: "Unsupported content type.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
// Verify the targeted book exists; we don't want stray uploads for nonexistent ids.
|
||||
bool bookExists = await context.Books.AsNoTracking().AnyAsync(b => b.Id == id);
|
||||
@@ -1106,18 +1106,18 @@ public class BooksController(
|
||||
if(userId is null) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0)
|
||||
return BadRequest("No file provided.");
|
||||
return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(file.Length > 50 * 1024 * 1024)
|
||||
return BadRequest("File exceeds 50 MB limit.");
|
||||
return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
if(!_pendingAllowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!string.IsNullOrEmpty(file.ContentType) &&
|
||||
!_pendingAllowedContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
return BadRequest("Unsupported content type.");
|
||||
return Problem(detail: "Unsupported content type.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
// Cleanup: each user gets at most ONE pending NEW-book cover at a time. Replace any
|
||||
// prior upload (keyed on EntityId=0) before storing the new one.
|
||||
|
||||
@@ -454,15 +454,15 @@ public class CompaniesController(MarechaiContext context) : ControllerBase
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<CompanyMergePreviewDto>> GetMergePreviewAsync(int targetId, int sourceId)
|
||||
{
|
||||
if(targetId == sourceId) return BadRequest("Cannot merge a company into itself.");
|
||||
if(targetId == sourceId) return Problem(detail: "Cannot merge a company into itself.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
Company target = await context.Companies.FindAsync(targetId);
|
||||
|
||||
if(target is null) return NotFound("Target company not found.");
|
||||
if(target is null) return Problem(detail: "Target company not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
Company source = await context.Companies.FindAsync(sourceId);
|
||||
|
||||
if(source is null) return NotFound("Source company not found.");
|
||||
if(source is null) return Problem(detail: "Source company not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
// Logos — direct re-parent, no dedup
|
||||
int logosCount = await context.CompanyLogos.CountAsync(l => l.CompanyId == sourceId);
|
||||
@@ -654,19 +654,19 @@ public class CompaniesController(MarechaiContext context) : ControllerBase
|
||||
|
||||
if(userId is null) return Unauthorized();
|
||||
|
||||
if(request is null) return BadRequest("Merge request body is required.");
|
||||
if(request is null) return Problem(detail: "Merge request body is required.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(string.IsNullOrWhiteSpace(request.Name)) return BadRequest("Name is required.");
|
||||
if(string.IsNullOrWhiteSpace(request.Name)) return Problem(detail: "Name is required.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(targetId == sourceId) return BadRequest("Cannot merge a company into itself.");
|
||||
if(targetId == sourceId) return Problem(detail: "Cannot merge a company into itself.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
Company target = await context.Companies.FindAsync(targetId);
|
||||
|
||||
if(target is null) return NotFound("Target company not found.");
|
||||
if(target is null) return Problem(detail: "Target company not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
Company source = await context.Companies.FindAsync(sourceId);
|
||||
|
||||
if(source is null) return NotFound("Source company not found.");
|
||||
if(source is null) return Problem(detail: "Source company not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
string sourceName = source.Name;
|
||||
|
||||
@@ -947,7 +947,7 @@ public class CompaniesController(MarechaiContext context) : ControllerBase
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
|
||||
return Conflict($"Merge failed: {ex.Message}");
|
||||
return Problem(detail: $"Merge failed: {ex.Message}", statusCode: StatusCodes.Status409Conflict);
|
||||
}
|
||||
|
||||
// Mark pending suggestions for the deleted source company as Stale (mirrors DeleteAsync).
|
||||
|
||||
@@ -257,18 +257,18 @@ public class GpuPhotosController(MarechaiContext context, IConfiguration configu
|
||||
if(userId is null) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0)
|
||||
return BadRequest("No file provided.");
|
||||
return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(file.Length > 50 * 1024 * 1024)
|
||||
return BadRequest("File exceeds 50 MB limit.");
|
||||
return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
|
||||
if(!_allowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP, TIFF, BMP.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP, TIFF, BMP.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!string.IsNullOrEmpty(file.ContentType) && !_allowedContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
return BadRequest("Unsupported content type.");
|
||||
return Problem(detail: "Unsupported content type.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
// Read the file into memory
|
||||
using var ms = new MemoryStream();
|
||||
@@ -454,29 +454,30 @@ public class GpuPhotosController(MarechaiContext context, IConfiguration configu
|
||||
|
||||
if(string.IsNullOrEmpty(userId)) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0) return BadRequest("No file provided.");
|
||||
if(file is null || file.Length == 0) return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(file.Length > 50 * 1024 * 1024) return BadRequest("File exceeds 50 MB limit.");
|
||||
if(file.Length > 50 * 1024 * 1024) return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
|
||||
if(!_pendingAllowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!string.IsNullOrEmpty(file.ContentType) &&
|
||||
!_pendingAllowedContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
return BadRequest("Unsupported content type.");
|
||||
return Problem(detail: "Unsupported content type.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool gpuExists = await context.Gpus.AnyAsync(g => g.Id == gpuId);
|
||||
|
||||
if(!gpuExists) return NotFound("GPU not found.");
|
||||
if(!gpuExists) return Problem(detail: "GPU not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
int currentCount = PendingImageStore.CountByUploaderForParentEntity(_assetRootPath, "gpus", userId,
|
||||
(byte)SuggestionEntityType.GpuPhoto, gpuId);
|
||||
|
||||
if(currentCount >= PendingPhotosPerUserPerGpuCap)
|
||||
return Conflict($"You already have {currentCount} pending photos for this GPU. Maximum is " +
|
||||
$"{PendingPhotosPerUserPerGpuCap} per GPU. Submit or remove some first.");
|
||||
return Problem(detail: $"You already have {currentCount} pending photos for this GPU. Maximum is " +
|
||||
$"{PendingPhotosPerUserPerGpuCap} per GPU. Submit or remove some first.",
|
||||
statusCode: StatusCodes.Status409Conflict);
|
||||
|
||||
Guid guid;
|
||||
|
||||
@@ -607,15 +608,15 @@ public class GpuPhotosController(MarechaiContext context, IConfiguration configu
|
||||
string userId = User.FindFirstValue(ClaimTypes.Sid);
|
||||
if(string.IsNullOrEmpty(userId)) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0) return BadRequest("No file provided.");
|
||||
if(file.Length > 50 * 1024 * 1024) return BadRequest("File exceeds 50 MB limit.");
|
||||
if(file is null || file.Length == 0) return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
if(file.Length > 50 * 1024 * 1024) return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
if(!_adminBatchAllowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP, AVIF, BMP, TIFF.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP, AVIF, BMP, TIFF.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool gpuExists = await context.Gpus.AnyAsync(g => g.Id == gpuId);
|
||||
if(!gpuExists) return NotFound("GPU not found.");
|
||||
if(!gpuExists) return Problem(detail: "GPU not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
// Persist FIRST (with a temporary placeholder for dimensions), then content-sniff
|
||||
// and either re-write the sidecar with real dimensions or reject + delete the file.
|
||||
@@ -632,22 +633,22 @@ public class GpuPhotosController(MarechaiContext context, IConfiguration configu
|
||||
if(imagePath is null)
|
||||
{
|
||||
PendingImageStore.Delete(_assetRootPath, "gpus", guid);
|
||||
return BadRequest("Failed to persist uploaded file.");
|
||||
return Problem(detail: "Failed to persist uploaded file.", statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
(string magickFormat, int width, int height) = Photos.Identify(imagePath);
|
||||
if(magickFormat is null || !_adminBatchAllowedMagickFormats.Contains(magickFormat))
|
||||
{
|
||||
PendingImageStore.Delete(_assetRootPath, "gpus", guid);
|
||||
return StatusCode(StatusCodes.Status415UnsupportedMediaType,
|
||||
"File content does not match a supported image format.");
|
||||
return Problem(detail: "File content does not match a supported image format.",
|
||||
statusCode: StatusCodes.Status415UnsupportedMediaType);
|
||||
}
|
||||
|
||||
byte[] thumbBytes = Photos.GenerateThumbnailJpeg(imagePath);
|
||||
if(thumbBytes is null)
|
||||
{
|
||||
PendingImageStore.Delete(_assetRootPath, "gpus", guid);
|
||||
return BadRequest("Failed to generate thumbnail for the uploaded image.");
|
||||
return Problem(detail: "Failed to generate thumbnail for the uploaded image.", statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
await PendingImageStore.StoreThumbnailAsync(_assetRootPath, "gpus", guid, thumbBytes);
|
||||
@@ -726,13 +727,13 @@ public class GpuPhotosController(MarechaiContext context, IConfiguration configu
|
||||
if(string.IsNullOrEmpty(userId)) return Unauthorized();
|
||||
|
||||
if(request is null || request.Items is null || request.Items.Count == 0)
|
||||
return BadRequest("No items provided.");
|
||||
return Problem(detail: "No items provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool gpuExists = await context.Gpus.AnyAsync(g => g.Id == request.GpuId);
|
||||
if(!gpuExists) return NotFound("GPU not found.");
|
||||
if(!gpuExists) return Problem(detail: "GPU not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
bool licenseExists = await context.Licenses.AnyAsync(l => l.Id == request.LicenseId);
|
||||
if(!licenseExists) return NotFound("License not found.");
|
||||
if(!licenseExists) return Problem(detail: "License not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
// Validate every pending id belongs to the caller and matches gpu/admin scope.
|
||||
foreach(AdminGpuPhotoBatchCommitItemDto item in request.Items)
|
||||
@@ -740,15 +741,15 @@ public class GpuPhotosController(MarechaiContext context, IConfiguration configu
|
||||
PendingImageStore.PendingMetadata meta =
|
||||
await PendingImageStore.GetMetadataAsync(_assetRootPath, "gpus", item.PendingId);
|
||||
|
||||
if(meta is null) return NotFound($"Pending image {item.PendingId} not found.");
|
||||
if(meta is null) return Problem(detail: $"Pending image {item.PendingId} not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
if(meta.EntityType != (byte)SuggestionEntityType.GpuPhoto)
|
||||
return BadRequest($"Pending image {item.PendingId} is not a GPU photo.");
|
||||
return Problem(detail: $"Pending image {item.PendingId} is not a GPU photo.", statusCode: StatusCodes.Status400BadRequest);
|
||||
if(!meta.IsAdminStaging)
|
||||
return BadRequest($"Pending image {item.PendingId} is not an admin-staged image.");
|
||||
return Problem(detail: $"Pending image {item.PendingId} is not an admin-staged image.", statusCode: StatusCodes.Status400BadRequest);
|
||||
if(!string.Equals(meta.UploadedById, userId, StringComparison.Ordinal))
|
||||
return Forbid();
|
||||
if(meta.ParentEntityId != request.GpuId)
|
||||
return BadRequest($"Pending image {item.PendingId} was staged for a different GPU.");
|
||||
return Problem(detail: $"Pending image {item.PendingId} was staged for a different GPU.", statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
Guid jobId = batchJobs.StartJob(userId, request.GpuId, request.Items.Count);
|
||||
|
||||
@@ -97,7 +97,7 @@ public class GpuVideosController(MarechaiContext context) : ControllerBase
|
||||
if(userId is null) return Unauthorized();
|
||||
|
||||
if(string.IsNullOrWhiteSpace(request.Provider) || string.IsNullOrWhiteSpace(request.VideoId))
|
||||
return BadRequest("Provider and Video ID are required.");
|
||||
return Problem(detail: "Provider and Video ID are required.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string provider = request.Provider.Trim();
|
||||
string videoId = request.VideoId.Trim();
|
||||
@@ -111,7 +111,7 @@ public class GpuVideosController(MarechaiContext context) : ControllerBase
|
||||
v.Provider == provider &&
|
||||
v.VideoId == videoId);
|
||||
|
||||
if(exists) return Conflict("This video is already linked to this GPU.");
|
||||
if(exists) return Problem(detail: "This video is already linked to this GPU.", statusCode: StatusCodes.Status409Conflict);
|
||||
|
||||
var model = new GpuVideo
|
||||
{
|
||||
|
||||
@@ -257,18 +257,18 @@ public class MachinePhotosController(MarechaiContext context, IConfiguration con
|
||||
if(userId is null) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0)
|
||||
return BadRequest("No file provided.");
|
||||
return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(file.Length > 50 * 1024 * 1024)
|
||||
return BadRequest("File exceeds 50 MB limit.");
|
||||
return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
|
||||
if(!_allowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP, TIFF, BMP.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP, TIFF, BMP.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!string.IsNullOrEmpty(file.ContentType) && !_allowedContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
return BadRequest("Unsupported content type.");
|
||||
return Problem(detail: "Unsupported content type.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
// Read the file into memory
|
||||
using var ms = new MemoryStream();
|
||||
@@ -456,29 +456,30 @@ public class MachinePhotosController(MarechaiContext context, IConfiguration con
|
||||
|
||||
if(string.IsNullOrEmpty(userId)) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0) return BadRequest("No file provided.");
|
||||
if(file is null || file.Length == 0) return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(file.Length > 50 * 1024 * 1024) return BadRequest("File exceeds 50 MB limit.");
|
||||
if(file.Length > 50 * 1024 * 1024) return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
|
||||
if(!_pendingAllowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!string.IsNullOrEmpty(file.ContentType) &&
|
||||
!_pendingAllowedContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
return BadRequest("Unsupported content type.");
|
||||
return Problem(detail: "Unsupported content type.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool machineExists = await context.Machines.AnyAsync(m => m.Id == machineId);
|
||||
|
||||
if(!machineExists) return NotFound("Machine not found.");
|
||||
if(!machineExists) return Problem(detail: "Machine not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
int currentCount = PendingImageStore.CountByUploaderForParentEntity(_assetRootPath, "machines", userId,
|
||||
(byte)SuggestionEntityType.MachinePhoto, machineId);
|
||||
|
||||
if(currentCount >= PendingPhotosPerUserPerMachineCap)
|
||||
return Conflict($"You already have {currentCount} pending photos for this machine. Maximum is " +
|
||||
$"{PendingPhotosPerUserPerMachineCap} per machine. Submit or remove some first.");
|
||||
return Problem(detail: $"You already have {currentCount} pending photos for this machine. Maximum is " +
|
||||
$"{PendingPhotosPerUserPerMachineCap} per machine. Submit or remove some first.",
|
||||
statusCode: StatusCodes.Status409Conflict);
|
||||
|
||||
Guid guid;
|
||||
|
||||
@@ -609,15 +610,15 @@ public class MachinePhotosController(MarechaiContext context, IConfiguration con
|
||||
string userId = User.FindFirstValue(ClaimTypes.Sid);
|
||||
if(string.IsNullOrEmpty(userId)) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0) return BadRequest("No file provided.");
|
||||
if(file.Length > 50 * 1024 * 1024) return BadRequest("File exceeds 50 MB limit.");
|
||||
if(file is null || file.Length == 0) return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
if(file.Length > 50 * 1024 * 1024) return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
if(!_adminBatchAllowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP, AVIF, BMP, TIFF.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP, AVIF, BMP, TIFF.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool machineExists = await context.Machines.AnyAsync(m => m.Id == machineId);
|
||||
if(!machineExists) return NotFound("Machine not found.");
|
||||
if(!machineExists) return Problem(detail: "Machine not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
// Persist FIRST (with a temporary placeholder for dimensions), then content-sniff
|
||||
// and either re-write the sidecar with real dimensions or reject + delete the file.
|
||||
@@ -634,22 +635,22 @@ public class MachinePhotosController(MarechaiContext context, IConfiguration con
|
||||
if(imagePath is null)
|
||||
{
|
||||
PendingImageStore.Delete(_assetRootPath, "machines", guid);
|
||||
return BadRequest("Failed to persist uploaded file.");
|
||||
return Problem(detail: "Failed to persist uploaded file.", statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
(string magickFormat, int width, int height) = Photos.Identify(imagePath);
|
||||
if(magickFormat is null || !_adminBatchAllowedMagickFormats.Contains(magickFormat))
|
||||
{
|
||||
PendingImageStore.Delete(_assetRootPath, "machines", guid);
|
||||
return StatusCode(StatusCodes.Status415UnsupportedMediaType,
|
||||
"File content does not match a supported image format.");
|
||||
return Problem(detail: "File content does not match a supported image format.",
|
||||
statusCode: StatusCodes.Status415UnsupportedMediaType);
|
||||
}
|
||||
|
||||
byte[] thumbBytes = Photos.GenerateThumbnailJpeg(imagePath);
|
||||
if(thumbBytes is null)
|
||||
{
|
||||
PendingImageStore.Delete(_assetRootPath, "machines", guid);
|
||||
return BadRequest("Failed to generate thumbnail for the uploaded image.");
|
||||
return Problem(detail: "Failed to generate thumbnail for the uploaded image.", statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
await PendingImageStore.StoreThumbnailAsync(_assetRootPath, "machines", guid, thumbBytes);
|
||||
@@ -728,13 +729,13 @@ public class MachinePhotosController(MarechaiContext context, IConfiguration con
|
||||
if(string.IsNullOrEmpty(userId)) return Unauthorized();
|
||||
|
||||
if(request is null || request.Items is null || request.Items.Count == 0)
|
||||
return BadRequest("No items provided.");
|
||||
return Problem(detail: "No items provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool machineExists = await context.Machines.AnyAsync(m => m.Id == request.MachineId);
|
||||
if(!machineExists) return NotFound("Machine not found.");
|
||||
if(!machineExists) return Problem(detail: "Machine not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
bool licenseExists = await context.Licenses.AnyAsync(l => l.Id == request.LicenseId);
|
||||
if(!licenseExists) return NotFound("License not found.");
|
||||
if(!licenseExists) return Problem(detail: "License not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
// Validate every pending id belongs to the caller and matches machine/admin scope.
|
||||
foreach(AdminMachinePhotoBatchCommitItemDto item in request.Items)
|
||||
@@ -742,15 +743,15 @@ public class MachinePhotosController(MarechaiContext context, IConfiguration con
|
||||
PendingImageStore.PendingMetadata meta =
|
||||
await PendingImageStore.GetMetadataAsync(_assetRootPath, "machines", item.PendingId);
|
||||
|
||||
if(meta is null) return NotFound($"Pending image {item.PendingId} not found.");
|
||||
if(meta is null) return Problem(detail: $"Pending image {item.PendingId} not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
if(meta.EntityType != (byte)SuggestionEntityType.MachinePhoto)
|
||||
return BadRequest($"Pending image {item.PendingId} is not a machine photo.");
|
||||
return Problem(detail: $"Pending image {item.PendingId} is not a machine photo.", statusCode: StatusCodes.Status400BadRequest);
|
||||
if(!meta.IsAdminStaging)
|
||||
return BadRequest($"Pending image {item.PendingId} is not an admin-staged image.");
|
||||
return Problem(detail: $"Pending image {item.PendingId} is not an admin-staged image.", statusCode: StatusCodes.Status400BadRequest);
|
||||
if(!string.Equals(meta.UploadedById, userId, StringComparison.Ordinal))
|
||||
return Forbid();
|
||||
if(meta.ParentEntityId != request.MachineId)
|
||||
return BadRequest($"Pending image {item.PendingId} was staged for a different machine.");
|
||||
return Problem(detail: $"Pending image {item.PendingId} was staged for a different machine.", statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
Guid jobId = batchJobs.StartJob(userId, request.MachineId, request.Items.Count);
|
||||
|
||||
@@ -98,7 +98,7 @@ public class MachineVideosController(MarechaiContext context) : ControllerBase
|
||||
if(userId is null) return Unauthorized();
|
||||
|
||||
if(string.IsNullOrWhiteSpace(request.Provider) || string.IsNullOrWhiteSpace(request.VideoId))
|
||||
return BadRequest("Provider and Video ID are required.");
|
||||
return Problem(detail: "Provider and Video ID are required.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string provider = request.Provider.Trim();
|
||||
string videoId = request.VideoId.Trim();
|
||||
@@ -112,7 +112,7 @@ public class MachineVideosController(MarechaiContext context) : ControllerBase
|
||||
v.Provider == provider &&
|
||||
v.VideoId == videoId);
|
||||
|
||||
if(exists) return Conflict("This video is already linked to this machine.");
|
||||
if(exists) return Problem(detail: "This video is already linked to this machine.", statusCode: StatusCodes.Status409Conflict);
|
||||
|
||||
var model = new MachineVideo
|
||||
{
|
||||
|
||||
@@ -389,19 +389,19 @@ public class MagazineIssuesController(
|
||||
if(userId is null) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0)
|
||||
return BadRequest("No file provided.");
|
||||
return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(file.Length > 50 * 1024 * 1024)
|
||||
return BadRequest("File exceeds 50 MB limit.");
|
||||
return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
|
||||
if(!_allowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP, TIFF, BMP.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP, TIFF, BMP.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!string.IsNullOrEmpty(file.ContentType) &&
|
||||
!_allowedContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
return BadRequest("Unsupported content type.");
|
||||
return Problem(detail: "Unsupported content type.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
MagazineIssue issue = await context.MagazineIssues.FindAsync(id);
|
||||
|
||||
@@ -557,18 +557,18 @@ public class MagazineIssuesController(
|
||||
if(userId is null) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0)
|
||||
return BadRequest("No file provided.");
|
||||
return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(file.Length > 50 * 1024 * 1024)
|
||||
return BadRequest("File exceeds 50 MB limit.");
|
||||
return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
if(!Marechai.Server.Helpers.PendingImageStore.AllowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!string.IsNullOrEmpty(file.ContentType) &&
|
||||
!Marechai.Server.Helpers.PendingImageStore.AllowedContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
return BadRequest("Unsupported content type.");
|
||||
return Problem(detail: "Unsupported content type.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
// Verify the targeted issue exists; we don't want stray uploads for nonexistent ids.
|
||||
bool issueExists = await context.MagazineIssues.AsNoTracking().AnyAsync(mi => mi.Id == id);
|
||||
@@ -613,18 +613,18 @@ public class MagazineIssuesController(
|
||||
if(userId is null) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0)
|
||||
return BadRequest("No file provided.");
|
||||
return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(file.Length > 50 * 1024 * 1024)
|
||||
return BadRequest("File exceeds 50 MB limit.");
|
||||
return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
if(!Marechai.Server.Helpers.PendingImageStore.AllowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!string.IsNullOrEmpty(file.ContentType) &&
|
||||
!Marechai.Server.Helpers.PendingImageStore.AllowedContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
return BadRequest("Unsupported content type.");
|
||||
return Problem(detail: "Unsupported content type.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
// Cleanup: each user gets at most ONE pending NEW-issue cover at a time. Replace any
|
||||
// prior upload (keyed on EntityId=0) before storing the new one.
|
||||
|
||||
@@ -1118,19 +1118,19 @@ public class PeopleController(
|
||||
if(userId is null) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0)
|
||||
return BadRequest("No file provided.");
|
||||
return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(file.Length > 50 * 1024 * 1024)
|
||||
return BadRequest("File exceeds 50 MB limit.");
|
||||
return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
|
||||
if(!_allowedPhotoExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP, TIFF, BMP.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP, TIFF, BMP.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!string.IsNullOrEmpty(file.ContentType) &&
|
||||
!_allowedPhotoContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
return BadRequest("Unsupported content type.");
|
||||
return Problem(detail: "Unsupported content type.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
Person person = await context.People.FindAsync(id);
|
||||
|
||||
@@ -1245,18 +1245,18 @@ public class PeopleController(
|
||||
if(userId is null) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0)
|
||||
return BadRequest("No file provided.");
|
||||
return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(file.Length > 50 * 1024 * 1024)
|
||||
return BadRequest("File exceeds 50 MB limit.");
|
||||
return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
if(!_pendingAllowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!string.IsNullOrEmpty(file.ContentType) &&
|
||||
!_pendingAllowedContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
return BadRequest("Unsupported content type.");
|
||||
return Problem(detail: "Unsupported content type.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
// Verify the targeted person exists; we don't want stray uploads for nonexistent ids.
|
||||
bool personExists = await context.People.AsNoTracking().AnyAsync(p => p.Id == id);
|
||||
@@ -1298,18 +1298,18 @@ public class PeopleController(
|
||||
if(userId is null) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0)
|
||||
return BadRequest("No file provided.");
|
||||
return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(file.Length > 50 * 1024 * 1024)
|
||||
return BadRequest("File exceeds 50 MB limit.");
|
||||
return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
if(!_pendingAllowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!string.IsNullOrEmpty(file.ContentType) &&
|
||||
!_pendingAllowedContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
return BadRequest("Unsupported content type.");
|
||||
return Problem(detail: "Unsupported content type.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
// Cleanup: each user gets at most ONE pending NEW-person photo at a time. Replace
|
||||
// any prior upload (keyed on EntityId=0) before storing the new one.
|
||||
|
||||
@@ -257,18 +257,18 @@ public class ProcessorPhotosController(MarechaiContext context, IConfiguration c
|
||||
if(userId is null) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0)
|
||||
return BadRequest("No file provided.");
|
||||
return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(file.Length > 50 * 1024 * 1024)
|
||||
return BadRequest("File exceeds 50 MB limit.");
|
||||
return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
|
||||
if(!_allowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP, TIFF, BMP.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP, TIFF, BMP.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!string.IsNullOrEmpty(file.ContentType) && !_allowedContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
return BadRequest("Unsupported content type.");
|
||||
return Problem(detail: "Unsupported content type.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
// Read the file into memory
|
||||
using var ms = new MemoryStream();
|
||||
@@ -455,29 +455,30 @@ public class ProcessorPhotosController(MarechaiContext context, IConfiguration c
|
||||
|
||||
if(string.IsNullOrEmpty(userId)) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0) return BadRequest("No file provided.");
|
||||
if(file is null || file.Length == 0) return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(file.Length > 50 * 1024 * 1024) return BadRequest("File exceeds 50 MB limit.");
|
||||
if(file.Length > 50 * 1024 * 1024) return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
|
||||
if(!_pendingAllowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!string.IsNullOrEmpty(file.ContentType) &&
|
||||
!_pendingAllowedContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
return BadRequest("Unsupported content type.");
|
||||
return Problem(detail: "Unsupported content type.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool processorExists = await context.Processors.AnyAsync(p => p.Id == processorId);
|
||||
|
||||
if(!processorExists) return NotFound("Processor not found.");
|
||||
if(!processorExists) return Problem(detail: "Processor not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
int currentCount = PendingImageStore.CountByUploaderForParentEntity(_assetRootPath, "processors", userId,
|
||||
(byte)SuggestionEntityType.ProcessorPhoto, processorId);
|
||||
|
||||
if(currentCount >= PendingPhotosPerUserPerProcessorCap)
|
||||
return Conflict($"You already have {currentCount} pending photos for this processor. Maximum is " +
|
||||
$"{PendingPhotosPerUserPerProcessorCap} per processor. Submit or remove some first.");
|
||||
return Problem(detail: $"You already have {currentCount} pending photos for this processor. Maximum is " +
|
||||
$"{PendingPhotosPerUserPerProcessorCap} per processor. Submit or remove some first.",
|
||||
statusCode: StatusCodes.Status409Conflict);
|
||||
|
||||
Guid guid;
|
||||
|
||||
@@ -616,15 +617,15 @@ public class ProcessorPhotosController(MarechaiContext context, IConfiguration c
|
||||
string userId = User.FindFirstValue(ClaimTypes.Sid);
|
||||
if(string.IsNullOrEmpty(userId)) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0) return BadRequest("No file provided.");
|
||||
if(file.Length > 50 * 1024 * 1024) return BadRequest("File exceeds 50 MB limit.");
|
||||
if(file is null || file.Length == 0) return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
if(file.Length > 50 * 1024 * 1024) return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
if(!_adminBatchAllowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP, AVIF, BMP, TIFF.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP, AVIF, BMP, TIFF.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool processorExists = await context.Processors.AnyAsync(p => p.Id == processorId);
|
||||
if(!processorExists) return NotFound("Processor not found.");
|
||||
if(!processorExists) return Problem(detail: "Processor not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
// Persist FIRST (with a temporary placeholder for dimensions), then content-sniff
|
||||
// and either re-write the sidecar with real dimensions or reject + delete the file.
|
||||
@@ -641,22 +642,22 @@ public class ProcessorPhotosController(MarechaiContext context, IConfiguration c
|
||||
if(imagePath is null)
|
||||
{
|
||||
PendingImageStore.Delete(_assetRootPath, "processors", guid);
|
||||
return BadRequest("Failed to persist uploaded file.");
|
||||
return Problem(detail: "Failed to persist uploaded file.", statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
(string magickFormat, int width, int height) = Photos.Identify(imagePath);
|
||||
if(magickFormat is null || !_adminBatchAllowedMagickFormats.Contains(magickFormat))
|
||||
{
|
||||
PendingImageStore.Delete(_assetRootPath, "processors", guid);
|
||||
return StatusCode(StatusCodes.Status415UnsupportedMediaType,
|
||||
"File content does not match a supported image format.");
|
||||
return Problem(detail: "File content does not match a supported image format.",
|
||||
statusCode: StatusCodes.Status415UnsupportedMediaType);
|
||||
}
|
||||
|
||||
byte[] thumbBytes = Photos.GenerateThumbnailJpeg(imagePath);
|
||||
if(thumbBytes is null)
|
||||
{
|
||||
PendingImageStore.Delete(_assetRootPath, "processors", guid);
|
||||
return BadRequest("Failed to generate thumbnail for the uploaded image.");
|
||||
return Problem(detail: "Failed to generate thumbnail for the uploaded image.", statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
await PendingImageStore.StoreThumbnailAsync(_assetRootPath, "processors", guid, thumbBytes);
|
||||
@@ -735,13 +736,13 @@ public class ProcessorPhotosController(MarechaiContext context, IConfiguration c
|
||||
if(string.IsNullOrEmpty(userId)) return Unauthorized();
|
||||
|
||||
if(request is null || request.Items is null || request.Items.Count == 0)
|
||||
return BadRequest("No items provided.");
|
||||
return Problem(detail: "No items provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool processorExists = await context.Processors.AnyAsync(p => p.Id == request.ProcessorId);
|
||||
if(!processorExists) return NotFound("Processor not found.");
|
||||
if(!processorExists) return Problem(detail: "Processor not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
bool licenseExists = await context.Licenses.AnyAsync(l => l.Id == request.LicenseId);
|
||||
if(!licenseExists) return NotFound("License not found.");
|
||||
if(!licenseExists) return Problem(detail: "License not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
// Validate every pending id belongs to the caller and matches processor/admin scope.
|
||||
foreach(AdminProcessorPhotoBatchCommitItemDto item in request.Items)
|
||||
@@ -749,15 +750,15 @@ public class ProcessorPhotosController(MarechaiContext context, IConfiguration c
|
||||
PendingImageStore.PendingMetadata meta =
|
||||
await PendingImageStore.GetMetadataAsync(_assetRootPath, "processors", item.PendingId);
|
||||
|
||||
if(meta is null) return NotFound($"Pending image {item.PendingId} not found.");
|
||||
if(meta is null) return Problem(detail: $"Pending image {item.PendingId} not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
if(meta.EntityType != (byte)SuggestionEntityType.ProcessorPhoto)
|
||||
return BadRequest($"Pending image {item.PendingId} is not a processor photo.");
|
||||
return Problem(detail: $"Pending image {item.PendingId} is not a processor photo.", statusCode: StatusCodes.Status400BadRequest);
|
||||
if(!meta.IsAdminStaging)
|
||||
return BadRequest($"Pending image {item.PendingId} is not an admin-staged image.");
|
||||
return Problem(detail: $"Pending image {item.PendingId} is not an admin-staged image.", statusCode: StatusCodes.Status400BadRequest);
|
||||
if(!string.Equals(meta.UploadedById, userId, StringComparison.Ordinal))
|
||||
return Forbid();
|
||||
if(meta.ParentEntityId != request.ProcessorId)
|
||||
return BadRequest($"Pending image {item.PendingId} was staged for a different processor.");
|
||||
return Problem(detail: $"Pending image {item.PendingId} was staged for a different processor.", statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
Guid jobId = batchJobs.StartJob(userId, request.ProcessorId, request.Items.Count);
|
||||
|
||||
@@ -98,7 +98,7 @@ public class ProcessorVideosController(MarechaiContext context) : ControllerBase
|
||||
if(userId is null) return Unauthorized();
|
||||
|
||||
if(string.IsNullOrWhiteSpace(request.Provider) || string.IsNullOrWhiteSpace(request.VideoId))
|
||||
return BadRequest("Provider and Video ID are required.");
|
||||
return Problem(detail: "Provider and Video ID are required.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string provider = request.Provider.Trim();
|
||||
string videoId = request.VideoId.Trim();
|
||||
@@ -112,7 +112,7 @@ public class ProcessorVideosController(MarechaiContext context) : ControllerBase
|
||||
v.Provider == provider &&
|
||||
v.VideoId == videoId);
|
||||
|
||||
if(exists) return Conflict("This video is already linked to this processor.");
|
||||
if(exists) return Problem(detail: "This video is already linked to this processor.", statusCode: StatusCodes.Status409Conflict);
|
||||
|
||||
var model = new ProcessorVideo
|
||||
{
|
||||
|
||||
@@ -1304,7 +1304,7 @@ public class SoftwareController(MarechaiContext context, IMemoryCache cache, Use
|
||||
// Same scope as the orphan-addons grid: we only allow this PATCH on
|
||||
// rows that page would list (a DLC, or a Game carrying a DLC genre).
|
||||
if(model.Kind != SoftwareKind.Dlc && !(model.Kind == SoftwareKind.Game && hasDlcGenre))
|
||||
return BadRequest("This endpoint only accepts DLC rows or Game rows carrying a DLC / add-on genre.");
|
||||
return Problem(detail: "This endpoint only accepts DLC rows or Game rows carrying a DLC / add-on genre.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
ActionResult validationError = await ApplyBaseSoftwareLinkAsync(model, request.BaseSoftwareId, dlcGenreIds,
|
||||
HttpContext.RequestAborted);
|
||||
@@ -1344,7 +1344,7 @@ public class SoftwareController(MarechaiContext context, IMemoryCache cache, Use
|
||||
if(userId is null) return Unauthorized();
|
||||
|
||||
if(request.SoftwareIds is null || request.SoftwareIds.Count == 0)
|
||||
return BadRequest("softwareIds must contain at least one id.");
|
||||
return Problem(detail: "softwareIds must contain at least one id.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
int[] dlcGenreIds = await GetDlcGenreIdsAsync(HttpContext.RequestAborted);
|
||||
|
||||
@@ -1441,7 +1441,7 @@ public class SoftwareController(MarechaiContext context, IMemoryCache cache, Use
|
||||
// invalid state — the operator must reclassify Kind via the full
|
||||
// edit page if they really want to unlink.
|
||||
if(model.Kind == SoftwareKind.Dlc)
|
||||
return BadRequest("Cannot clear base software on a DLC row. Change Kind first via the full edit page.");
|
||||
return Problem(detail: "Cannot clear base software on a DLC row. Change Kind first via the full edit page.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
model.BaseSoftwareId = null;
|
||||
|
||||
@@ -1450,21 +1450,21 @@ public class SoftwareController(MarechaiContext context, IMemoryCache cache, Use
|
||||
|
||||
ulong baseId = baseSoftwareId.Value;
|
||||
|
||||
if(baseId == model.Id) return BadRequest("Software cannot reference itself as its base.");
|
||||
if(baseId == model.Id) return Problem(detail: "Software cannot reference itself as its base.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
Software baseSw = await context.Softwares.FindAsync([baseId], cancellationToken);
|
||||
|
||||
if(baseSw is null) return BadRequest("Base software not found.");
|
||||
if(baseSw is null) return Problem(detail: "Base software not found.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(baseSw.Kind == SoftwareKind.Dlc)
|
||||
return BadRequest("Base software is itself a DLC / add-on; chained DLCs are not allowed.");
|
||||
return Problem(detail: "Base software is itself a DLC / add-on; chained DLCs are not allowed.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool baseHasDlcGenre = await context.GenresBySoftware
|
||||
.AnyAsync(g => g.SoftwareId == baseId &&
|
||||
dlcGenreIds.Contains(g.GenreId), cancellationToken);
|
||||
|
||||
if(baseHasDlcGenre)
|
||||
return BadRequest("Base software is a misclassified Game carrying a DLC / add-on genre.");
|
||||
return Problem(detail: "Base software is a misclassified Game carrying a DLC / add-on genre.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
model.BaseSoftwareId = baseId;
|
||||
|
||||
@@ -1522,10 +1522,10 @@ public class SoftwareController(MarechaiContext context, IMemoryCache cache, Use
|
||||
model.BaseSoftwareId = dto.BaseSoftwareId;
|
||||
|
||||
if(model.Kind == SoftwareKind.Dlc && model.BaseSoftwareId is null)
|
||||
return BadRequest("DLC / Addon software must have a base software.");
|
||||
return Problem(detail: "DLC / Addon software must have a base software.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(model.Kind != SoftwareKind.Dlc && model.BaseSoftwareId is not null)
|
||||
return BadRequest("Only DLC / Addon software can have a base software.");
|
||||
return Problem(detail: "Only DLC / Addon software can have a base software.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
await context.News.AddAsync(new News
|
||||
{
|
||||
@@ -1561,10 +1561,10 @@ public class SoftwareController(MarechaiContext context, IMemoryCache cache, Use
|
||||
};
|
||||
|
||||
if(model.Kind == SoftwareKind.Dlc && model.BaseSoftwareId is null)
|
||||
return BadRequest("DLC / Addon software must have a base software.");
|
||||
return Problem(detail: "DLC / Addon software must have a base software.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(model.Kind != SoftwareKind.Dlc && model.BaseSoftwareId is not null)
|
||||
return BadRequest("Only DLC / Addon software can have a base software.");
|
||||
return Problem(detail: "Only DLC / Addon software can have a base software.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
await context.Softwares.AddAsync(model);
|
||||
await context.SaveChangesWithUserAsync(userId);
|
||||
@@ -1622,15 +1622,15 @@ public class SoftwareController(MarechaiContext context, IMemoryCache cache, Use
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<SoftwareMergePreviewDto>> GetMergePreviewAsync(ulong targetId, ulong sourceId)
|
||||
{
|
||||
if(targetId == sourceId) return BadRequest("Cannot merge a software entry into itself.");
|
||||
if(targetId == sourceId) return Problem(detail: "Cannot merge a software entry into itself.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
Software target = await context.Softwares.FindAsync(targetId);
|
||||
|
||||
if(target is null) return NotFound("Target software not found.");
|
||||
if(target is null) return Problem(detail: "Target software not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
Software source = await context.Softwares.FindAsync(sourceId);
|
||||
|
||||
if(source is null) return NotFound("Source software not found.");
|
||||
if(source is null) return Problem(detail: "Source software not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
// Extract suggested release title from name difference
|
||||
string suggestedTitle = source.Name;
|
||||
@@ -1786,18 +1786,18 @@ public class SoftwareController(MarechaiContext context, IMemoryCache cache, Use
|
||||
|
||||
if(userId is null) return Unauthorized();
|
||||
|
||||
if(targetId == sourceId) return BadRequest("Cannot merge a software entry into itself.");
|
||||
if(targetId == sourceId) return Problem(detail: "Cannot merge a software entry into itself.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
Software target = await context.Softwares.FindAsync(targetId);
|
||||
|
||||
if(target is null) return NotFound("Target software not found.");
|
||||
if(target is null) return Problem(detail: "Target software not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
Software source = await context.Softwares.FindAsync(sourceId);
|
||||
|
||||
if(source is null) return NotFound("Source software not found.");
|
||||
if(source is null) return Problem(detail: "Source software not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
if((source.Kind == SoftwareKind.Dlc) != (target.Kind == SoftwareKind.Dlc))
|
||||
return Conflict("Cannot merge DLC / Addon software with non-DLC software.");
|
||||
return Problem(detail: "Cannot merge DLC / Addon software with non-DLC software.", statusCode: StatusCodes.Status409Conflict);
|
||||
|
||||
await using var transaction = await context.Database.BeginTransactionAsync();
|
||||
|
||||
@@ -2886,7 +2886,7 @@ public class SoftwareController(MarechaiContext context, IMemoryCache cache, Use
|
||||
public async Task<IActionResult> UpsertMyRatingAsync(ulong id, [FromBody] SetRatingRequest dto)
|
||||
{
|
||||
if(dto.Rating < 0 || dto.Rating > 5)
|
||||
return BadRequest("Rating must be between 0 and 5.");
|
||||
return Problem(detail: "Rating must be between 0 and 5.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
// Snap to nearest 0.5
|
||||
float snapped = MathF.Round(dto.Rating * 2f) / 2f;
|
||||
@@ -3036,7 +3036,7 @@ public class SoftwareController(MarechaiContext context, IMemoryCache cache, Use
|
||||
|
||||
bool exists = await context.SoftwareUserReviews.AnyAsync(r => r.UserId == userId && r.SoftwareId == id);
|
||||
|
||||
if(exists) return Conflict("You have already reviewed this software.");
|
||||
if(exists) return Problem(detail: "You have already reviewed this software.", statusCode: StatusCodes.Status409Conflict);
|
||||
|
||||
var review = new SoftwareUserReview
|
||||
{
|
||||
@@ -3229,7 +3229,7 @@ public class SoftwareController(MarechaiContext context, IMemoryCache cache, Use
|
||||
bool alreadyReported =
|
||||
await context.ReviewReports.AnyAsync(r => r.ReporterId == userId && r.ReviewId == reviewId);
|
||||
|
||||
if(alreadyReported) return Conflict("You have already reported this review.");
|
||||
if(alreadyReported) return Problem(detail: "You have already reported this review.", statusCode: StatusCodes.Status409Conflict);
|
||||
|
||||
context.ReviewReports.Add(new ReviewReport
|
||||
{
|
||||
|
||||
@@ -183,24 +183,24 @@ public class SoftwareCoversController(MarechaiContext context, IConfiguration co
|
||||
if(userId is null) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0)
|
||||
return BadRequest("No file provided.");
|
||||
return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(file.Length > 50 * 1024 * 1024)
|
||||
return BadRequest("File exceeds 50 MB limit.");
|
||||
return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
|
||||
if(!_allowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP, TIFF, BMP.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP, TIFF, BMP.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!string.IsNullOrEmpty(file.ContentType) &&
|
||||
!_allowedContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
return BadRequest("Unsupported content type.");
|
||||
return Problem(detail: "Unsupported content type.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool releaseExists = await context.SoftwareReleases.AnyAsync(r => r.Id == (ulong)releaseId);
|
||||
|
||||
if(!releaseExists)
|
||||
return BadRequest("Referenced software release does not exist.");
|
||||
return Problem(detail: "Referenced software release does not exist.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
await file.CopyToAsync(ms);
|
||||
@@ -278,13 +278,13 @@ public class SoftwareCoversController(MarechaiContext context, IConfiguration co
|
||||
.FirstOrDefaultAsync(r => r.Id == (ulong)dto.SoftwareReleaseId);
|
||||
|
||||
if(newRelease is null)
|
||||
return BadRequest("Referenced software release does not exist.");
|
||||
return Problem(detail: "Referenced software release does not exist.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
ulong? currentSoftwareId = model.Release.SoftwareId ?? model.Release.SoftwareVersion?.SoftwareId;
|
||||
ulong? newSoftwareId = newRelease.SoftwareId ?? newRelease.SoftwareVersion?.SoftwareId;
|
||||
|
||||
if(currentSoftwareId != newSoftwareId)
|
||||
return BadRequest("Release does not belong to the same software.");
|
||||
return Problem(detail: "Release does not belong to the same software.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
model.SoftwareReleaseId = (ulong)dto.SoftwareReleaseId;
|
||||
}
|
||||
@@ -406,22 +406,22 @@ public class SoftwareCoversController(MarechaiContext context, IConfiguration co
|
||||
|
||||
if(string.IsNullOrEmpty(userId)) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0) return BadRequest("No file provided.");
|
||||
if(file is null || file.Length == 0) return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(file.Length > 50 * 1024 * 1024) return BadRequest("File exceeds 50 MB limit.");
|
||||
if(file.Length > 50 * 1024 * 1024) return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
|
||||
if(!_pendingAllowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!string.IsNullOrEmpty(file.ContentType) &&
|
||||
!_pendingAllowedContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
return BadRequest("Unsupported content type.");
|
||||
return Problem(detail: "Unsupported content type.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool releaseExists = await context.SoftwareReleases.AnyAsync(r => r.Id == releaseId);
|
||||
|
||||
if(!releaseExists) return NotFound("Software release not found.");
|
||||
if(!releaseExists) return Problem(detail: "Software release not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
long parentEntityId = (long)releaseId;
|
||||
|
||||
@@ -429,8 +429,9 @@ public class SoftwareCoversController(MarechaiContext context, IConfiguration co
|
||||
userId, (byte)SuggestionEntityType.SoftwareCover, parentEntityId);
|
||||
|
||||
if(currentCount >= PendingPhotosPerUserPerReleaseCap)
|
||||
return Conflict($"You already have {currentCount} pending cover images for this release. Maximum is " +
|
||||
$"{PendingPhotosPerUserPerReleaseCap} per release. Submit or remove some first.");
|
||||
return Problem(detail: $"You already have {currentCount} pending cover images for this release. Maximum is " +
|
||||
$"{PendingPhotosPerUserPerReleaseCap} per release. Submit or remove some first.",
|
||||
statusCode: StatusCodes.Status409Conflict);
|
||||
|
||||
Guid guid;
|
||||
|
||||
@@ -561,15 +562,15 @@ public class SoftwareCoversController(MarechaiContext context, IConfiguration co
|
||||
string userId = User.FindFirstValue(ClaimTypes.Sid);
|
||||
if(string.IsNullOrEmpty(userId)) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0) return BadRequest("No file provided.");
|
||||
if(file.Length > 50 * 1024 * 1024) return BadRequest("File exceeds 50 MB limit.");
|
||||
if(file is null || file.Length == 0) return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
if(file.Length > 50 * 1024 * 1024) return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
if(!_adminBatchAllowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP, AVIF, BMP, TIFF.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP, AVIF, BMP, TIFF.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool releaseExists = await context.SoftwareReleases.AnyAsync(r => r.Id == releaseId);
|
||||
if(!releaseExists) return NotFound("Software release not found.");
|
||||
if(!releaseExists) return Problem(detail: "Software release not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
// Persist FIRST (with a temporary placeholder for dimensions), then content-sniff
|
||||
// and either re-write the sidecar with real dimensions or reject + delete the file.
|
||||
@@ -586,22 +587,22 @@ public class SoftwareCoversController(MarechaiContext context, IConfiguration co
|
||||
if(imagePath is null)
|
||||
{
|
||||
PendingImageStore.Delete(_assetRootPath, "software-covers", guid);
|
||||
return BadRequest("Failed to persist uploaded file.");
|
||||
return Problem(detail: "Failed to persist uploaded file.", statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
(string magickFormat, int width, int height) = Photos.Identify(imagePath);
|
||||
if(magickFormat is null || !_adminBatchAllowedMagickFormats.Contains(magickFormat))
|
||||
{
|
||||
PendingImageStore.Delete(_assetRootPath, "software-covers", guid);
|
||||
return StatusCode(StatusCodes.Status415UnsupportedMediaType,
|
||||
"File content does not match a supported image format.");
|
||||
return Problem(detail: "File content does not match a supported image format.",
|
||||
statusCode: StatusCodes.Status415UnsupportedMediaType);
|
||||
}
|
||||
|
||||
byte[] thumbBytes = Photos.GenerateThumbnailJpeg(imagePath);
|
||||
if(thumbBytes is null)
|
||||
{
|
||||
PendingImageStore.Delete(_assetRootPath, "software-covers", guid);
|
||||
return BadRequest("Failed to generate thumbnail for the uploaded image.");
|
||||
return Problem(detail: "Failed to generate thumbnail for the uploaded image.", statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
await PendingImageStore.StoreThumbnailAsync(_assetRootPath, "software-covers", guid, thumbBytes);
|
||||
@@ -681,10 +682,10 @@ public class SoftwareCoversController(MarechaiContext context, IConfiguration co
|
||||
if(string.IsNullOrEmpty(userId)) return Unauthorized();
|
||||
|
||||
if(request is null || request.Items is null || request.Items.Count == 0)
|
||||
return BadRequest("No items provided.");
|
||||
return Problem(detail: "No items provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool releaseExists = await context.SoftwareReleases.AnyAsync(r => r.Id == request.SoftwareReleaseId);
|
||||
if(!releaseExists) return NotFound("Software release not found.");
|
||||
if(!releaseExists) return Problem(detail: "Software release not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
// Validate every pending id belongs to the caller and matches release/admin scope.
|
||||
foreach(AdminBatchCommitItemDto item in request.Items)
|
||||
@@ -692,17 +693,17 @@ public class SoftwareCoversController(MarechaiContext context, IConfiguration co
|
||||
PendingImageStore.PendingMetadata meta =
|
||||
await PendingImageStore.GetMetadataAsync(_assetRootPath, "software-covers", item.PendingId);
|
||||
|
||||
if(meta is null) return NotFound($"Pending image {item.PendingId} not found.");
|
||||
if(meta is null) return Problem(detail: $"Pending image {item.PendingId} not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
if(meta.EntityType != (byte)SuggestionEntityType.SoftwareCover)
|
||||
return BadRequest($"Pending image {item.PendingId} is not a software cover.");
|
||||
return Problem(detail: $"Pending image {item.PendingId} is not a software cover.", statusCode: StatusCodes.Status400BadRequest);
|
||||
if(!meta.IsAdminStaging)
|
||||
return BadRequest($"Pending image {item.PendingId} is not an admin-staged image.");
|
||||
return Problem(detail: $"Pending image {item.PendingId} is not an admin-staged image.", statusCode: StatusCodes.Status400BadRequest);
|
||||
if(!string.Equals(meta.UploadedById, userId, StringComparison.Ordinal))
|
||||
return Forbid();
|
||||
if(meta.ParentEntityId != (long)request.SoftwareReleaseId)
|
||||
return BadRequest($"Pending image {item.PendingId} was staged for a different release.");
|
||||
return Problem(detail: $"Pending image {item.PendingId} was staged for a different release.", statusCode: StatusCodes.Status400BadRequest);
|
||||
if(!Enum.IsDefined(typeof(SoftwareCoverType), (byte)item.Type))
|
||||
return BadRequest($"Invalid cover type {item.Type} for pending image {item.PendingId}.");
|
||||
return Problem(detail: $"Invalid cover type {item.Type} for pending image {item.PendingId}.", statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
Guid jobId = batchJobs.StartJob(userId, (long)request.SoftwareReleaseId, request.Items.Count);
|
||||
|
||||
@@ -192,32 +192,32 @@ public class SoftwarePromoArtController(MarechaiContext context, IConfiguration
|
||||
if(userId is null) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0)
|
||||
return BadRequest("No file provided.");
|
||||
return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(file.Length > 50 * 1024 * 1024)
|
||||
return BadRequest("File exceeds 50 MB limit.");
|
||||
return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
|
||||
if(!_allowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP, TIFF, BMP.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP, TIFF, BMP.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!string.IsNullOrEmpty(file.ContentType) &&
|
||||
!_allowedContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
return BadRequest("Unsupported content type.");
|
||||
return Problem(detail: "Unsupported content type.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string trimmedGroup = groupName?.Trim();
|
||||
|
||||
if(string.IsNullOrWhiteSpace(trimmedGroup))
|
||||
return BadRequest("Group name is required.");
|
||||
return Problem(detail: "Group name is required.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(trimmedGroup.Length > 256)
|
||||
return BadRequest("Group name exceeds 256 characters.");
|
||||
return Problem(detail: "Group name exceeds 256 characters.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool softwareExists = await context.Softwares.AnyAsync(s => s.Id == softwareId);
|
||||
|
||||
if(!softwareExists)
|
||||
return BadRequest("Referenced software does not exist.");
|
||||
return Problem(detail: "Referenced software does not exist.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
SoftwarePromoArtGroup group =
|
||||
await context.SoftwarePromoArtGroups.FirstOrDefaultAsync(g => g.Name == trimmedGroup);
|
||||
@@ -301,10 +301,10 @@ public class SoftwarePromoArtController(MarechaiContext context, IConfiguration
|
||||
string trimmedGroup = dto.GroupName.Trim();
|
||||
|
||||
if(string.IsNullOrWhiteSpace(trimmedGroup))
|
||||
return BadRequest("Group name cannot be empty.");
|
||||
return Problem(detail: "Group name cannot be empty.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(trimmedGroup.Length > 256)
|
||||
return BadRequest("Group name exceeds 256 characters.");
|
||||
return Problem(detail: "Group name exceeds 256 characters.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
SoftwarePromoArtGroup group =
|
||||
await context.SoftwarePromoArtGroups.FirstOrDefaultAsync(g => g.Name == trimmedGroup);
|
||||
@@ -459,22 +459,22 @@ public class SoftwarePromoArtController(MarechaiContext context, IConfiguration
|
||||
|
||||
if(string.IsNullOrEmpty(userId)) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0) return BadRequest("No file provided.");
|
||||
if(file is null || file.Length == 0) return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(file.Length > 50 * 1024 * 1024) return BadRequest("File exceeds 50 MB limit.");
|
||||
if(file.Length > 50 * 1024 * 1024) return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
|
||||
if(!_pendingAllowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!string.IsNullOrEmpty(file.ContentType) &&
|
||||
!_pendingAllowedContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
return BadRequest("Unsupported content type.");
|
||||
return Problem(detail: "Unsupported content type.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool softwareExists = await context.Softwares.AnyAsync(s => s.Id == softwareId);
|
||||
|
||||
if(!softwareExists) return NotFound("Software not found.");
|
||||
if(!softwareExists) return Problem(detail: "Software not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
long parentEntityId = (long)softwareId;
|
||||
|
||||
@@ -482,8 +482,9 @@ public class SoftwarePromoArtController(MarechaiContext context, IConfiguration
|
||||
userId, (byte)SuggestionEntityType.SoftwarePromoArt, parentEntityId);
|
||||
|
||||
if(currentCount >= PendingPhotosPerUserPerSoftwareCap)
|
||||
return Conflict($"You already have {currentCount} pending promo art images for this software. Maximum " +
|
||||
$"is {PendingPhotosPerUserPerSoftwareCap} per software. Submit or remove some first.");
|
||||
return Problem(detail: $"You already have {currentCount} pending promo art images for this software. Maximum " +
|
||||
$"is {PendingPhotosPerUserPerSoftwareCap} per software. Submit or remove some first.",
|
||||
statusCode: StatusCodes.Status409Conflict);
|
||||
|
||||
Guid guid;
|
||||
|
||||
|
||||
@@ -362,11 +362,11 @@ public class SoftwareReleasesController(MarechaiContext contex
|
||||
|
||||
// Enforce mutual exclusivity: cannot change IsCompilation after creation
|
||||
if(model.IsCompilation != dto.IsCompilation)
|
||||
return BadRequest("Cannot change a release between single-release and compilation modes.");
|
||||
return Problem(detail: "Cannot change a release between single-release and compilation modes.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
// Cannot change SoftwareId after creation
|
||||
if(model.SoftwareId != dto.SoftwareId)
|
||||
return BadRequest("Cannot change the software associated with a release.");
|
||||
return Problem(detail: "Cannot change the software associated with a release.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
model.Title = dto.Title;
|
||||
model.SoftwareVersionId = dto.SoftwareVersionId;
|
||||
@@ -497,24 +497,24 @@ public class SoftwareReleasesController(MarechaiContext contex
|
||||
if(release is null) return NotFound();
|
||||
|
||||
if(!release.IsCompilation)
|
||||
return BadRequest("Cannot add included versions to a non-compilation release.");
|
||||
return Problem(detail: "Cannot add included versions to a non-compilation release.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
// Ensure this is a versioned compilation (not a versionless one)
|
||||
bool hasIncludedSoftware = await context.SoftwareBySoftwareRelease
|
||||
.AnyAsync(x => x.ReleaseId == releaseId);
|
||||
|
||||
if(hasIncludedSoftware)
|
||||
return BadRequest("Cannot add included versions to a versionless compilation. Use included software instead.");
|
||||
return Problem(detail: "Cannot add included versions to a versionless compilation. Use included software instead.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool exists = await context.SoftwareVersionBySoftwareRelease
|
||||
.AnyAsync(x => x.ReleaseId == releaseId
|
||||
&& x.SoftwareVersionId == dto.SoftwareVersionId);
|
||||
|
||||
if(exists) return BadRequest("This version is already included in the release.");
|
||||
if(exists) return Problem(detail: "This version is already included in the release.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool versionExists = await context.SoftwareVersions.AnyAsync(v => v.Id == dto.SoftwareVersionId);
|
||||
|
||||
if(!versionExists) return BadRequest("The specified software version does not exist.");
|
||||
if(!versionExists) return Problem(detail: "The specified software version does not exist.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
await context.SoftwareVersionBySoftwareRelease.AddAsync(new SoftwareVersionBySoftwareRelease
|
||||
{
|
||||
@@ -544,7 +544,7 @@ public class SoftwareReleasesController(MarechaiContext contex
|
||||
if(release is null) return NotFound();
|
||||
|
||||
if(!release.IsCompilation)
|
||||
return BadRequest("Cannot remove included versions from a non-compilation release.");
|
||||
return Problem(detail: "Cannot remove included versions from a non-compilation release.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
SoftwareVersionBySoftwareRelease entry =
|
||||
await context.SoftwareVersionBySoftwareRelease
|
||||
@@ -675,24 +675,24 @@ public class SoftwareReleasesController(MarechaiContext contex
|
||||
if(release is null) return NotFound();
|
||||
|
||||
if(!release.IsCompilation)
|
||||
return BadRequest("Cannot add included software to a non-compilation release.");
|
||||
return Problem(detail: "Cannot add included software to a non-compilation release.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
// Ensure this is a versionless compilation (not a versioned one)
|
||||
bool hasIncludedVersions = await context.SoftwareVersionBySoftwareRelease
|
||||
.AnyAsync(x => x.ReleaseId == releaseId);
|
||||
|
||||
if(hasIncludedVersions)
|
||||
return BadRequest("Cannot add included software to a versioned compilation. Use included versions instead.");
|
||||
return Problem(detail: "Cannot add included software to a versioned compilation. Use included versions instead.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool exists = await context.SoftwareBySoftwareRelease
|
||||
.AnyAsync(x => x.ReleaseId == releaseId
|
||||
&& x.SoftwareId == dto.SoftwareId);
|
||||
|
||||
if(exists) return BadRequest("This software is already included in the release.");
|
||||
if(exists) return Problem(detail: "This software is already included in the release.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool softwareExists = await context.Softwares.AnyAsync(s => s.Id == dto.SoftwareId);
|
||||
|
||||
if(!softwareExists) return BadRequest("The specified software does not exist.");
|
||||
if(!softwareExists) return Problem(detail: "The specified software does not exist.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
await context.SoftwareBySoftwareRelease.AddAsync(new SoftwareBySoftwareRelease
|
||||
{
|
||||
@@ -722,7 +722,7 @@ public class SoftwareReleasesController(MarechaiContext contex
|
||||
if(release is null) return NotFound();
|
||||
|
||||
if(!release.IsCompilation)
|
||||
return BadRequest("Cannot remove included software from a non-compilation release.");
|
||||
return Problem(detail: "Cannot remove included software from a non-compilation release.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
SoftwareBySoftwareRelease entry =
|
||||
await context.SoftwareBySoftwareRelease
|
||||
@@ -858,11 +858,11 @@ public class SoftwareReleasesController(MarechaiContext contex
|
||||
.AnyAsync(x => x.SoftwareReleaseId == releaseId
|
||||
&& x.UnM49Id == dto.UnM49Id);
|
||||
|
||||
if(exists) return BadRequest("This region is already assigned to the release.");
|
||||
if(exists) return Problem(detail: "This region is already assigned to the release.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool regionExists = await context.UnM49.AnyAsync(r => r.Id == dto.UnM49Id);
|
||||
|
||||
if(!regionExists) return BadRequest("The specified region does not exist.");
|
||||
if(!regionExists) return Problem(detail: "The specified region does not exist.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
await context.UnM49BySoftwareRelease.AddAsync(new UnM49BySoftwareRelease
|
||||
{
|
||||
@@ -937,11 +937,11 @@ public class SoftwareReleasesController(MarechaiContext contex
|
||||
.AnyAsync(x => x.SoftwareReleaseId == releaseId
|
||||
&& x.LanguageCode == dto.LanguageCode);
|
||||
|
||||
if(exists) return BadRequest("This language is already assigned to the release.");
|
||||
if(exists) return Problem(detail: "This language is already assigned to the release.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool languageExists = await context.Iso639.AnyAsync(l => l.Id == dto.LanguageCode);
|
||||
|
||||
if(!languageExists) return BadRequest("The specified language does not exist.");
|
||||
if(!languageExists) return Problem(detail: "The specified language does not exist.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
await context.LanguageBySoftwareRelease.AddAsync(new LanguageBySoftwareRelease
|
||||
{
|
||||
|
||||
@@ -296,32 +296,32 @@ public class SoftwareScreenshotsController(MarechaiContext context, IConfigu
|
||||
if(userId is null) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0)
|
||||
return BadRequest("No file provided.");
|
||||
return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(file.Length > 50 * 1024 * 1024)
|
||||
return BadRequest("File exceeds 50 MB limit.");
|
||||
return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
|
||||
if(!_allowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP, TIFF, BMP.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP, TIFF, BMP.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!string.IsNullOrEmpty(file.ContentType) &&
|
||||
!_allowedContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
return BadRequest("Unsupported content type.");
|
||||
return Problem(detail: "Unsupported content type.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
// Validate that the referenced software exists
|
||||
bool softwareExists = await context.Softwares.AnyAsync(s => s.Id == softwareId);
|
||||
|
||||
if(!softwareExists)
|
||||
return BadRequest("Referenced software does not exist.");
|
||||
return Problem(detail: "Referenced software does not exist.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(softwarePlatformId.HasValue)
|
||||
{
|
||||
bool platformExists = await context.SoftwarePlatforms.AnyAsync(p => p.Id == softwarePlatformId.Value);
|
||||
|
||||
if(!platformExists)
|
||||
return BadRequest("Referenced software platform does not exist.");
|
||||
return Problem(detail: "Referenced software platform does not exist.", statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
if(softwareVersionId.HasValue)
|
||||
@@ -330,7 +330,7 @@ public class SoftwareScreenshotsController(MarechaiContext context, IConfigu
|
||||
v.SoftwareId == softwareId);
|
||||
|
||||
if(!versionExists)
|
||||
return BadRequest("Referenced software version does not exist or does not belong to this software.");
|
||||
return Problem(detail: "Referenced software version does not exist or does not belong to this software.", statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
// Optional group: resolve-or-create by canonical English name. Mirrors the PromoArt
|
||||
@@ -633,22 +633,22 @@ public class SoftwareScreenshotsController(MarechaiContext context, IConfigu
|
||||
|
||||
if(string.IsNullOrEmpty(userId)) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0) return BadRequest("No file provided.");
|
||||
if(file is null || file.Length == 0) return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(file.Length > 50 * 1024 * 1024) return BadRequest("File exceeds 50 MB limit.");
|
||||
if(file.Length > 50 * 1024 * 1024) return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
|
||||
if(!_pendingAllowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!string.IsNullOrEmpty(file.ContentType) &&
|
||||
!_pendingAllowedContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
return BadRequest("Unsupported content type.");
|
||||
return Problem(detail: "Unsupported content type.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool softwareExists = await context.Softwares.AnyAsync(s => s.Id == softwareId);
|
||||
|
||||
if(!softwareExists) return NotFound("Software not found.");
|
||||
if(!softwareExists) return Problem(detail: "Software not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
long parentEntityId = (long)softwareId;
|
||||
|
||||
@@ -656,8 +656,9 @@ public class SoftwareScreenshotsController(MarechaiContext context, IConfigu
|
||||
userId, (byte)SuggestionEntityType.SoftwareScreenshot, parentEntityId);
|
||||
|
||||
if(currentCount >= PendingScreenshotsPerUserPerSoftwareCap)
|
||||
return Conflict($"You already have {currentCount} pending screenshot images for this software. Maximum " +
|
||||
$"is {PendingScreenshotsPerUserPerSoftwareCap} per software. Submit or remove some first.");
|
||||
return Problem(detail: $"You already have {currentCount} pending screenshot images for this software. Maximum " +
|
||||
$"is {PendingScreenshotsPerUserPerSoftwareCap} per software. Submit or remove some first.",
|
||||
statusCode: StatusCodes.Status409Conflict);
|
||||
|
||||
Guid guid;
|
||||
|
||||
@@ -788,15 +789,15 @@ public class SoftwareScreenshotsController(MarechaiContext context, IConfigu
|
||||
string userId = User.FindFirstValue(ClaimTypes.Sid);
|
||||
if(string.IsNullOrEmpty(userId)) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0) return BadRequest("No file provided.");
|
||||
if(file.Length > 50 * 1024 * 1024) return BadRequest("File exceeds 50 MB limit.");
|
||||
if(file is null || file.Length == 0) return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
if(file.Length > 50 * 1024 * 1024) return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
if(!_adminBatchAllowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP, AVIF, BMP, TIFF.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP, AVIF, BMP, TIFF.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool softwareExists = await context.Softwares.AnyAsync(s => s.Id == (ulong)softwareId);
|
||||
if(!softwareExists) return NotFound("Software not found.");
|
||||
if(!softwareExists) return Problem(detail: "Software not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
// Persist FIRST (with a temporary placeholder for dimensions), then content-sniff
|
||||
// and either re-write the sidecar with real dimensions or reject + delete the file.
|
||||
@@ -813,22 +814,22 @@ public class SoftwareScreenshotsController(MarechaiContext context, IConfigu
|
||||
if(imagePath is null)
|
||||
{
|
||||
PendingImageStore.Delete(_assetRootPath, "software-screenshots", guid);
|
||||
return BadRequest("Failed to persist uploaded file.");
|
||||
return Problem(detail: "Failed to persist uploaded file.", statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
(string magickFormat, int width, int height) = Photos.Identify(imagePath);
|
||||
if(magickFormat is null || !_adminBatchAllowedMagickFormats.Contains(magickFormat))
|
||||
{
|
||||
PendingImageStore.Delete(_assetRootPath, "software-screenshots", guid);
|
||||
return StatusCode(StatusCodes.Status415UnsupportedMediaType,
|
||||
"File content does not match a supported image format.");
|
||||
return Problem(detail: "File content does not match a supported image format.",
|
||||
statusCode: StatusCodes.Status415UnsupportedMediaType);
|
||||
}
|
||||
|
||||
byte[] thumbBytes = Photos.GenerateThumbnailJpeg(imagePath);
|
||||
if(thumbBytes is null)
|
||||
{
|
||||
PendingImageStore.Delete(_assetRootPath, "software-screenshots", guid);
|
||||
return BadRequest("Failed to generate thumbnail for the uploaded image.");
|
||||
return Problem(detail: "Failed to generate thumbnail for the uploaded image.", statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
await PendingImageStore.StoreThumbnailAsync(_assetRootPath, "software-screenshots", guid, thumbBytes);
|
||||
@@ -907,16 +908,16 @@ public class SoftwareScreenshotsController(MarechaiContext context, IConfigu
|
||||
if(string.IsNullOrEmpty(userId)) return Unauthorized();
|
||||
|
||||
if(request is null || request.Items is null || request.Items.Count == 0)
|
||||
return BadRequest("No items provided.");
|
||||
return Problem(detail: "No items provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool softwareExists = await context.Softwares.AnyAsync(s => s.Id == (ulong)request.SoftwareId);
|
||||
if(!softwareExists) return NotFound("Software not found.");
|
||||
if(!softwareExists) return Problem(detail: "Software not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
if(request.SoftwarePlatformId.HasValue)
|
||||
{
|
||||
ulong platformId = (ulong)request.SoftwarePlatformId.Value;
|
||||
bool platformExists = await context.SoftwarePlatforms.AnyAsync(p => p.Id == platformId);
|
||||
if(!platformExists) return NotFound("Software platform not found.");
|
||||
if(!platformExists) return Problem(detail: "Software platform not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
}
|
||||
|
||||
if(request.SoftwareVersionId.HasValue)
|
||||
@@ -925,7 +926,7 @@ public class SoftwareScreenshotsController(MarechaiContext context, IConfigu
|
||||
ulong softwareIdScope = (ulong)request.SoftwareId;
|
||||
bool versionExists = await context.SoftwareVersions.AnyAsync(v => v.Id == versionId &&
|
||||
v.SoftwareId == softwareIdScope);
|
||||
if(!versionExists) return NotFound("Software version not found.");
|
||||
if(!versionExists) return Problem(detail: "Software version not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
}
|
||||
|
||||
// Validate every pending id belongs to the caller and matches software/admin scope.
|
||||
@@ -934,15 +935,15 @@ public class SoftwareScreenshotsController(MarechaiContext context, IConfigu
|
||||
PendingImageStore.PendingMetadata meta =
|
||||
await PendingImageStore.GetMetadataAsync(_assetRootPath, "software-screenshots", item.PendingId);
|
||||
|
||||
if(meta is null) return NotFound($"Pending image {item.PendingId} not found.");
|
||||
if(meta is null) return Problem(detail: $"Pending image {item.PendingId} not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
if(meta.EntityType != (byte)SuggestionEntityType.SoftwareScreenshot)
|
||||
return BadRequest($"Pending image {item.PendingId} is not a software screenshot.");
|
||||
return Problem(detail: $"Pending image {item.PendingId} is not a software screenshot.", statusCode: StatusCodes.Status400BadRequest);
|
||||
if(!meta.IsAdminStaging)
|
||||
return BadRequest($"Pending image {item.PendingId} is not an admin-staged image.");
|
||||
return Problem(detail: $"Pending image {item.PendingId} is not an admin-staged image.", statusCode: StatusCodes.Status400BadRequest);
|
||||
if(!string.Equals(meta.UploadedById, userId, StringComparison.Ordinal))
|
||||
return Forbid();
|
||||
if(meta.ParentEntityId != request.SoftwareId)
|
||||
return BadRequest($"Pending image {item.PendingId} was staged for a different software.");
|
||||
return Problem(detail: $"Pending image {item.PendingId} was staged for a different software.", statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
Guid jobId = batchJobs.StartJob(userId, request.SoftwareId, request.Items.Count);
|
||||
|
||||
@@ -98,7 +98,7 @@ public class SoftwareVideosController(MarechaiContext context) : ControllerBase
|
||||
if(userId is null) return Unauthorized();
|
||||
|
||||
if(string.IsNullOrWhiteSpace(request.Provider) || string.IsNullOrWhiteSpace(request.VideoId))
|
||||
return BadRequest("Provider and Video ID are required.");
|
||||
return Problem(detail: "Provider and Video ID are required.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string provider = request.Provider.Trim();
|
||||
string videoId = request.VideoId.Trim();
|
||||
@@ -112,7 +112,7 @@ public class SoftwareVideosController(MarechaiContext context) : ControllerBase
|
||||
v.Provider == provider &&
|
||||
v.VideoId == videoId);
|
||||
|
||||
if(exists) return Conflict("This video is already linked to this software.");
|
||||
if(exists) return Problem(detail: "This video is already linked to this software.", statusCode: StatusCodes.Status409Conflict);
|
||||
|
||||
var model = new SoftwareVideo
|
||||
{
|
||||
|
||||
@@ -257,18 +257,18 @@ public class SoundSynthPhotosController(MarechaiContext context, IConfiguration
|
||||
if(userId is null) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0)
|
||||
return BadRequest("No file provided.");
|
||||
return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(file.Length > 50 * 1024 * 1024)
|
||||
return BadRequest("File exceeds 50 MB limit.");
|
||||
return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
|
||||
if(!_allowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP, TIFF, BMP.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP, TIFF, BMP.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!string.IsNullOrEmpty(file.ContentType) && !_allowedContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
return BadRequest("Unsupported content type.");
|
||||
return Problem(detail: "Unsupported content type.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
// Read the file into memory
|
||||
using var ms = new MemoryStream();
|
||||
@@ -455,29 +455,30 @@ public class SoundSynthPhotosController(MarechaiContext context, IConfiguration
|
||||
|
||||
if(string.IsNullOrEmpty(userId)) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0) return BadRequest("No file provided.");
|
||||
if(file is null || file.Length == 0) return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(file.Length > 50 * 1024 * 1024) return BadRequest("File exceeds 50 MB limit.");
|
||||
if(file.Length > 50 * 1024 * 1024) return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
|
||||
if(!_pendingAllowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
if(!string.IsNullOrEmpty(file.ContentType) &&
|
||||
!_pendingAllowedContentTypes.Contains(file.ContentType.ToLowerInvariant()))
|
||||
return BadRequest("Unsupported content type.");
|
||||
return Problem(detail: "Unsupported content type.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool soundSynthExists = await context.SoundSynths.AnyAsync(s => s.Id == soundSynthId);
|
||||
|
||||
if(!soundSynthExists) return NotFound("Sound synthesizer not found.");
|
||||
if(!soundSynthExists) return Problem(detail: "Sound synthesizer not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
int currentCount = PendingImageStore.CountByUploaderForParentEntity(_assetRootPath, "sound-synths", userId,
|
||||
(byte)SuggestionEntityType.SoundSynthPhoto, soundSynthId);
|
||||
|
||||
if(currentCount >= PendingPhotosPerUserPerSoundSynthCap)
|
||||
return Conflict($"You already have {currentCount} pending photos for this sound synth. Maximum is " +
|
||||
$"{PendingPhotosPerUserPerSoundSynthCap} per sound synth. Submit or remove some first.");
|
||||
return Problem(detail: $"You already have {currentCount} pending photos for this sound synth. Maximum is " +
|
||||
$"{PendingPhotosPerUserPerSoundSynthCap} per sound synth. Submit or remove some first.",
|
||||
statusCode: StatusCodes.Status409Conflict);
|
||||
|
||||
Guid guid;
|
||||
|
||||
@@ -616,15 +617,15 @@ public class SoundSynthPhotosController(MarechaiContext context, IConfiguration
|
||||
string userId = User.FindFirstValue(ClaimTypes.Sid);
|
||||
if(string.IsNullOrEmpty(userId)) return Unauthorized();
|
||||
|
||||
if(file is null || file.Length == 0) return BadRequest("No file provided.");
|
||||
if(file.Length > 50 * 1024 * 1024) return BadRequest("File exceeds 50 MB limit.");
|
||||
if(file is null || file.Length == 0) return Problem(detail: "No file provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
if(file.Length > 50 * 1024 * 1024) return Problem(detail: "File exceeds 50 MB limit.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string extension = Path.GetExtension(file.FileName)?.ToLowerInvariant() ?? string.Empty;
|
||||
if(!_adminBatchAllowedExtensions.Contains(extension))
|
||||
return BadRequest("Unsupported file format. Accepted: JPEG, PNG, WebP, AVIF, BMP, TIFF.");
|
||||
return Problem(detail: "Unsupported file format. Accepted: JPEG, PNG, WebP, AVIF, BMP, TIFF.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool soundSynthExists = await context.SoundSynths.AnyAsync(s => s.Id == soundSynthId);
|
||||
if(!soundSynthExists) return NotFound("Sound synthesizer not found.");
|
||||
if(!soundSynthExists) return Problem(detail: "Sound synthesizer not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
// Persist FIRST (with a temporary placeholder for dimensions), then content-sniff
|
||||
// and either re-write the sidecar with real dimensions or reject + delete the file.
|
||||
@@ -641,22 +642,22 @@ public class SoundSynthPhotosController(MarechaiContext context, IConfiguration
|
||||
if(imagePath is null)
|
||||
{
|
||||
PendingImageStore.Delete(_assetRootPath, "sound-synths", guid);
|
||||
return BadRequest("Failed to persist uploaded file.");
|
||||
return Problem(detail: "Failed to persist uploaded file.", statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
(string magickFormat, int width, int height) = Photos.Identify(imagePath);
|
||||
if(magickFormat is null || !_adminBatchAllowedMagickFormats.Contains(magickFormat))
|
||||
{
|
||||
PendingImageStore.Delete(_assetRootPath, "sound-synths", guid);
|
||||
return StatusCode(StatusCodes.Status415UnsupportedMediaType,
|
||||
"File content does not match a supported image format.");
|
||||
return Problem(detail: "File content does not match a supported image format.",
|
||||
statusCode: StatusCodes.Status415UnsupportedMediaType);
|
||||
}
|
||||
|
||||
byte[] thumbBytes = Photos.GenerateThumbnailJpeg(imagePath);
|
||||
if(thumbBytes is null)
|
||||
{
|
||||
PendingImageStore.Delete(_assetRootPath, "sound-synths", guid);
|
||||
return BadRequest("Failed to generate thumbnail for the uploaded image.");
|
||||
return Problem(detail: "Failed to generate thumbnail for the uploaded image.", statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
await PendingImageStore.StoreThumbnailAsync(_assetRootPath, "sound-synths", guid, thumbBytes);
|
||||
@@ -735,13 +736,13 @@ public class SoundSynthPhotosController(MarechaiContext context, IConfiguration
|
||||
if(string.IsNullOrEmpty(userId)) return Unauthorized();
|
||||
|
||||
if(request is null || request.Items is null || request.Items.Count == 0)
|
||||
return BadRequest("No items provided.");
|
||||
return Problem(detail: "No items provided.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
bool soundSynthExists = await context.SoundSynths.AnyAsync(s => s.Id == request.SoundSynthId);
|
||||
if(!soundSynthExists) return NotFound("Sound synthesizer not found.");
|
||||
if(!soundSynthExists) return Problem(detail: "Sound synthesizer not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
bool licenseExists = await context.Licenses.AnyAsync(l => l.Id == request.LicenseId);
|
||||
if(!licenseExists) return NotFound("License not found.");
|
||||
if(!licenseExists) return Problem(detail: "License not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
// Validate every pending id belongs to the caller and matches sound-synth/admin scope.
|
||||
foreach(AdminSoundSynthPhotoBatchCommitItemDto item in request.Items)
|
||||
@@ -749,16 +750,16 @@ public class SoundSynthPhotosController(MarechaiContext context, IConfiguration
|
||||
PendingImageStore.PendingMetadata meta =
|
||||
await PendingImageStore.GetMetadataAsync(_assetRootPath, "sound-synths", item.PendingId);
|
||||
|
||||
if(meta is null) return NotFound($"Pending image {item.PendingId} not found.");
|
||||
if(meta is null) return Problem(detail: $"Pending image {item.PendingId} not found.", statusCode: StatusCodes.Status404NotFound);
|
||||
if(meta.EntityType != (byte)SuggestionEntityType.SoundSynthPhoto)
|
||||
return BadRequest($"Pending image {item.PendingId} is not a sound synth photo.");
|
||||
return Problem(detail: $"Pending image {item.PendingId} is not a sound synth photo.", statusCode: StatusCodes.Status400BadRequest);
|
||||
if(!meta.IsAdminStaging)
|
||||
return BadRequest($"Pending image {item.PendingId} is not an admin-staged image.");
|
||||
return Problem(detail: $"Pending image {item.PendingId} is not an admin-staged image.", statusCode: StatusCodes.Status400BadRequest);
|
||||
if(!string.Equals(meta.UploadedById, userId, StringComparison.Ordinal))
|
||||
return Forbid();
|
||||
if(meta.ParentEntityId != request.SoundSynthId)
|
||||
return BadRequest(
|
||||
$"Pending image {item.PendingId} was staged for a different sound synthesizer.");
|
||||
return Problem(detail: $"Pending image {item.PendingId} was staged for a different sound synthesizer.",
|
||||
statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
Guid jobId = batchJobs.StartJob(userId, request.SoundSynthId, request.Items.Count);
|
||||
|
||||
@@ -98,7 +98,7 @@ public class SoundSynthVideosController(MarechaiContext context) : ControllerBas
|
||||
if(userId is null) return Unauthorized();
|
||||
|
||||
if(string.IsNullOrWhiteSpace(request.Provider) || string.IsNullOrWhiteSpace(request.VideoId))
|
||||
return BadRequest("Provider and Video ID are required.");
|
||||
return Problem(detail: "Provider and Video ID are required.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
string provider = request.Provider.Trim();
|
||||
string videoId = request.VideoId.Trim();
|
||||
@@ -112,7 +112,7 @@ public class SoundSynthVideosController(MarechaiContext context) : ControllerBas
|
||||
v.Provider == provider &&
|
||||
v.VideoId == videoId);
|
||||
|
||||
if(exists) return Conflict("This video is already linked to this sound synth.");
|
||||
if(exists) return Problem(detail: "This video is already linked to this sound synth.", statusCode: StatusCodes.Status409Conflict);
|
||||
|
||||
var model = new SoundSynthVideo
|
||||
{
|
||||
|
||||
@@ -263,7 +263,7 @@ public class UsersController(UserManager<ApplicationUser> userManager,
|
||||
{
|
||||
ApplicationUser user = await userManager.FindByIdAsync(id);
|
||||
|
||||
if(user == null) return NotFound("User not found");
|
||||
if(user == null) return Problem(detail: "User not found", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
IList<string> roles = await userManager.GetRolesAsync(user);
|
||||
|
||||
@@ -346,7 +346,7 @@ public class UsersController(UserManager<ApplicationUser> userManager,
|
||||
|
||||
ApplicationUser user = await userManager.FindByIdAsync(id);
|
||||
|
||||
if(user == null) return NotFound("User not found");
|
||||
if(user == null) return Problem(detail: "User not found", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
user.UserName = request.UserName;
|
||||
user.Email = request.Email;
|
||||
@@ -389,7 +389,7 @@ public class UsersController(UserManager<ApplicationUser> userManager,
|
||||
{
|
||||
ApplicationUser user = await userManager.FindByIdAsync(id);
|
||||
|
||||
if(user == null) return NotFound("User not found");
|
||||
if(user == null) return Problem(detail: "User not found", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
// Delegate to the shared deletion service so the admin path applies the same anonymise-content +
|
||||
// hard-delete-state + avatar-file-cleanup as the GDPR self-service path. The admin path is
|
||||
@@ -398,7 +398,7 @@ public class UsersController(UserManager<ApplicationUser> userManager,
|
||||
|
||||
bool ok = await userAccountDeletionService.PurgeAsync(id, actorUserIdForLog: actorId ?? "admin");
|
||||
|
||||
if(!ok) return BadRequest("Failed to purge user account.");
|
||||
if(!ok) return Problem(detail: "Failed to purge user account.", statusCode: StatusCodes.Status400BadRequest);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
@@ -416,7 +416,7 @@ public class UsersController(UserManager<ApplicationUser> userManager,
|
||||
|
||||
ApplicationUser user = await userManager.FindByIdAsync(id);
|
||||
|
||||
if(user == null) return NotFound("User not found");
|
||||
if(user == null) return Problem(detail: "User not found", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
// Remove old password and set new one
|
||||
IdentityResult removeResult = await userManager.RemovePasswordAsync(user);
|
||||
@@ -443,7 +443,7 @@ public class UsersController(UserManager<ApplicationUser> userManager,
|
||||
|
||||
ApplicationUser user = await userManager.FindByIdAsync(id);
|
||||
|
||||
if(user == null) return NotFound("User not found");
|
||||
if(user == null) return Problem(detail: "User not found", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
IdentityResult result = await userManager.AddToRoleAsync(user, request.RoleName);
|
||||
|
||||
@@ -462,7 +462,7 @@ public class UsersController(UserManager<ApplicationUser> userManager,
|
||||
{
|
||||
ApplicationUser user = await userManager.FindByIdAsync(id);
|
||||
|
||||
if(user == null) return NotFound("User not found");
|
||||
if(user == null) return Problem(detail: "User not found", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
IdentityResult result = await userManager.RemoveFromRoleAsync(user, roleName);
|
||||
|
||||
@@ -685,7 +685,7 @@ public class UsersController(UserManager<ApplicationUser> userManager,
|
||||
{
|
||||
ApplicationUser user = await userManager.FindByIdAsync(id);
|
||||
|
||||
if(user == null) return NotFound("User not found");
|
||||
if(user == null) return Problem(detail: "User not found", statusCode: StatusCodes.Status404NotFound);
|
||||
|
||||
await userManager.SetTwoFactorEnabledAsync(user, false);
|
||||
await userManager.ResetAuthenticatorKeyAsync(user);
|
||||
|
||||
@@ -356,7 +356,7 @@ public class BooksService(Marechai.ApiClient.Client client, ReferenceDataCache r
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -374,7 +374,7 @@ public class BooksService(Marechai.ApiClient.Client client, ReferenceDataCache r
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -392,7 +392,7 @@ public class BooksService(Marechai.ApiClient.Client client, ReferenceDataCache r
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -426,7 +426,7 @@ public class BooksService(Marechai.ApiClient.Client client, ReferenceDataCache r
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -444,7 +444,7 @@ public class BooksService(Marechai.ApiClient.Client client, ReferenceDataCache r
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -464,7 +464,7 @@ public class BooksService(Marechai.ApiClient.Client client, ReferenceDataCache r
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -482,7 +482,7 @@ public class BooksService(Marechai.ApiClient.Client client, ReferenceDataCache r
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -502,7 +502,7 @@ public class BooksService(Marechai.ApiClient.Client client, ReferenceDataCache r
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -520,7 +520,7 @@ public class BooksService(Marechai.ApiClient.Client client, ReferenceDataCache r
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -540,7 +540,7 @@ public class BooksService(Marechai.ApiClient.Client client, ReferenceDataCache r
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -558,7 +558,7 @@ public class BooksService(Marechai.ApiClient.Client client, ReferenceDataCache r
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -578,7 +578,7 @@ public class BooksService(Marechai.ApiClient.Client client, ReferenceDataCache r
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -596,7 +596,7 @@ public class BooksService(Marechai.ApiClient.Client client, ReferenceDataCache r
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -616,7 +616,7 @@ public class BooksService(Marechai.ApiClient.Client client, ReferenceDataCache r
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -697,4 +697,21 @@ public class BooksService(Marechai.ApiClient.Client client, ReferenceDataCache r
|
||||
public Task<List<MachineFamilyDto>> GetAllMachineFamiliesAsync() => referenceData.GetMachineFamiliesAsync();
|
||||
|
||||
public Task<List<Iso31661NumericDto>> GetCountriesAsync() => referenceData.GetCountriesAsync();
|
||||
|
||||
static string ExtractDetail(ApiException ex)
|
||||
{
|
||||
// Kiota maps server error responses to a typed ProblemDetails (which inherits from
|
||||
// ApiException). The base Exception.Message just returns "Exception of type 'X' was
|
||||
// thrown." — the real, user-facing text lives on Detail / Title. Surface those when
|
||||
// present, falling back to Message only if the server gave us nothing useful.
|
||||
if(ex is ProblemDetails pd)
|
||||
{
|
||||
if(!string.IsNullOrWhiteSpace(pd.Detail)) return pd.Detail;
|
||||
if(!string.IsNullOrWhiteSpace(pd.Title)) return pd.Title;
|
||||
}
|
||||
|
||||
if(ex is { ResponseStatusCode: 0 } || string.IsNullOrWhiteSpace(ex.Message)) return "Unknown error";
|
||||
|
||||
return ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,7 +337,7 @@ public class DocumentsService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -355,7 +355,7 @@ public class DocumentsService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -373,7 +373,7 @@ public class DocumentsService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -407,7 +407,7 @@ public class DocumentsService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -425,7 +425,7 @@ public class DocumentsService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -445,7 +445,7 @@ public class DocumentsService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -463,7 +463,7 @@ public class DocumentsService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -483,7 +483,7 @@ public class DocumentsService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -501,7 +501,7 @@ public class DocumentsService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -521,7 +521,7 @@ public class DocumentsService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -539,7 +539,7 @@ public class DocumentsService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -559,7 +559,7 @@ public class DocumentsService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -577,7 +577,7 @@ public class DocumentsService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -646,4 +646,21 @@ public class DocumentsService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
public Task<List<MachineFamilyDto>> GetAllMachineFamiliesAsync() => referenceData.GetMachineFamiliesAsync();
|
||||
|
||||
public Task<List<Iso31661NumericDto>> GetCountriesAsync() => referenceData.GetCountriesAsync();
|
||||
|
||||
static string ExtractDetail(ApiException ex)
|
||||
{
|
||||
// Kiota maps server error responses to a typed ProblemDetails (which inherits from
|
||||
// ApiException). The base Exception.Message just returns "Exception of type 'X' was
|
||||
// thrown." — the real, user-facing text lives on Detail / Title. Surface those when
|
||||
// present, falling back to Message only if the server gave us nothing useful.
|
||||
if(ex is ProblemDetails pd)
|
||||
{
|
||||
if(!string.IsNullOrWhiteSpace(pd.Detail)) return pd.Detail;
|
||||
if(!string.IsNullOrWhiteSpace(pd.Title)) return pd.Title;
|
||||
}
|
||||
|
||||
if(ex is { ResponseStatusCode: 0 } || string.IsNullOrWhiteSpace(ex.Message)) return "Unknown error";
|
||||
|
||||
return ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ public class GpusService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractErrorMessage(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -164,7 +164,7 @@ public class GpusService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractErrorMessage(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -182,7 +182,7 @@ public class GpusService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractErrorMessage(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -229,7 +229,7 @@ public class GpusService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractErrorMessage(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -247,7 +247,7 @@ public class GpusService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractErrorMessage(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -358,7 +358,7 @@ public class GpusService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractErrorMessage(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -376,7 +376,7 @@ public class GpusService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractErrorMessage(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
|
||||
@@ -69,7 +69,7 @@ public class InstructionSetExtensionsService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -87,7 +87,7 @@ public class InstructionSetExtensionsService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -105,7 +105,7 @@ public class InstructionSetExtensionsService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -126,4 +126,21 @@ public class InstructionSetExtensionsService(Marechai.ApiClient.Client client)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static string ExtractDetail(ApiException ex)
|
||||
{
|
||||
// Kiota maps server error responses to a typed ProblemDetails (which inherits from
|
||||
// ApiException). The base Exception.Message just returns "Exception of type 'X' was
|
||||
// thrown." — the real, user-facing text lives on Detail / Title. Surface those when
|
||||
// present, falling back to Message only if the server gave us nothing useful.
|
||||
if(ex is ProblemDetails pd)
|
||||
{
|
||||
if(!string.IsNullOrWhiteSpace(pd.Detail)) return pd.Detail;
|
||||
if(!string.IsNullOrWhiteSpace(pd.Title)) return pd.Title;
|
||||
}
|
||||
|
||||
if(ex is { ResponseStatusCode: 0 } || string.IsNullOrWhiteSpace(ex.Message)) return "Unknown error";
|
||||
|
||||
return ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ public class MachineFamiliesService(Marechai.ApiClient.Client client, ReferenceD
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -79,7 +79,7 @@ public class MachineFamiliesService(Marechai.ApiClient.Client client, ReferenceD
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -99,7 +99,7 @@ public class MachineFamiliesService(Marechai.ApiClient.Client client, ReferenceD
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -134,4 +134,21 @@ public class MachineFamiliesService(Marechai.ApiClient.Client client, ReferenceD
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static string ExtractDetail(ApiException ex)
|
||||
{
|
||||
// Kiota maps server error responses to a typed ProblemDetails (which inherits from
|
||||
// ApiException). The base Exception.Message just returns "Exception of type 'X' was
|
||||
// thrown." — the real, user-facing text lives on Detail / Title. Surface those when
|
||||
// present, falling back to Message only if the server gave us nothing useful.
|
||||
if(ex is ProblemDetails pd)
|
||||
{
|
||||
if(!string.IsNullOrWhiteSpace(pd.Detail)) return pd.Detail;
|
||||
if(!string.IsNullOrWhiteSpace(pd.Title)) return pd.Title;
|
||||
}
|
||||
|
||||
if(ex is { ResponseStatusCode: 0 } || string.IsNullOrWhiteSpace(ex.Message)) return "Unknown error";
|
||||
|
||||
return ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,7 +354,7 @@ public class MagazinesService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -372,7 +372,7 @@ public class MagazinesService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -390,7 +390,7 @@ public class MagazinesService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -503,7 +503,7 @@ public class MagazinesService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -525,7 +525,7 @@ public class MagazinesService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -547,7 +547,7 @@ public class MagazinesService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -567,7 +567,7 @@ public class MagazinesService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -613,7 +613,7 @@ public class MagazinesService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -631,7 +631,7 @@ public class MagazinesService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -651,7 +651,7 @@ public class MagazinesService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -669,7 +669,7 @@ public class MagazinesService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -689,7 +689,7 @@ public class MagazinesService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -707,7 +707,7 @@ public class MagazinesService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -727,7 +727,7 @@ public class MagazinesService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -745,7 +745,7 @@ public class MagazinesService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -765,7 +765,7 @@ public class MagazinesService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -783,7 +783,7 @@ public class MagazinesService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -803,7 +803,7 @@ public class MagazinesService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -821,7 +821,7 @@ public class MagazinesService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -905,6 +905,23 @@ public class MagazinesService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
|
||||
public Task<List<Iso31661NumericDto>> GetCountriesAsync() => referenceData.GetCountriesAsync();
|
||||
|
||||
static string ExtractDetail(ApiException ex)
|
||||
{
|
||||
// Kiota maps server error responses to a typed ProblemDetails (which inherits from
|
||||
// ApiException). The base Exception.Message just returns "Exception of type 'X' was
|
||||
// thrown." — the real, user-facing text lives on Detail / Title. Surface those when
|
||||
// present, falling back to Message only if the server gave us nothing useful.
|
||||
if(ex is ProblemDetails pd)
|
||||
{
|
||||
if(!string.IsNullOrWhiteSpace(pd.Detail)) return pd.Detail;
|
||||
if(!string.IsNullOrWhiteSpace(pd.Title)) return pd.Title;
|
||||
}
|
||||
|
||||
if(ex is { ResponseStatusCode: 0 } || string.IsNullOrWhiteSpace(ex.Message)) return "Unknown error";
|
||||
|
||||
return ex.Message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders a Kiota-thrown <see cref="ProblemDetails"/> into a human-readable string for
|
||||
/// surfacing in toasts / inline error messages. The Kiota-generated <c>ProblemDetails</c>
|
||||
|
||||
@@ -135,19 +135,37 @@ public sealed class OldDosImportsService(Client client, ILogger<OldDosImportsSer
|
||||
|
||||
/// <summary>
|
||||
/// Build a human-readable diagnostic from a Kiota <see cref="ApiException"/>. Includes the HTTP
|
||||
/// status code plus, when present, a short slice of <c>ex.Message</c> (which usually carries the
|
||||
/// server's response body) so the admin dialog shows more than just <c>HTTP 404</c>. When the
|
||||
/// message is empty the endpoint path is appended so it's clear which call failed.
|
||||
/// status code plus, when present, a short slice of the extracted ProblemDetails detail / title
|
||||
/// (which is the human-friendly text the server intended to surface) so the admin dialog shows
|
||||
/// more than just <c>HTTP 404</c>. When the message is empty the endpoint path is appended so
|
||||
/// it's clear which call failed.
|
||||
/// </summary>
|
||||
static string BuildErrorMessage(string endpoint, ApiException ex)
|
||||
{
|
||||
string status = ex.ResponseStatusCode > 0 ? $"HTTP {ex.ResponseStatusCode}" : "HTTP error";
|
||||
string msg = ex.Message;
|
||||
string msg = ExtractDetail(ex);
|
||||
if(string.IsNullOrWhiteSpace(msg)) return $"{status} on {endpoint} (no response body)";
|
||||
if(msg.Length > 400) msg = msg.Substring(0, 400) + "…";
|
||||
return $"{status} on {endpoint} — {msg}";
|
||||
}
|
||||
|
||||
static string ExtractDetail(ApiException ex)
|
||||
{
|
||||
// Kiota maps server error responses to a typed ProblemDetails (which inherits from
|
||||
// ApiException). The base Exception.Message just returns "Exception of type 'X' was
|
||||
// thrown." — the real, user-facing text lives on Detail / Title. Surface those when
|
||||
// present, falling back to Message only if the server gave us nothing useful.
|
||||
if(ex is ProblemDetails pd)
|
||||
{
|
||||
if(!string.IsNullOrWhiteSpace(pd.Detail)) return pd.Detail;
|
||||
if(!string.IsNullOrWhiteSpace(pd.Title)) return pd.Title;
|
||||
}
|
||||
|
||||
if(ex is { ResponseStatusCode: 0 } || string.IsNullOrWhiteSpace(ex.Message)) return "Unknown error";
|
||||
|
||||
return ex.Message;
|
||||
}
|
||||
|
||||
public async Task<bool> SkipAsync(long id)
|
||||
{
|
||||
try { await client.OldDos.Pending[(int)id].Skip.PostAsync(); return true; }
|
||||
|
||||
@@ -69,7 +69,7 @@ public class ResolutionsService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -87,7 +87,7 @@ public class ResolutionsService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -105,11 +105,28 @@ public class ResolutionsService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
static string ExtractDetail(ApiException ex)
|
||||
{
|
||||
// Kiota maps server error responses to a typed ProblemDetails (which inherits from
|
||||
// ApiException). The base Exception.Message just returns "Exception of type 'X' was
|
||||
// thrown." — the real, user-facing text lives on Detail / Title. Surface those when
|
||||
// present, falling back to Message only if the server gave us nothing useful.
|
||||
if(ex is ProblemDetails pd)
|
||||
{
|
||||
if(!string.IsNullOrWhiteSpace(pd.Detail)) return pd.Detail;
|
||||
if(!string.IsNullOrWhiteSpace(pd.Title)) return pd.Title;
|
||||
}
|
||||
|
||||
if(ex is { ResponseStatusCode: 0 } || string.IsNullOrWhiteSpace(ex.Message)) return "Unknown error";
|
||||
|
||||
return ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ public class ScreensService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -87,7 +87,7 @@ public class ScreensService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -105,7 +105,7 @@ public class ScreensService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -151,7 +151,7 @@ public class ScreensService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -169,11 +169,28 @@ public class ScreensService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
static string ExtractDetail(ApiException ex)
|
||||
{
|
||||
// Kiota maps server error responses to a typed ProblemDetails (which inherits from
|
||||
// ApiException). The base Exception.Message just returns "Exception of type 'X' was
|
||||
// thrown." — the real, user-facing text lives on Detail / Title. Surface those when
|
||||
// present, falling back to Message only if the server gave us nothing useful.
|
||||
if(ex is ProblemDetails pd)
|
||||
{
|
||||
if(!string.IsNullOrWhiteSpace(pd.Detail)) return pd.Detail;
|
||||
if(!string.IsNullOrWhiteSpace(pd.Title)) return pd.Title;
|
||||
}
|
||||
|
||||
if(ex is { ResponseStatusCode: 0 } || string.IsNullOrWhiteSpace(ex.Message)) return "Unknown error";
|
||||
|
||||
return ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ public class SoftwareFamiliesService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -87,7 +87,7 @@ public class SoftwareFamiliesService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -105,7 +105,7 @@ public class SoftwareFamiliesService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -140,7 +140,7 @@ public class SoftwareFamiliesService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -158,7 +158,7 @@ public class SoftwareFamiliesService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -195,4 +195,21 @@ public class SoftwareFamiliesService(Marechai.ApiClient.Client client)
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static string ExtractDetail(ApiException ex)
|
||||
{
|
||||
// Kiota maps server error responses to a typed ProblemDetails (which inherits from
|
||||
// ApiException). The base Exception.Message just returns "Exception of type 'X' was
|
||||
// thrown." — the real, user-facing text lives on Detail / Title. Surface those when
|
||||
// present, falling back to Message only if the server gave us nothing useful.
|
||||
if(ex is ProblemDetails pd)
|
||||
{
|
||||
if(!string.IsNullOrWhiteSpace(pd.Detail)) return pd.Detail;
|
||||
if(!string.IsNullOrWhiteSpace(pd.Title)) return pd.Title;
|
||||
}
|
||||
|
||||
if(ex is { ResponseStatusCode: 0 } || string.IsNullOrWhiteSpace(ex.Message)) return "Unknown error";
|
||||
|
||||
return ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ public class SoftwarePlatformsService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -87,7 +87,7 @@ public class SoftwarePlatformsService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -105,11 +105,28 @@ public class SoftwarePlatformsService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
static string ExtractDetail(ApiException ex)
|
||||
{
|
||||
// Kiota maps server error responses to a typed ProblemDetails (which inherits from
|
||||
// ApiException). The base Exception.Message just returns "Exception of type 'X' was
|
||||
// thrown." — the real, user-facing text lives on Detail / Title. Surface those when
|
||||
// present, falling back to Message only if the server gave us nothing useful.
|
||||
if(ex is ProblemDetails pd)
|
||||
{
|
||||
if(!string.IsNullOrWhiteSpace(pd.Detail)) return pd.Detail;
|
||||
if(!string.IsNullOrWhiteSpace(pd.Title)) return pd.Title;
|
||||
}
|
||||
|
||||
if(ex is { ResponseStatusCode: 0 } || string.IsNullOrWhiteSpace(ex.Message)) return "Unknown error";
|
||||
|
||||
return ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ public class SoftwareVersionsService(Marechai.ApiClient.Client client, Reference
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -101,7 +101,7 @@ public class SoftwareVersionsService(Marechai.ApiClient.Client client, Reference
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -119,7 +119,7 @@ public class SoftwareVersionsService(Marechai.ApiClient.Client client, Reference
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -154,7 +154,7 @@ public class SoftwareVersionsService(Marechai.ApiClient.Client client, Reference
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -172,7 +172,7 @@ public class SoftwareVersionsService(Marechai.ApiClient.Client client, Reference
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractDetail(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -211,4 +211,21 @@ public class SoftwareVersionsService(Marechai.ApiClient.Client client, Reference
|
||||
}
|
||||
|
||||
public Task<List<LicenseDto>> GetAllLicensesAsync() => referenceData.GetLicensesAsync();
|
||||
|
||||
static string ExtractDetail(ApiException ex)
|
||||
{
|
||||
// Kiota maps server error responses to a typed ProblemDetails (which inherits from
|
||||
// ApiException). The base Exception.Message just returns "Exception of type 'X' was
|
||||
// thrown." — the real, user-facing text lives on Detail / Title. Surface those when
|
||||
// present, falling back to Message only if the server gave us nothing useful.
|
||||
if(ex is ProblemDetails pd)
|
||||
{
|
||||
if(!string.IsNullOrWhiteSpace(pd.Detail)) return pd.Detail;
|
||||
if(!string.IsNullOrWhiteSpace(pd.Title)) return pd.Title;
|
||||
}
|
||||
|
||||
if(ex is { ResponseStatusCode: 0 } || string.IsNullOrWhiteSpace(ex.Message)) return "Unknown error";
|
||||
|
||||
return ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ public class SoundSynthsService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
return (null, ExtractErrorMessage(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -173,7 +173,7 @@ public class SoundSynthsService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractErrorMessage(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -191,7 +191,7 @@ public class SoundSynthsService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractErrorMessage(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -290,7 +290,7 @@ public class SoundSynthsService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractErrorMessage(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -308,7 +308,7 @@ public class SoundSynthsService(Marechai.ApiClient.Client client)
|
||||
}
|
||||
catch(ApiException ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
return (false, ExtractErrorMessage(ex));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
|
||||
@@ -142,19 +142,37 @@ public sealed class WwpcImportsService(Client client, ILogger<WwpcImportsService
|
||||
|
||||
/// <summary>
|
||||
/// Build a human-readable diagnostic from a Kiota <see cref="ApiException"/>. Includes the HTTP
|
||||
/// status code plus, when present, a short slice of <c>ex.Message</c> (which usually carries the
|
||||
/// server's response body) so the admin dialog shows more than just <c>HTTP 404</c>. When the
|
||||
/// message is empty the endpoint path is appended so it's clear which call failed.
|
||||
/// status code plus, when present, a short slice of the extracted ProblemDetails detail / title
|
||||
/// (which is the human-friendly text the server intended to surface) so the admin dialog shows
|
||||
/// more than just <c>HTTP 404</c>. When the message is empty the endpoint path is appended so
|
||||
/// it's clear which call failed.
|
||||
/// </summary>
|
||||
static string BuildErrorMessage(string endpoint, ApiException ex)
|
||||
{
|
||||
string status = ex.ResponseStatusCode > 0 ? $"HTTP {ex.ResponseStatusCode}" : "HTTP error";
|
||||
string msg = ex.Message;
|
||||
string msg = ExtractDetail(ex);
|
||||
if(string.IsNullOrWhiteSpace(msg)) return $"{status} on {endpoint} (no response body)";
|
||||
if(msg.Length > 400) msg = msg.Substring(0, 400) + "…";
|
||||
return $"{status} on {endpoint} — {msg}";
|
||||
}
|
||||
|
||||
static string ExtractDetail(ApiException ex)
|
||||
{
|
||||
// Kiota maps server error responses to a typed ProblemDetails (which inherits from
|
||||
// ApiException). The base Exception.Message just returns "Exception of type 'X' was
|
||||
// thrown." — the real, user-facing text lives on Detail / Title. Surface those when
|
||||
// present, falling back to Message only if the server gave us nothing useful.
|
||||
if(ex is ProblemDetails pd)
|
||||
{
|
||||
if(!string.IsNullOrWhiteSpace(pd.Detail)) return pd.Detail;
|
||||
if(!string.IsNullOrWhiteSpace(pd.Title)) return pd.Title;
|
||||
}
|
||||
|
||||
if(ex is { ResponseStatusCode: 0 } || string.IsNullOrWhiteSpace(ex.Message)) return "Unknown error";
|
||||
|
||||
return ex.Message;
|
||||
}
|
||||
|
||||
public async Task<bool> SkipAsync(long id)
|
||||
{
|
||||
try { await client.Wwpc.Pending[(int)id].Skip.PostAsync(); return true; }
|
||||
|
||||
Reference in New Issue
Block a user