diff --git a/contrib/Makefile b/contrib/Makefile index 7c73406b..8e0e5567 100644 --- a/contrib/Makefile +++ b/contrib/Makefile @@ -45,9 +45,12 @@ copy-binaries: build_publish_Org build_publish_Api build_publish_Blogs stop-serv echo "$${project} -> $${LCAPI}" ; \ BINDIR="$(SLNDIR)/src/Yavsc.$${project}/bin/$(CONFIGURATION)/$(DOTNET_FRAMEWORK)/publish" ; \ echo BINDIR: $${BINDIR} ; \ - ls $${BINDIR}/appsettings* 2>&1 ; \ - mv $${BINDIR}/appsettings-$${LCAPI}.json \ + rm -f $${BINDIR}/appsettings-$${LCAPI}-dist.json ; \ + mv -f $${BINDIR}/appsettings-$${LCAPI}.json \ $${BINDIR}/appsettings-$${LCAPI}-dist.json ; \ + rm -f $${BINDIR}/appsettings-$${LCAPI}.json ; \ + sync ; \ + ls $${BINDIR}/appsettings* 2>&1 ; \ sudo cp -a $${BINDIR}/* $(BASEAPPDIR) ; \ done @sudo chown -R $(USER_AND_GROUP) $(BASEAPPDIR) @@ -56,9 +59,12 @@ copy-binaries: build_publish_Org build_publish_Api build_publish_Blogs stop-serv sudo cp $^ $@ sudo chown root:root $@ -build_publish_%: +build_publish_%: clean_publish_dir_% @ASPNETCORE_ENV=$(CONFIGURATION) dotnet publish $(SLNDIR)/src/Yavsc.$*/Yavsc.$*.csproj +clean_publish_dir_%: + @rm -rf $(SLNDIR)/src/Yavsc.$*/bin/$(CONFIGURATION)/$(DOTNET_FRAMEWORK)/publish + install: build_publish copy-binaries copy-services @sudo systemctl daemon-reload @for project in $(APP_PROJECT_NAMES); \ diff --git a/src/Yavsc.Blogs/Program.cs b/src/Yavsc.Blogs/Program.cs index 673fff20..ea9f044b 100644 --- a/src/Yavsc.Blogs/Program.cs +++ b/src/Yavsc.Blogs/Program.cs @@ -18,7 +18,6 @@ using Yavsc.Interface; using Yavsc.Models; using Yavsc.Services; using Yavsc.Server.Helpers; -using Yavsc.Extensions; namespace Yavsc.Blogs; internal class Program diff --git a/src/Yavsc.Blogs/Yavsc.Blogs.csproj b/src/Yavsc.Blogs/Yavsc.Blogs.csproj index db36859c..7961fba2 100644 --- a/src/Yavsc.Blogs/Yavsc.Blogs.csproj +++ b/src/Yavsc.Blogs/Yavsc.Blogs.csproj @@ -10,6 +10,5 @@ - - \ No newline at end of file + diff --git a/src/Yavsc.Org.Tests/WebServerFixture.cs b/src/Yavsc.Org.Tests/WebServerFixture.cs index 2ced3a09..099d1e0a 100644 --- a/src/Yavsc.Org.Tests/WebServerFixture.cs +++ b/src/Yavsc.Org.Tests/WebServerFixture.cs @@ -121,7 +121,7 @@ namespace Yavsc.Org.Tests { var builder = WebApplication.CreateBuilder(); - builder.AddConfiguration("org").AddInMemoryCollection(new Dictionary + builder.AddConfiguration(null).AddInMemoryCollection(new Dictionary { [$"ConnectionStrings:{YavscConstants.YavscConnectionStringName}"] = "InMemory" }); diff --git a/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj b/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj index 6b532415..f2e137fe 100644 --- a/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj +++ b/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj @@ -48,7 +48,4 @@ - - - diff --git a/src/Yavsc.Org/Helpers/UserHelpers.cs b/src/Yavsc.Org/Helpers/UserHelpers.cs deleted file mode 100644 index ca3d9ec3..00000000 --- a/src/Yavsc.Org/Helpers/UserHelpers.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Yavsc.Models; -using Yavsc.Models.Blog; - -namespace Yavsc.Helpers -{ - public static class UserHelpers - { - public static IEnumerable UserPosts(this ApplicationDbContext dbContext, string posterId, string? readerId) - { - if (readerId == null) - { - var userPosts = dbContext.blogSpotPublications.Include( - b => b.BlogPost - ).Where(x => x.BlogPost.AuthorId == posterId) - .Select(x => x.BlogPost).ToArray(); - return userPosts; - } - else - { - long[] readerCirclesMemberships = - dbContext.Circle.Include(c => c.Members) - .Where(c => c.Members.Any(m => m.MemberId == readerId)) - .Select(c => c.Id).ToArray(); - return dbContext.BlogSpot.Include( - b => b.Author - ).Include(p => p.ACL).Where(x => x.Author.Id == posterId && - (x.ACL.Count == 0 || x.ACL.Any(a => readerCirclesMemberships.Contains(a.CircleId)))); - } - } - } -} diff --git a/src/Yavsc.Server/Helpers/ConfigHelpers.cs b/src/Yavsc.Server/Helpers/ConfigHelpers.cs index 26c8c887..fa3fb832 100644 --- a/src/Yavsc.Server/Helpers/ConfigHelpers.cs +++ b/src/Yavsc.Server/Helpers/ConfigHelpers.cs @@ -6,14 +6,24 @@ namespace Yavsc.Server.Helpers; public static class ConfigHelpers { - public static IConfigurationBuilder AddConfiguration(this WebApplicationBuilder builder, string appName ) + public static IConfigurationBuilder AddConfiguration(this WebApplicationBuilder builder, string appName) + { + if (string.IsNullOrEmpty(appName)) { var rootConfig = builder.Configuration - .AddJsonFile($"appsettings-{appName}.json", optional: false, reloadOnChange: false); + .AddJsonFile($"appsettings.json", optional: false, reloadOnChange: false); + + rootConfig.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: false); - rootConfig.AddJsonFile($"appsettings-{appName}.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: false); - return rootConfig.AddEnvironmentVariables(); - } + + var specRootConfig = builder.Configuration + .AddJsonFile($"appsettings-{appName}.json", optional: false, reloadOnChange: false); + + specRootConfig.AddJsonFile($"appsettings-{appName}.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: false); + + return specRootConfig.AddEnvironmentVariables(); + + } } \ No newline at end of file diff --git a/src/Yavsc.Server/Helpers/UserHelpers.cs b/src/Yavsc.Server/Helpers/UserHelpers.cs index 8620ad2d..63716963 100644 --- a/src/Yavsc.Server/Helpers/UserHelpers.cs +++ b/src/Yavsc.Server/Helpers/UserHelpers.cs @@ -1,9 +1,35 @@ +using Microsoft.EntityFrameworkCore; using System.Security.Claims; +using Yavsc.Models; +using Yavsc.Models.Blog; namespace Yavsc.Server.Helpers { public static class UserHelpers { + public static IEnumerable UserPosts(this ApplicationDbContext dbContext, string posterId, string? readerId) + { + if (readerId == null) + { + var userPosts = dbContext.blogSpotPublications.Include( + b => b.BlogPost + ).Where(x => x.BlogPost.AuthorId == posterId) + .Select(x => x.BlogPost).ToArray(); + return userPosts; + } + else + { + long[] readerCirclesMemberships = + dbContext.Circle.Include(c => c.Members) + .Where(c => c.Members.Any(m => m.MemberId == readerId)) + .Select(c => c.Id).ToArray(); + return dbContext.BlogSpot.Include( + b => b.Author + ).Include(p => p.ACL).Where(x => x.Author.Id == posterId && + (x.ACL.Count == 0 || x.ACL.Any(a => readerCirclesMemberships.Contains(a.CircleId)))); + } + } + public static string GetUserId(this ClaimsPrincipal user) { return user.FindFirstValue("sub"); diff --git a/src/Yavsc.Server/Services/BlogSpotService.cs b/src/Yavsc.Server/Services/BlogSpotService.cs new file mode 100644 index 00000000..8dfa9e33 --- /dev/null +++ b/src/Yavsc.Server/Services/BlogSpotService.cs @@ -0,0 +1,268 @@ +using System.Diagnostics; +using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; +using Microsoft.EntityFrameworkCore; +using Yavsc; +using Yavsc.Models; +using Yavsc.Models.Blog; +using Yavsc.Server.Exceptions; +using Yavsc.Server.Helpers; +using Yavsc.Services; +using Yavsc.ViewModels.Auth; +using Microsoft.AspNetCore.Http; + +public class BlogSpotService +{ + private readonly ApplicationDbContext _context; + private readonly IAuthorizationService _authorizationService; + private readonly IFileSystemAuthManager fileSystemAuthManager; + + public BlogSpotService(ApplicationDbContext context, + IAuthorizationService authorizationService, + IFileSystemAuthManager fileSystemAuthManager) + { + _authorizationService = authorizationService; + _context = context; + this.fileSystemAuthManager = fileSystemAuthManager; + } + + public BlogPost Create(string userId, BlogPost post, IFormFileCollection files) + { + // Sauvegarder le post d'abord pour obtenir son ID + _context.BlogSpot.Add(post); + _context.SaveChanges(userId); + + // Traiter les fichiers attachés s'il y en a + if (files != null && files.Count > 0) + { + var user = _context.Users.FirstOrDefault(u => u.Id == userId); + if (user != null) + { + try + { + // Créer un répertoire pour les fichiers du blog + string blogFilesSubdir = $"blogs/{post.Id}"; + string destDir = Path.Combine( + AbstractFileSystemHelpers.UserFilesDirName, + user.UserName, + blogFilesSubdir + ); + var di = new DirectoryInfo(destDir); + if (!di.Exists) di.Create(); + + // Traiter chaque fichier + foreach (var formFile in files) + { + var fileInfo = user.ReceiveUserFile(destDir, formFile); + if (fileInfo != null && !fileInfo.QuotaOffense) + { + // Créer une entrée UploadedFile si nécessaire + var uploadedFile = new UploadedFile + { + Path = fileInfo.FileName, + ContentType = formFile.ContentType, + Length = formFile.Length + }; + _context.UploadedFiles.Add(uploadedFile); + _context.SaveChanges(userId); + + // Lier le fichier au post + var attachment = new BlogAttachedFile + { + PostId = post.Id, + FileId = uploadedFile.Id + }; + _context.BlogAttachedFiles.Add(attachment); + } + } + _context.SaveChanges(userId); + } + catch (Exception ex) + { + // Logger l'erreur mais ne pas échouer la création du post + System.Diagnostics.Debug.WriteLine($"Erreur lors du traitement des fichiers : {ex.Message}"); + } + } + } + + return post; + } + public async Task GetPostForEdition(ClaimsPrincipal user, long blogPostId) + { + var blog = await _context.BlogSpot.Include(x => x.Author).Include(x => x.ACL).SingleAsync(m => m.Id == blogPostId); + var auth = await _authorizationService.AuthorizeAsync(user, blog, new EditPermission()); + if (!auth.Succeeded) + { + throw new AuthorizationFailureException(auth); + } + var pub = await _context.blogSpotPublications.AnyAsync(x => x.BlogpostId == blog.Id); + + return new BlogPostEditViewModel(blog, pub); + } + + public async Task Details(ClaimsPrincipal user, long blogPostId) + { + BlogPost blog = await _context.BlogSpot + .Include(p => p.Author) + .Include(p => p.Tags) + .Include(p => p.Comments) + .Include(p => p.ACL) + .SingleAsync(m => m.Id == blogPostId); + if (blog == null) + { + return null; + } + var auth = await _authorizationService.AuthorizeAsync(user, blog, new ReadPermission()); + if (!auth.Succeeded) + { + throw new AuthorizationFailureException(auth); + } + foreach (var c in blog.Comments) + { + c.Author = _context.Users.First(u => u.Id == c.AuthorId); + } + return blog; + } + + public async Task Modify(ClaimsPrincipal user, BlogPostEditViewModel blogEdit) + { + var blog = _context.BlogSpot.SingleOrDefault(b => b.Id == blogEdit.Id); + Debug.Assert(blog != null); + var auth = await _authorizationService.AuthorizeAsync(user, blog, new EditPermission()); + if (!auth.Succeeded) + { + throw new AuthorizationFailureException(auth); + } + blog.Article = blogEdit.Article; + blog.Title = blogEdit.Title; + blog.Photo = blogEdit.Photo; + blog.ACL = blogEdit.ACL; + // saves the change + _context.Update(blog); + var publication = await _context.blogSpotPublications.SingleOrDefaultAsync + (p => p.BlogpostId == blogEdit.Id); + if (publication != null) + { + if (!blogEdit.Publish) + { + _context.blogSpotPublications.Remove(publication); + } + } + else + { + if (blogEdit.Publish) + { + _context.blogSpotPublications.Add( + new BlogSpotPublication + { + BlogpostId = blogEdit.Id + } + ); + } + } + _context.SaveChanges(user.GetUserId()); + } + + public async Task Modify(ClaimsPrincipal user, BlogPost blog) + { + var existing = await _context.BlogSpot.Include(b => b.ACL).SingleOrDefaultAsync(b => b.Id == blog.Id); + if (existing == null) + { + throw new InvalidOperationException($"Blog post {blog.Id} not found."); + } + + var auth = await _authorizationService.AuthorizeAsync(user, existing, new EditPermission()); + if (!auth.Succeeded) + { + throw new AuthorizationFailureException(auth); + } + + existing.Title = blog.Title; + existing.Article = blog.Article; + existing.Photo = blog.Photo; + existing.ACL = blog.ACL; + + _context.Update(existing); + _context.SaveChanges(user.GetUserId()); + } + + public async Task> Index(ClaimsPrincipal user, string id, int skip = 0, int take = 25) + { + IEnumerable posts; + + if (user.Identity.IsAuthenticated) + { + string viewerId = user.GetUserId(); + long[] userCircles = await _context.Circle.Include(c => c.Members). + Where(c => c.Members.Any(m => m.MemberId == viewerId)) + .Select(c => c.Id).ToArrayAsync(); + + posts = _context.BlogSpot + .Include(b => b.Author) + .Include(p => p.ACL) + .Include(p => p.Tags) + .Include(p => p.Comments) + .Where(p => p.ACL == null + || p.ACL.Count == 0 + || (p.AuthorId == viewerId) + || (userCircles != null && + p.ACL.Any(a => userCircles.Contains(a.CircleId))) + ); + } + else + { + posts = _context.blogSpotPublications + .Include(p => p.BlogPost) + .Include(b => b.BlogPost.Author) + .Include(p => p.BlogPost.ACL) + .Include(p => p.BlogPost.Tags) + .Include(p => p.BlogPost.Comments) + .Where(p => p.BlogPost.ACL == null + || p.BlogPost.ACL.Count == 0) + .Select(p => p.BlogPost).ToArray(); + } + + var data = posts.OrderByDescending(p => p.DateModified) + .Skip(skip) + .Take(take); + return data; + } + + public async Task Delete(ClaimsPrincipal user, long id) + { + var uid = user.GetUserId(); + BlogPost blog = _context.BlogSpot.Single(m => m.Id == id); + + _context.BlogSpot.Remove(blog); + _context.SaveChanges(user.GetUserId()); + } + + public async Task> UserPosts( + string posterName, + string? readerId, + int pageLen = 10, + int pageNum = 0) + { + string? posterId = (await _context.Users.SingleOrDefaultAsync(u => u.UserName == posterName))?.Id ?? null; + if (posterId == null) return Array.Empty(); + return _context.UserPosts(posterId, readerId); + } + + public object? GetTitle(string title) + { + return _context.BlogSpot.Include( + b => b.Author + ).Where(x => x.Title == title).OrderByDescending( + x => x.DateCreated + ).ToList(); + } + + public async Task GetBlogPostAsync(long value) + { + return await _context.BlogSpot + .Include(b => b.Author) + .Include(b => b.ACL) + .SingleOrDefaultAsync(x => x.Id == value); + } + +} diff --git a/src/Yavsc.Org/Extensions/PermissionHandler.cs b/src/Yavsc.Server/Services/PermissionHandler.cs similarity index 96% rename from src/Yavsc.Org/Extensions/PermissionHandler.cs rename to src/Yavsc.Server/Services/PermissionHandler.cs index 15f168ec..6848070a 100644 --- a/src/Yavsc.Org/Extensions/PermissionHandler.cs +++ b/src/Yavsc.Server/Services/PermissionHandler.cs @@ -1,12 +1,14 @@ using System.Security.Claims; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; using Microsoft.EntityFrameworkCore; using Yavsc.Models; using Yavsc.Models.Blog; using Yavsc.Server.Helpers; using Yavsc.ViewModels.Auth; -namespace Yavsc.Extensions; +namespace Yavsc.Services; public class PermissionHandler : IAuthorizationHandler {