| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253 |
- using Application.Abstractions.Messaging;
- using Application.Abstractions.Data;
- using SharedKernel.Results;
- using Microsoft.EntityFrameworkCore;
- namespace Application.Features.Admin.NavMenu.Update;
- public sealed class Handler(IAppDbContext db) : ICommandHandler<Command, Result>
- {
- public async Task<Result> Handle(Command request, CancellationToken ct)
- {
- if (string.IsNullOrWhiteSpace(request.Label))
- {
- return Result.Failure(Error.Problem("NavMenu.LabelRequired", "메뉴 명은 필수입니다."));
- }
- if (string.IsNullOrWhiteSpace(request.Href))
- {
- return Result.Failure(Error.Problem("NavMenu.HrefRequired", "링크 주소는 필수입니다."));
- }
- var label = request.Label.Trim();
- var href = request.Href.Trim();
- if (label.Length > 50)
- {
- return Result.Failure(Error.Problem("NavMenu.LabelTooLong", "메뉴 명은 50자 이하여야 합니다."));
- }
- if (href.Length > 200 || !IsSafeHref(href))
- {
- return Result.Failure(Error.Problem("NavMenu.HrefInvalid", "링크는 200자 이하이며 / 또는 http(s):// 로 시작해야 합니다."));
- }
- var item = await db.NavMenuItem.FirstOrDefaultAsync(c => c.ID == request.ID, ct);
- if (item is null)
- {
- return Result.Failure(Error.NotFound("NavMenu.NotFound", "메뉴를 찾을 수 없습니다."));
- }
- item.Update(label, href, request.SortOrder, request.IsVisible);
- await db.SaveChangesAsync(ct);
- return Result.Success();
- }
- // 링크 스킴 허용목록 — 익명 렌더 XSS(javascript: 등) 차단. 내부 경로(/) 또는 http(s) 만 허용.
- private static bool IsSafeHref(string href)
- {
- return href.StartsWith('/') || href.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || href.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
- }
- }
|