fix(api): harden billing/blog validation and update rc14 changelog

This commit is contained in:
Paul Schneider 2026-09-06 19:16:41 +01:00
commit ad5e9090ee
13 changed files with 168 additions and 53 deletions

View file

@ -51,6 +51,14 @@ namespace Yavsc.Controllers
.Distinct()
.ToArray();
// Some providers are brittle when translating Contains over an
// empty in-memory array. If there is no candidate activity code,
// the catalog is empty by definition.
if (codes.Length == 0)
{
return Ok(new List<ActivityInfo>());
}
var performerCounts = await (
from ua in _context.UserActivities.AsNoTracking()
where !string.IsNullOrWhiteSpace(ua.DoesCode) && codes.Contains(ua.DoesCode)
@ -64,10 +72,10 @@ namespace Yavsc.Controllers
var filteredActivities = activities
.Where(a =>
(performerCounts.TryGetValue(a.Code, out var ownCount) && ownCount > 0)
(TryGetPerformerCount(performerCounts, a.Code, out var ownCount) && ownCount > 0)
|| (a.Children ?? new List<Activity>())
.Where(c => !c.Hidden)
.Any(c => performerCounts.TryGetValue(c.Code, out var childCount) && childCount > 0))
.Any(c => TryGetPerformerCount(performerCounts, c.Code, out var childCount) && childCount > 0))
.ToList();
return Ok(filteredActivities.Select(a => ToBrowseItem(a, performerCounts)).ToList());
@ -269,10 +277,10 @@ namespace Yavsc.Controllers
Code = activity.Code,
Name = activity.Name,
ParentCode = activity.ParentCode,
Description = activity.Description,
Description = activity.Description ?? string.Empty,
Photo = activity.Photo,
Rate = activity.Rate,
PerformerCount = performerCounts.TryGetValue(activity.Code, out var count) ? count : 0,
PerformerCount = TryGetPerformerCount(performerCounts, activity.Code, out var count) ? count : 0,
Forms = (activity.Forms ?? Enumerable.Empty<CommandForm>())
.Select(f => new CommandFormSummary
{
@ -283,17 +291,17 @@ namespace Yavsc.Controllers
.ToList(),
Children = (activity.Children ?? Enumerable.Empty<Activity>())
.Where(c => !c.Hidden)
.Where(c => performerCounts.TryGetValue(c.Code, out var childCount) && childCount > 0)
.Where(c => TryGetPerformerCount(performerCounts, c.Code, out var childCount) && childCount > 0)
.OrderByDescending(c => c.Rate)
.Select(c => new ActivityInfo
{
Code = c.Code,
Name = c.Name,
ParentCode = c.ParentCode,
Description = c.Description,
Description = c.Description ?? string.Empty,
Photo = c.Photo,
Rate = c.Rate,
PerformerCount = performerCounts.TryGetValue(c.Code, out var childCount) ? childCount : 0,
PerformerCount = TryGetPerformerCount(performerCounts, c.Code, out var childCount) ? childCount : 0,
Forms = (c.Forms ?? Enumerable.Empty<CommandForm>())
.Select(f => new CommandFormSummary
{
@ -306,5 +314,19 @@ namespace Yavsc.Controllers
.ToList(),
};
}
private static bool TryGetPerformerCount(
IReadOnlyDictionary<string, int> performerCounts,
string code,
out int count)
{
if (string.IsNullOrWhiteSpace(code))
{
count = 0;
return false;
}
return performerCounts.TryGetValue(code, out count);
}
}
}

View file

@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
using Yavsc.Helpers;
using Yavsc.Models;
using Yavsc.Services;
using Yavsc.Server.Helpers;
using Yavsc.ViewModels.FrontOffice;
namespace Yavsc.ApiControllers
@ -15,10 +16,10 @@ namespace Yavsc.ApiControllers
private IBillingService billing;
public FrontOfficeApiController(ApplicationDbContext context, IBillingService billing)
public FrontOfficeApiController(ApplicationDbContext context, IBillingService billing = null)
{
dbContext = context;
this.billing = billing;
this.billing = billing ?? new BillingService(context);
}
[HttpGet("profiles/{actCode}")]
@ -36,7 +37,7 @@ namespace Yavsc.ApiControllers
if (billing == null) return BadRequest();
billing.Status = QueryStatus.Rejected;
dbContext.SaveChanges();
dbContext.SaveChanges(User.GetUserId());
return Ok();
}
@ -48,7 +49,7 @@ namespace Yavsc.ApiControllers
var billing = BillingService.GetBillable(dbContext, billingCode, queryId);
if (billing == null) return BadRequest();
billing.Status = QueryStatus.Accepted;
dbContext.SaveChanges();
dbContext.SaveChanges(User.GetUserId());
return Ok();
}
}

View file

@ -82,19 +82,17 @@ public class HairCutQueryApiController : Controller
public async Task<IActionResult> PostQuery([FromBody] HairCutQuery query, CancellationToken cancellationToken)
{
var uid = User.GetUserId();
if (string.IsNullOrWhiteSpace(query.ClientId))
{
query.ClientId = uid;
}
query.ClientId = uid;
ModelState.Remove("Client");
ModelState.Remove("ClientId");
ModelState.Remove("UserCreated");
ModelState.Remove("UserModified");
ModelState.Remove("SelectedProfile");
ModelState.Remove("Prestation");
if (query.ClientId != uid && !User.IsInRole(Constants.AdminGroupName))
{
ModelState.AddModelError("ClientId", "You can only create your own HairCutQuery");
return BadRequest(ModelState);
}
ModelState.Remove("PerformerProfile");
ModelState.Remove("Context");
ModelState.Remove("Regularization");
query.Prestation = await _context.HairPrestation
.SingleOrDefaultAsync(p => p.Id == query.PrestationId, cancellationToken);

View file

@ -89,18 +89,16 @@ public class HairMultiCutQueryApiController : Controller
public async Task<IActionResult> PostQuery([FromBody] HairMultiCutQuery query, CancellationToken cancellationToken)
{
var uid = User.GetUserId();
if (string.IsNullOrWhiteSpace(query.ClientId))
{
query.ClientId = uid;
}
query.ClientId = uid;
ModelState.Remove("Client");
ModelState.Remove("ClientId");
if (query.ClientId != uid && !User.IsInRole(Constants.AdminGroupName))
{
ModelState.AddModelError("ClientId", "You can only create your own HairMultiCutQuery");
return BadRequest(ModelState);
}
ModelState.Remove("UserCreated");
ModelState.Remove("UserModified");
ModelState.Remove("SelectedProfile");
ModelState.Remove("PerformerProfile");
ModelState.Remove("Context");
ModelState.Remove("Regularization");
if (query.Prestations is null || query.Prestations.Count == 0)
{

View file

@ -65,18 +65,17 @@ public class RdvQueryApiController : Controller
public async Task<IActionResult> PostQuery([FromBody] RdvQuery query, CancellationToken cancellationToken)
{
var uid = User.GetUserId();
if (string.IsNullOrWhiteSpace(query.ClientId))
{
query.ClientId = uid;
}
// Security: the caller always posts for themselves.
query.ClientId = uid;
ModelState.Remove("Client");
ModelState.Remove("ClientId");
if (query.ClientId != uid && !User.IsInRole(Constants.AdminGroupName))
{
ModelState.AddModelError("ClientId", "You can only create your own RdvQuery");
return BadRequest(ModelState);
}
ModelState.Remove("UserCreated");
ModelState.Remove("UserModified");
ModelState.Remove("SelectedProfile");
ModelState.Remove("PerformerProfile");
ModelState.Remove("Context");
ModelState.Remove("Regularization");
if (!ModelState.IsValid)
{
@ -123,23 +122,44 @@ public class RdvQueryApiController : Controller
[HttpPut("{id}")]
public async Task<IActionResult> PutQuery([FromRoute] long id, [FromBody] RdvQuery query, CancellationToken cancellationToken)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (id != query.Id)
{
return BadRequest();
}
var uid = User.GetUserId();
if (query.ClientId != uid && !User.IsInRole(Constants.AdminGroupName))
var existing = await _context.RdvQueries
.Include(q => q.Location)
.SingleOrDefaultAsync(q => q.Id == id, cancellationToken);
if (existing is null)
{
return NotFound();
}
if (existing.ClientId != uid && !User.IsInRole(Constants.AdminGroupName))
{
return Forbid();
}
_context.Entry(query).State = EntityState.Modified;
existing.ActivityCode = query.ActivityCode;
existing.PerformerId = query.PerformerId;
existing.Consent = query.Consent;
existing.EventDate = query.EventDate;
existing.LocationType = query.LocationType;
existing.Reason = query.Reason;
existing.Status = query.Status;
existing.Provisional = query.Provisional;
if (query.Location is not null)
{
var resolvedLocation = await _context.Locations.FirstOrDefaultAsync(
x => x.Address == query.Location.Address
&& x.Longitude == query.Location.Longitude
&& x.Latitude == query.Location.Latitude,
cancellationToken);
existing.Location = resolvedLocation ?? query.Location;
if (resolvedLocation is null)
{
_context.Attach(query.Location);
}
}
try
{