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();
}
}