Fix custom formula wizard bugs and add field name validation
- Fix Add Field blanking inputs: cfFields was IIFE-scoped so inline oninput handlers couldn't reach it; expose cfUpdateField on window - Fix ManualUnitPrice dropped in buildItemFromData: condition excluded isCustomFormulaItem, causing FixedRate items to reprice from scratch - Fix formula card missing on job pages: load CustomFormulaTemplates in PopulateJobItemDropDownsAsync so Details, EditItems, and Edit all get it; add customFormulaTemplates + formulaEvalUrl to Details and EditItems pageMeta - Add NCalc field name validation: client-side inline feedback (is-invalid + message on oninput) and pre-save sweep; server-side ValidateTemplateFields on Create and Update; rules: letter-start, letters/digits/underscores only, no duplicates, "rate" reserved Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3001,6 +3001,9 @@ public class CompanySettingsController : Controller
|
||||
if (!ModelState.IsValid)
|
||||
return Json(new { success = false, message = "Invalid data." });
|
||||
|
||||
var fieldError = ValidateTemplateFields(dto.FieldsJson);
|
||||
if (fieldError != null) return Json(new { success = false, message = fieldError });
|
||||
|
||||
var companyId = _tenantContext.GetCurrentCompanyId()!.Value;
|
||||
var entity = _mapper.Map<CustomItemTemplate>(dto);
|
||||
entity.CompanyId = companyId;
|
||||
@@ -3019,6 +3022,9 @@ public class CompanySettingsController : Controller
|
||||
if (!ModelState.IsValid)
|
||||
return Json(new { success = false, message = "Invalid data." });
|
||||
|
||||
var fieldError = ValidateTemplateFields(dto.FieldsJson);
|
||||
if (fieldError != null) return Json(new { success = false, message = fieldError });
|
||||
|
||||
var companyId = _tenantContext.GetCurrentCompanyId()!.Value;
|
||||
var entity = await _unitOfWork.CustomItemTemplates.GetByIdAsync(dto.Id);
|
||||
if (entity == null || entity.CompanyId != companyId)
|
||||
@@ -3136,6 +3142,44 @@ public class CompanySettingsController : Controller
|
||||
|
||||
return Json(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates field variable names in a fieldsJson array against NCalc identifier rules:
|
||||
/// must start with a letter, contain only letters/digits/underscores, and not use the
|
||||
/// reserved name "rate" (which is auto-populated from the template's Default Rate).
|
||||
/// Returns an error message string on failure, or null if all names are valid.
|
||||
/// </summary>
|
||||
private static string? ValidateTemplateFields(string? fieldsJson)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fieldsJson)) return null;
|
||||
|
||||
List<System.Text.Json.JsonElement>? fields;
|
||||
try
|
||||
{
|
||||
fields = System.Text.Json.JsonSerializer.Deserialize<List<System.Text.Json.JsonElement>>(fieldsJson);
|
||||
}
|
||||
catch { return "Invalid fields JSON."; }
|
||||
|
||||
if (fields == null) return null;
|
||||
|
||||
var nameRegex = new System.Text.RegularExpressions.Regex(@"^[a-zA-Z][a-zA-Z0-9_]*$");
|
||||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var field in fields)
|
||||
{
|
||||
var name = field.TryGetProperty("name", out var nameProp) ? nameProp.GetString() ?? "" : "";
|
||||
if (string.IsNullOrEmpty(name))
|
||||
return "All fields must have a variable name.";
|
||||
if (name == "rate")
|
||||
return $"\"rate\" is a reserved variable name — it is pre-populated from the template's Default Rate.";
|
||||
if (!nameRegex.IsMatch(name))
|
||||
return $"Invalid field name \"{name}\": must start with a letter and contain only letters, digits, or underscores.";
|
||||
if (!seen.Add(name))
|
||||
return $"Duplicate field name \"{name}\".";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public record SaveTemplateJsonRequest(int Id, string? Subject, string? Body);
|
||||
|
||||
Reference in New Issue
Block a user