using System.Text.Json; using JobTrackerApi.Data; using JobTrackerApi.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace JobTrackerApi.Controllers; [ApiController] [Route("api/career/evidence")] [Authorize(AuthenticationSchemes = "local")] public sealed class CareerEvidenceController(JobTrackerContext db, UserManager users) : ControllerBase { public sealed record EvidenceRequest(string? Category, string? Title, string? Statement, IReadOnlyList? Tags, string? SourceType, string? SourceReference, bool? IsVerified); public sealed record EvidenceDto(int Id, string Category, string Title, string Statement, IReadOnlyList Tags, string SourceType, string? SourceReference, bool IsVerified, DateTimeOffset UpdatedAtUtc); [HttpGet] public async Task>> List(CancellationToken ct) { var owner = await OwnerAsync(); if (owner is null) return Unauthorized(); var rows = await db.Set().AsNoTracking().Where(x => x.OwnerUserId == owner).OrderByDescending(x => x.UpdatedAtUtc).ToListAsync(ct); return Ok(rows.Select(ToDto)); } [HttpPost] public async Task> Create(EvidenceRequest request, CancellationToken ct) { var owner = await OwnerAsync(); if (owner is null) return Unauthorized(); var error = Validate(request); if (error is not null) return BadRequest(error); var row = new CareerEvidence { OwnerUserId = owner }; Apply(row, request); db.Add(row); await db.SaveChangesAsync(ct); return Ok(ToDto(row)); } [HttpPut("{id:int}")] public async Task> Update(int id, EvidenceRequest request, CancellationToken ct) { var owner = await OwnerAsync(); if (owner is null) return Unauthorized(); var row = await db.Set().FirstOrDefaultAsync(x => x.Id == id && x.OwnerUserId == owner, ct); if (row is null) return NotFound(); var error = Validate(request); if (error is not null) return BadRequest(error); Apply(row, request); await db.SaveChangesAsync(ct); return Ok(ToDto(row)); } [HttpDelete("{id:int}")] public async Task Delete(int id, CancellationToken ct) { var owner = await OwnerAsync(); if (owner is null) return Unauthorized(); var row = await db.Set().FirstOrDefaultAsync(x => x.Id == id && x.OwnerUserId == owner, ct); if (row is null) return NotFound(); db.Remove(row); await db.SaveChangesAsync(ct); return NoContent(); } private async Task OwnerAsync() => (await users.GetUserAsync(User))?.Id; private static string? Validate(EvidenceRequest value) => string.IsNullOrWhiteSpace(value.Title) ? "Title is required." : string.IsNullOrWhiteSpace(value.Statement) ? "Evidence is required." : null; private static void Apply(CareerEvidence row, EvidenceRequest value) { row.Category = Clean(value.Category, "achievement", 32); row.Title = Clean(value.Title, string.Empty, 255); row.Statement = value.Statement!.Trim(); row.TagsJson = JsonSerializer.Serialize((value.Tags ?? []).Select(x => x.Trim()).Where(x => x.Length > 0).Distinct(StringComparer.OrdinalIgnoreCase).Take(30)); row.SourceType = Clean(value.SourceType, "manual", 32); row.SourceReference = string.IsNullOrWhiteSpace(value.SourceReference) ? null : value.SourceReference.Trim()[..Math.Min(value.SourceReference.Trim().Length, 500)]; row.IsVerified = value.IsVerified ?? true; row.UpdatedAtUtc = DateTimeOffset.UtcNow; } private static string Clean(string? value, string fallback, int max) { var text = string.IsNullOrWhiteSpace(value) ? fallback : value.Trim(); return text[..Math.Min(text.Length, max)]; } private static EvidenceDto ToDto(CareerEvidence row) => new(row.Id, row.Category, row.Title, row.Statement, JsonSerializer.Deserialize>(row.TagsJson) ?? [], row.SourceType, row.SourceReference, row.IsVerified, row.UpdatedAtUtc); }