-
Notifications
You must be signed in to change notification settings - Fork 53
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #148 from fluentcms/138-revise-pageservice-based-o…
…n-latest-changes-for-siteservice-and-hostservice 138 revise pageservice based on latest changes for siteservice and hostservice
- Loading branch information
Showing
15 changed files
with
253 additions
and
109 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,5 @@ | |
|
||
public class PageSearchRequest | ||
{ | ||
public Guid? ParentId { get; set; } | ||
public Guid SiteId { get; set; } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,107 +1,204 @@ | ||
using FluentCMS.Entities; | ||
using FluentCMS.Repositories.Abstractions; | ||
using System.Collections.Immutable; | ||
|
||
namespace FluentCMS.Services; | ||
|
||
public interface IPageService | ||
{ | ||
Task<IEnumerable<Page>> GetBySiteId(Guid siteId, CancellationToken cancellationToken = default); | ||
Task<Page> GetById(Guid id, CancellationToken cancellationToken = default); | ||
Task<IEnumerable<Page>> GetByParentId(Guid id); | ||
Task<Page> Create(Page page, CancellationToken cancellationToken = default); | ||
Task<Page> Update(Page page, CancellationToken cancellationToken = default); | ||
Task Delete(Page page, CancellationToken cancellationToken = default); | ||
Task<IEnumerable<Page>> GetBySiteIdAndParentId(Guid siteId, Guid? parentId = null); | ||
Task Delete(Guid id, CancellationToken cancellationToken = default); | ||
} | ||
|
||
public class PageService : IPageService | ||
public class PageService : BaseService<Page>, IPageService | ||
{ | ||
private readonly IPageRepository _pageRepository; | ||
private readonly IApplicationContext _applicationContext; | ||
private readonly ISiteRepository _siteRepository; | ||
|
||
public PageService(IPageRepository pageRepository, IApplicationContext applicationContext) | ||
public PageService(IApplicationContext applicationContext, IPageRepository pageRepository, ISiteRepository siteRepository) : base(applicationContext) | ||
{ | ||
_pageRepository = pageRepository; | ||
_applicationContext = applicationContext; | ||
_siteRepository = siteRepository; | ||
} | ||
|
||
public async Task<IEnumerable<Page>> GetBySiteId(Guid siteId, CancellationToken cancellationToken = default) | ||
public async Task<Page> Create(Page page, CancellationToken cancellationToken = default) | ||
{ | ||
return await _pageRepository.GetBySiteId(siteId, cancellationToken); | ||
} | ||
// Check if site id exists | ||
var site = (await _siteRepository.GetById(page.SiteId, cancellationToken)) ?? | ||
throw new AppException(ExceptionCodes.SiteNotFound); | ||
|
||
public async Task<Page> GetById(Guid id, CancellationToken cancellationToken = default) | ||
{ | ||
var page = await _pageRepository.GetById(id, cancellationToken) ?? throw new Exception(id.ToString()); | ||
// check if user is siteAdmin or superAdmin | ||
if (!Current.IsInRole(site.AdminRoleIds)) | ||
throw new AppPermissionException(); | ||
|
||
return page; | ||
// Fetch pages beforehand to avoid multiple db calls | ||
var pages = (await _pageRepository.GetAll(cancellationToken)).ToList(); | ||
|
||
//If Parent Id is assigned | ||
if (page.ParentId != null) | ||
ValidateParentPage(page, pages); | ||
|
||
// fetch list of all pages | ||
ValidateUrl(page, pages); | ||
|
||
// prepare entity for db | ||
PrepareForCreate(page); | ||
|
||
return await _pageRepository.Create(page, cancellationToken) ?? | ||
throw new AppException(ExceptionCodes.PageUnableToCreate); | ||
} | ||
|
||
public async Task<Page> Create(Page page, CancellationToken cancellationToken = default) | ||
private static void ValidateUrl(Page page, List<Page> pages) | ||
{ | ||
// TODO: check permissions, only admins can create a page | ||
// Except for the first site, which is created by the system | ||
//urls can be cached in a dictionary to avoid multiple list traversal | ||
var cachedUrls = new Dictionary<Guid, string>(); | ||
|
||
// normalizing the page path to lowercase | ||
NormalizePath(page); | ||
await CheckForDuplicatePath(page); | ||
// Build Url based on parent | ||
var fullUrl = BuildFullPath(page, pages, cachedUrls); | ||
|
||
page.CreatedBy = _applicationContext.Current?.User?.UserName ?? string.Empty; | ||
page.LastUpdatedBy = _applicationContext.Current?.User?.UserName ?? string.Empty; | ||
// Check if url is unique | ||
if (pages.Any(x => BuildFullPath(x, pages, cachedUrls).Equals(fullUrl))) | ||
throw new AppException(ExceptionCodes.PageUrlNotUnique); | ||
|
||
var newPage = await _pageRepository.Create(page, cancellationToken); | ||
return newPage ?? throw new Exception("Page not created"); | ||
} | ||
|
||
private async Task CheckForDuplicatePath(Page page) | ||
private static string BuildFullPath(Page page, IEnumerable<Page> pages, Dictionary<Guid, string> cachedUrls) | ||
{ | ||
// check if the page path is unique | ||
var samePathPage = await _pageRepository.GetByPath(page.Path); | ||
if (samePathPage != null && samePathPage?.Id != page.Id) | ||
//Traverse the pages to root (ParentId == null) and keep them in an array | ||
var parents = new List<string> { page.Path }; | ||
var parentId = page.ParentId; | ||
|
||
while (parentId != null) | ||
{ | ||
throw new ApplicationException("Page path must be unique"); | ||
if (cachedUrls.ContainsKey(parentId.Value)) | ||
{ | ||
parents.Add(cachedUrls[parentId.Value]); | ||
continue; | ||
} | ||
var parent = pages.Single(x => x.Id == parentId); | ||
parents.Add(parent.Path); | ||
cachedUrls[parent.Id] = parent.Path; | ||
parentId = parent.ParentId; | ||
} | ||
//Build Path string from List of Parents | ||
return string.Join("/", parents.Reverse<string>()); | ||
} | ||
|
||
public async Task<Page> Update(Page page, CancellationToken cancellationToken = default) | ||
private void ValidateParentPage(Page page, List<Page> pages) | ||
{ | ||
// TODO: check permissions, only admins can create a page | ||
// Except for the first site, which is created by the system | ||
var parent = pages.SingleOrDefault(x => x.Id == page.ParentId); | ||
|
||
// normalizing the page path to lowercase | ||
NormalizePath(page); | ||
// If parent id is not a valid page id | ||
if (parent is null) | ||
throw new AppException(ExceptionCodes.PageParentPageNotFound); | ||
|
||
// check if the page path is unique | ||
await CheckForDuplicatePath(page); | ||
// If parent id is not on the same site | ||
if (parent.SiteId != page.SiteId) | ||
throw new AppException(ExceptionCodes.PageParentMustBeOnTheSameSite); | ||
|
||
var newPage = await _pageRepository.Update(page, cancellationToken); | ||
return newPage ?? throw new Exception("Page not created"); | ||
// if page viewRoles are a subset of parent view roles | ||
if (!page.ViewRoleIds.ToImmutableHashSet().IsSubsetOf(parent.ViewRoleIds)) | ||
throw new AppException(ExceptionCodes.PageViewPermissionsAreNotASubsetOfParent); | ||
} | ||
|
||
private static void NormalizePath(Page page) | ||
public async Task Delete(Guid id, CancellationToken cancellationToken = default) | ||
{ | ||
page.Path = page.Path.ToLower(); | ||
//fetch original page from db | ||
var originalPage = await _pageRepository.GetById(id, cancellationToken) ?? | ||
throw new AppException(ExceptionCodes.PageNotFound); | ||
|
||
// fetch site | ||
var site = (await _siteRepository.GetById(originalPage.SiteId, cancellationToken)) ?? | ||
throw new AppException(ExceptionCodes.SiteNotFound); | ||
|
||
// check if user is siteAdmin, superAdmin or PageAdmin | ||
if (!HasAdminPermission(site, originalPage)) | ||
throw new AppPermissionException(); | ||
|
||
// check that it does not have any children | ||
var pages = (await _pageRepository.GetAll(cancellationToken)).ToList(); | ||
if (pages.Any(x => x.ParentId == id && x.SiteId == originalPage.SiteId)) | ||
throw new AppException(ExceptionCodes.PageHasChildren); | ||
|
||
await _pageRepository.Delete(id, cancellationToken); | ||
} | ||
|
||
public Task Delete(Page page, CancellationToken cancellationToken = default) | ||
public async Task<Page> GetById(Guid id, CancellationToken cancellationToken = default) | ||
{ | ||
return _pageRepository.Delete(page.Id); | ||
//fetch page from db | ||
var page = await _pageRepository.GetById(id, cancellationToken) ?? | ||
throw new AppException(ExceptionCodes.PageNotFound); | ||
|
||
// fetch site | ||
var site = (await _siteRepository.GetById(page.SiteId, cancellationToken)) ?? | ||
throw new AppException(ExceptionCodes.SiteNotFound); | ||
|
||
// check current user has permission to view page or is site admin or page admin | ||
return HasViewPermission(site, page) ? page : throw new AppPermissionException(); | ||
} | ||
|
||
public Task<IEnumerable<Page>> GetByParentId(Guid id) | ||
public async Task<IEnumerable<Page>> GetBySiteId(Guid siteId, CancellationToken cancellationToken = default) | ||
{ | ||
return _pageRepository.GetByParentId(id); | ||
//fetch site from db | ||
var site = await _siteRepository.GetById(siteId, cancellationToken) ?? | ||
throw new AppException(ExceptionCodes.SiteNotFound); | ||
|
||
// fetch pages from db | ||
var pages = await _pageRepository.GetBySiteId(siteId, cancellationToken); | ||
|
||
// if current user is page viewer or page admin or site admin | ||
return pages.Where(page => HasViewPermission(site, page)); | ||
} | ||
|
||
public async Task<IEnumerable<Page>> GetBySiteIdAndParentId(Guid siteId, Guid? parentId) | ||
public async Task<Page> Update(Page page, CancellationToken cancellationToken = default) | ||
{ | ||
var sitePages = await _pageRepository.GetBySiteIdAndParentId(siteId); | ||
if (parentId is null) | ||
//fetch original page from db | ||
var originalPage = await _pageRepository.GetById(page.Id, cancellationToken) ?? | ||
throw new AppException(ExceptionCodes.PageNotFound); | ||
|
||
// Check if site id exists | ||
var site = (await _siteRepository.GetById(page.SiteId, cancellationToken)) ?? | ||
throw new AppException(ExceptionCodes.SiteNotFound); | ||
|
||
// check if user is siteAdmin or superAdmin or pageAdmin | ||
if (!HasAdminPermission(site, originalPage)) | ||
throw new AppPermissionException(); | ||
|
||
// site id cannot be changed | ||
if (originalPage.SiteId != page.SiteId) | ||
throw new AppException(ExceptionCodes.PageSiteIdCannotBeChanged); | ||
|
||
// fetch list of all pages | ||
var pages = (await _pageRepository.GetAll(cancellationToken)).ToList(); | ||
|
||
//If Parent Id is changed validate again | ||
if (page.ParentId != originalPage.ParentId) | ||
{ | ||
return sitePages; | ||
//validate parent | ||
ValidateParentPage(page, pages); | ||
|
||
//TODO: we should validate children permissions too! | ||
} | ||
// todo: handle edge-case that multiple sites contain the same parent page | ||
var childPages = await _pageRepository.GetBySiteIdAndParentId(parentId.Value); | ||
return childPages; | ||
|
||
ValidateUrl(page, pages); | ||
|
||
// prepare entity for db | ||
PrepareForUpdate(page); | ||
|
||
return await _pageRepository.Update(page, cancellationToken) | ||
?? throw new AppException(ExceptionCodes.PageUnableToUpdate); | ||
} | ||
|
||
private bool HasAdminPermission(Site site, Page page) | ||
{ | ||
return Current.IsInRole(site.AdminRoleIds) || Current.IsInRole(page.AdminRoleIds); | ||
} | ||
|
||
private bool HasViewPermission(Site site, Page page) | ||
{ | ||
return HasAdminPermission(site, page) || Current.IsInRole(page.ViewRoleIds); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.