feat: V2 microservices stack — backend services, gateway, JWT auth

Add full V2 architecture: identity, content, studio (.NET 10) and file,
render, notification, gateway (Go) services with vendored deps, plus DB
migrations, event/API contracts, and an init-db script.

Wire the Next.js frontend to the gateway: server-side JWT auth routes
(login/register/refresh/logout/me), gateway fetch helper, and session/
cookie/jwt helpers under src/lib.

Containerize the stack via docker-compose.v2.yml and per-service
Dockerfiles. Base images resolve through a Nexus mirror (Docker Hub) and
MCR directly; npm/NuGet pull from Nexus groups. Self-host fonts via
next/font/local to avoid Google Fonts (geo-blocked).

Add CI workflow and ignore .env.v2, *.stackdump, and .NET bin/obj.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-05-29 23:29:31 +03:30
parent 53ea78a00d
commit 90ac0b81d1
7636 changed files with 3707504 additions and 240 deletions
+19
View File
@@ -0,0 +1,19 @@
# Build output — rebuilt inside container
**/bin/
**/obj/
# Local dev secrets
**/appsettings.Development.json
**/appsettings.*.Local.json
**/*.user
# IDE / OS
.vs/
.idea/
.DS_Store
Thumbs.db
# Docker files
Dockerfile
.dockerignore
docker-compose*.yml
+24
View File
@@ -0,0 +1,24 @@
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
EXPOSE 8080
# The .NET base image ships neither wget nor curl, which the container healthcheck needs.
# Copy a single static busybox binary named `wget` (busybox dispatches on argv[0]).
# This stays fully offline — no apt/network — matching the vendored Go builds.
COPY --from=busybox:1.36 /bin/busybox /usr/bin/wget
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
# Restore is its own cached layer: it only re-runs when the .csproj (deps) changes,
# not on every source edit. Critical here — NuGet restore is the slow step.
COPY NuGet.Config .
COPY ["FlatRender.ContentSvc/FlatRender.ContentSvc.csproj", "FlatRender.ContentSvc/"]
RUN dotnet restore "FlatRender.ContentSvc/FlatRender.ContentSvc.csproj"
COPY . .
# Single publish compiles + packages; --no-restore reuses the cached restore above.
RUN dotnet publish "FlatRender.ContentSvc/FlatRender.ContentSvc.csproj" \
-c Release -o /app/publish --no-restore /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "FlatRender.ContentSvc.dll"]
@@ -0,0 +1,3 @@
<Solution>
<Project Path="FlatRender.ContentSvc/FlatRender.ContentSvc.csproj" />
</Solution>
@@ -0,0 +1,279 @@
using FlatRender.ContentSvc.Domain.Entities;
using FlatRender.ContentSvc.Domain.Enums;
using FlatRender.ContentSvc.Infrastructure.Data;
using FlatRender.ContentSvc.Models.Requests;
using FlatRender.ContentSvc.Models.Responses;
using Microsoft.EntityFrameworkCore;
namespace FlatRender.ContentSvc.Application.Services;
public class CmsService(ContentDbContext db)
{
// ── Blogs ─────────────────────────────────────────────────────────────────
public async Task<PagedResponse<BlogSummaryResponse>> GetBlogsAsync(BlogListRequest req)
{
var kind = Enum.TryParse<BlogKind>(req.Kind, true, out var k) ? k : BlogKind.Blog;
var q = db.Blogs.Where(x => x.Kind == kind);
if (!string.IsNullOrWhiteSpace(req.Search))
q = q.Where(x => EF.Functions.ILike(x.Title, $"%{req.Search}%"));
if (req.IsPublished.HasValue)
q = q.Where(x => x.IsPublished == req.IsPublished);
var total = await q.LongCountAsync();
var items = await q.OrderByDescending(x => x.PublishDate ?? x.CreatedAt)
.Skip((req.Page - 1) * req.PageSize).Take(req.PageSize).ToListAsync();
return new PagedResponse<BlogSummaryResponse>(
items.Select(MapBlogSummary),
new PaginationMeta(req.Page, req.PageSize, total, (int)Math.Ceiling((double)total / req.PageSize))
);
}
public async Task<BlogDetailResponse> GetBlogBySlugAsync(string slug)
{
var blog = await db.Blogs.FirstOrDefaultAsync(x => x.Slug == slug)
?? throw new KeyNotFoundException($"Blog '{slug}' not found");
await db.Blogs.Where(x => x.Id == blog.Id)
.ExecuteUpdateAsync(s => s.SetProperty(x => x.ViewCount, x => x.ViewCount + 1));
return MapBlogDetail(blog);
}
public async Task<BlogDetailResponse> CreateBlogAsync(BlogListRequest _, CreateBlogRequest req, Guid? authorId)
{
var kind = Enum.TryParse<BlogKind>(req.Kind, true, out var k) ? k : BlogKind.Blog;
var blog = new Blog
{
TenantId = null, Kind = kind, Slug = req.Slug, Title = req.Title,
ShortDescription = req.ShortDescription, Content = req.Content,
MetaTitle = req.MetaTitle, MetaDescription = req.MetaDescription, MetaKeywords = req.MetaKeywords,
IncludeInSiteMap = req.IncludeInSiteMap, Image = req.Image, Cover = req.Cover,
AuthorUserId = authorId, AuthorDisplayName = req.AuthorDisplayName,
IsPublished = req.IsPublished, PublishDate = req.PublishDate
};
db.Blogs.Add(blog);
await db.SaveChangesAsync();
return MapBlogDetail(blog);
}
public async Task<BlogDetailResponse> UpdateBlogAsync(Guid id, UpdateBlogRequest req)
{
var blog = await db.Blogs.FindAsync(id)
?? throw new KeyNotFoundException($"Blog {id} not found");
blog.Slug = req.Slug; blog.Title = req.Title; blog.ShortDescription = req.ShortDescription;
blog.Content = req.Content; blog.MetaTitle = req.MetaTitle; blog.MetaDescription = req.MetaDescription;
blog.MetaKeywords = req.MetaKeywords; blog.IncludeInSiteMap = req.IncludeInSiteMap;
blog.Image = req.Image; blog.Cover = req.Cover; blog.AuthorDisplayName = req.AuthorDisplayName;
blog.IsPublished = req.IsPublished; blog.PublishDate = req.PublishDate;
blog.UpdatedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
return MapBlogDetail(blog);
}
public async Task DeleteBlogAsync(Guid id)
{
var blog = await db.Blogs.FindAsync(id) ?? throw new KeyNotFoundException($"Blog {id} not found");
blog.DeletedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
}
// ── Comments ──────────────────────────────────────────────────────────────
public async Task<PagedResponse<CommentResponse>> GetCommentsAsync(
int page, int pageSize, Guid? blogId, Guid? containerId, bool? isApproved)
{
var q = db.Comments.AsQueryable();
if (blogId.HasValue) q = q.Where(x => x.BlogId == blogId);
if (containerId.HasValue) q = q.Where(x => x.ContainerId == containerId);
if (isApproved.HasValue) q = q.Where(x => x.IsApproved == isApproved);
var total = await q.LongCountAsync();
var items = await q.OrderByDescending(x => x.CreatedAt).Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
return new PagedResponse<CommentResponse>(
items.Select(MapComment),
new PaginationMeta(page, pageSize, total, (int)Math.Ceiling((double)total / pageSize))
);
}
public async Task<CommentResponse> CreateCommentAsync(CreateCommentRequest req, Guid userId)
{
if (req.BlogId == null && req.ContainerId == null)
throw new ArgumentException("Either BlogId or ContainerId must be provided");
if (req.BlogId != null && req.ContainerId != null)
throw new ArgumentException("Only one of BlogId or ContainerId may be provided");
var comment = new Comment
{
UserId = userId, BlogId = req.BlogId, ContainerId = req.ContainerId,
ParentCommentId = req.ParentCommentId, Content = req.Content, Rate = req.Rate
};
db.Comments.Add(comment);
await db.SaveChangesAsync();
return MapComment(comment);
}
public async Task ApproveCommentAsync(Guid id, bool approve)
{
var comment = await db.Comments.FindAsync(id)
?? throw new KeyNotFoundException($"Comment {id} not found");
comment.IsApproved = approve;
comment.UpdatedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
}
public async Task DeleteCommentAsync(Guid id)
{
var comment = await db.Comments.FindAsync(id)
?? throw new KeyNotFoundException($"Comment {id} not found");
comment.DeletedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
}
// ── Slides ────────────────────────────────────────────────────────────────
public async Task<List<SlideResponse>> GetSlidesAsync(Guid? tenantId)
{
var q = db.NewSlides.Where(x => x.IsActive &&
(x.TenantId == null || x.TenantId == tenantId) &&
(x.ExpireDate == null || x.ExpireDate > DateTime.UtcNow));
return await q.OrderBy(x => x.Sort).Select(s => new SlideResponse(
s.Id, s.Keyword, s.Title, s.Image, s.Parameter, s.SlideType.ToString(),
s.ExpireDate, s.Sort, s.IsActive
)).ToListAsync();
}
public async Task<SlideResponse> CreateSlideAsync(CreateSlideRequest req)
{
if (!Enum.TryParse<SlideType>(req.SlideType, true, out var type))
throw new ArgumentException($"Invalid SlideType: {req.SlideType}");
var slide = new NewSlide
{
Keyword = req.Keyword, Title = req.Title, Image = req.Image,
Parameter = req.Parameter, SlideType = type, ExpireDate = req.ExpireDate,
Sort = req.Sort, IsActive = req.IsActive
};
db.NewSlides.Add(slide);
await db.SaveChangesAsync();
return new SlideResponse(slide.Id, slide.Keyword, slide.Title, slide.Image, slide.Parameter,
slide.SlideType.ToString(), slide.ExpireDate, slide.Sort, slide.IsActive);
}
public async Task DeleteSlideAsync(Guid id)
{
var slide = await db.NewSlides.FindAsync(id) ?? throw new KeyNotFoundException($"Slide {id} not found");
db.NewSlides.Remove(slide);
await db.SaveChangesAsync();
}
// ── Home Page Events ──────────────────────────────────────────────────────
public async Task<List<HomePageEventResponse>> GetHomePageEventsAsync(Guid? tenantId)
{
return await db.HomePageEvents
.Where(x => x.IsActive && (x.TenantId == null || x.TenantId == tenantId))
.OrderBy(x => x.Sort)
.Select(e => new HomePageEventResponse(
e.Id, e.Title, e.Subtitle, e.Description, e.Badge, e.BadgeClass,
e.ButtonText, e.ButtonUrl, e.ButtonClass, e.Color, e.BackgroundColor,
e.TextColor, e.Image, e.IsActive, e.Sort, e.StartsAt, e.EndsAt
)).ToListAsync();
}
// ── Website Settings ──────────────────────────────────────────────────────
public async Task<List<WebsiteSettingResponse>> GetSettingsAsync(Guid? tenantId, bool includeSecret = false)
{
var q = db.WebsiteSettings.Where(x => x.TenantId == tenantId);
if (!includeSecret) q = q.Where(x => !x.IsSecret);
return await q.Select(s => new WebsiteSettingResponse(s.Id, s.Key, s.Value, s.Description, s.IsSecret)).ToListAsync();
}
public async Task<WebsiteSettingResponse> UpsertSettingAsync(Guid? tenantId, UpsertWebsiteSettingRequest req)
{
var setting = await db.WebsiteSettings.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.Key == req.Key);
if (setting == null)
{
setting = new WebsiteSetting { TenantId = tenantId, Key = req.Key };
db.WebsiteSettings.Add(setting);
}
setting.Value = req.Value;
setting.Description = req.Description;
setting.IsSecret = req.IsSecret;
setting.UpdatedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
return new WebsiteSettingResponse(setting.Id, setting.Key, setting.Value, setting.Description, setting.IsSecret);
}
// ── Favorites ─────────────────────────────────────────────────────────────
public async Task<List<FavoriteFolderResponse>> GetFavoriteFoldersAsync(Guid userId)
{
return await db.FavoriteFolders
.Where(x => x.UserId == userId)
.Select(f => new FavoriteFolderResponse(
f.Id, f.Name, f.Description,
db.FavoriteContainers.Count(fc => fc.FolderId == f.Id),
f.CreatedAt
)).ToListAsync();
}
public async Task<FavoriteFolderResponse> CreateFavoriteFolderAsync(Guid userId, Guid tenantId, CreateFavoriteFolderRequest req)
{
var folder = new FavoriteFolder { UserId = userId, TenantId = tenantId, Name = req.Name, Description = req.Description };
db.FavoriteFolders.Add(folder);
await db.SaveChangesAsync();
return new FavoriteFolderResponse(folder.Id, folder.Name, folder.Description, 0, folder.CreatedAt);
}
public async Task DeleteFavoriteFolderAsync(Guid userId, Guid id)
{
var folder = await db.FavoriteFolders.FirstOrDefaultAsync(x => x.Id == id && x.UserId == userId)
?? throw new KeyNotFoundException($"Folder {id} not found");
db.FavoriteFolders.Remove(folder);
await db.SaveChangesAsync();
}
public async Task AddFavoriteContainerAsync(Guid userId, Guid tenantId, AddFavoriteContainerRequest req)
{
var exists = await db.FavoriteContainers.AnyAsync(x => x.UserId == userId && x.ContainerId == req.ContainerId);
if (exists) return;
db.FavoriteContainers.Add(new FavoriteContainer
{
UserId = userId, TenantId = tenantId, ContainerId = req.ContainerId,
FolderId = req.FolderId, Note = req.Note
});
await db.SaveChangesAsync();
}
public async Task RemoveFavoriteContainerAsync(Guid userId, Guid containerId)
{
var fav = await db.FavoriteContainers.FirstOrDefaultAsync(x => x.UserId == userId && x.ContainerId == containerId)
?? throw new KeyNotFoundException($"Favorite not found");
db.FavoriteContainers.Remove(fav);
await db.SaveChangesAsync();
}
// ── Mappers ───────────────────────────────────────────────────────────────
private static BlogSummaryResponse MapBlogSummary(Blog b) => new(
b.Id, b.Slug, b.Title, b.ShortDescription, b.Image, b.Cover,
b.AuthorDisplayName, b.IsPublished, b.PublishDate, b.ViewCount, b.CreatedAt
);
private static BlogDetailResponse MapBlogDetail(Blog b) => new(
b.Id, b.Slug, b.Title, b.ShortDescription, b.Content, b.MetaTitle, b.MetaDescription,
b.MetaKeywords, b.IncludeInSiteMap, b.Image, b.Cover, b.AuthorUserId, b.AuthorDisplayName,
b.IsPublished, b.PublishDate, b.ViewCount, b.CreatedAt, b.UpdatedAt
);
private static CommentResponse MapComment(Comment c) => new(
c.Id, c.UserId, c.BlogId, c.ContainerId, c.ParentCommentId,
c.Content, c.Rate, c.IsApproved, c.IsPinned, c.CreatedAt
);
}
@@ -0,0 +1,228 @@
using FlatRender.ContentSvc.Domain.Entities;
using FlatRender.ContentSvc.Infrastructure.Data;
using FlatRender.ContentSvc.Models.Requests;
using FlatRender.ContentSvc.Models.Responses;
using Microsoft.EntityFrameworkCore;
namespace FlatRender.ContentSvc.Application.Services;
public class TaxonomyService(ContentDbContext db)
{
public async Task<List<CategoryResponse>> GetCategoryTreeAsync()
{
var all = await db.Categories
.Where(x => x.ParentId == null)
.Include(x => x.Children)
.OrderBy(x => x.Sort)
.ToListAsync();
return all.Select(MapCategory).ToList();
}
public async Task<CategoryResponse> CreateCategoryAsync(CreateCategoryRequest req)
{
var cat = new Category
{
ParentId = req.ParentId,
Name = req.Name,
Slug = req.Slug,
Description = req.Description,
ImageUrl = req.ImageUrl,
Icon = req.Icon,
MetaTitle = req.MetaTitle,
MetaDescription = req.MetaDescription,
MetaKeywords = req.MetaKeywords,
BotFollow = req.BotFollow,
Sort = req.Sort,
IsActive = req.IsActive
};
db.Categories.Add(cat);
await db.SaveChangesAsync();
return MapCategory(cat);
}
public async Task<CategoryResponse> UpdateCategoryAsync(Guid id, UpdateCategoryRequest req)
{
var cat = await db.Categories.FindAsync(id)
?? throw new KeyNotFoundException($"Category {id} not found");
cat.ParentId = req.ParentId;
cat.Name = req.Name;
cat.Slug = req.Slug;
cat.Description = req.Description;
cat.ImageUrl = req.ImageUrl;
cat.Icon = req.Icon;
cat.MetaTitle = req.MetaTitle;
cat.MetaDescription = req.MetaDescription;
cat.MetaKeywords = req.MetaKeywords;
cat.BotFollow = req.BotFollow;
cat.Sort = req.Sort;
cat.IsActive = req.IsActive;
cat.UpdatedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
return MapCategory(cat);
}
public async Task DeleteCategoryAsync(Guid id)
{
var cat = await db.Categories.FindAsync(id)
?? throw new KeyNotFoundException($"Category {id} not found");
cat.DeletedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
}
public async Task<PagedResponse<TagResponse>> GetTagsAsync(int page, int pageSize, string? search)
{
var q = db.Tags.AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
q = q.Where(x => EF.Functions.ILike(x.Name, $"%{search}%"));
var total = await q.LongCountAsync();
var items = await q.OrderBy(x => x.Name).Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
return new PagedResponse<TagResponse>(
items.Select(t => new TagResponse(t.Id, t.Name, t.LatinName, t.Slug, t.AppliesToMode, t.IsActive)),
new PaginationMeta(page, pageSize, total, (int)Math.Ceiling((double)total / pageSize))
);
}
public async Task<TagResponse> CreateTagAsync(CreateTagRequest req)
{
var tag = new Tag
{
Name = req.Name,
LatinName = req.LatinName,
Slug = req.Slug,
AppliesToMode = req.AppliesToMode,
IsActive = req.IsActive
};
db.Tags.Add(tag);
await db.SaveChangesAsync();
return new TagResponse(tag.Id, tag.Name, tag.LatinName, tag.Slug, tag.AppliesToMode, tag.IsActive);
}
public async Task<TagResponse> UpdateTagAsync(Guid id, UpdateTagRequest req)
{
var tag = await db.Tags.FindAsync(id)
?? throw new KeyNotFoundException($"Tag {id} not found");
tag.Name = req.Name;
tag.LatinName = req.LatinName;
tag.Slug = req.Slug;
tag.AppliesToMode = req.AppliesToMode;
tag.IsActive = req.IsActive;
tag.UpdatedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
return new TagResponse(tag.Id, tag.Name, tag.LatinName, tag.Slug, tag.AppliesToMode, tag.IsActive);
}
public async Task DeleteTagAsync(Guid id)
{
var tag = await db.Tags.FindAsync(id)
?? throw new KeyNotFoundException($"Tag {id} not found");
tag.DeletedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
}
public async Task<PagedResponse<FontResponse>> GetFontsAsync(int page, int pageSize, string? search, string? direction)
{
var q = db.Fonts.AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
q = q.Where(x => EF.Functions.ILike(x.Name, $"%{search}%"));
if (!string.IsNullOrWhiteSpace(direction))
q = q.Where(x => x.Direction == direction);
var total = await q.LongCountAsync();
var items = await q.OrderBy(x => x.Sort).Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
return new PagedResponse<FontResponse>(
items.Select(MapFont),
new PaginationMeta(page, pageSize, total, (int)Math.Ceiling((double)total / pageSize))
);
}
public async Task<FontResponse> CreateFontAsync(CreateFontRequest req)
{
var font = new Font
{
Name = req.Name, OriginalName = req.OriginalName, SystemName = req.SystemName,
Family = req.Family, Weight = req.Weight, Style = req.Style, Direction = req.Direction,
FileUrl = req.FileUrl, SampleImageUrl = req.SampleImageUrl, IsPremium = req.IsPremium,
IsActive = req.IsActive, InstalledOnNodes = req.InstalledOnNodes, Sort = req.Sort
};
db.Fonts.Add(font);
await db.SaveChangesAsync();
return MapFont(font);
}
public async Task<FontResponse> UpdateFontAsync(Guid id, UpdateFontRequest req)
{
var font = await db.Fonts.FindAsync(id)
?? throw new KeyNotFoundException($"Font {id} not found");
font.Name = req.Name; font.OriginalName = req.OriginalName; font.SystemName = req.SystemName;
font.Family = req.Family; font.Weight = req.Weight; font.Style = req.Style; font.Direction = req.Direction;
font.FileUrl = req.FileUrl; font.SampleImageUrl = req.SampleImageUrl; font.IsPremium = req.IsPremium;
font.IsActive = req.IsActive; font.InstalledOnNodes = req.InstalledOnNodes; font.Sort = req.Sort;
font.UpdatedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
return MapFont(font);
}
public async Task DeleteFontAsync(Guid id)
{
var font = await db.Fonts.FindAsync(id)
?? throw new KeyNotFoundException($"Font {id} not found");
font.DeletedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
}
public async Task<PagedResponse<MusicTrackResponse>> GetMusicTracksAsync(int page, int pageSize, string? search)
{
var q = db.MusicTracks.AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
q = q.Where(x => EF.Functions.ILike(x.Name, $"%{search}%"));
var total = await q.LongCountAsync();
var items = await q.OrderBy(x => x.Sort).Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
return new PagedResponse<MusicTrackResponse>(
items.Select(MapMusic),
new PaginationMeta(page, pageSize, total, (int)Math.Ceiling((double)total / pageSize))
);
}
public async Task<MusicTrackResponse> CreateMusicTrackAsync(CreateMusicTrackRequest req)
{
var track = new MusicTrack
{
Name = req.Name, Caption = req.Caption, Keywords = req.Keywords, Url = req.Url,
WaveformData = req.WaveformData, DurationSec = req.DurationSec, Bpm = req.Bpm,
Genre = req.Genre, Mood = req.Mood, IsPremium = req.IsPremium, IsActive = req.IsActive, Sort = req.Sort
};
db.MusicTracks.Add(track);
await db.SaveChangesAsync();
return MapMusic(track);
}
public async Task DeleteMusicTrackAsync(Guid id)
{
var track = await db.MusicTracks.FindAsync(id)
?? throw new KeyNotFoundException($"MusicTrack {id} not found");
track.DeletedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
}
private static CategoryResponse MapCategory(Category c) => new(
c.Id, c.ParentId, c.Name, c.Slug, c.Description, c.ImageUrl, c.Icon,
c.IsActive, c.Sort, c.Children.Select(MapCategory).ToList()
);
private static FontResponse MapFont(Font f) => new(
f.Id, f.Name, f.OriginalName, f.SystemName, f.Family, f.Weight, f.Style,
f.Direction, f.FileUrl, f.SampleImageUrl, f.IsPremium, f.IsActive, f.InstalledOnNodes
);
private static MusicTrackResponse MapMusic(MusicTrack m) => new(
m.Id, m.Name, m.Caption, m.Url, m.WaveformData, m.DurationSec, m.Bpm, m.Genre, m.Mood, m.IsPremium
);
}
@@ -0,0 +1,294 @@
using FlatRender.ContentSvc.Domain.Entities;
using FlatRender.ContentSvc.Domain.Enums;
using FlatRender.ContentSvc.Infrastructure.Data;
using FlatRender.ContentSvc.Models.Requests;
using FlatRender.ContentSvc.Models.Responses;
using Microsoft.EntityFrameworkCore;
namespace FlatRender.ContentSvc.Application.Services;
public class TemplateService(ContentDbContext db)
{
public async Task<PagedResponse<ContainerSummaryResponse>> GetContainersAsync(ContainerListRequest req)
{
var q = db.ProjectContainers
.Include(x => x.ContainerCategories).ThenInclude(x => x.Category)
.Include(x => x.ContainerTags).ThenInclude(x => x.Tag)
.AsQueryable();
if (!string.IsNullOrWhiteSpace(req.Search))
q = q.Where(x => EF.Functions.ILike(x.Name, $"%{req.Search}%") ||
EF.Functions.ILike(x.Slug, $"%{req.Search}%"));
if (req.CategoryId.HasValue)
q = q.Where(x => x.ContainerCategories.Any(cc => cc.CategoryId == req.CategoryId));
if (!string.IsNullOrWhiteSpace(req.TagSlug))
q = q.Where(x => x.ContainerTags.Any(ct => ct.Tag.Slug == req.TagSlug));
if (req.IsPublished.HasValue)
q = q.Where(x => x.IsPublished == req.IsPublished);
if (req.IsPremium.HasValue)
q = q.Where(x => x.IsPremium == req.IsPremium);
if (!string.IsNullOrWhiteSpace(req.Mode))
{
if (Enum.TryParse<ChooseMode>(req.Mode, true, out var mode))
q = q.Where(x => x.PrimaryMode == mode);
}
q = req.Sort switch
{
"sort_asc" => q.OrderBy(x => x.Sort),
"name_asc" => q.OrderBy(x => x.Name),
"view_count_desc" => q.OrderByDescending(x => x.ViewCount),
_ => q.OrderByDescending(x => x.SortDate)
};
var total = await q.LongCountAsync();
var items = await q.Skip((req.Page - 1) * req.PageSize).Take(req.PageSize).ToListAsync();
return new PagedResponse<ContainerSummaryResponse>(
items.Select(MapContainerSummary),
new PaginationMeta(req.Page, req.PageSize, total, (int)Math.Ceiling((double)total / req.PageSize))
);
}
public async Task<ContainerDetailResponse> GetContainerBySlugAsync(string slug)
{
var container = await db.ProjectContainers
.Include(x => x.ContainerCategories).ThenInclude(x => x.Category)
.Include(x => x.ContainerTags).ThenInclude(x => x.Tag)
.Include(x => x.Projects.Where(p => p.DeletedAt == null))
.FirstOrDefaultAsync(x => x.Slug == slug)
?? throw new KeyNotFoundException($"Container '{slug}' not found");
return MapContainerDetail(container);
}
public async Task<ContainerDetailResponse> GetContainerByIdAsync(Guid id)
{
var container = await db.ProjectContainers
.Include(x => x.ContainerCategories).ThenInclude(x => x.Category)
.Include(x => x.ContainerTags).ThenInclude(x => x.Tag)
.Include(x => x.Projects.Where(p => p.DeletedAt == null))
.FirstOrDefaultAsync(x => x.Id == id)
?? throw new KeyNotFoundException($"Container {id} not found");
return MapContainerDetail(container);
}
public async Task<ContainerDetailResponse> CreateContainerAsync(CreateContainerRequest req)
{
if (!Enum.TryParse<ChooseMode>(req.PrimaryMode, true, out var mode))
throw new ArgumentException($"Invalid PrimaryMode: {req.PrimaryMode}");
var container = new ProjectContainer
{
Slug = req.Slug, Name = req.Name, Description = req.Description, Keywords = req.Keywords,
NewsText = req.NewsText, Image = req.Image, Demo = req.Demo, FullDemo = req.FullDemo,
MiniDemo = req.MiniDemo, DemoScriptTag = req.DemoScriptTag, IsPublished = req.IsPublished,
IsPremium = req.IsPremium, IsMockup = req.IsMockup, PrimaryMode = mode, Sort = req.Sort,
SortDate = DateTime.UtcNow
};
db.ProjectContainers.Add(container);
await db.SaveChangesAsync();
await SyncContainerCategoriesAsync(container.Id, req.CategoryIds);
await SyncContainerTagsAsync(container.Id, req.TagIds);
return await GetContainerByIdAsync(container.Id);
}
public async Task<ContainerDetailResponse> UpdateContainerAsync(Guid id, UpdateContainerRequest req)
{
var container = await db.ProjectContainers.FindAsync(id)
?? throw new KeyNotFoundException($"Container {id} not found");
if (!Enum.TryParse<ChooseMode>(req.PrimaryMode, true, out var mode))
throw new ArgumentException($"Invalid PrimaryMode: {req.PrimaryMode}");
container.Slug = req.Slug; container.Name = req.Name; container.Description = req.Description;
container.Keywords = req.Keywords; container.NewsText = req.NewsText; container.Image = req.Image;
container.Demo = req.Demo; container.FullDemo = req.FullDemo; container.MiniDemo = req.MiniDemo;
container.DemoScriptTag = req.DemoScriptTag; container.IsPublished = req.IsPublished;
container.IsPremium = req.IsPremium; container.IsMockup = req.IsMockup;
container.PrimaryMode = mode; container.Sort = req.Sort; container.UpdatedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
await SyncContainerCategoriesAsync(id, req.CategoryIds);
await SyncContainerTagsAsync(id, req.TagIds);
return await GetContainerByIdAsync(id);
}
public async Task DeleteContainerAsync(Guid id)
{
var container = await db.ProjectContainers.FindAsync(id)
?? throw new KeyNotFoundException($"Container {id} not found");
container.DeletedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
}
public async Task<ProjectDetailResponse> GetProjectDetailAsync(Guid id)
{
var project = await db.Projects
.Include(x => x.Scenes.Where(s => s.DeletedAt == null)).ThenInclude(x => x.RepeaterItems)
.Include(x => x.Scenes).ThenInclude(x => x.ContentElements)
.Include(x => x.Scenes).ThenInclude(x => x.ColorElements)
.Include(x => x.Scenes).ThenInclude(x => x.ColorPresets).ThenInclude(x => x.Items)
.Include(x => x.Scenes).ThenInclude(x => x.Characters).ThenInclude(x => x.Controllers).ThenInclude(x => x.Options)
.Include(x => x.SharedColors)
.Include(x => x.SharedLayers)
.FirstOrDefaultAsync(x => x.Id == id)
?? throw new KeyNotFoundException($"Project {id} not found");
return MapProjectDetail(project);
}
public async Task<ProjectDetailResponse> CreateProjectAsync(CreateProjectRequest req)
{
if (!Enum.TryParse<ChooseMode>(req.ChooseMode, true, out var chooseMode))
throw new ArgumentException($"Invalid ChooseMode: {req.ChooseMode}");
if (!Enum.TryParse<ResolutionKind>(req.Resolution, true, out var resolution))
throw new ArgumentException($"Invalid Resolution: {req.Resolution}");
var project = new Project
{
ContainerId = req.ContainerId, ProjectServerId = req.ProjectServerId, Name = req.Name,
Description = req.Description, Image = req.Image, FullDemo = req.FullDemo,
DemoScriptTag = req.DemoScriptTag, DownloadLink = req.DownloadLink, Folder = req.Folder,
OriginalWidth = req.OriginalWidth, OriginalHeight = req.OriginalHeight, Aspect = req.Aspect,
ProjectDurationSec = req.ProjectDurationSec, MinDurationSec = req.MinDurationSec,
MaxDurationSec = req.MaxDurationSec, FreeFps = req.FreeFps, ChooseMode = chooseMode,
Resolution = resolution, VipFactor = req.VipFactor, RenderAepComp = req.RenderAepComp,
IsPublished = req.IsPublished, Sort = req.Sort
};
db.Projects.Add(project);
await db.SaveChangesAsync();
return await GetProjectDetailAsync(project.Id);
}
public async Task<ProjectDetailResponse> UpdateProjectAsync(Guid id, UpdateProjectRequest req)
{
var project = await db.Projects.FindAsync(id)
?? throw new KeyNotFoundException($"Project {id} not found");
if (!Enum.TryParse<ChooseMode>(req.ChooseMode, true, out var chooseMode))
throw new ArgumentException($"Invalid ChooseMode: {req.ChooseMode}");
if (!Enum.TryParse<ResolutionKind>(req.Resolution, true, out var resolution))
throw new ArgumentException($"Invalid Resolution: {req.Resolution}");
project.Name = req.Name; project.Description = req.Description; project.Image = req.Image;
project.FullDemo = req.FullDemo; project.DemoScriptTag = req.DemoScriptTag;
project.DownloadLink = req.DownloadLink; project.Folder = req.Folder;
project.OriginalWidth = req.OriginalWidth; project.OriginalHeight = req.OriginalHeight;
project.Aspect = req.Aspect; project.ProjectDurationSec = req.ProjectDurationSec;
project.MinDurationSec = req.MinDurationSec; project.MaxDurationSec = req.MaxDurationSec;
project.FreeFps = req.FreeFps; project.ChooseMode = chooseMode; project.Resolution = resolution;
project.VipFactor = req.VipFactor; project.RenderAepComp = req.RenderAepComp;
project.SharedLayerImage = req.SharedLayerImage; project.SharedColorsSvg = req.SharedColorsSvg;
project.SharedColorPresetsSvg = req.SharedColorPresetsSvg;
project.IsPublished = req.IsPublished; project.Sort = req.Sort;
project.UpdatedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
return await GetProjectDetailAsync(project.Id);
}
public async Task DeleteProjectAsync(Guid id)
{
var project = await db.Projects.FindAsync(id)
?? throw new KeyNotFoundException($"Project {id} not found");
project.DeletedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
}
public async Task IncrementContainerViewAsync(Guid id)
{
await db.ProjectContainers
.Where(x => x.Id == id)
.ExecuteUpdateAsync(s => s.SetProperty(x => x.ViewCount, x => x.ViewCount + 1));
}
// ── Helpers ───────────────────────────────────────────────────────────────
private async Task SyncContainerCategoriesAsync(Guid containerId, List<Guid> categoryIds)
{
var existing = await db.ContainerCategories.Where(x => x.ContainerId == containerId).ToListAsync();
db.ContainerCategories.RemoveRange(existing);
for (int i = 0; i < categoryIds.Count; i++)
db.ContainerCategories.Add(new ContainerCategory { ContainerId = containerId, CategoryId = categoryIds[i], Sort = i });
await db.SaveChangesAsync();
}
private async Task SyncContainerTagsAsync(Guid containerId, List<Guid> tagIds)
{
var existing = await db.ContainerTags.Where(x => x.ContainerId == containerId).ToListAsync();
db.ContainerTags.RemoveRange(existing);
foreach (var tagId in tagIds)
db.ContainerTags.Add(new ContainerTag { ContainerId = containerId, TagId = tagId });
await db.SaveChangesAsync();
}
private static ContainerSummaryResponse MapContainerSummary(ProjectContainer c) => new(
c.Id, c.Slug, c.Name, c.Description, c.Image, c.Demo, c.MiniDemo,
c.IsPublished, c.IsPremium, c.IsMockup, c.PrimaryMode.ToString(),
c.RateAvg, c.RateCount, c.ViewCount, c.UseCount, c.Sort, c.SortDate,
c.ContainerCategories.Select(cc => cc.Category.Slug).ToList(),
c.ContainerTags.Select(ct => ct.Tag.Name).ToList()
);
private static ContainerDetailResponse MapContainerDetail(ProjectContainer c) => new(
c.Id, c.Slug, c.Name, c.Description, c.Keywords, c.NewsText,
c.Image, c.Demo, c.FullDemo, c.MiniDemo, c.DemoScriptTag,
c.IsPublished, c.IsPremium, c.IsMockup, c.PrimaryMode.ToString(),
c.RateAvg, c.RateCount, c.ViewCount, c.UseCount, c.Sort, c.SortDate,
c.Projects.Select(MapProject).ToList(),
c.ContainerCategories.Select(cc => MapCategoryFlat(cc.Category)).ToList(),
c.ContainerTags.Select(ct => new TagResponse(ct.Tag.Id, ct.Tag.Name, ct.Tag.LatinName, ct.Tag.Slug, ct.Tag.AppliesToMode, ct.Tag.IsActive)).ToList()
);
private static CategoryResponse MapCategoryFlat(Category c) => new(
c.Id, c.ParentId, c.Name, c.Slug, c.Description, c.ImageUrl, c.Icon,
c.IsActive, c.Sort, []
);
private static ProjectResponse MapProject(Project p) => new(
p.Id, p.ContainerId, p.Name, p.Image, p.FullDemo,
p.OriginalWidth, p.OriginalHeight, p.Aspect,
p.ProjectDurationSec, p.MinDurationSec, p.MaxDurationSec,
p.FreeFps, p.ChooseMode.ToString(), p.Resolution.ToString(),
p.IsPublished, p.Sort
);
private static ProjectDetailResponse MapProjectDetail(Project p) => new(
p.Id, p.ContainerId, p.Name, p.Description, p.Image, p.FullDemo, p.DemoScriptTag, p.DownloadLink,
p.OriginalWidth, p.OriginalHeight, p.Aspect,
p.ProjectDurationSec, p.MinDurationSec, p.MaxDurationSec,
p.FreeFps, p.ChooseMode.ToString(), p.Resolution.ToString(), p.VipFactor, p.RenderAepComp,
p.SharedLayerImage, p.IsPublished, p.Sort,
p.Scenes.Select(MapScene).ToList(),
p.SharedColors.Select(sc => new SharedColorResponse(sc.Id, sc.ElementKey, sc.Title, sc.Icon, sc.AttrValue.ToString(), sc.DefaultColor, sc.Sort)).ToList(),
p.SharedLayers.Select(MapSharedLayer).ToList()
);
private static SceneResponse MapScene(Scene s) => new(
s.Id, s.ProjectId, s.Key, s.Title, s.LocalizedTitle, s.SceneType.ToString(),
s.Image, s.Demo, s.SnapshotUrl, s.GenerateKf,
s.DefaultDurationSec, s.MinDurationSec, s.MaxDurationSec, s.OverlapAtEndSec,
s.CanHandleDuration, s.ManualColorSelection, s.Sort, s.IsActive
);
private static SharedLayerResponse MapSharedLayer(SharedLayer sl) => new(
sl.Id, sl.Key, sl.Title, sl.LocalizedTitle, sl.Hint, sl.Type.ToString(), sl.DefaultValue,
sl.FontId, sl.FontFace, sl.FontSize, sl.IsFontChangeable, sl.IsFontSizeChangeable,
sl.Justify.ToString(), sl.CanJustify, sl.PositionInContainer, sl.IsTextBox, sl.MaxSize,
sl.VideoSupport, sl.MinDurationSec, sl.MaxDurationSec, sl.Width, sl.Height,
sl.MappedList, sl.AiInputType.ToString(), sl.IsHidden, sl.IsFocused,
sl.VirtualCount, sl.Sort
);
}
@@ -0,0 +1,165 @@
using System.Security.Claims;
using FlatRender.ContentSvc.Application.Services;
using FlatRender.ContentSvc.Models.Requests;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace FlatRender.ContentSvc.Controllers;
[ApiController]
[Route("v1/blogs")]
public class BlogsController(CmsService svc) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> List([FromQuery] BlogListRequest req) =>
Ok(await svc.GetBlogsAsync(req));
[HttpGet("{slug}")]
public async Task<IActionResult> Get(string slug) =>
Ok(await svc.GetBlogBySlugAsync(slug));
[Authorize(Roles = "Admin")]
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateBlogRequest req)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier) is { } s ? Guid.Parse(s) : (Guid?)null;
return Ok(await svc.CreateBlogAsync(new BlogListRequest(), req, userId));
}
[Authorize(Roles = "Admin")]
[HttpPut("{id:guid}")]
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateBlogRequest req) =>
Ok(await svc.UpdateBlogAsync(id, req));
[Authorize(Roles = "Admin")]
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id)
{
await svc.DeleteBlogAsync(id);
return NoContent();
}
}
[ApiController]
[Route("v1/comments")]
public class CommentsController(CmsService svc) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> List(
[FromQuery] int page = 1, [FromQuery] int pageSize = 20,
[FromQuery] Guid? blogId = null, [FromQuery] Guid? containerId = null,
[FromQuery] bool? isApproved = null) =>
Ok(await svc.GetCommentsAsync(page, pageSize, blogId, containerId, isApproved));
[Authorize]
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateCommentRequest req)
{
var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
return Ok(await svc.CreateCommentAsync(req, userId));
}
[Authorize(Roles = "Admin")]
[HttpPatch("{id:guid}/approve")]
public async Task<IActionResult> Approve(Guid id, [FromQuery] bool approve = true)
{
await svc.ApproveCommentAsync(id, approve);
return NoContent();
}
[Authorize(Roles = "Admin")]
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id)
{
await svc.DeleteCommentAsync(id);
return NoContent();
}
}
[ApiController]
[Route("v1/slides")]
public class SlidesController(CmsService svc) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> Get([FromQuery] Guid? tenantId = null) =>
Ok(await svc.GetSlidesAsync(tenantId));
[Authorize(Roles = "Admin")]
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateSlideRequest req) =>
Ok(await svc.CreateSlideAsync(req));
[Authorize(Roles = "Admin")]
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id)
{
await svc.DeleteSlideAsync(id);
return NoContent();
}
}
[ApiController]
[Route("v1/home-events")]
public class HomePageEventsController(CmsService svc) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> Get([FromQuery] Guid? tenantId = null) =>
Ok(await svc.GetHomePageEventsAsync(tenantId));
}
[ApiController]
[Route("v1/settings")]
public class WebsiteSettingsController(CmsService svc) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> Get([FromQuery] Guid? tenantId = null) =>
Ok(await svc.GetSettingsAsync(tenantId, includeSecret: false));
[Authorize(Roles = "Admin")]
[HttpGet("all")]
public async Task<IActionResult> GetAll([FromQuery] Guid? tenantId = null) =>
Ok(await svc.GetSettingsAsync(tenantId, includeSecret: true));
[Authorize(Roles = "Admin")]
[HttpPut]
public async Task<IActionResult> Upsert([FromQuery] Guid? tenantId, [FromBody] UpsertWebsiteSettingRequest req) =>
Ok(await svc.UpsertSettingAsync(tenantId, req));
}
[ApiController]
[Route("v1/favorites")]
[Authorize]
public class FavoritesController(CmsService svc) : ControllerBase
{
private Guid UserId => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
private Guid TenantId => Guid.Parse(User.FindFirstValue("tenant_id") ?? "00000000-0000-0000-0000-000000000001");
[HttpGet("folders")]
public async Task<IActionResult> GetFolders() =>
Ok(await svc.GetFavoriteFoldersAsync(UserId));
[HttpPost("folders")]
public async Task<IActionResult> CreateFolder([FromBody] CreateFavoriteFolderRequest req) =>
Ok(await svc.CreateFavoriteFolderAsync(UserId, TenantId, req));
[HttpDelete("folders/{id:guid}")]
public async Task<IActionResult> DeleteFolder(Guid id)
{
await svc.DeleteFavoriteFolderAsync(UserId, id);
return NoContent();
}
[HttpPost("containers")]
public async Task<IActionResult> AddContainer([FromBody] AddFavoriteContainerRequest req)
{
await svc.AddFavoriteContainerAsync(UserId, TenantId, req);
return NoContent();
}
[HttpDelete("containers/{containerId:guid}")]
public async Task<IActionResult> RemoveContainer(Guid containerId)
{
await svc.RemoveFavoriteContainerAsync(UserId, containerId);
return NoContent();
}
}
@@ -0,0 +1,108 @@
using FlatRender.ContentSvc.Application.Services;
using FlatRender.ContentSvc.Models.Requests;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace FlatRender.ContentSvc.Controllers;
[ApiController]
[Route("v1")]
public class TaxonomyController(TaxonomyService svc) : ControllerBase
{
// ── Categories ────────────────────────────────────────────────────────────
[HttpGet("categories")]
public async Task<IActionResult> GetCategories() =>
Ok(await svc.GetCategoryTreeAsync());
[Authorize(Roles = "Admin")]
[HttpPost("categories")]
public async Task<IActionResult> CreateCategory([FromBody] CreateCategoryRequest req) =>
Ok(await svc.CreateCategoryAsync(req));
[Authorize(Roles = "Admin")]
[HttpPut("categories/{id:guid}")]
public async Task<IActionResult> UpdateCategory(Guid id, [FromBody] UpdateCategoryRequest req) =>
Ok(await svc.UpdateCategoryAsync(id, req));
[Authorize(Roles = "Admin")]
[HttpDelete("categories/{id:guid}")]
public async Task<IActionResult> DeleteCategory(Guid id)
{
await svc.DeleteCategoryAsync(id);
return NoContent();
}
// ── Tags ──────────────────────────────────────────────────────────────────
[HttpGet("tags")]
public async Task<IActionResult> GetTags(
[FromQuery] int page = 1, [FromQuery] int pageSize = 50,
[FromQuery] string? search = null) =>
Ok(await svc.GetTagsAsync(page, pageSize, search));
[Authorize(Roles = "Admin")]
[HttpPost("tags")]
public async Task<IActionResult> CreateTag([FromBody] CreateTagRequest req) =>
Ok(await svc.CreateTagAsync(req));
[Authorize(Roles = "Admin")]
[HttpPut("tags/{id:guid}")]
public async Task<IActionResult> UpdateTag(Guid id, [FromBody] UpdateTagRequest req) =>
Ok(await svc.UpdateTagAsync(id, req));
[Authorize(Roles = "Admin")]
[HttpDelete("tags/{id:guid}")]
public async Task<IActionResult> DeleteTag(Guid id)
{
await svc.DeleteTagAsync(id);
return NoContent();
}
// ── Fonts ─────────────────────────────────────────────────────────────────
[HttpGet("fonts")]
public async Task<IActionResult> GetFonts(
[FromQuery] int page = 1, [FromQuery] int pageSize = 50,
[FromQuery] string? search = null, [FromQuery] string? direction = null) =>
Ok(await svc.GetFontsAsync(page, pageSize, search, direction));
[Authorize(Roles = "Admin")]
[HttpPost("fonts")]
public async Task<IActionResult> CreateFont([FromBody] CreateFontRequest req) =>
Ok(await svc.CreateFontAsync(req));
[Authorize(Roles = "Admin")]
[HttpPut("fonts/{id:guid}")]
public async Task<IActionResult> UpdateFont(Guid id, [FromBody] UpdateFontRequest req) =>
Ok(await svc.UpdateFontAsync(id, req));
[Authorize(Roles = "Admin")]
[HttpDelete("fonts/{id:guid}")]
public async Task<IActionResult> DeleteFont(Guid id)
{
await svc.DeleteFontAsync(id);
return NoContent();
}
// ── Music Tracks ──────────────────────────────────────────────────────────
[HttpGet("music")]
public async Task<IActionResult> GetMusicTracks(
[FromQuery] int page = 1, [FromQuery] int pageSize = 50,
[FromQuery] string? search = null) =>
Ok(await svc.GetMusicTracksAsync(page, pageSize, search));
[Authorize(Roles = "Admin")]
[HttpPost("music")]
public async Task<IActionResult> CreateMusicTrack([FromBody] CreateMusicTrackRequest req) =>
Ok(await svc.CreateMusicTrackAsync(req));
[Authorize(Roles = "Admin")]
[HttpDelete("music/{id:guid}")]
public async Task<IActionResult> DeleteMusicTrack(Guid id)
{
await svc.DeleteMusicTrackAsync(id);
return NoContent();
}
}
@@ -0,0 +1,70 @@
using FlatRender.ContentSvc.Application.Services;
using FlatRender.ContentSvc.Models.Requests;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace FlatRender.ContentSvc.Controllers;
[ApiController]
[Route("v1/templates")]
public class TemplatesController(TemplateService svc) : ControllerBase
{
// ── Containers ────────────────────────────────────────────────────────────
[HttpGet]
public async Task<IActionResult> ListContainers([FromQuery] ContainerListRequest req) =>
Ok(await svc.GetContainersAsync(req));
[HttpGet("{slug}")]
public async Task<IActionResult> GetContainer(string slug)
{
await svc.IncrementContainerViewAsync(
(await svc.GetContainerBySlugAsync(slug)).Id);
return Ok(await svc.GetContainerBySlugAsync(slug));
}
[Authorize(Roles = "Admin")]
[HttpPost]
public async Task<IActionResult> CreateContainer([FromBody] CreateContainerRequest req) =>
Ok(await svc.CreateContainerAsync(req));
[Authorize(Roles = "Admin")]
[HttpPut("{id:guid}")]
public async Task<IActionResult> UpdateContainer(Guid id, [FromBody] UpdateContainerRequest req) =>
Ok(await svc.UpdateContainerAsync(id, req));
[Authorize(Roles = "Admin")]
[HttpDelete("{id:guid}")]
public async Task<IActionResult> DeleteContainer(Guid id)
{
await svc.DeleteContainerAsync(id);
return NoContent();
}
}
[ApiController]
[Route("v1/projects")]
public class ProjectsController(TemplateService svc) : ControllerBase
{
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetProject(Guid id) =>
Ok(await svc.GetProjectDetailAsync(id));
[Authorize(Roles = "Admin")]
[HttpPost]
public async Task<IActionResult> CreateProject([FromBody] CreateProjectRequest req) =>
Ok(await svc.CreateProjectAsync(req));
[Authorize(Roles = "Admin")]
[HttpPut("{id:guid}")]
public async Task<IActionResult> UpdateProject(Guid id, [FromBody] UpdateProjectRequest req) =>
Ok(await svc.UpdateProjectAsync(id, req));
[Authorize(Roles = "Admin")]
[HttpDelete("{id:guid}")]
public async Task<IActionResult> DeleteProject(Guid id)
{
await svc.DeleteProjectAsync(id);
return NoContent();
}
}
@@ -0,0 +1,142 @@
namespace FlatRender.ContentSvc.Domain.Entities;
public class SceneCharacter
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid SceneId { get; set; }
public Scene Scene { get; set; } = default!;
public string Key { get; set; } = default!;
public string Name { get; set; } = default!;
public string? Icon { get; set; }
public int Sort { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public ICollection<SceneCharacterController> Controllers { get; set; } = [];
}
public class SceneCharacterController
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid SceneCharacterId { get; set; }
public SceneCharacter Character { get; set; } = default!;
public string Name { get; set; } = default!;
public string Key { get; set; } = default!;
public string? DefaultValue { get; set; }
public int Sort { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public ICollection<SceneControllerOption> Options { get; set; } = [];
}
public class SceneControllerOption
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid ControllerId { get; set; }
public SceneCharacterController Controller { get; set; } = default!;
public string Name { get; set; } = default!;
public string? Icon { get; set; }
public string Value { get; set; } = default!;
public int Sort { get; set; }
}
public class ProjectCharacterController
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid ProjectId { get; set; }
public Project Project { get; set; } = default!;
public string Name { get; set; } = default!;
public string Key { get; set; } = default!;
public int Sort { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public ICollection<ProjectCharacterControllerOption> Options { get; set; } = [];
}
public class ProjectCharacterControllerOption
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid ControllerId { get; set; }
public ProjectCharacterController Controller { get; set; } = default!;
public string Name { get; set; } = default!;
public string? Icon { get; set; }
public string Value { get; set; } = default!;
public int Sort { get; set; }
}
public class ProjectCharacterPreset
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid ProjectId { get; set; }
public Project Project { get; set; } = default!;
public Guid Key { get; set; } = Guid.NewGuid();
public string Name { get; set; } = default!;
public string? Icon { get; set; }
public int Sort { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public ICollection<PresetCharacterController> PresetControllers { get; set; } = [];
}
public class PresetCharacterController
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid CharacterPresetId { get; set; }
public ProjectCharacterPreset Preset { get; set; } = default!;
public string Name { get; set; } = default!;
public string Key { get; set; } = default!;
public string Value { get; set; } = default!;
public int Sort { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
public class PresetStory
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid ProjectId { get; set; }
public Project Project { get; set; } = default!;
public string Name { get; set; } = default!;
public string? Description { get; set; }
public string? Demo { get; set; }
public Guid? MusicId { get; set; }
public MusicTrack? Music { get; set; }
public string? ScenesSpa { get; set; }
public int Sort { get; set; }
public bool IsPublished { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public DateTime? DeletedAt { get; set; }
public ICollection<PresetScene> Scenes { get; set; } = [];
}
public class PresetScene
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid PresetStoryId { get; set; }
public PresetStory Story { get; set; } = default!;
public Guid SceneId { get; set; }
public Scene Scene { get; set; } = default!;
public int Sort { get; set; }
public decimal? DefaultDurationSec { get; set; }
}
@@ -0,0 +1,205 @@
using FlatRender.ContentSvc.Domain.Enums;
namespace FlatRender.ContentSvc.Domain.Entities;
public class Blog
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid? TenantId { get; set; }
public BlogKind Kind { get; set; } = BlogKind.Blog;
public string Slug { get; set; } = default!;
public string Title { get; set; } = default!;
public string? ShortDescription { get; set; }
public string Content { get; set; } = default!;
public string? MetaTitle { get; set; }
public string? MetaDescription { get; set; }
public string? MetaKeywords { get; set; }
public bool IncludeInSiteMap { get; set; } = true;
public string? Image { get; set; }
public string? Cover { get; set; }
public Guid? AuthorUserId { get; set; }
public string? AuthorDisplayName { get; set; }
public bool IsPublished { get; set; }
public DateTime? PublishDate { get; set; }
public long ViewCount { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public DateTime? DeletedAt { get; set; }
public ICollection<Comment> Comments { get; set; } = [];
}
public class Comment
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid? TenantId { get; set; }
public Guid UserId { get; set; }
public Guid? BlogId { get; set; }
public Blog? Blog { get; set; }
public Guid? ContainerId { get; set; }
public ProjectContainer? Container { get; set; }
public Guid? ParentCommentId { get; set; }
public Comment? ParentComment { get; set; }
public string Content { get; set; } = default!;
public decimal? Rate { get; set; }
public bool IsApproved { get; set; }
public bool IsPinned { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public DateTime? DeletedAt { get; set; }
public ICollection<Comment> Replies { get; set; } = [];
}
public class HomePageEvent
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid? TenantId { get; set; }
public string? Title { get; set; }
public string? Subtitle { get; set; }
public string? Description { get; set; }
public string? Badge { get; set; }
public string? BadgeClass { get; set; }
public string? ButtonText { get; set; }
public string? ButtonUrl { get; set; }
public string? ButtonClass { get; set; }
public string? Color { get; set; }
public string? BackgroundColor { get; set; }
public string? TextColor { get; set; }
public string? Image { get; set; }
public bool IsActive { get; set; } = true;
public int Sort { get; set; }
public DateTime? StartsAt { get; set; }
public DateTime? EndsAt { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
public class NewSlide
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid? TenantId { get; set; }
public string? Keyword { get; set; }
public string? Title { get; set; }
public string? Image { get; set; }
public string? Parameter { get; set; }
public SlideType SlideType { get; set; } = SlideType.Hero;
public DateTime? ExpireDate { get; set; }
public int Sort { get; set; }
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
public class InternalRoute
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid? TenantId { get; set; }
public string? Name { get; set; }
public string? Image { get; set; }
public string Slug { get; set; } = default!;
public int Priority { get; set; } = 5;
public DateTime? LastDate { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
public class CustomRoute
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid? TenantId { get; set; }
public string Target { get; set; } = default!;
public string Destination { get; set; } = default!;
public int RedirectCode { get; set; } = 301;
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
public class WebsiteSetting
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid? TenantId { get; set; }
public string Key { get; set; } = default!;
public string Value { get; set; } = "{}";
public string? Description { get; set; }
public bool IsSecret { get; set; }
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
public class LearnArticle
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid? TenantId { get; set; }
public string Title { get; set; } = default!;
public string? Body { get; set; }
public string? DemoUrl { get; set; }
public string? Mode { get; set; }
public int Sort { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
public class Training
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid? TenantId { get; set; }
public string Title { get; set; } = default!;
public string? Description { get; set; }
public string? VideoUrl { get; set; }
public string? ThumbnailUrl { get; set; }
public int Sort { get; set; }
public bool IsPublished { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
public class FavoriteFolder
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid UserId { get; set; }
public Guid TenantId { get; set; }
public string Name { get; set; } = default!;
public string? Description { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public ICollection<FavoriteContainer> Containers { get; set; } = [];
}
public class FavoriteContainer
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid UserId { get; set; }
public Guid TenantId { get; set; }
public Guid ContainerId { get; set; }
public ProjectContainer Container { get; set; } = default!;
public Guid? FolderId { get; set; }
public FavoriteFolder? Folder { get; set; }
public string? Note { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
@@ -0,0 +1,297 @@
using FlatRender.ContentSvc.Domain.Enums;
namespace FlatRender.ContentSvc.Domain.Entities;
public class Scene
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid ProjectId { get; set; }
public Project Project { get; set; } = default!;
public string Key { get; set; } = default!;
public string Title { get; set; } = default!;
public string? LocalizedTitle { get; set; }
public SceneKind SceneType { get; set; } = SceneKind.Normal;
public string? Image { get; set; }
public string? Demo { get; set; }
public string? SceneColorSvg { get; set; }
public string? SnapshotUrl { get; set; }
public bool GenerateKf { get; set; }
public decimal? DefaultDurationSec { get; set; }
public decimal? MinDurationSec { get; set; }
public decimal? MaxDurationSec { get; set; }
public decimal OverlapAtEndSec { get; set; }
public bool CanHandleDuration { get; set; } = true;
public bool ManualColorSelection { get; set; }
public int Sort { get; set; }
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public DateTime? DeletedAt { get; set; }
public ICollection<RepeaterItem> RepeaterItems { get; set; } = [];
public ICollection<SceneContentElement> ContentElements { get; set; } = [];
public ICollection<SceneColorElement> ColorElements { get; set; } = [];
public ICollection<SceneColorPreset> ColorPresets { get; set; } = [];
public ICollection<SceneCharacter> Characters { get; set; } = [];
public ICollection<SceneCategory> SceneCategories { get; set; } = [];
}
public class RepeaterItem
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid SceneId { get; set; }
public Scene Scene { get; set; } = default!;
public string Title { get; set; } = default!;
public string RepeatBoxKey { get; set; } = default!;
public string RepeatItemKey { get; set; } = default!;
public int MaxRepeatCount { get; set; } = 10;
public bool UserCanChangeSort { get; set; } = true;
public RepeatSortStrategy RepeatSortStrategy { get; set; } = RepeatSortStrategy.Manual;
public int Sort { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public ICollection<SceneContentElement> ContentElements { get; set; } = [];
}
public class SceneContentElement
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid SceneId { get; set; }
public Scene Scene { get; set; } = default!;
public Guid? RepeaterItemId { get; set; }
public string Key { get; set; } = default!;
public string Title { get; set; } = default!;
public string? LocalizedTitle { get; set; }
public string? Hint { get; set; }
public ContentElementType Type { get; set; }
public string? DefaultValue { get; set; }
// Text
public Guid? FontId { get; set; }
public string? FontFace { get; set; }
public string? FontFaceName { get; set; }
public int? FontSize { get; set; }
public int? DefaultFontSize { get; set; }
public string? DefaultFontFace { get; set; }
public bool IsFontChangeable { get; set; } = true;
public bool IsFontSizeChangeable { get; set; } = true;
public JustifyKind Justify { get; set; } = JustifyKind.CENTER_JUSTIFY;
public bool CanJustify { get; set; } = true;
public int PositionInContainer { get; set; }
public bool IsTextBox { get; set; }
public int? MaxSize { get; set; }
public string? DirectionLayerKey { get; set; }
public int DirectionLayerValue { get; set; }
// Media
public bool VideoSupport { get; set; }
public decimal? MinDurationSec { get; set; }
public decimal? MaxDurationSec { get; set; }
public int? Width { get; set; }
public int? Height { get; set; }
public string? Thumbnail { get; set; }
// Misc
public string? MappedList { get; set; }
public string? CounterMode { get; set; }
public AiInputType AiInputType { get; set; } = AiInputType.None;
public bool IsHidden { get; set; }
public bool IsFocused { get; set; }
public string? OpacityControllerKey { get; set; }
// Legacy design pattern variants
public string? Dp1Image { get; set; }
public string? Dp1Title { get; set; }
public string? Dp2Image { get; set; }
public string? Dp2Title { get; set; }
public string? Dp3Image { get; set; }
public string? Dp3Title { get; set; }
public string? Dp4Image { get; set; }
public string? Dp4Title { get; set; }
public int VirtualCount { get; set; } = 1;
public int Sort { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
public class SceneColorElement
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid SceneId { get; set; }
public Scene Scene { get; set; } = default!;
public string ElementKey { get; set; } = default!;
public string Title { get; set; } = default!;
public string? Icon { get; set; }
public AttrValueKind AttrValue { get; set; } = AttrValueKind.fill;
public string DefaultColor { get; set; } = default!;
public int Sort { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
public class SceneColorPreset
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid SceneId { get; set; }
public string? Name { get; set; }
public int Sort { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public ICollection<SceneColorPresetItem> Items { get; set; } = [];
}
public class SceneColorPresetItem
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid PresetId { get; set; }
public SceneColorPreset Preset { get; set; } = default!;
public string ElementKey { get; set; } = default!;
public string Value { get; set; } = default!;
public int Sort { get; set; }
}
public class SharedColor
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid ProjectId { get; set; }
public Project Project { get; set; } = default!;
public string ElementKey { get; set; } = default!;
public string Title { get; set; } = default!;
public string? Icon { get; set; }
public AttrValueKind AttrValue { get; set; } = AttrValueKind.fill;
public string DefaultColor { get; set; } = default!;
public int Sort { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
public class SharedLayer
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid ProjectId { get; set; }
public Project Project { get; set; } = default!;
public string Key { get; set; } = default!;
public string Title { get; set; } = default!;
public string? LocalizedTitle { get; set; }
public string? Hint { get; set; }
public ContentElementType Type { get; set; }
public string? DefaultValue { get; set; }
public Guid? FontId { get; set; }
public string? FontFace { get; set; }
public string? FontFaceName { get; set; }
public int? FontSize { get; set; }
public int? DefaultFontSize { get; set; }
public string? DefaultFontFace { get; set; }
public bool IsFontChangeable { get; set; } = true;
public bool IsFontSizeChangeable { get; set; } = true;
public JustifyKind Justify { get; set; } = JustifyKind.CENTER_JUSTIFY;
public bool CanJustify { get; set; } = true;
public int PositionInContainer { get; set; }
public bool IsTextBox { get; set; }
public int? MaxSize { get; set; }
public string? DirectionLayerKey { get; set; }
public int DirectionLayerValue { get; set; }
public bool VideoSupport { get; set; }
public decimal? MinDurationSec { get; set; }
public decimal? MaxDurationSec { get; set; }
public int? Width { get; set; }
public int? Height { get; set; }
public string? Thumbnail { get; set; }
public string? MappedList { get; set; }
public string? CounterMode { get; set; }
public AiInputType AiInputType { get; set; } = AiInputType.None;
public bool IsHidden { get; set; }
public bool IsFocused { get; set; }
// Legacy design pattern variants
public string? Dp1Image { get; set; }
public string? Dp1Title { get; set; }
public string? Dp2Image { get; set; }
public string? Dp2Title { get; set; }
public string? Dp3Image { get; set; }
public string? Dp3Title { get; set; }
public string? Dp4Image { get; set; }
public string? Dp4Title { get; set; }
public int VirtualCount { get; set; } = 1;
public int Sort { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
public class SceneCategory
{
public Guid SceneId { get; set; }
public Scene Scene { get; set; } = default!;
public Guid CategoryId { get; set; }
public Category Category { get; set; } = default!;
}
public class SharedColorPreset
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid ProjectId { get; set; }
public Project Project { get; set; } = default!;
public string? Name { get; set; }
public int Sort { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public ICollection<SharedColorPresetItem> Items { get; set; } = [];
}
public class SharedColorPresetItem
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid PresetId { get; set; }
public SharedColorPreset Preset { get; set; } = default!;
public string ElementKey { get; set; } = default!;
public string Value { get; set; } = default!;
public int Sort { get; set; }
}
public class TemplateSvgPreview
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid? ProjectId { get; set; }
public Guid? SceneId { get; set; }
public string? SourceImageUrl { get; set; }
public string SvgUrl { get; set; } = default!;
public string? ThumbnailUrl { get; set; }
public string ColorZones { get; set; } = "[]";
public int? Width { get; set; }
public int? Height { get; set; }
public string? GenerationMethod { get; set; }
public bool GeneratedByAi { get; set; }
public decimal? QualityScore { get; set; }
public Guid? CreatedByUserId { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
@@ -0,0 +1,111 @@
namespace FlatRender.ContentSvc.Domain.Entities;
public class Category
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid? ParentId { get; set; }
public Category? Parent { get; set; }
public string Name { get; set; } = default!;
public string Slug { get; set; } = default!;
public string? Description { get; set; }
public string? ImageUrl { get; set; }
public string? Icon { get; set; }
public string? MetaTitle { get; set; }
public string? MetaDescription { get; set; }
public string? MetaKeywords { get; set; }
public bool BotFollow { get; set; } = true;
public int Sort { get; set; }
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public DateTime? DeletedAt { get; set; }
public ICollection<Category> Children { get; set; } = [];
}
public class Tag
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; } = default!;
public string? LatinName { get; set; }
public string Slug { get; set; } = default!;
public string? AppliesToMode { get; set; }
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public DateTime? DeletedAt { get; set; }
}
public class Font
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; } = default!;
public string? OriginalName { get; set; }
public string? SystemName { get; set; }
public string? Family { get; set; }
public int? Weight { get; set; }
public string? Style { get; set; }
public string Direction { get; set; } = "LTR";
public string? FileUrl { get; set; }
public string? SampleImageUrl { get; set; }
public bool IsPremium { get; set; }
public bool IsActive { get; set; } = true;
public bool InstalledOnNodes { get; set; }
public int Sort { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public DateTime? DeletedAt { get; set; }
}
public class MusicTrack
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; } = default!;
public string? Caption { get; set; }
public string? Keywords { get; set; }
public string Url { get; set; } = default!;
public string? WaveformData { get; set; }
public decimal DurationSec { get; set; }
public int? Bpm { get; set; }
public string? Genre { get; set; }
public string? Mood { get; set; }
public bool IsPremium { get; set; }
public bool IsActive { get; set; } = true;
public int Sort { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public DateTime? DeletedAt { get; set; }
}
public class ProjectServer
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; } = default!;
public string Region { get; set; } = default!;
public string? Ip { get; set; }
public string? PhysicalPathOutput { get; set; }
public string? DefaultProjectAddress { get; set; }
public string? RenderOutputLocation { get; set; }
public string? PreNeedFolderAddress { get; set; }
public string? MinioEndpoint { get; set; }
public string? MinioBucketTemplates { get; set; }
public string? MinioBucketOutputs { get; set; }
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
public class AdminFile
{
public Guid Id { get; set; } = Guid.NewGuid();
public string? Name { get; set; }
public string Url { get; set; } = default!;
public string? ThumbnailUrl { get; set; }
public string? FileType { get; set; }
public long? SizeBytes { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
@@ -0,0 +1,116 @@
using FlatRender.ContentSvc.Domain.Enums;
namespace FlatRender.ContentSvc.Domain.Entities;
public class ProjectContainer
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid? TenantId { get; set; }
public string Slug { get; set; } = default!;
public string Name { get; set; } = default!;
public string? Description { get; set; }
public string? Keywords { get; set; }
public string? NewsText { get; set; }
public string? Image { get; set; }
public string? Demo { get; set; }
public string? FullDemo { get; set; }
public string? MiniDemo { get; set; }
public string? DemoScriptTag { get; set; }
public bool IsPublished { get; set; }
public bool IsPremium { get; set; }
public bool IsMockup { get; set; }
public ChooseMode PrimaryMode { get; set; } = ChooseMode.FLEXIBLE;
public decimal? RateAvg { get; set; }
public int RateCount { get; set; }
public long ViewCount { get; set; }
public long UseCount { get; set; }
public int Sort { get; set; }
public DateTime SortDate { get; set; } = DateTime.UtcNow;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public DateTime? DeletedAt { get; set; }
public ICollection<Project> Projects { get; set; } = [];
public ICollection<ContainerCategory> ContainerCategories { get; set; } = [];
public ICollection<ContainerTag> ContainerTags { get; set; } = [];
public ICollection<Comment> Comments { get; set; } = [];
}
public class ContainerCategory
{
public Guid ContainerId { get; set; }
public ProjectContainer Container { get; set; } = default!;
public Guid CategoryId { get; set; }
public Category Category { get; set; } = default!;
public int Sort { get; set; }
}
public class ContainerTag
{
public Guid ContainerId { get; set; }
public ProjectContainer Container { get; set; } = default!;
public Guid TagId { get; set; }
public Tag Tag { get; set; } = default!;
}
public class Project
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid ContainerId { get; set; }
public ProjectContainer Container { get; set; } = default!;
public Guid? ProjectServerId { get; set; }
public string Name { get; set; } = default!;
public string? Description { get; set; }
public string? Image { get; set; }
public string? FullDemo { get; set; }
public string? DemoScriptTag { get; set; }
public string? DownloadLink { get; set; }
public string? AepMinioBucket { get; set; }
public string? AepMinioKey { get; set; }
public string? AepFileUrl { get; set; }
public string? AepFileMd5 { get; set; }
public long? AepFileSizeBytes { get; set; }
public DateTime? AepUploadedAt { get; set; }
public string? Folder { get; set; }
public int OriginalWidth { get; set; }
public int OriginalHeight { get; set; }
public string? Aspect { get; set; }
public decimal ProjectDurationSec { get; set; }
public decimal? MinDurationSec { get; set; }
public decimal? MaxDurationSec { get; set; }
public int FreeFps { get; set; } = 30;
public ChooseMode ChooseMode { get; set; }
public ResolutionKind Resolution { get; set; } = ResolutionKind.FullHD;
public decimal VipFactor { get; set; } = 1.0m;
public string RenderAepComp { get; set; } = "flatrender";
public string? SharedLayerImage { get; set; }
public string? SharedColorsSvg { get; set; }
public string? SharedColorPresetsSvg { get; set; }
public bool IsPublished { get; set; }
public int Sort { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public DateTime? DeletedAt { get; set; }
public ICollection<Scene> Scenes { get; set; } = [];
public ICollection<SharedColor> SharedColors { get; set; } = [];
public ICollection<SharedColorPreset> SharedColorPresets { get; set; } = [];
public ICollection<SharedLayer> SharedLayers { get; set; } = [];
public ICollection<ProjectCharacterController> CharacterControllers { get; set; } = [];
public ICollection<ProjectCharacterPreset> CharacterPresets { get; set; } = [];
public ICollection<PresetStory> PresetStories { get; set; } = [];
}
@@ -0,0 +1,18 @@
namespace FlatRender.ContentSvc.Domain.Enums;
public enum ChooseMode { FIX, FLEXIBLE, MockUp, MusicVisualizer, VoiceOver }
public enum ResolutionKind { HD, FullHD, TwoK, FourK }
public enum SceneKind { Normal, Config, DesignStart, DesignEnd }
public enum ContentElementType
{
Text, TextArea, Media, Audio, Voiceover,
CheckBox, DropDown, Fill, Color, Number,
Date, Toggle, Slider, Counter, Hidden
}
public enum JustifyKind { LEFT_JUSTIFY, CENTER_JUSTIFY, RIGHT_JUSTIFY, FULL_JUSTIFY }
public enum AiInputType { None, TitleSuggest, BodySuggest, TranslateRtl, TranslateLtr, RemoveBG, UpscaleImage, TTS }
public enum RepeatSortStrategy { Manual, Alphabetical, Numerical, InsertOrder }
public enum AttrValueKind { fill, stroke, tracking, dropshadow }
public enum BlogKind { Blog, Landing }
public enum SlideType { Hero, Promo, Tutorial, Category, Custom }
public enum ContainerFavoriteKind { Container }
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.2.0" />
<PackageReference Include="EFCore.NamingConventions" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.*">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.*" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.*" />
</ItemGroup>
</Project>
@@ -0,0 +1,6 @@
@FlatRender.ContentSvc_HostAddress = http://localhost:5088
GET {{FlatRender.ContentSvc_HostAddress}}/weatherforecast/
Accept: application/json
###
@@ -0,0 +1,903 @@
using FlatRender.ContentSvc.Domain.Entities;
using FlatRender.ContentSvc.Domain.Enums;
using Microsoft.EntityFrameworkCore;
namespace FlatRender.ContentSvc.Infrastructure.Data;
public class ContentDbContext(DbContextOptions<ContentDbContext> options) : DbContext(options)
{
// Taxonomy
public DbSet<Category> Categories => Set<Category>();
public DbSet<Tag> Tags => Set<Tag>();
public DbSet<Font> Fonts => Set<Font>();
public DbSet<MusicTrack> MusicTracks => Set<MusicTrack>();
public DbSet<ProjectServer> ProjectServers => Set<ProjectServer>();
public DbSet<AdminFile> AdminFiles => Set<AdminFile>();
// Templates
public DbSet<ProjectContainer> ProjectContainers => Set<ProjectContainer>();
public DbSet<ContainerCategory> ContainerCategories => Set<ContainerCategory>();
public DbSet<ContainerTag> ContainerTags => Set<ContainerTag>();
public DbSet<Project> Projects => Set<Project>();
// Scenes
public DbSet<Scene> Scenes => Set<Scene>();
public DbSet<SceneCategory> SceneCategories => Set<SceneCategory>();
public DbSet<RepeaterItem> RepeaterItems => Set<RepeaterItem>();
public DbSet<SceneContentElement> SceneContentElements => Set<SceneContentElement>();
public DbSet<SceneColorElement> SceneColorElements => Set<SceneColorElement>();
public DbSet<SceneColorPreset> SceneColorPresets => Set<SceneColorPreset>();
public DbSet<SceneColorPresetItem> SceneColorPresetItems => Set<SceneColorPresetItem>();
public DbSet<SharedColor> SharedColors => Set<SharedColor>();
public DbSet<SharedColorPreset> SharedColorPresets => Set<SharedColorPreset>();
public DbSet<SharedColorPresetItem> SharedColorPresetItems => Set<SharedColorPresetItem>();
public DbSet<SharedLayer> SharedLayers => Set<SharedLayer>();
public DbSet<TemplateSvgPreview> TemplateSvgPreviews => Set<TemplateSvgPreview>();
// Characters
public DbSet<SceneCharacter> SceneCharacters => Set<SceneCharacter>();
public DbSet<SceneCharacterController> SceneCharacterControllers => Set<SceneCharacterController>();
public DbSet<SceneControllerOption> SceneControllerOptions => Set<SceneControllerOption>();
public DbSet<ProjectCharacterController> ProjectCharacterControllers => Set<ProjectCharacterController>();
public DbSet<ProjectCharacterControllerOption> ProjectCharacterControllerOptions => Set<ProjectCharacterControllerOption>();
public DbSet<ProjectCharacterPreset> ProjectCharacterPresets => Set<ProjectCharacterPreset>();
public DbSet<PresetCharacterController> PresetCharacterControllers => Set<PresetCharacterController>();
public DbSet<PresetStory> PresetStories => Set<PresetStory>();
public DbSet<PresetScene> PresetScenes => Set<PresetScene>();
// CMS
public DbSet<Blog> Blogs => Set<Blog>();
public DbSet<Comment> Comments => Set<Comment>();
public DbSet<HomePageEvent> HomePageEvents => Set<HomePageEvent>();
public DbSet<NewSlide> NewSlides => Set<NewSlide>();
public DbSet<InternalRoute> InternalRoutes => Set<InternalRoute>();
public DbSet<CustomRoute> CustomRoutes => Set<CustomRoute>();
public DbSet<WebsiteSetting> WebsiteSettings => Set<WebsiteSetting>();
public DbSet<LearnArticle> LearnArticles => Set<LearnArticle>();
public DbSet<Training> Trainings => Set<Training>();
public DbSet<FavoriteFolder> FavoriteFolders => Set<FavoriteFolder>();
public DbSet<FavoriteContainer> FavoriteContainers => Set<FavoriteContainer>();
protected override void OnModelCreating(ModelBuilder mb)
{
mb.HasDefaultSchema("content");
// Native PostgreSQL enums are registered on the EF provider via npgsql.MapEnum<T>()
// in Program.cs (EF Core 9+ approach), covering both model + runtime ADO mapping.
ConfigureTaxonomy(mb);
ConfigureTemplates(mb);
ConfigureScenes(mb);
ConfigureCharacters(mb);
ConfigureCms(mb);
}
private static void ConfigureTaxonomy(ModelBuilder mb)
{
mb.Entity<Category>(e =>
{
e.ToTable("categories");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.ParentId).HasColumnName("parent_id");
e.Property(x => x.Name).HasColumnName("name").IsRequired();
e.Property(x => x.Slug).HasColumnName("slug").HasColumnType("citext").IsRequired();
e.Property(x => x.Description).HasColumnName("description");
e.Property(x => x.ImageUrl).HasColumnName("image_url");
e.Property(x => x.Icon).HasColumnName("icon");
e.Property(x => x.MetaTitle).HasColumnName("meta_title");
e.Property(x => x.MetaDescription).HasColumnName("meta_description");
e.Property(x => x.MetaKeywords).HasColumnName("meta_keywords");
e.Property(x => x.BotFollow).HasColumnName("bot_follow");
e.Property(x => x.Sort).HasColumnName("sort");
e.Property(x => x.IsActive).HasColumnName("is_active");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
e.Property(x => x.DeletedAt).HasColumnName("deleted_at");
e.HasOne(x => x.Parent).WithMany(x => x.Children).HasForeignKey(x => x.ParentId).OnDelete(DeleteBehavior.Restrict);
e.HasQueryFilter(x => x.DeletedAt == null);
});
mb.Entity<Tag>(e =>
{
e.ToTable("tags");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.Name).HasColumnName("name").IsRequired();
e.Property(x => x.LatinName).HasColumnName("latin_name");
e.Property(x => x.Slug).HasColumnName("slug").HasColumnType("citext").IsRequired();
e.Property(x => x.AppliesToMode).HasColumnName("applies_to_mode");
e.Property(x => x.IsActive).HasColumnName("is_active");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
e.Property(x => x.DeletedAt).HasColumnName("deleted_at");
e.HasQueryFilter(x => x.DeletedAt == null);
});
mb.Entity<Font>(e =>
{
e.ToTable("fonts");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.Name).HasColumnName("name").IsRequired();
e.Property(x => x.OriginalName).HasColumnName("original_name");
e.Property(x => x.SystemName).HasColumnName("system_name");
e.Property(x => x.Family).HasColumnName("family");
e.Property(x => x.Weight).HasColumnName("weight");
e.Property(x => x.Style).HasColumnName("style");
e.Property(x => x.Direction).HasColumnName("direction");
e.Property(x => x.FileUrl).HasColumnName("file_url");
e.Property(x => x.SampleImageUrl).HasColumnName("sample_image_url");
e.Property(x => x.IsPremium).HasColumnName("is_premium");
e.Property(x => x.IsActive).HasColumnName("is_active");
e.Property(x => x.InstalledOnNodes).HasColumnName("installed_on_nodes");
e.Property(x => x.Sort).HasColumnName("sort");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
e.Property(x => x.DeletedAt).HasColumnName("deleted_at");
e.HasQueryFilter(x => x.DeletedAt == null);
});
mb.Entity<MusicTrack>(e =>
{
e.ToTable("music_tracks");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.Name).HasColumnName("name").IsRequired();
e.Property(x => x.Caption).HasColumnName("caption");
e.Property(x => x.Keywords).HasColumnName("keywords");
e.Property(x => x.Url).HasColumnName("url").IsRequired();
e.Property(x => x.WaveformData).HasColumnName("waveform_data").HasColumnType("jsonb");
e.Property(x => x.DurationSec).HasColumnName("duration_sec");
e.Property(x => x.Bpm).HasColumnName("bpm");
e.Property(x => x.Genre).HasColumnName("genre");
e.Property(x => x.Mood).HasColumnName("mood");
e.Property(x => x.IsPremium).HasColumnName("is_premium");
e.Property(x => x.IsActive).HasColumnName("is_active");
e.Property(x => x.Sort).HasColumnName("sort");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
e.Property(x => x.DeletedAt).HasColumnName("deleted_at");
e.HasQueryFilter(x => x.DeletedAt == null);
});
mb.Entity<ProjectServer>(e =>
{
e.ToTable("project_servers");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.Name).HasColumnName("name").IsRequired();
e.Property(x => x.Region).HasColumnName("region").IsRequired();
e.Property(x => x.Ip).HasColumnName("ip");
e.Property(x => x.PhysicalPathOutput).HasColumnName("physical_path_output");
e.Property(x => x.DefaultProjectAddress).HasColumnName("default_project_address");
e.Property(x => x.RenderOutputLocation).HasColumnName("render_output_location");
e.Property(x => x.PreNeedFolderAddress).HasColumnName("pre_need_folder_address");
e.Property(x => x.MinioEndpoint).HasColumnName("minio_endpoint");
e.Property(x => x.MinioBucketTemplates).HasColumnName("minio_bucket_templates");
e.Property(x => x.MinioBucketOutputs).HasColumnName("minio_bucket_outputs");
e.Property(x => x.IsActive).HasColumnName("is_active");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
});
mb.Entity<AdminFile>(e =>
{
e.ToTable("admin_files");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.Name).HasColumnName("name");
e.Property(x => x.Url).HasColumnName("url").IsRequired();
e.Property(x => x.ThumbnailUrl).HasColumnName("thumbnail_url");
e.Property(x => x.FileType).HasColumnName("file_type");
e.Property(x => x.SizeBytes).HasColumnName("size_bytes");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
});
}
private static void ConfigureTemplates(ModelBuilder mb)
{
mb.Entity<ProjectContainer>(e =>
{
e.ToTable("project_containers");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.TenantId).HasColumnName("tenant_id");
e.Property(x => x.Slug).HasColumnName("slug").HasColumnType("citext").IsRequired();
e.Property(x => x.Name).HasColumnName("name").IsRequired();
e.Property(x => x.Description).HasColumnName("description");
e.Property(x => x.Keywords).HasColumnName("keywords");
e.Property(x => x.NewsText).HasColumnName("news_text");
e.Property(x => x.Image).HasColumnName("image");
e.Property(x => x.Demo).HasColumnName("demo");
e.Property(x => x.FullDemo).HasColumnName("full_demo");
e.Property(x => x.MiniDemo).HasColumnName("mini_demo");
e.Property(x => x.DemoScriptTag).HasColumnName("demo_script_tag");
e.Property(x => x.IsPublished).HasColumnName("is_published");
e.Property(x => x.IsPremium).HasColumnName("is_premium");
e.Property(x => x.IsMockup).HasColumnName("is_mockup");
e.Property(x => x.PrimaryMode).HasColumnName("primary_mode");
e.Property(x => x.RateAvg).HasColumnName("rate_avg");
e.Property(x => x.RateCount).HasColumnName("rate_count");
e.Property(x => x.ViewCount).HasColumnName("view_count");
e.Property(x => x.UseCount).HasColumnName("use_count");
e.Property(x => x.Sort).HasColumnName("sort");
e.Property(x => x.SortDate).HasColumnName("sort_date");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
e.Property(x => x.DeletedAt).HasColumnName("deleted_at");
e.HasQueryFilter(x => x.DeletedAt == null);
});
mb.Entity<ContainerCategory>(e =>
{
e.ToTable("container_categories");
e.HasKey(x => new { x.ContainerId, x.CategoryId });
e.Property(x => x.ContainerId).HasColumnName("container_id");
e.Property(x => x.CategoryId).HasColumnName("category_id");
e.Property(x => x.Sort).HasColumnName("sort");
e.HasOne(x => x.Container).WithMany(x => x.ContainerCategories).HasForeignKey(x => x.ContainerId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Category).WithMany().HasForeignKey(x => x.CategoryId).OnDelete(DeleteBehavior.Cascade);
});
mb.Entity<ContainerTag>(e =>
{
e.ToTable("container_tags");
e.HasKey(x => new { x.ContainerId, x.TagId });
e.Property(x => x.ContainerId).HasColumnName("container_id");
e.Property(x => x.TagId).HasColumnName("tag_id");
e.HasOne(x => x.Container).WithMany(x => x.ContainerTags).HasForeignKey(x => x.ContainerId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Tag).WithMany().HasForeignKey(x => x.TagId).OnDelete(DeleteBehavior.Cascade);
});
mb.Entity<Project>(e =>
{
e.ToTable("projects");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.ContainerId).HasColumnName("container_id");
e.Property(x => x.ProjectServerId).HasColumnName("project_server_id");
e.Property(x => x.Name).HasColumnName("name").IsRequired();
e.Property(x => x.Description).HasColumnName("description");
e.Property(x => x.Image).HasColumnName("image");
e.Property(x => x.FullDemo).HasColumnName("full_demo");
e.Property(x => x.DemoScriptTag).HasColumnName("demo_script_tag");
e.Property(x => x.DownloadLink).HasColumnName("download_link");
e.Property(x => x.AepMinioBucket).HasColumnName("aep_minio_bucket");
e.Property(x => x.AepMinioKey).HasColumnName("aep_minio_key");
e.Property(x => x.AepFileUrl).HasColumnName("aep_file_url");
e.Property(x => x.AepFileMd5).HasColumnName("aep_file_md5");
e.Property(x => x.AepFileSizeBytes).HasColumnName("aep_file_size_bytes");
e.Property(x => x.AepUploadedAt).HasColumnName("aep_uploaded_at");
e.Property(x => x.Folder).HasColumnName("folder");
e.Property(x => x.OriginalWidth).HasColumnName("original_width");
e.Property(x => x.OriginalHeight).HasColumnName("original_height");
e.Property(x => x.Aspect).HasColumnName("aspect");
e.Property(x => x.ProjectDurationSec).HasColumnName("project_duration_sec");
e.Property(x => x.MinDurationSec).HasColumnName("min_duration_sec");
e.Property(x => x.MaxDurationSec).HasColumnName("max_duration_sec");
e.Property(x => x.FreeFps).HasColumnName("free_fps");
e.Property(x => x.ChooseMode).HasColumnName("choose_mode");
e.Property(x => x.Resolution).HasColumnName("resolution");
e.Property(x => x.VipFactor).HasColumnName("vip_factor");
e.Property(x => x.RenderAepComp).HasColumnName("render_aep_comp");
e.Property(x => x.SharedLayerImage).HasColumnName("shared_layer_image");
e.Property(x => x.SharedColorsSvg).HasColumnName("shared_colors_svg");
e.Property(x => x.SharedColorPresetsSvg).HasColumnName("shared_color_presets_svg");
e.Property(x => x.IsPublished).HasColumnName("is_published");
e.Property(x => x.Sort).HasColumnName("sort");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
e.Property(x => x.DeletedAt).HasColumnName("deleted_at");
e.HasOne(x => x.Container).WithMany(x => x.Projects).HasForeignKey(x => x.ContainerId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
}
private static void ConfigureScenes(ModelBuilder mb)
{
mb.Entity<Scene>(e =>
{
e.ToTable("scenes");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.ProjectId).HasColumnName("project_id");
e.Property(x => x.Key).HasColumnName("key").IsRequired();
e.Property(x => x.Title).HasColumnName("title").IsRequired();
e.Property(x => x.LocalizedTitle).HasColumnName("localized_title").HasColumnType("jsonb");
e.Property(x => x.SceneType).HasColumnName("scene_type");
e.Property(x => x.Image).HasColumnName("image");
e.Property(x => x.Demo).HasColumnName("demo");
e.Property(x => x.SceneColorSvg).HasColumnName("scene_color_svg");
e.Property(x => x.SnapshotUrl).HasColumnName("snapshot_url");
e.Property(x => x.GenerateKf).HasColumnName("generate_kf");
e.Property(x => x.DefaultDurationSec).HasColumnName("default_duration_sec");
e.Property(x => x.MinDurationSec).HasColumnName("min_duration_sec");
e.Property(x => x.MaxDurationSec).HasColumnName("max_duration_sec");
e.Property(x => x.OverlapAtEndSec).HasColumnName("overlap_at_end_sec");
e.Property(x => x.CanHandleDuration).HasColumnName("can_handle_duration");
e.Property(x => x.ManualColorSelection).HasColumnName("manual_color_selection");
e.Property(x => x.Sort).HasColumnName("sort");
e.Property(x => x.IsActive).HasColumnName("is_active");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
e.Property(x => x.DeletedAt).HasColumnName("deleted_at");
e.HasOne(x => x.Project).WithMany(x => x.Scenes).HasForeignKey(x => x.ProjectId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
mb.Entity<SceneCategory>(e =>
{
e.ToTable("scene_categories");
e.HasKey(x => new { x.SceneId, x.CategoryId });
e.Property(x => x.SceneId).HasColumnName("scene_id");
e.Property(x => x.CategoryId).HasColumnName("category_id");
e.HasOne(x => x.Scene).WithMany(x => x.SceneCategories).HasForeignKey(x => x.SceneId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Category).WithMany().HasForeignKey(x => x.CategoryId).OnDelete(DeleteBehavior.Cascade);
});
mb.Entity<RepeaterItem>(e =>
{
e.ToTable("repeater_items");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.SceneId).HasColumnName("scene_id");
e.Property(x => x.Title).HasColumnName("title").IsRequired();
e.Property(x => x.RepeatBoxKey).HasColumnName("repeat_box_key").IsRequired();
e.Property(x => x.RepeatItemKey).HasColumnName("repeat_item_key").IsRequired();
e.Property(x => x.MaxRepeatCount).HasColumnName("max_repeat_count");
e.Property(x => x.UserCanChangeSort).HasColumnName("user_can_change_sort");
e.Property(x => x.RepeatSortStrategy).HasColumnName("repeat_sort_strategy");
e.Property(x => x.Sort).HasColumnName("sort");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
e.HasOne(x => x.Scene).WithMany(x => x.RepeaterItems).HasForeignKey(x => x.SceneId).OnDelete(DeleteBehavior.Cascade);
});
mb.Entity<SceneContentElement>(e =>
{
e.ToTable("scene_content_elements");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.SceneId).HasColumnName("scene_id");
e.Property(x => x.RepeaterItemId).HasColumnName("repeater_item_id");
e.Property(x => x.Key).HasColumnName("key").IsRequired();
e.Property(x => x.Title).HasColumnName("title").IsRequired();
e.Property(x => x.LocalizedTitle).HasColumnName("localized_title").HasColumnType("jsonb");
e.Property(x => x.Hint).HasColumnName("hint");
e.Property(x => x.Type).HasColumnName("type");
e.Property(x => x.DefaultValue).HasColumnName("default_value");
e.Property(x => x.FontId).HasColumnName("font_id");
e.Property(x => x.FontFace).HasColumnName("font_face");
e.Property(x => x.FontFaceName).HasColumnName("font_face_name");
e.Property(x => x.FontSize).HasColumnName("font_size");
e.Property(x => x.DefaultFontSize).HasColumnName("default_font_size");
e.Property(x => x.DefaultFontFace).HasColumnName("default_font_face");
e.Property(x => x.IsFontChangeable).HasColumnName("is_font_changeable");
e.Property(x => x.IsFontSizeChangeable).HasColumnName("is_font_size_changeable");
e.Property(x => x.Justify).HasColumnName("justify");
e.Property(x => x.CanJustify).HasColumnName("can_justify");
e.Property(x => x.PositionInContainer).HasColumnName("position_in_container");
e.Property(x => x.IsTextBox).HasColumnName("is_text_box");
e.Property(x => x.MaxSize).HasColumnName("max_size");
e.Property(x => x.DirectionLayerKey).HasColumnName("direction_layer_key");
e.Property(x => x.DirectionLayerValue).HasColumnName("direction_layer_value");
e.Property(x => x.VideoSupport).HasColumnName("video_support");
e.Property(x => x.MinDurationSec).HasColumnName("min_duration_sec");
e.Property(x => x.MaxDurationSec).HasColumnName("max_duration_sec");
e.Property(x => x.Width).HasColumnName("width");
e.Property(x => x.Height).HasColumnName("height");
e.Property(x => x.Thumbnail).HasColumnName("thumbnail");
e.Property(x => x.MappedList).HasColumnName("mapped_list").HasColumnType("jsonb");
e.Property(x => x.CounterMode).HasColumnName("counter_mode");
e.Property(x => x.AiInputType).HasColumnName("ai_input_type");
e.Property(x => x.IsHidden).HasColumnName("is_hidden");
e.Property(x => x.IsFocused).HasColumnName("is_focused");
e.Property(x => x.OpacityControllerKey).HasColumnName("opacity_controller_key");
e.Property(x => x.Dp1Image).HasColumnName("dp1_image");
e.Property(x => x.Dp1Title).HasColumnName("dp1_title");
e.Property(x => x.Dp2Image).HasColumnName("dp2_image");
e.Property(x => x.Dp2Title).HasColumnName("dp2_title");
e.Property(x => x.Dp3Image).HasColumnName("dp3_image");
e.Property(x => x.Dp3Title).HasColumnName("dp3_title");
e.Property(x => x.Dp4Image).HasColumnName("dp4_image");
e.Property(x => x.Dp4Title).HasColumnName("dp4_title");
e.Property(x => x.VirtualCount).HasColumnName("virtual_count");
e.Property(x => x.Sort).HasColumnName("sort");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
e.HasOne(x => x.Scene).WithMany(x => x.ContentElements).HasForeignKey(x => x.SceneId).OnDelete(DeleteBehavior.Cascade);
});
mb.Entity<SceneColorElement>(e =>
{
e.ToTable("scene_color_elements");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.SceneId).HasColumnName("scene_id");
e.Property(x => x.ElementKey).HasColumnName("element_key").IsRequired();
e.Property(x => x.Title).HasColumnName("title").IsRequired();
e.Property(x => x.Icon).HasColumnName("icon");
e.Property(x => x.AttrValue).HasColumnName("attr_value");
e.Property(x => x.DefaultColor).HasColumnName("default_color").IsRequired();
e.Property(x => x.Sort).HasColumnName("sort");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
e.HasOne(x => x.Scene).WithMany(x => x.ColorElements).HasForeignKey(x => x.SceneId).OnDelete(DeleteBehavior.Cascade);
});
mb.Entity<SceneColorPreset>(e =>
{
e.ToTable("scene_color_presets");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.SceneId).HasColumnName("scene_id");
e.Property(x => x.Name).HasColumnName("name");
e.Property(x => x.Sort).HasColumnName("sort");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.HasOne<Scene>().WithMany(x => x.ColorPresets).HasForeignKey(x => x.SceneId).OnDelete(DeleteBehavior.Cascade);
});
mb.Entity<SceneColorPresetItem>(e =>
{
e.ToTable("scene_color_preset_items");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.PresetId).HasColumnName("preset_id");
e.Property(x => x.ElementKey).HasColumnName("element_key").IsRequired();
e.Property(x => x.Value).HasColumnName("value").IsRequired();
e.Property(x => x.Sort).HasColumnName("sort");
e.HasOne(x => x.Preset).WithMany(x => x.Items).HasForeignKey(x => x.PresetId).OnDelete(DeleteBehavior.Cascade);
});
mb.Entity<SharedColor>(e =>
{
e.ToTable("shared_colors");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.ProjectId).HasColumnName("project_id");
e.Property(x => x.ElementKey).HasColumnName("element_key").IsRequired();
e.Property(x => x.Title).HasColumnName("title").IsRequired();
e.Property(x => x.Icon).HasColumnName("icon");
e.Property(x => x.AttrValue).HasColumnName("attr_value");
e.Property(x => x.DefaultColor).HasColumnName("default_color").IsRequired();
e.Property(x => x.Sort).HasColumnName("sort");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
e.HasOne(x => x.Project).WithMany(x => x.SharedColors).HasForeignKey(x => x.ProjectId).OnDelete(DeleteBehavior.Cascade);
});
mb.Entity<SharedColorPreset>(e =>
{
e.ToTable("shared_color_presets");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.ProjectId).HasColumnName("project_id");
e.Property(x => x.Name).HasColumnName("name");
e.Property(x => x.Sort).HasColumnName("sort");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.HasOne(x => x.Project).WithMany(x => x.SharedColorPresets).HasForeignKey(x => x.ProjectId).OnDelete(DeleteBehavior.Cascade);
});
mb.Entity<SharedColorPresetItem>(e =>
{
e.ToTable("shared_color_preset_items");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.PresetId).HasColumnName("preset_id");
e.Property(x => x.ElementKey).HasColumnName("element_key").IsRequired();
e.Property(x => x.Value).HasColumnName("value").IsRequired();
e.Property(x => x.Sort).HasColumnName("sort");
e.HasOne(x => x.Preset).WithMany(x => x.Items).HasForeignKey(x => x.PresetId).OnDelete(DeleteBehavior.Cascade);
});
mb.Entity<SharedLayer>(e =>
{
e.ToTable("shared_layers");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.ProjectId).HasColumnName("project_id");
e.Property(x => x.Key).HasColumnName("key").IsRequired();
e.Property(x => x.Title).HasColumnName("title").IsRequired();
e.Property(x => x.LocalizedTitle).HasColumnName("localized_title").HasColumnType("jsonb");
e.Property(x => x.Hint).HasColumnName("hint");
e.Property(x => x.Type).HasColumnName("type");
e.Property(x => x.DefaultValue).HasColumnName("default_value");
e.Property(x => x.FontId).HasColumnName("font_id");
e.Property(x => x.FontFace).HasColumnName("font_face");
e.Property(x => x.FontFaceName).HasColumnName("font_face_name");
e.Property(x => x.FontSize).HasColumnName("font_size");
e.Property(x => x.DefaultFontSize).HasColumnName("default_font_size");
e.Property(x => x.DefaultFontFace).HasColumnName("default_font_face");
e.Property(x => x.IsFontChangeable).HasColumnName("is_font_changeable");
e.Property(x => x.IsFontSizeChangeable).HasColumnName("is_font_size_changeable");
e.Property(x => x.Justify).HasColumnName("justify");
e.Property(x => x.CanJustify).HasColumnName("can_justify");
e.Property(x => x.PositionInContainer).HasColumnName("position_in_container");
e.Property(x => x.IsTextBox).HasColumnName("is_text_box");
e.Property(x => x.MaxSize).HasColumnName("max_size");
e.Property(x => x.DirectionLayerKey).HasColumnName("direction_layer_key");
e.Property(x => x.DirectionLayerValue).HasColumnName("direction_layer_value");
e.Property(x => x.VideoSupport).HasColumnName("video_support");
e.Property(x => x.MinDurationSec).HasColumnName("min_duration_sec");
e.Property(x => x.MaxDurationSec).HasColumnName("max_duration_sec");
e.Property(x => x.Width).HasColumnName("width");
e.Property(x => x.Height).HasColumnName("height");
e.Property(x => x.Thumbnail).HasColumnName("thumbnail");
e.Property(x => x.MappedList).HasColumnName("mapped_list").HasColumnType("jsonb");
e.Property(x => x.CounterMode).HasColumnName("counter_mode");
e.Property(x => x.AiInputType).HasColumnName("ai_input_type");
e.Property(x => x.IsHidden).HasColumnName("is_hidden");
e.Property(x => x.IsFocused).HasColumnName("is_focused");
e.Property(x => x.Dp1Image).HasColumnName("dp1_image");
e.Property(x => x.Dp1Title).HasColumnName("dp1_title");
e.Property(x => x.Dp2Image).HasColumnName("dp2_image");
e.Property(x => x.Dp2Title).HasColumnName("dp2_title");
e.Property(x => x.Dp3Image).HasColumnName("dp3_image");
e.Property(x => x.Dp3Title).HasColumnName("dp3_title");
e.Property(x => x.Dp4Image).HasColumnName("dp4_image");
e.Property(x => x.Dp4Title).HasColumnName("dp4_title");
e.Property(x => x.VirtualCount).HasColumnName("virtual_count");
e.Property(x => x.Sort).HasColumnName("sort");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
e.HasOne(x => x.Project).WithMany(x => x.SharedLayers).HasForeignKey(x => x.ProjectId).OnDelete(DeleteBehavior.Cascade);
});
mb.Entity<TemplateSvgPreview>(e =>
{
e.ToTable("template_svg_previews");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.ProjectId).HasColumnName("project_id");
e.Property(x => x.SceneId).HasColumnName("scene_id");
e.Property(x => x.SourceImageUrl).HasColumnName("source_image_url");
e.Property(x => x.SvgUrl).HasColumnName("svg_url").IsRequired();
e.Property(x => x.ThumbnailUrl).HasColumnName("thumbnail_url");
e.Property(x => x.ColorZones).HasColumnName("color_zones").HasColumnType("jsonb").IsRequired();
e.Property(x => x.Width).HasColumnName("width");
e.Property(x => x.Height).HasColumnName("height");
e.Property(x => x.GenerationMethod).HasColumnName("generation_method");
e.Property(x => x.GeneratedByAi).HasColumnName("generated_by_ai");
e.Property(x => x.QualityScore).HasColumnName("quality_score");
e.Property(x => x.CreatedByUserId).HasColumnName("created_by_user_id");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
});
}
private static void ConfigureCharacters(ModelBuilder mb)
{
mb.Entity<SceneCharacter>(e =>
{
e.ToTable("scene_characters");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.SceneId).HasColumnName("scene_id");
e.Property(x => x.Key).HasColumnName("key").IsRequired();
e.Property(x => x.Name).HasColumnName("name").IsRequired();
e.Property(x => x.Icon).HasColumnName("icon");
e.Property(x => x.Sort).HasColumnName("sort");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
e.HasOne(x => x.Scene).WithMany(x => x.Characters).HasForeignKey(x => x.SceneId).OnDelete(DeleteBehavior.Cascade);
});
mb.Entity<SceneCharacterController>(e =>
{
e.ToTable("scene_character_controllers");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.SceneCharacterId).HasColumnName("scene_character_id");
e.Property(x => x.Name).HasColumnName("name").IsRequired();
e.Property(x => x.Key).HasColumnName("key").IsRequired();
e.Property(x => x.DefaultValue).HasColumnName("default_value");
e.Property(x => x.Sort).HasColumnName("sort");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
e.HasOne(x => x.Character).WithMany(x => x.Controllers).HasForeignKey(x => x.SceneCharacterId).OnDelete(DeleteBehavior.Cascade);
});
mb.Entity<SceneControllerOption>(e =>
{
e.ToTable("scene_controller_options");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.ControllerId).HasColumnName("controller_id");
e.Property(x => x.Name).HasColumnName("name").IsRequired();
e.Property(x => x.Icon).HasColumnName("icon");
e.Property(x => x.Value).HasColumnName("value").IsRequired();
e.Property(x => x.Sort).HasColumnName("sort");
e.HasOne(x => x.Controller).WithMany(x => x.Options).HasForeignKey(x => x.ControllerId).OnDelete(DeleteBehavior.Cascade);
});
mb.Entity<ProjectCharacterController>(e =>
{
e.ToTable("project_character_controllers");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.ProjectId).HasColumnName("project_id");
e.Property(x => x.Name).HasColumnName("name").IsRequired();
e.Property(x => x.Key).HasColumnName("key").IsRequired();
e.Property(x => x.Sort).HasColumnName("sort");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
e.HasOne(x => x.Project).WithMany(x => x.CharacterControllers).HasForeignKey(x => x.ProjectId).OnDelete(DeleteBehavior.Cascade);
});
mb.Entity<ProjectCharacterControllerOption>(e =>
{
e.ToTable("project_character_controller_options");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.ControllerId).HasColumnName("controller_id");
e.Property(x => x.Name).HasColumnName("name").IsRequired();
e.Property(x => x.Icon).HasColumnName("icon");
e.Property(x => x.Value).HasColumnName("value").IsRequired();
e.Property(x => x.Sort).HasColumnName("sort");
e.HasOne(x => x.Controller).WithMany(x => x.Options).HasForeignKey(x => x.ControllerId).OnDelete(DeleteBehavior.Cascade);
});
mb.Entity<ProjectCharacterPreset>(e =>
{
e.ToTable("project_character_presets");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.ProjectId).HasColumnName("project_id");
e.Property(x => x.Key).HasColumnName("key");
e.Property(x => x.Name).HasColumnName("name").IsRequired();
e.Property(x => x.Icon).HasColumnName("icon");
e.Property(x => x.Sort).HasColumnName("sort");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
e.HasOne(x => x.Project).WithMany(x => x.CharacterPresets).HasForeignKey(x => x.ProjectId).OnDelete(DeleteBehavior.Cascade);
});
mb.Entity<PresetCharacterController>(e =>
{
e.ToTable("preset_character_controllers");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.CharacterPresetId).HasColumnName("character_preset_id");
e.Property(x => x.Name).HasColumnName("name").IsRequired();
e.Property(x => x.Key).HasColumnName("key").IsRequired();
e.Property(x => x.Value).HasColumnName("value").IsRequired();
e.Property(x => x.Sort).HasColumnName("sort");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.HasOne(x => x.Preset).WithMany(x => x.PresetControllers).HasForeignKey(x => x.CharacterPresetId).OnDelete(DeleteBehavior.Cascade);
});
mb.Entity<PresetStory>(e =>
{
e.ToTable("preset_stories");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.ProjectId).HasColumnName("project_id");
e.Property(x => x.Name).HasColumnName("name").IsRequired();
e.Property(x => x.Description).HasColumnName("description");
e.Property(x => x.Demo).HasColumnName("demo");
e.Property(x => x.MusicId).HasColumnName("music_id");
e.Property(x => x.ScenesSpa).HasColumnName("scenes_spa");
e.Property(x => x.Sort).HasColumnName("sort");
e.Property(x => x.IsPublished).HasColumnName("is_published");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
e.Property(x => x.DeletedAt).HasColumnName("deleted_at");
e.HasOne(x => x.Project).WithMany(x => x.PresetStories).HasForeignKey(x => x.ProjectId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Music).WithMany().HasForeignKey(x => x.MusicId).OnDelete(DeleteBehavior.SetNull);
e.HasQueryFilter(x => x.DeletedAt == null);
});
mb.Entity<PresetScene>(e =>
{
e.ToTable("preset_scenes");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.PresetStoryId).HasColumnName("preset_story_id");
e.Property(x => x.SceneId).HasColumnName("scene_id");
e.Property(x => x.Sort).HasColumnName("sort");
e.Property(x => x.DefaultDurationSec).HasColumnName("default_duration_sec");
e.HasOne(x => x.Story).WithMany(x => x.Scenes).HasForeignKey(x => x.PresetStoryId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Scene).WithMany().HasForeignKey(x => x.SceneId).OnDelete(DeleteBehavior.Cascade);
});
}
private static void ConfigureCms(ModelBuilder mb)
{
mb.Entity<Blog>(e =>
{
e.ToTable("blogs");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.TenantId).HasColumnName("tenant_id");
e.Property(x => x.Kind).HasColumnName("kind");
e.Property(x => x.Slug).HasColumnName("slug").HasColumnType("citext").IsRequired();
e.Property(x => x.Title).HasColumnName("title").IsRequired();
e.Property(x => x.ShortDescription).HasColumnName("short_description");
e.Property(x => x.Content).HasColumnName("content").IsRequired();
e.Property(x => x.MetaTitle).HasColumnName("meta_title");
e.Property(x => x.MetaDescription).HasColumnName("meta_description");
e.Property(x => x.MetaKeywords).HasColumnName("meta_keywords");
e.Property(x => x.IncludeInSiteMap).HasColumnName("include_in_site_map");
e.Property(x => x.Image).HasColumnName("image");
e.Property(x => x.Cover).HasColumnName("cover");
e.Property(x => x.AuthorUserId).HasColumnName("author_user_id");
e.Property(x => x.AuthorDisplayName).HasColumnName("author_display_name");
e.Property(x => x.IsPublished).HasColumnName("is_published");
e.Property(x => x.PublishDate).HasColumnName("publish_date");
e.Property(x => x.ViewCount).HasColumnName("view_count");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
e.Property(x => x.DeletedAt).HasColumnName("deleted_at");
e.HasQueryFilter(x => x.DeletedAt == null);
});
mb.Entity<Comment>(e =>
{
e.ToTable("comments");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.TenantId).HasColumnName("tenant_id");
e.Property(x => x.UserId).HasColumnName("user_id");
e.Property(x => x.BlogId).HasColumnName("blog_id");
e.Property(x => x.ContainerId).HasColumnName("container_id");
e.Property(x => x.ParentCommentId).HasColumnName("parent_comment_id");
e.Property(x => x.Content).HasColumnName("content").IsRequired();
e.Property(x => x.Rate).HasColumnName("rate");
e.Property(x => x.IsApproved).HasColumnName("is_approved");
e.Property(x => x.IsPinned).HasColumnName("is_pinned");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
e.Property(x => x.DeletedAt).HasColumnName("deleted_at");
e.HasOne(x => x.Blog).WithMany(x => x.Comments).HasForeignKey(x => x.BlogId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Container).WithMany(x => x.Comments).HasForeignKey(x => x.ContainerId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.ParentComment).WithMany(x => x.Replies).HasForeignKey(x => x.ParentCommentId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
mb.Entity<HomePageEvent>(e =>
{
e.ToTable("home_page_events");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.TenantId).HasColumnName("tenant_id");
e.Property(x => x.Title).HasColumnName("title");
e.Property(x => x.Subtitle).HasColumnName("subtitle");
e.Property(x => x.Description).HasColumnName("description");
e.Property(x => x.Badge).HasColumnName("badge");
e.Property(x => x.BadgeClass).HasColumnName("badge_class");
e.Property(x => x.ButtonText).HasColumnName("button_text");
e.Property(x => x.ButtonUrl).HasColumnName("button_url");
e.Property(x => x.ButtonClass).HasColumnName("button_class");
e.Property(x => x.Color).HasColumnName("color");
e.Property(x => x.BackgroundColor).HasColumnName("background_color");
e.Property(x => x.TextColor).HasColumnName("text_color");
e.Property(x => x.Image).HasColumnName("image");
e.Property(x => x.IsActive).HasColumnName("is_active");
e.Property(x => x.Sort).HasColumnName("sort");
e.Property(x => x.StartsAt).HasColumnName("starts_at");
e.Property(x => x.EndsAt).HasColumnName("ends_at");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
});
mb.Entity<NewSlide>(e =>
{
e.ToTable("new_slides");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.TenantId).HasColumnName("tenant_id");
e.Property(x => x.Keyword).HasColumnName("keyword");
e.Property(x => x.Title).HasColumnName("title");
e.Property(x => x.Image).HasColumnName("image");
e.Property(x => x.Parameter).HasColumnName("parameter");
e.Property(x => x.SlideType).HasColumnName("slide_type");
e.Property(x => x.ExpireDate).HasColumnName("expire_date");
e.Property(x => x.Sort).HasColumnName("sort");
e.Property(x => x.IsActive).HasColumnName("is_active");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
});
mb.Entity<InternalRoute>(e =>
{
e.ToTable("internal_routes");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.TenantId).HasColumnName("tenant_id");
e.Property(x => x.Name).HasColumnName("name");
e.Property(x => x.Image).HasColumnName("image");
e.Property(x => x.Slug).HasColumnName("slug").HasColumnType("citext").IsRequired();
e.Property(x => x.Priority).HasColumnName("priority");
e.Property(x => x.LastDate).HasColumnName("last_date");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
});
mb.Entity<CustomRoute>(e =>
{
e.ToTable("custom_routes");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.TenantId).HasColumnName("tenant_id");
e.Property(x => x.Target).HasColumnName("target").IsRequired();
e.Property(x => x.Destination).HasColumnName("destination").IsRequired();
e.Property(x => x.RedirectCode).HasColumnName("redirect_code");
e.Property(x => x.IsActive).HasColumnName("is_active");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
});
mb.Entity<WebsiteSetting>(e =>
{
e.ToTable("website_settings");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.TenantId).HasColumnName("tenant_id");
e.Property(x => x.Key).HasColumnName("key").IsRequired();
e.Property(x => x.Value).HasColumnName("value").HasColumnType("jsonb").IsRequired();
e.Property(x => x.Description).HasColumnName("description");
e.Property(x => x.IsSecret).HasColumnName("is_secret");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
});
mb.Entity<LearnArticle>(e =>
{
e.ToTable("learn");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.TenantId).HasColumnName("tenant_id");
e.Property(x => x.Title).HasColumnName("title").IsRequired();
e.Property(x => x.Body).HasColumnName("body");
e.Property(x => x.DemoUrl).HasColumnName("demo_url");
e.Property(x => x.Mode).HasColumnName("mode");
e.Property(x => x.Sort).HasColumnName("sort");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
});
mb.Entity<Training>(e =>
{
e.ToTable("trainings");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.TenantId).HasColumnName("tenant_id");
e.Property(x => x.Title).HasColumnName("title").IsRequired();
e.Property(x => x.Description).HasColumnName("description");
e.Property(x => x.VideoUrl).HasColumnName("video_url");
e.Property(x => x.ThumbnailUrl).HasColumnName("thumbnail_url");
e.Property(x => x.Sort).HasColumnName("sort");
e.Property(x => x.IsPublished).HasColumnName("is_published");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
});
mb.Entity<FavoriteFolder>(e =>
{
e.ToTable("favorite_folders");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.UserId).HasColumnName("user_id");
e.Property(x => x.TenantId).HasColumnName("tenant_id");
e.Property(x => x.Name).HasColumnName("name").IsRequired();
e.Property(x => x.Description).HasColumnName("description");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
});
mb.Entity<FavoriteContainer>(e =>
{
e.ToTable("favorite_containers");
e.HasKey(x => x.Id);
e.Property(x => x.Id).HasColumnName("id");
e.Property(x => x.UserId).HasColumnName("user_id");
e.Property(x => x.TenantId).HasColumnName("tenant_id");
e.Property(x => x.ContainerId).HasColumnName("container_id");
e.Property(x => x.FolderId).HasColumnName("folder_id");
e.Property(x => x.Note).HasColumnName("note");
e.Property(x => x.CreatedAt).HasColumnName("created_at");
e.HasOne(x => x.Container).WithMany().HasForeignKey(x => x.ContainerId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Folder).WithMany(x => x.Containers).HasForeignKey(x => x.FolderId).OnDelete(DeleteBehavior.SetNull);
});
}
}
@@ -0,0 +1,18 @@
using Npgsql;
namespace FlatRender.ContentSvc.Infrastructure.Data;
/// <summary>
/// Npgsql name translator that returns CLR names verbatim. The database enum labels
/// match the C# enum member names exactly (e.g. 'FIX', 'MockUp', 'fill', 'LEFT_JUSTIFY'),
/// so no snake_case translation may be applied to enum values. PG type names are passed
/// explicitly wherever this translator is used, so type-name translation is moot.
/// </summary>
public sealed class PreserveCaseNameTranslator : INpgsqlNameTranslator
{
public static readonly PreserveCaseNameTranslator Instance = new();
public string TranslateTypeName(string clrName) => clrName;
public string TranslateMemberName(string clrName) => clrName;
}
@@ -0,0 +1,44 @@
using System.Text.Json;
using FlatRender.ContentSvc.Models.Responses;
namespace FlatRender.ContentSvc.Middleware;
public class ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
public async Task InvokeAsync(HttpContext ctx)
{
try
{
await next(ctx);
}
catch (Exception ex)
{
logger.LogError(ex, "Unhandled exception");
await WriteError(ctx, ex);
}
}
private static Task WriteError(HttpContext ctx, Exception ex)
{
var (status, code) = ex switch
{
KeyNotFoundException => (404, "not_found"),
UnauthorizedAccessException => (401, "unauthorized"),
InvalidOperationException => (400, "invalid_operation"),
ArgumentException => (400, "bad_request"),
NotImplementedException => (501, "not_implemented"),
_ => (500, "internal_error")
};
ctx.Response.StatusCode = status;
ctx.Response.ContentType = "application/json";
var error = new { error = new ApiError(code, ex.Message, ctx.TraceIdentifier) };
return ctx.Response.WriteAsync(JsonSerializer.Serialize(error, JsonOptions));
}
}
@@ -0,0 +1,300 @@
namespace FlatRender.ContentSvc.Models.Requests;
// ── Taxonomy ─────────────────────────────────────────────────────────────────
public record CreateCategoryRequest(
Guid? ParentId,
string Name,
string Slug,
string? Description,
string? ImageUrl,
string? Icon,
string? MetaTitle,
string? MetaDescription,
string? MetaKeywords,
bool BotFollow,
int Sort,
bool IsActive
);
public record UpdateCategoryRequest(
Guid? ParentId,
string Name,
string Slug,
string? Description,
string? ImageUrl,
string? Icon,
string? MetaTitle,
string? MetaDescription,
string? MetaKeywords,
bool BotFollow,
int Sort,
bool IsActive
);
public record CreateTagRequest(
string Name,
string? LatinName,
string Slug,
string? AppliesToMode,
bool IsActive
);
public record UpdateTagRequest(
string Name,
string? LatinName,
string Slug,
string? AppliesToMode,
bool IsActive
);
public record CreateFontRequest(
string Name,
string? OriginalName,
string? SystemName,
string? Family,
int? Weight,
string? Style,
string Direction,
string? FileUrl,
string? SampleImageUrl,
bool IsPremium,
bool IsActive,
bool InstalledOnNodes,
int Sort
);
public record UpdateFontRequest(
string Name,
string? OriginalName,
string? SystemName,
string? Family,
int? Weight,
string? Style,
string Direction,
string? FileUrl,
string? SampleImageUrl,
bool IsPremium,
bool IsActive,
bool InstalledOnNodes,
int Sort
);
public record CreateMusicTrackRequest(
string Name,
string? Caption,
string? Keywords,
string Url,
string? WaveformData,
decimal DurationSec,
int? Bpm,
string? Genre,
string? Mood,
bool IsPremium,
bool IsActive,
int Sort
);
public record UpdateMusicTrackRequest(
string Name,
string? Caption,
string? Keywords,
string Url,
string? WaveformData,
decimal DurationSec,
int? Bpm,
string? Genre,
string? Mood,
bool IsPremium,
bool IsActive,
int Sort
);
// ── Templates ────────────────────────────────────────────────────────────────
public record CreateContainerRequest(
string Slug,
string Name,
string? Description,
string? Keywords,
string? NewsText,
string? Image,
string? Demo,
string? FullDemo,
string? MiniDemo,
string? DemoScriptTag,
bool IsPublished,
bool IsPremium,
bool IsMockup,
string PrimaryMode,
int Sort,
List<Guid> CategoryIds,
List<Guid> TagIds
);
public record UpdateContainerRequest(
string Slug,
string Name,
string? Description,
string? Keywords,
string? NewsText,
string? Image,
string? Demo,
string? FullDemo,
string? MiniDemo,
string? DemoScriptTag,
bool IsPublished,
bool IsPremium,
bool IsMockup,
string PrimaryMode,
int Sort,
List<Guid> CategoryIds,
List<Guid> TagIds
);
public record ContainerListRequest(
int Page = 1,
int PageSize = 20,
string? Search = null,
Guid? CategoryId = null,
string? TagSlug = null,
bool? IsPublished = null,
bool? IsPremium = null,
string? Mode = null,
string? Sort = "sort_date_desc"
);
public record CreateProjectRequest(
Guid ContainerId,
Guid? ProjectServerId,
string Name,
string? Description,
string? Image,
string? FullDemo,
string? DemoScriptTag,
string? DownloadLink,
string? Folder,
int OriginalWidth,
int OriginalHeight,
string? Aspect,
decimal ProjectDurationSec,
decimal? MinDurationSec,
decimal? MaxDurationSec,
int FreeFps,
string ChooseMode,
string Resolution,
decimal VipFactor,
string RenderAepComp,
bool IsPublished,
int Sort
);
public record UpdateProjectRequest(
string Name,
string? Description,
string? Image,
string? FullDemo,
string? DemoScriptTag,
string? DownloadLink,
string? Folder,
int OriginalWidth,
int OriginalHeight,
string? Aspect,
decimal ProjectDurationSec,
decimal? MinDurationSec,
decimal? MaxDurationSec,
int FreeFps,
string ChooseMode,
string Resolution,
decimal VipFactor,
string RenderAepComp,
string? SharedLayerImage,
string? SharedColorsSvg,
string? SharedColorPresetsSvg,
bool IsPublished,
int Sort
);
// ── CMS ──────────────────────────────────────────────────────────────────────
public record CreateBlogRequest(
string Slug,
string Title,
string? ShortDescription,
string Content,
string? MetaTitle,
string? MetaDescription,
string? MetaKeywords,
bool IncludeInSiteMap,
string? Image,
string? Cover,
string? AuthorDisplayName,
bool IsPublished,
DateTime? PublishDate,
string Kind = "Blog"
);
public record UpdateBlogRequest(
string Slug,
string Title,
string? ShortDescription,
string Content,
string? MetaTitle,
string? MetaDescription,
string? MetaKeywords,
bool IncludeInSiteMap,
string? Image,
string? Cover,
string? AuthorDisplayName,
bool IsPublished,
DateTime? PublishDate
);
public record BlogListRequest(
int Page = 1,
int PageSize = 20,
string? Search = null,
bool? IsPublished = null,
string Kind = "Blog"
);
public record CreateCommentRequest(
Guid? BlogId,
Guid? ContainerId,
Guid? ParentCommentId,
string Content,
decimal? Rate
);
public record CreateSlideRequest(
string? Keyword,
string? Title,
string? Image,
string? Parameter,
string SlideType,
DateTime? ExpireDate,
int Sort,
bool IsActive
);
public record UpdateSlideRequest(
string? Keyword,
string? Title,
string? Image,
string? Parameter,
string SlideType,
DateTime? ExpireDate,
int Sort,
bool IsActive
);
public record UpsertWebsiteSettingRequest(
string Key,
string Value,
string? Description,
bool IsSecret
);
public record CreateFavoriteFolderRequest(string Name, string? Description);
public record UpdateFavoriteFolderRequest(string Name, string? Description);
public record AddFavoriteContainerRequest(Guid ContainerId, Guid? FolderId, string? Note);
@@ -0,0 +1,452 @@
using FlatRender.ContentSvc.Domain.Enums;
namespace FlatRender.ContentSvc.Models.Responses;
// ── Pagination ───────────────────────────────────────────────────────────────
public record PagedResponse<T>(IEnumerable<T> Items, PaginationMeta Meta);
public record PaginationMeta(int Page, int PageSize, long Total, int TotalPages);
public record ApiError(string Code, string Message, string? TraceId = null);
// ── Taxonomy ─────────────────────────────────────────────────────────────────
public record CategoryResponse(
Guid Id,
Guid? ParentId,
string Name,
string Slug,
string? Description,
string? ImageUrl,
string? Icon,
bool IsActive,
int Sort,
List<CategoryResponse> Children
);
public record TagResponse(
Guid Id,
string Name,
string? LatinName,
string Slug,
string? AppliesToMode,
bool IsActive
);
public record FontResponse(
Guid Id,
string Name,
string? OriginalName,
string? SystemName,
string? Family,
int? Weight,
string? Style,
string Direction,
string? FileUrl,
string? SampleImageUrl,
bool IsPremium,
bool IsActive,
bool InstalledOnNodes
);
public record MusicTrackResponse(
Guid Id,
string Name,
string? Caption,
string Url,
string? WaveformData,
decimal DurationSec,
int? Bpm,
string? Genre,
string? Mood,
bool IsPremium
);
// ── Templates ────────────────────────────────────────────────────────────────
public record ContainerSummaryResponse(
Guid Id,
string Slug,
string Name,
string? Description,
string? Image,
string? Demo,
string? MiniDemo,
bool IsPublished,
bool IsPremium,
bool IsMockup,
string PrimaryMode,
decimal? RateAvg,
int RateCount,
long ViewCount,
long UseCount,
int Sort,
DateTime SortDate,
List<string> CategorySlugs,
List<string> Tags
);
public record ContainerDetailResponse(
Guid Id,
string Slug,
string Name,
string? Description,
string? Keywords,
string? NewsText,
string? Image,
string? Demo,
string? FullDemo,
string? MiniDemo,
string? DemoScriptTag,
bool IsPublished,
bool IsPremium,
bool IsMockup,
string PrimaryMode,
decimal? RateAvg,
int RateCount,
long ViewCount,
long UseCount,
int Sort,
DateTime SortDate,
List<ProjectResponse> Projects,
List<CategoryResponse> Categories,
List<TagResponse> Tags
);
public record ProjectResponse(
Guid Id,
Guid ContainerId,
string Name,
string? Image,
string? FullDemo,
int OriginalWidth,
int OriginalHeight,
string? Aspect,
decimal ProjectDurationSec,
decimal? MinDurationSec,
decimal? MaxDurationSec,
int FreeFps,
string ChooseMode,
string Resolution,
bool IsPublished,
int Sort
);
public record ProjectDetailResponse(
Guid Id,
Guid ContainerId,
string Name,
string? Description,
string? Image,
string? FullDemo,
string? DemoScriptTag,
string? DownloadLink,
int OriginalWidth,
int OriginalHeight,
string? Aspect,
decimal ProjectDurationSec,
decimal? MinDurationSec,
decimal? MaxDurationSec,
int FreeFps,
string ChooseMode,
string Resolution,
decimal VipFactor,
string RenderAepComp,
string? SharedLayerImage,
bool IsPublished,
int Sort,
List<SceneResponse> Scenes,
List<SharedColorResponse> SharedColors,
List<SharedLayerResponse> SharedLayers
);
// ── Scenes ───────────────────────────────────────────────────────────────────
public record SceneResponse(
Guid Id,
Guid ProjectId,
string Key,
string Title,
string? LocalizedTitle,
string SceneType,
string? Image,
string? Demo,
string? SnapshotUrl,
bool GenerateKf,
decimal? DefaultDurationSec,
decimal? MinDurationSec,
decimal? MaxDurationSec,
decimal OverlapAtEndSec,
bool CanHandleDuration,
bool ManualColorSelection,
int Sort,
bool IsActive
);
public record SceneDetailResponse(
Guid Id,
Guid ProjectId,
string Key,
string Title,
string? LocalizedTitle,
string SceneType,
string? Image,
string? Demo,
string? SnapshotUrl,
bool GenerateKf,
decimal? DefaultDurationSec,
decimal? MinDurationSec,
decimal? MaxDurationSec,
decimal OverlapAtEndSec,
bool CanHandleDuration,
bool ManualColorSelection,
int Sort,
bool IsActive,
List<RepeaterItemResponse> RepeaterItems,
List<ContentElementResponse> ContentElements,
List<ColorElementResponse> ColorElements,
List<ColorPresetResponse> ColorPresets,
List<CharacterResponse> Characters
);
public record RepeaterItemResponse(
Guid Id,
string Title,
string RepeatBoxKey,
string RepeatItemKey,
int MaxRepeatCount,
bool UserCanChangeSort,
string RepeatSortStrategy,
int Sort,
List<ContentElementResponse> ContentElements
);
public record ContentElementResponse(
Guid Id,
Guid? RepeaterItemId,
string Key,
string Title,
string? LocalizedTitle,
string? Hint,
string Type,
string? DefaultValue,
Guid? FontId,
string? FontFace,
string? FontFaceName,
int? FontSize,
int? DefaultFontSize,
string? DefaultFontFace,
bool IsFontChangeable,
bool IsFontSizeChangeable,
string Justify,
bool CanJustify,
int PositionInContainer,
bool IsTextBox,
int? MaxSize,
string? DirectionLayerKey,
int DirectionLayerValue,
bool VideoSupport,
decimal? MinDurationSec,
decimal? MaxDurationSec,
int? Width,
int? Height,
string? Thumbnail,
string? MappedList,
string? CounterMode,
string AiInputType,
bool IsHidden,
bool IsFocused,
string? OpacityControllerKey,
int VirtualCount,
int Sort
);
public record ColorElementResponse(
Guid Id,
string ElementKey,
string Title,
string? Icon,
string AttrValue,
string DefaultColor,
int Sort
);
public record ColorPresetResponse(
Guid Id,
string? Name,
int Sort,
List<ColorPresetItemResponse> Items
);
public record ColorPresetItemResponse(
Guid Id,
string ElementKey,
string Value,
int Sort
);
public record SharedColorResponse(
Guid Id,
string ElementKey,
string Title,
string? Icon,
string AttrValue,
string DefaultColor,
int Sort
);
public record SharedLayerResponse(
Guid Id,
string Key,
string Title,
string? LocalizedTitle,
string? Hint,
string Type,
string? DefaultValue,
Guid? FontId,
string? FontFace,
int? FontSize,
bool IsFontChangeable,
bool IsFontSizeChangeable,
string Justify,
bool CanJustify,
int PositionInContainer,
bool IsTextBox,
int? MaxSize,
bool VideoSupport,
decimal? MinDurationSec,
decimal? MaxDurationSec,
int? Width,
int? Height,
string? MappedList,
string AiInputType,
bool IsHidden,
bool IsFocused,
int VirtualCount,
int Sort
);
// ── Characters ───────────────────────────────────────────────────────────────
public record CharacterResponse(
Guid Id,
string Key,
string Name,
string? Icon,
int Sort,
List<CharacterControllerResponse> Controllers
);
public record CharacterControllerResponse(
Guid Id,
string Name,
string Key,
string? DefaultValue,
int Sort,
List<ControllerOptionResponse> Options
);
public record ControllerOptionResponse(
Guid Id,
string Name,
string? Icon,
string Value,
int Sort
);
// ── CMS ──────────────────────────────────────────────────────────────────────
public record BlogSummaryResponse(
Guid Id,
string Slug,
string Title,
string? ShortDescription,
string? Image,
string? Cover,
string? AuthorDisplayName,
bool IsPublished,
DateTime? PublishDate,
long ViewCount,
DateTime CreatedAt
);
public record BlogDetailResponse(
Guid Id,
string Slug,
string Title,
string? ShortDescription,
string Content,
string? MetaTitle,
string? MetaDescription,
string? MetaKeywords,
bool IncludeInSiteMap,
string? Image,
string? Cover,
Guid? AuthorUserId,
string? AuthorDisplayName,
bool IsPublished,
DateTime? PublishDate,
long ViewCount,
DateTime CreatedAt,
DateTime UpdatedAt
);
public record CommentResponse(
Guid Id,
Guid UserId,
Guid? BlogId,
Guid? ContainerId,
Guid? ParentCommentId,
string Content,
decimal? Rate,
bool IsApproved,
bool IsPinned,
DateTime CreatedAt
);
public record SlideResponse(
Guid Id,
string? Keyword,
string? Title,
string? Image,
string? Parameter,
string SlideType,
DateTime? ExpireDate,
int Sort,
bool IsActive
);
public record HomePageEventResponse(
Guid Id,
string? Title,
string? Subtitle,
string? Description,
string? Badge,
string? BadgeClass,
string? ButtonText,
string? ButtonUrl,
string? ButtonClass,
string? Color,
string? BackgroundColor,
string? TextColor,
string? Image,
bool IsActive,
int Sort,
DateTime? StartsAt,
DateTime? EndsAt
);
public record WebsiteSettingResponse(
Guid Id,
string Key,
string Value,
string? Description,
bool IsSecret
);
public record FavoriteFolderResponse(
Guid Id,
string Name,
string? Description,
int ContainerCount,
DateTime CreatedAt
);
@@ -0,0 +1,127 @@
using System.Text;
using FlatRender.ContentSvc.Application.Services;
using FlatRender.ContentSvc.Domain.Enums;
using FlatRender.ContentSvc.Infrastructure.Data;
using FlatRender.ContentSvc.Middleware;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using Npgsql;
var builder = WebApplication.CreateBuilder(args);
// ── Database ──────────────────────────────────────────────────────────────────
// Native PostgreSQL enums are mapped on the EF provider so Npgsql can read/write
// them at runtime (HasPostgresEnum in the model alone is not enough on Npgsql 8+).
// PG labels match the C# enum member names exactly, so preserve case verbatim.
var enumTr = PreserveCaseNameTranslator.Instance;
builder.Services.AddDbContext<ContentDbContext>(options =>
options.UseNpgsql(
builder.Configuration.GetConnectionString("Postgres"),
npgsql =>
{
npgsql.MapEnum<ChooseMode>("choose_mode", "content", enumTr);
npgsql.MapEnum<ResolutionKind>("resolution_kind", "content", enumTr);
npgsql.MapEnum<SceneKind>("scene_kind", "content", enumTr);
npgsql.MapEnum<ContentElementType>("content_element_type", "content", enumTr);
npgsql.MapEnum<JustifyKind>("justify_kind", "content", enumTr);
npgsql.MapEnum<AiInputType>("ai_input_type", "content", enumTr);
npgsql.MapEnum<RepeatSortStrategy>("repeat_sort_strategy", "content", enumTr);
npgsql.MapEnum<AttrValueKind>("attr_value_kind", "content", enumTr);
npgsql.MapEnum<BlogKind>("blog_kind", "content", enumTr);
npgsql.MapEnum<SlideType>("slide_type", "content", enumTr);
})
.UseSnakeCaseNamingConvention());
// ── JWT Auth ──────────────────────────────────────────────────────────────────
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"]!))
};
});
builder.Services.AddAuthorization();
// ── Application Services ──────────────────────────────────────────────────────
builder.Services.AddScoped<TaxonomyService>();
builder.Services.AddScoped<TemplateService>();
builder.Services.AddScoped<CmsService>();
// ── HTTP ──────────────────────────────────────────────────────────────────────
builder.Services.AddRouting(opts =>
{
opts.LowercaseUrls = true;
opts.AppendTrailingSlash = false; // prevent 301 redirects from gateway calls
});
builder.Services.AddControllers()
.AddJsonOptions(opts =>
{
opts.JsonSerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.SnakeCaseLower;
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "FlatRender Content API", Version = "v1" });
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
Description = "JWT Bearer token"
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } },
Array.Empty<string>()
}
});
});
builder.Services.AddHealthChecks()
.AddCheck("db", () => HealthCheckResult.Healthy());
builder.Services.AddCors(opts => opts.AddDefaultPolicy(p =>
p.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()));
// ── Build ─────────────────────────────────────────────────────────────────────
var app = builder.Build();
app.UseMiddleware<ExceptionMiddleware>();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ContentDbContext>();
db.Database.Migrate();
}
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("/health", new HealthCheckOptions { AllowCachingResponses = false });
app.Run();
@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5088",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7250;http://localhost:5088",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
@@ -0,0 +1,19 @@
{
"ConnectionStrings": {
"Postgres": "Host=localhost;Port=5432;Database=flatrender;Username=postgres;Password=postgres;Search Path=content,public"
},
"Jwt": {
"Secret": "your-256-bit-secret-key-change-in-production",
"Issuer": "flatrender",
"Audience": "flatrender",
"AccessTokenExpiryMinutes": 15,
"RefreshTokenExpiryDays": 30
},
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.AspNetCore": "Information",
"Microsoft.EntityFrameworkCore": "Information"
}
}
}
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="http://171.22.25.73:8081/repository/nuget-group/index.json" protocolVersion="3" allowInsecureConnections="true" />
</packageSources>
</configuration>
+30
View File
@@ -0,0 +1,30 @@
version: "3.9"
services:
postgres:
image: postgres:16
environment:
POSTGRES_DB: flatrender
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"
volumes:
- pg_content_data:/var/lib/postgresql/data
- ../../backend/db/migrations:/docker-entrypoint-initdb.d:ro
content-svc:
build: .
ports:
- "5011:8080"
environment:
ASPNETCORE_ENVIRONMENT: Development
ConnectionStrings__Postgres: "Host=postgres;Port=5432;Database=flatrender;Username=postgres;Password=postgres;Search Path=content,public"
Jwt__Secret: "dev-secret-32-chars-minimum-here!!"
Jwt__Issuer: "flatrender"
Jwt__Audience: "flatrender"
depends_on:
- postgres
volumes:
pg_content_data: