Handler.cs 1.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
  1. using Application.Abstractions.Messaging;
  2. using Application.Abstractions.Data;
  3. using SharedKernel.Results;
  4. using Microsoft.EntityFrameworkCore;
  5. namespace Application.Features.Admin.NavMenu.Update;
  6. public sealed class Handler(IAppDbContext db) : ICommandHandler<Command, Result>
  7. {
  8. public async Task<Result> Handle(Command request, CancellationToken ct)
  9. {
  10. if (string.IsNullOrWhiteSpace(request.Label))
  11. {
  12. return Result.Failure(Error.Problem("NavMenu.LabelRequired", "메뉴 명은 필수입니다."));
  13. }
  14. if (string.IsNullOrWhiteSpace(request.Href))
  15. {
  16. return Result.Failure(Error.Problem("NavMenu.HrefRequired", "링크 주소는 필수입니다."));
  17. }
  18. var label = request.Label.Trim();
  19. var href = request.Href.Trim();
  20. if (label.Length > 50)
  21. {
  22. return Result.Failure(Error.Problem("NavMenu.LabelTooLong", "메뉴 명은 50자 이하여야 합니다."));
  23. }
  24. if (href.Length > 200 || !IsSafeHref(href))
  25. {
  26. return Result.Failure(Error.Problem("NavMenu.HrefInvalid", "링크는 200자 이하이며 / 또는 http(s):// 로 시작해야 합니다."));
  27. }
  28. var item = await db.NavMenuItem.FirstOrDefaultAsync(c => c.ID == request.ID, ct);
  29. if (item is null)
  30. {
  31. return Result.Failure(Error.NotFound("NavMenu.NotFound", "메뉴를 찾을 수 없습니다."));
  32. }
  33. item.Update(label, href, request.SortOrder, request.IsVisible);
  34. await db.SaveChangesAsync(ct);
  35. return Result.Success();
  36. }
  37. // 링크 스킴 허용목록 — 익명 렌더 XSS(javascript: 등) 차단. 내부 경로(/) 또는 http(s) 만 허용.
  38. private static bool IsSafeHref(string href)
  39. {
  40. return href.StartsWith('/') || href.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || href.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
  41. }
  42. }