using System.Security.Claims; using foodmarket.Domain.Common; using foodmarket.Infrastructure.Persistence; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace foodmarket.Api.Controllers.Common; /// Sprint 19: CRUD сохранённых пользовательских пресетов фильтров. /// Per-user внутри org: пользователь видит/правит только свои пресеты. /// Tenant-isolation — стандартный query-filter; per-user — здесь на уровне /// контроллера через WHERE UserId = currentUserId. [ApiController] [Authorize] [Route("api/user/presets")] public class UserPresetsController : ControllerBase { private readonly AppDbContext _db; public UserPresetsController(AppDbContext db) => _db = db; public record PresetDto(Guid Id, string PageKey, string Name, string ConfigJson, DateTime UpdatedAt); public record CreateRequest(string PageKey, string Name, string ConfigJson); public record UpdateRequest(string Name, string ConfigJson); private Guid? CurrentUserId() { var raw = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"); return Guid.TryParse(raw, out var id) ? id : null; } [HttpGet] public async Task>> List( [FromQuery] string? pageKey, CancellationToken ct) { var userId = CurrentUserId(); if (userId is null) return Forbid(); var q = _db.UserPresets.AsNoTracking().Where(p => p.UserId == userId); if (!string.IsNullOrWhiteSpace(pageKey)) q = q.Where(p => p.PageKey == pageKey); var items = await q.OrderBy(p => p.Name) .Select(p => new PresetDto(p.Id, p.PageKey, p.Name, p.ConfigJson, p.UpdatedAt)) .ToListAsync(ct); return items; } [HttpPost] public async Task> Create([FromBody] CreateRequest req, CancellationToken ct) { var userId = CurrentUserId(); if (userId is null) return Forbid(); if (string.IsNullOrWhiteSpace(req.PageKey)) return BadRequest(new { error = "pageKey обязателен." }); if (string.IsNullOrWhiteSpace(req.Name)) return BadRequest(new { error = "Имя пресета обязательно." }); // ConfigJson — schema-less, но должен быть валидным JSON // (требование jsonb-колонки). Пустые → "{}". var json = string.IsNullOrWhiteSpace(req.ConfigJson) ? "{}" : req.ConfigJson; try { System.Text.Json.JsonDocument.Parse(json); } catch { return BadRequest(new { error = "configJson должен быть валидным JSON." }); } var entity = new UserPreset { UserId = userId.Value, PageKey = req.PageKey.Trim(), Name = req.Name.Trim(), ConfigJson = json, UpdatedAt = DateTime.UtcNow, }; _db.UserPresets.Add(entity); try { await _db.SaveChangesAsync(ct); } catch (DbUpdateException ex) when (ex.InnerException?.Message.Contains("IX_user_presets_Org_User_PageKey_Name") == true) { return Conflict(new { error = "Пресет с таким именем уже существует на этой странице." }); } return new PresetDto(entity.Id, entity.PageKey, entity.Name, entity.ConfigJson, entity.UpdatedAt); } [HttpPut("{id:guid}")] public async Task> Update(Guid id, [FromBody] UpdateRequest req, CancellationToken ct) { var userId = CurrentUserId(); if (userId is null) return Forbid(); var e = await _db.UserPresets.FirstOrDefaultAsync(p => p.Id == id && p.UserId == userId, ct); if (e is null) return NotFound(); if (!string.IsNullOrWhiteSpace(req.Name)) e.Name = req.Name.Trim(); if (!string.IsNullOrWhiteSpace(req.ConfigJson)) { try { System.Text.Json.JsonDocument.Parse(req.ConfigJson); } catch { return BadRequest(new { error = "configJson должен быть валидным JSON." }); } e.ConfigJson = req.ConfigJson; } e.UpdatedAt = DateTime.UtcNow; try { await _db.SaveChangesAsync(ct); } catch (DbUpdateException ex) when (ex.InnerException?.Message.Contains("IX_user_presets_Org_User_PageKey_Name") == true) { return Conflict(new { error = "Пресет с таким именем уже существует на этой странице." }); } return new PresetDto(e.Id, e.PageKey, e.Name, e.ConfigJson, e.UpdatedAt); } [HttpDelete("{id:guid}")] public async Task Delete(Guid id, CancellationToken ct) { var userId = CurrentUserId(); if (userId is null) return Forbid(); var e = await _db.UserPresets.FirstOrDefaultAsync(p => p.Id == id && p.UserId == userId, ct); if (e is null) return NotFound(); _db.UserPresets.Remove(e); await _db.SaveChangesAsync(ct); return NoContent(); } }