feat: harden auth ops and deployment baseline
This commit is contained in:
@@ -12,7 +12,7 @@ public class BlogService(IBlogPostRepository repository)
|
||||
|
||||
public async Task<(IEnumerable<BlogPost>, int)> GetPublishedPagedAsync(
|
||||
int page, int pageSize, int? categoryId = null, CancellationToken ct = default) =>
|
||||
await repository.GetPublishedPagedAsync(page, pageSize, categoryId, ct);
|
||||
await repository.GetPublishedPagedAsync(NormalizePage(page), NormalizePageSize(pageSize), categoryId, ct);
|
||||
|
||||
public async Task<IEnumerable<BlogPost>> GetAllAsync(CancellationToken ct = default) =>
|
||||
await repository.GetAllForAdminAsync(ct);
|
||||
@@ -22,8 +22,11 @@ public class BlogService(IBlogPostRepository repository)
|
||||
|
||||
public async Task<int> CreateAsync(BlogPost post, CancellationToken ct = default)
|
||||
{
|
||||
post.Slug = GenerateSlug(post.Title);
|
||||
post.IsPublished = false;
|
||||
ValidatePost(post);
|
||||
post.Title = post.Title.Trim();
|
||||
post.Content = post.Content.Trim();
|
||||
post.Slug = await GenerateUniqueSlugAsync(post.Title, ct: ct);
|
||||
post.PublishedAt = post.IsPublished ? DateTime.UtcNow : null;
|
||||
return await repository.CreateAsync(post, ct);
|
||||
}
|
||||
|
||||
@@ -65,6 +68,10 @@ public class BlogService(IBlogPostRepository repository)
|
||||
post.SeoDescription = dto.SeoDescription;
|
||||
post.ThumbnailUrl = dto.ThumbnailUrl;
|
||||
post.IsPublished = dto.IsPublished;
|
||||
post.PublishedAt = dto.IsPublished
|
||||
? post.PublishedAt ?? DateTime.UtcNow
|
||||
: null;
|
||||
ValidatePost(post);
|
||||
|
||||
await UpdateAsync(post, ct);
|
||||
return post;
|
||||
@@ -81,6 +88,50 @@ public class BlogService(IBlogPostRepository repository)
|
||||
var slug = Regex.Replace(title.ToLowerInvariant(), @"[^\w\s-]", "");
|
||||
slug = Regex.Replace(slug, @"\s+", "-");
|
||||
slug = Regex.Replace(slug, @"-+", "-").Trim('-');
|
||||
if (string.IsNullOrWhiteSpace(slug))
|
||||
slug = $"post-{DateTime.UtcNow:yyyyMMddHHmmss}";
|
||||
return slug.Length > 100 ? slug[..100] : slug;
|
||||
}
|
||||
|
||||
private async Task<string> GenerateUniqueSlugAsync(string title, int? existingPostId = null, CancellationToken ct = default)
|
||||
{
|
||||
var baseSlug = GenerateSlug(title);
|
||||
var slug = baseSlug;
|
||||
var suffix = 2;
|
||||
var allPosts = (await repository.GetAllForAdminAsync(ct)).ToList();
|
||||
|
||||
while (allPosts.Any(x => x.Id != existingPostId && string.Equals(x.Slug, slug, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
var suffixText = $"-{suffix++}";
|
||||
var maxBaseLength = Math.Max(1, 100 - suffixText.Length);
|
||||
slug = $"{baseSlug[..Math.Min(baseSlug.Length, maxBaseLength)]}{suffixText}";
|
||||
}
|
||||
|
||||
return slug;
|
||||
}
|
||||
|
||||
private static void ValidatePost(BlogPost post)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(post.Title))
|
||||
throw new ValidationException("제목을 입력하세요.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(post.Content))
|
||||
throw new ValidationException("본문을 입력하세요.");
|
||||
|
||||
if (post.IsPublished && string.IsNullOrWhiteSpace(post.SeoTitle))
|
||||
throw new ValidationException("발행하려면 SEO 제목을 입력하세요.");
|
||||
|
||||
if (post.IsPublished && string.IsNullOrWhiteSpace(post.SeoDescription))
|
||||
throw new ValidationException("발행하려면 SEO 설명을 입력하세요.");
|
||||
}
|
||||
|
||||
private static int NormalizePage(int page) => Math.Max(1, page);
|
||||
|
||||
private static int NormalizePageSize(int pageSize) => Math.Clamp(pageSize, 1, 100);
|
||||
|
||||
public async Task<(int TotalPosts, int PublishedPosts)> GetStatsAsync(CancellationToken ct = default)
|
||||
{
|
||||
var posts = (await repository.GetAllForAdminAsync(ct)).ToList();
|
||||
return (posts.Count, posts.Count(x => x.IsPublished));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user