f0b9b222ff
Keep extraction heuristics out of manual save, version, and import paths so reviewed locations, URLs, dates, and languages round-trip unchanged.
76 lines
5.0 KiB
C#
76 lines
5.0 KiB
C#
using JobTrackerApi.Models;
|
|
|
|
namespace JobTrackerApi.Services;
|
|
|
|
// Phase 3 (validation): guards against abuse/DoS on the career-profile write path, NOT content
|
|
// completeness (that is CareerCompleteness's job — a WIP profile must always be savable). Enforces
|
|
// item counts and string lengths so a single PUT cannot store an unbounded blob. Returns an error
|
|
// string to reject, or null to accept.
|
|
public static class CareerProfileValidator
|
|
{
|
|
private const int MaxItemsPerSection = 200; // generous; a real CV has < 50
|
|
private const int MaxListEntries = 200; // bullets/skills/details per item
|
|
private const int MaxShortField = 500; // titles, names, single-line fields
|
|
private const int MaxLongField = 5000; // a single bullet/summary line
|
|
|
|
public static string? Validate(StructuredCvProfile p)
|
|
{
|
|
if (p.Jobs.Count > MaxItemsPerSection) return $"Too many experience entries (max {MaxItemsPerSection}).";
|
|
if (p.Education.Count > MaxItemsPerSection) return $"Too many education entries (max {MaxItemsPerSection}).";
|
|
if (p.Skills.Count > MaxItemsPerSection) return $"Too many skills (max {MaxItemsPerSection}).";
|
|
if (p.Projects.Count > MaxItemsPerSection) return $"Too many projects (max {MaxItemsPerSection}).";
|
|
if (p.Certifications.Count > MaxItemsPerSection) return $"Too many certifications (max {MaxItemsPerSection}).";
|
|
if (p.Languages.Count > MaxItemsPerSection) return $"Too many languages (max {MaxItemsPerSection}).";
|
|
if (p.Summary.Count > MaxListEntries) return $"Summary is too long (max {MaxListEntries} lines).";
|
|
|
|
foreach (var j in p.Jobs)
|
|
{
|
|
if (Over(j.Id, MaxShortField) || Over(j.Title, MaxShortField) || Over(j.Company, MaxShortField)
|
|
|| Over(j.Location, MaxShortField) || Over(j.Start, MaxShortField) || Over(j.End, MaxShortField))
|
|
return "An experience field exceeds the allowed length.";
|
|
if (j.Bullets.Count > MaxListEntries || j.Skills.Count > MaxListEntries) return "An experience has too many bullets/skills.";
|
|
if (j.Bullets.Any(b => Over(b, MaxLongField)) || j.Skills.Any(s => Over(s, MaxShortField))) return "An experience bullet or skill is too long.";
|
|
}
|
|
foreach (var e in p.Education)
|
|
{
|
|
if (Over(e.Id, MaxShortField) || Over(e.Qualification, MaxShortField) || Over(e.QualificationLevel, MaxShortField)
|
|
|| Over(e.Institution, MaxShortField) || Over(e.Location, MaxShortField)
|
|
|| Over(e.Start, MaxShortField) || Over(e.End, MaxShortField)) return "An education field exceeds the allowed length.";
|
|
if (e.Details.Count > MaxListEntries) return "An education entry has too many details.";
|
|
if (e.Details.Any(detail => Over(detail, MaxLongField))) return "An education detail is too long.";
|
|
}
|
|
foreach (var pr in p.Projects)
|
|
{
|
|
if (Over(pr.Id, MaxShortField) || Over(pr.Name, MaxShortField) || Over(pr.Role, MaxShortField)
|
|
|| Over(pr.Location, MaxShortField) || Over(pr.Start, MaxShortField) || Over(pr.End, MaxShortField)) return "A project field exceeds the allowed length.";
|
|
if (pr.Bullets.Count > MaxListEntries || pr.Skills.Count > MaxListEntries) return "A project has too many bullets/skills.";
|
|
if (pr.Bullets.Any(bullet => Over(bullet, MaxLongField)) || pr.Skills.Any(skill => Over(skill, MaxShortField))) return "A project bullet or skill is too long.";
|
|
}
|
|
foreach (var certification in p.Certifications)
|
|
{
|
|
if (Over(certification.Id, MaxShortField) || Over(certification.Name, MaxShortField)
|
|
|| Over(certification.Issuer, MaxShortField) || Over(certification.Location, MaxShortField)
|
|
|| Over(certification.Date, MaxShortField)) return "A certification field exceeds the allowed length.";
|
|
if (certification.Details.Count > MaxListEntries) return "A certification has too many details.";
|
|
if (certification.Details.Any(detail => Over(detail, MaxLongField))) return "A certification detail is too long.";
|
|
}
|
|
foreach (var language in p.Languages)
|
|
{
|
|
if (Over(language.Name, MaxShortField) || Over(language.Level, MaxShortField) || Over(language.Notes, MaxLongField))
|
|
return "A language field exceeds the allowed length.";
|
|
}
|
|
foreach (var s in p.Skills)
|
|
if (Over(s, MaxShortField)) return "A skill entry is too long.";
|
|
|
|
if (Over(p.Contact.FullName, MaxShortField) || Over(p.Contact.Email, MaxShortField)
|
|
|| Over(p.Contact.Headline, MaxShortField) || Over(p.Contact.Phone, MaxShortField)
|
|
|| Over(p.Contact.Location, MaxShortField) || Over(p.Contact.Website, MaxShortField)
|
|
|| Over(p.Contact.LinkedIn, MaxShortField))
|
|
return "A contact field exceeds the allowed length.";
|
|
|
|
return null;
|
|
}
|
|
|
|
private static bool Over(string? value, int max) => value is not null && value.Length > max;
|
|
}
|