refacto site settings
Some checks failed
Dotnet build and test / build (pull_request) Failing after 8m29s

This commit is contained in:
Paul Schneider 2026-09-17 22:22:18 +01:00
commit 3bc4142383
Signed by: notazof
GPG key ID: 1DD5D838E5343B06
33 changed files with 399 additions and 315 deletions

View file

@ -7,8 +7,6 @@ namespace Yavsc.Server.Helpers
{ {
public static class AbstractFileSystemHelpers public static class AbstractFileSystemHelpers
{ {
public static string UserBillsDirName { set; get; }
public static string UserFilesDirName { set; get; }
/// <summary> /// <summary>
/// Is Valid this Path? /// Is Valid this Path?
@ -64,17 +62,6 @@ namespace Yavsc.Server.Helpers
return sb.ToString(); return sb.ToString();
} }
public static UserDirectoryInfo GetUserFiles(string userId, string subdir)
{
UserDirectoryInfo di = new UserDirectoryInfo(UserFilesDirName, userId, subdir);
return di;
}
public static bool IsRegularFile(string userName, string subdir)
{
FileInfo fi = new FileInfo( Path.Combine(Path.Combine(UserFilesDirName, userName), subdir));
return fi.Exists;
}
// Server side only supports POSIX file systems // Server side only supports POSIX file systems
public const char RemoteDirectorySeparator = '/'; public const char RemoteDirectorySeparator = '/';

View file

@ -35,6 +35,7 @@ namespace Yavsc.ApiControllers
private readonly ILogger logger; private readonly ILogger logger;
private readonly IBillingService billingService; private readonly IBillingService billingService;
private readonly SiteSettings siteSettings;
public BillingController( public BillingController(
IAuthorizationService authorizationService, IAuthorizationService authorizationService,
@ -43,7 +44,8 @@ namespace Yavsc.ApiControllers
ApplicationDbContext context, ApplicationDbContext context,
IOptions<GoogleAuthSettings> googleSettings, IOptions<GoogleAuthSettings> googleSettings,
IYavscMessageSender GCMSender, IYavscMessageSender GCMSender,
IBillingService billingService IBillingService billingService,
IOptions<SiteSettings> siteSettings
) )
{ {
_googleSettings=googleSettings.Value; _googleSettings=googleSettings.Value;
@ -53,6 +55,7 @@ namespace Yavsc.ApiControllers
this._localizer = SR; this._localizer = SR;
_GCMSender=GCMSender; _GCMSender=GCMSender;
this.billingService=billingService; this.billingService=billingService;
this.siteSettings = siteSettings.Value;
} }
[HttpGet("facture-{billingCode}-{id}.pdf"), Authorize] [HttpGet("facture-{billingCode}-{id}.pdf"), Authorize]
@ -65,7 +68,7 @@ namespace Yavsc.ApiControllers
return new ChallengeResult(); return new ChallengeResult();
} }
var fi = bill.GetBillInfo(billingService); var fi = bill.GetBillInfo(billingService, siteSettings);
if (!fi.Exists) return Ok(new { Error = "Not generated" }); if (!fi.Exists) return Ok(new { Error = "Not generated" });
return File(fi.OpenRead(), "application/x-pdf", fi.Name); return File(fi.OpenRead(), "application/x-pdf", fi.Name);
@ -229,7 +232,7 @@ namespace Yavsc.ApiControllers
} }
if (Request.Form.Files.Count!=1) if (Request.Form.Files.Count!=1)
return new BadRequestResult(); return new BadRequestResult();
await User.ReceiveProSignatureAsync(billingCode,id,Request.Form.Files[0],"pro"); await User.ReceiveProSignatureAsync(billingCode,id,Request.Form.Files[0],"pro", siteSettings);
estimate.ProviderValidationDate = DateTime.UtcNow; estimate.ProviderValidationDate = DateTime.UtcNow;
dbContext.SaveChanges(User.GetUserId()); dbContext.SaveChanges(User.GetUserId());
// Notify the client // Notify the client
@ -273,7 +276,7 @@ namespace Yavsc.ApiControllers
} }
var filename = AbstractFileSystemHelpers.SignFileNameFormat("pro", billingCode, id); var filename = AbstractFileSystemHelpers.SignFileNameFormat("pro", billingCode, id);
FileInfo fi = new FileInfo(Path.Combine(AbstractFileSystemHelpers.UserBillsDirName, filename)); FileInfo fi = new FileInfo(Path.Combine(siteSettings.Bills, filename));
if (!fi.Exists) return NotFound(new { Error = "Professional signature not found" }); if (!fi.Exists) return NotFound(new { Error = "Professional signature not found" });
return File(fi.OpenRead(), "application/x-pdf", filename); ; return File(fi.OpenRead(), "application/x-pdf", filename); ;
} }
@ -310,7 +313,7 @@ namespace Yavsc.ApiControllers
} }
if (Request.Form.Files.Count!=1) if (Request.Form.Files.Count!=1)
return new BadRequestResult(); return new BadRequestResult();
await User.ReceiveProSignatureAsync(billingCode,id,Request.Form.Files[0],"cli"); await User.ReceiveProSignatureAsync(billingCode,id,Request.Form.Files[0],"cli", siteSettings);
estimate.ClientValidationDate = DateTime.UtcNow; estimate.ClientValidationDate = DateTime.UtcNow;
dbContext.SaveChanges(User.GetUserId()); dbContext.SaveChanges(User.GetUserId());
return Ok (new { ClientValidationDate = estimate.ClientValidationDate }); return Ok (new { ClientValidationDate = estimate.ClientValidationDate });
@ -327,9 +330,9 @@ namespace Yavsc.ApiControllers
} }
var filename = AbstractFileSystemHelpers.SignFileNameFormat("pro", billingCode, id); var filename = AbstractFileSystemHelpers.SignFileNameFormat("pro", billingCode, id);
FileInfo fi = new FileInfo(Path.Combine(AbstractFileSystemHelpers.UserBillsDirName, filename)); FileInfo fi = new FileInfo(Path.Combine(siteSettings.Bills, filename));
if (!fi.Exists) return NotFound(new { Error = "Professional signature not found" }); if (!fi.Exists) return NotFound(new { Error = "Professional signature not found" });
return File(fi.OpenRead(), "application/x-pdf", filename); ; return File(fi.OpenRead(), "application/x-pdf", filename);
} }
/// <summary> /// <summary>
@ -405,7 +408,8 @@ namespace Yavsc.ApiControllers
FileReceivedInfo fi; FileReceivedInfo fi;
try try
{ {
fi = await User.ReceiveEstimateSignatureAsync(id, type, payload, token); fi = await User.ReceiveEstimateSignatureAsync(id, type, payload,
siteSettings, token);
} }
catch (Exception ex) catch (Exception ex)
{ {

View file

@ -2,13 +2,13 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Http;
using ImageMagick; using ImageMagick;
using Yavsc.Models; using Yavsc.Models;
using Yavsc.Api.Helpers; using Yavsc.Api.Helpers;
using Yavsc.Server.Helpers; using Yavsc.Server.Helpers;
using System.Diagnostics; using System.Diagnostics;
using Microsoft.Extensions.Options;
namespace Yavsc.WebApi.Controllers namespace Yavsc.WebApi.Controllers
{ {
@ -28,13 +28,16 @@ namespace Yavsc.WebApi.Controllers
]; ];
readonly ApplicationDbContext _dbContext; readonly ApplicationDbContext _dbContext;
private readonly SiteSettings siteSettings;
private readonly ILogger _logger; private readonly ILogger _logger;
public ApiAccountController( public ApiAccountController(
ILoggerFactory loggerFactory, ApplicationDbContext dbContext) ILoggerFactory loggerFactory, ApplicationDbContext dbContext,
IOptions<SiteSettings> siteSettings)
{ {
_logger = loggerFactory.CreateLogger(nameof(ApiAccountController)); _logger = loggerFactory.CreateLogger(nameof(ApiAccountController));
_dbContext = dbContext; _dbContext = dbContext;
this.siteSettings = siteSettings.Value;
} }
[HttpGet("me")] [HttpGet("me")]
@ -146,7 +149,7 @@ namespace Yavsc.WebApi.Controllers
try try
{ {
var info = user.ReceiveAvatar(avatarFile); var info = user.ReceiveAvatar(avatarFile, siteSettings);
await _dbContext.SaveChangesAsync(); await _dbContext.SaveChangesAsync();
return Ok(new return Ok(new
{ {

View file

@ -21,7 +21,6 @@ internal class Program
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.AddConfiguration("api"); builder.AddConfiguration("api");
Config.SiteSetup = builder.Configuration.GetSection("Site").Get<SiteSettings>() ?? new SiteSettings();
var services = builder.Services; var services = builder.Services;

View file

@ -5,7 +5,9 @@
"CorsAllowedOrigins": [ "CorsAllowedOrigins": [
"https://yavsc.pschneider.fr", "https://yavsc.pschneider.fr",
"https://blogs.pschneider.fr" "https://blogs.pschneider.fr"
] ],
"Bills": "bills",
"TempDir": "temp"
}, },
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {

View file

@ -11,6 +11,7 @@ using Yavsc.Server.Helpers;
using Yavsc.Tests.Shared; using Yavsc.Tests.Shared;
using Yavsc.Blogs.Tests.Fixtures; using Yavsc.Blogs.Tests.Fixtures;
using BlogPostDto = Yavsc.Blogspot.BlogPostDto; using BlogPostDto = Yavsc.Blogspot.BlogPostDto;
using NuGet.Protocol;
namespace Yavsc.Blogs.Tests; namespace Yavsc.Blogs.Tests;
@ -338,10 +339,9 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
ResetAndSeedDefaultUser(); ResetAndSeedDefaultUser();
using var http = NewClient(subject: "tester"); using var http = NewClient(subject: "tester");
var previousRoot = AbstractFileSystemHelpers.UserFilesDirName; // AttachFiles writes under the fixture-configured
var tempRoot = Path.Combine(Path.GetTempPath(), "yavsc-blogs-tests-files-" + Guid.NewGuid().ToString("N")); // SiteSettings.Blog (BlogFilesRoot), not a local temp dir.
Directory.CreateDirectory(tempRoot); var filesRoot = _fixture.BlogFilesRoot;
AbstractFileSystemHelpers.UserFilesDirName = tempRoot;
try try
{ {
@ -374,8 +374,10 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
DateModified = DateTime.UtcNow DateModified = DateTime.UtcNow
}; };
var form = new MultipartFormDataContent(); var form = new MultipartFormDataContent
form.Add(new StringContent(JsonSerializer.Serialize(update)), "blog"); {
{ new StringContent(JsonSerializer.Serialize(update)), "blog" }
};
var fileBytes = System.Text.Encoding.UTF8.GetBytes("payload test"); var fileBytes = System.Text.Encoding.UTF8.GetBytes("payload test");
var fileContent = new ByteArrayContent(fileBytes); var fileContent = new ByteArrayContent(fileBytes);
@ -405,8 +407,9 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
} }
finally finally
{ {
AbstractFileSystemHelpers.UserFilesDirName = previousRoot; // BlogFilesRoot is the fixture-shared temp root (one per
try { Directory.Delete(tempRoot, recursive: true); } catch { } // test run); leave it in place for the other multipart
// tests. The OS reclaims it.
} }
} }
@ -480,10 +483,9 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
ResetAndSeedDefaultUser(); ResetAndSeedDefaultUser();
using var http = NewClient(subject: "tester"); using var http = NewClient(subject: "tester");
var previousRoot = AbstractFileSystemHelpers.UserFilesDirName; // AttachFiles writes under the fixture-configured
var tempRoot = Path.Combine(Path.GetTempPath(), "yavsc-blogs-tests-files-" + Guid.NewGuid().ToString("N")); // SiteSettings.Blog (BlogFilesRoot), not a local temp dir.
Directory.CreateDirectory(tempRoot); var filesRoot = _fixture.BlogFilesRoot;
AbstractFileSystemHelpers.UserFilesDirName = tempRoot;
try try
{ {
@ -569,17 +571,18 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
// markdown link points: // markdown link points:
// {UserFilesDirName}/{user}/blogs/{postId}/{fileName}. // {UserFilesDirName}/{user}/blogs/{postId}/{fileName}.
var expectedFile = Path.Combine( var expectedFile = Path.Combine(
tempRoot, "tester", "blogs", created.Id.ToString(), "note.txt"); filesRoot, "tester", "blogs", created.Id.ToString(), "note.txt");
var written = Directory.Exists(tempRoot) var written = Directory.Exists(filesRoot)
? string.Join(", ", Directory.EnumerateFiles(tempRoot, "*", SearchOption.AllDirectories)) ? string.Join(", ", Directory.EnumerateFiles(filesRoot, "*", SearchOption.AllDirectories))
: "<none>"; : "<none>";
Assert.True(File.Exists(expectedFile), Assert.True(File.Exists(expectedFile),
$"Expected uploaded file at {expectedFile}. Actually written: {written}"); $"Expected uploaded file at {expectedFile}. Actually written: {written}");
} }
finally finally
{ {
AbstractFileSystemHelpers.UserFilesDirName = previousRoot; // BlogFilesRoot is the fixture-shared temp root (one per
try { Directory.Delete(tempRoot, recursive: true); } catch { } // test run); leave it in place for the other multipart
// tests. The OS reclaims it.
} }
} }

View file

@ -63,6 +63,16 @@ public sealed class BlogsWebServerFixture : WebHostFixture
public long PostId { get; private set; } public long PostId { get; private set; }
public string DefaultUserLogin { get => "alice"; } public string DefaultUserLogin { get => "alice"; }
/// <summary>Root directory under which
/// <c>BlogSpotService.AttachFiles</c> writes uploaded files
/// during tests (the configured <c>SiteSettings.Blog</c>).
/// Attachments land at
/// <c>{BlogFilesRoot}/{userName}/blogs/{postId}/{fileName}</c>.
/// A fresh per-run temp directory, so tests can assert on the
/// physical file without touching the developer's real
/// user-files tree.</summary>
public string BlogFilesRoot { get; private set; } = string.Empty;
// A single SqliteConnection held open at the static level, // A single SqliteConnection held open at the static level,
// mirroring how Yavsc.Org.Tests.WebServerFixture hoists its // mirroring how Yavsc.Org.Tests.WebServerFixture hoists its
// shared configuration into static slots. Closing the // shared configuration into static slots. Closing the
@ -112,6 +122,18 @@ public sealed class BlogsWebServerFixture : WebHostFixture
builder.Services.AddSingleton<IFileSystemAuthManager>( builder.Services.AddSingleton<IFileSystemAuthManager>(
new NoopFileSystemAuthManager()); new NoopFileSystemAuthManager());
// BlogSpotService.AttachFiles writes uploaded files under
// siteSettings.Blog. Without a configured SiteSettings the
// IOptions<SiteSettings> value has Blog == null and
// Path.Combine(null, …) throws inside AttachFiles' catch-all,
// silently persisting no attachment. Point Blog at a
// per-run temp directory so the multipart tests can assert
// on real files.
builder.Services.Configure<SiteSettings>(s =>
{
s.Blog = BlogFilesRoot;
});
// Real BlogSpotService — same instance the production host // Real BlogSpotService — same instance the production host
// builds (ApplicationDbContext, IAuthorizationService, // builds (ApplicationDbContext, IAuthorizationService,
// IFileSystemAuthManager). With PermissionHandler registered // IFileSystemAuthManager). With PermissionHandler registered

View file

@ -57,12 +57,31 @@ namespace Yavsc.Blogs.Controllers
[HttpPut("{id}")] [HttpPut("{id}")]
public async Task<IActionResult> PutBlog(long id) public async Task<IActionResult> PutBlog(long id)
{ {
var blog = await ReadPutBlogRequestAsync(); var blog = await ReadBlogRequestAsync();
if (blog is null) if (blog is null)
{ {
return BadRequest(ModelState); return BadRequest(ModelState);
} }
// Without [FromBody], the data-annotations attributes
// ([Required] on Title, etc.) are no longer evaluated by
// the model binder — the body is read manually. Run
// validation explicitly so an empty Title still yields
// 400 (and not a 500 from the DB NOT NULL constraint).
var validationContext = new System.ComponentModel.DataAnnotations.ValidationContext(blog);
var validationResults = new List<System.ComponentModel.DataAnnotations.ValidationResult>();
if (!System.ComponentModel.DataAnnotations.Validator.TryValidateObject(
blog, validationContext, validationResults, validateAllProperties: true))
{
foreach (var result in validationResults)
{
ModelState.AddModelError(
result.MemberNames.FirstOrDefault() ?? string.Empty,
result.ErrorMessage ?? "Invalid value.");
}
return BadRequest(ModelState);
}
// These properties are server-managed or optional graph members and // These properties are server-managed or optional graph members and
// should not block JSON payloads coming from API clients. // should not block JSON payloads coming from API clients.
ModelState.Remove(nameof(Models.Blog.BlogPost.Author)); ModelState.Remove(nameof(Models.Blog.BlogPost.Author));
@ -93,7 +112,8 @@ namespace Yavsc.Blogs.Controllers
try try
{ {
await blogSpotService.Modify(User, blog, files); await blogSpotService.Modify(User, blog);
blogSpotService.AttachFiles(files, User.GetUserId(), id);
} }
catch (AuthorizationFailureException) catch (AuthorizationFailureException)
{ {
@ -105,8 +125,14 @@ namespace Yavsc.Blogs.Controllers
// POST: api/v1/blogspot // POST: api/v1/blogspot
[HttpPost] [HttpPost]
public IActionResult PostBlog([FromBody] Models.Blog.BlogPost blog) public async Task<IActionResult> PostBlog()
{ {
var blog = await ReadBlogRequestAsync();
if (blog is null)
{
return BadRequest(ModelState);
}
// These properties are server-managed or optional graph members and // These properties are server-managed or optional graph members and
// should not block JSON payloads coming from API clients. // should not block JSON payloads coming from API clients.
ModelState.Remove(nameof(Models.Blog.BlogPost.Author)); ModelState.Remove(nameof(Models.Blog.BlogPost.Author));
@ -115,35 +141,46 @@ namespace Yavsc.Blogs.Controllers
ModelState.Remove(nameof(Models.Blog.BlogPost.UserCreated)); ModelState.Remove(nameof(Models.Blog.BlogPost.UserCreated));
ModelState.Remove(nameof(Models.Blog.BlogPost.UserModified)); ModelState.Remove(nameof(Models.Blog.BlogPost.UserModified));
// Without [FromBody], the data-annotations attributes
// ([Required] on Title, etc.) are no longer evaluated by
// the model binder — the body is read manually. Run
// validation explicitly so an empty Title still yields
// 400 (and not a 500 from the DB NOT NULL constraint).
var validationContext = new System.ComponentModel.DataAnnotations.ValidationContext(blog);
var validationResults = new List<System.ComponentModel.DataAnnotations.ValidationResult>();
if (!System.ComponentModel.DataAnnotations.Validator.TryValidateObject(
blog, validationContext, validationResults, validateAllProperties: true))
{
foreach (var result in validationResults)
{
ModelState.AddModelError(
result.MemberNames.FirstOrDefault() ?? string.Empty,
result.ErrorMessage ?? "Invalid value.");
}
return BadRequest(ModelState);
}
if (!ModelState.IsValid) if (!ModelState.IsValid)
{ {
return BadRequest(ModelState); return BadRequest(ModelState);
} }
// The BlogSpotService.Create() signature requires an
// IFormFileCollection for file uploads. Reading
// Request.Form.Files when the request is a plain JSON
// body (e.g. from PostIt) throws
// "This request does not have a Content-Type header.
// Forms are available from requests with bodies like
// POSTs and a form Content-Type of either
// application/x-www-form-urlencoded or
// multipart/form-data."
//
// Two valid use cases for this endpoint: // Two valid use cases for this endpoint:
// 1. JSON body only (no files) — PostIt path. // 1. JSON body only (no files) — PostIt path.
// 2. multipart/form-data with a 'blog' field + 0..N // 2. multipart/form-data with a 'blog' field + 0..N
// files — future browser / server-rendered path. // files — PostIt "create with attachments" path.
// //
// Branch on HasFormContentType: pass the form files when // Branch on HasFormContentType: pass the form files when
// present, pass an empty collection otherwise. The // present, pass an empty collection otherwise. Reading
// FileSystem branch in BlogSpotService.Create then // Request.Form.Files on a plain JSON request throws
// short-circuits to "no files to handle". // "This request does not have a Content-Type header…",
// and AttachFiles short-circuits on an empty collection.
var files = Request.HasFormContentType var files = Request.HasFormContentType
? Request.Form.Files ? Request.Form.Files
: (IFormFileCollection)new FormFileCollection(); : (IFormFileCollection)new FormFileCollection();
var uid = User.GetUserId(); var uid = User.GetUserId();
var post = blogSpotService.Create(uid, blog, files); var post = blogSpotService.Create(uid, blog);
blogSpotService.AttachFiles(files, uid, post.Id);
return CreatedAtRoute("GetBlog", new { id = post.Id }, return CreatedAtRoute("GetBlog", new { id = post.Id },
post.GetPayload()); post.GetPayload());
} }
@ -210,7 +247,17 @@ namespace Yavsc.Blogs.Controllers
base.Dispose(disposing); base.Dispose(disposing);
} }
private async Task<Models.Blog.BlogPost?> ReadPutBlogRequestAsync() /// <summary>
/// Read a <see cref="Models.Blog.BlogPost"/> from either a
/// JSON body or a multipart/form-data request carrying the
/// payload in a <c>blog</c> form field. Shared by
/// <see cref="PostBlog"/> and <see cref="PutBlog"/>: both
/// accept the two shapes so PostIt can attach files on
/// create as well as on update. Returns <c>null</c> (with
/// the reason recorded in <c>ModelState</c>) when the
/// multipart form has no usable <c>blog</c> field.
/// </summary>
private async Task<Models.Blog.BlogPost?> ReadBlogRequestAsync()
{ {
if (!Request.HasFormContentType) if (!Request.HasFormContentType)
{ {

View file

@ -12,6 +12,7 @@ namespace Yavsc.Blogs.Controllers
using Yavsc.Server.Helpers; using Yavsc.Server.Helpers;
using Yavsc.Abstract.Helpers; using Yavsc.Abstract.Helpers;
using Yavsc.Server.Models.FileSystem; using Yavsc.Server.Models.FileSystem;
using Microsoft.Extensions.Options;
[Authorize,Route(APIPrefix + "/fs")] [Authorize,Route(APIPrefix + "/fs")]
public partial class FileSystemApiController : Controller public partial class FileSystemApiController : Controller
@ -19,15 +20,18 @@ namespace Yavsc.Blogs.Controllers
readonly ApplicationDbContext dbContext; readonly ApplicationDbContext dbContext;
private readonly IAuthorizationService AuthorizationService; private readonly IAuthorizationService AuthorizationService;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly SiteSettings siteSettings;
public FileSystemApiController(ApplicationDbContext context, public FileSystemApiController(ApplicationDbContext context,
IAuthorizationService authorizationService, IAuthorizationService authorizationService,
ILoggerFactory loggerFactory) ILoggerFactory loggerFactory,
IOptions<SiteSettings> siteSettings)
{ {
AuthorizationService = authorizationService; AuthorizationService = authorizationService;
dbContext = context; dbContext = context;
_logger = loggerFactory.CreateLogger<FileSystemApiController>(); _logger = loggerFactory.CreateLogger<FileSystemApiController>();
this.siteSettings = siteSettings.Value;
} }
[HttpGet()] [HttpGet()]
@ -41,7 +45,7 @@ namespace Yavsc.Blogs.Controllers
{ {
if (!ModelState.IsValid) return new BadRequestObjectResult(ModelState); if (!ModelState.IsValid) return new BadRequestObjectResult(ModelState);
// _logger.LogInformation($"listing files from {User.Identity.Name}{subdir}"); // _logger.LogInformation($"listing files from {User.Identity.Name}{subdir}");
var files = AbstractFileSystemHelpers.GetUserFiles(User.GetUserId(), subdir); var files = FileSystemHelpers.GetUserFiles(siteSettings, User.GetUserId(), subdir);
return Ok(files); return Ok(files);
} }
@ -53,7 +57,7 @@ namespace Yavsc.Blogs.Controllers
List<FileReceivedInfo> received = new List<FileReceivedInfo>(); List<FileReceivedInfo> received = new List<FileReceivedInfo>();
InvalidPathException pathex = null; InvalidPathException pathex = null;
try { try {
destDir = User.EnsureDestinationDirectory(subdir); destDir = User.EnsureDestinationDirectory(subdir, siteSettings);
} catch (InvalidPathException ex) { } catch (InvalidPathException ex) {
pathex = ex; pathex = ex;
} }
@ -108,7 +112,7 @@ namespace Yavsc.Blogs.Controllers
var user = dbContext.Users.Single( var user = dbContext.Users.Single(
u => u.Id == uid u => u.Id == uid
); );
var info = user.MoveUserFileToDir(query.Id, query.To); var info = user.MoveUserFileToDir(query.Id, query.To, siteSettings);
if (!info.Done) return new BadRequestObjectResult(info); if (!info.Done) return new BadRequestObjectResult(info);
return Ok(new { moved = query.Id }); return Ok(new { moved = query.Id });
} }
@ -131,11 +135,11 @@ namespace Yavsc.Blogs.Controllers
try { try {
if (Config.UserFilesOptions.FileProvider.GetFileInfo(Path.Combine(user.UserName, query.Id)).Exists) if (Config.UserFilesOptions.FileProvider.GetFileInfo(Path.Combine(user.UserName, query.Id)).Exists)
{ {
var result = user.MoveUserFile(query.Id, query.To); var result = user.MoveUserFile(query.Id, query.To, siteSettings);
if (!result.Done) return new BadRequestObjectResult(result); if (!result.Done) return new BadRequestObjectResult(result);
} }
else { else {
var result = user.MoveUserDir(query.Id, query.To); var result = user.MoveUserDir(query.Id, query.To, siteSettings);
if (!result.Done) return new BadRequestObjectResult(result); if (!result.Done) return new BadRequestObjectResult(result);
} }
} }
@ -161,7 +165,7 @@ namespace Yavsc.Blogs.Controllers
); );
try { try {
var result = user.DeleteUserDirOrFile(id); var result = user.DeleteUserDirOrFile(id, siteSettings);
if (!result.Done) if (!result.Done)
return new BadRequestObjectResult(result); return new BadRequestObjectResult(result);
} }

View file

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.SignalR;
using Yavsc.Server.Helpers; using Yavsc.Server.Helpers;
using static Yavsc.Constants; using static Yavsc.Constants;
using Yavsc.Server.Hubs; using Yavsc.Server.Hubs;
using Microsoft.Extensions.Options;
namespace Yavsc.Blogs.Controllers namespace Yavsc.Blogs.Controllers
{ {
@ -21,13 +22,20 @@ namespace Yavsc.Blogs.Controllers
private readonly IHubContext<ChatHub> hubContext; private readonly IHubContext<ChatHub> hubContext;
readonly ApplicationDbContext dbContext; readonly ApplicationDbContext dbContext;
public FileSystemStreamController(ApplicationDbContext context, ILiveProcessor liveProcessor, ILoggerFactory loggerFactory, private readonly SiteSettings siteSettings;
IHubContext<ChatHub> hubContext)
public FileSystemStreamController(
ApplicationDbContext context,
ILiveProcessor liveProcessor,
ILoggerFactory loggerFactory,
IHubContext<ChatHub> hubContext,
IOptions<SiteSettings> siteSettings)
{ {
this.dbContext = context; this.dbContext = context;
this.logger = loggerFactory.CreateLogger<FileSystemStreamController>(); this.logger = loggerFactory.CreateLogger<FileSystemStreamController>();
this.liveProcessor = liveProcessor; this.liveProcessor = liveProcessor;
this.hubContext = hubContext; this.hubContext = hubContext;
this.siteSettings = siteSettings.Value;
} }
[Authorize, Route("put/{filename}")] [Authorize, Route("put/{filename}")]
@ -55,7 +63,7 @@ namespace Yavsc.Blogs.Controllers
); );
string destDir = HttpContext.User.EnsureDestinationDirectory(filePath); string destDir = HttpContext.User.EnsureDestinationDirectory(filePath, siteSettings);
logger.LogInformation($"Saving flow to {destDir}"); logger.LogInformation($"Saving flow to {destDir}");
var userId = User.GetUserId(); var userId = User.GetUserId();
var user = await dbContext.Users.FirstAsync(u => u.Id == userId); var user = await dbContext.Users.FirstAsync(u => u.Id == userId);

View file

@ -8,7 +8,8 @@
"CorsAllowedOrigins": [ "CorsAllowedOrigins": [
"https://localhost:5005", "https://localhost:5005",
"https://api.pschneider.fr" "https://api.pschneider.fr"
] ],
"Blog": "blog"
}, },
"ConnectionStrings": { "ConnectionStrings": {
"YavscConnection": "Server=localhost;Port=5432;Database=lame-db-name;Username=lame-user-name;Password=lame-password;" "YavscConnection": "Server=localhost;Port=5432;Database=lame-db-name;Username=lame-user-name;Password=lame-password;"

View file

@ -18,6 +18,7 @@ namespace Yavsc.Org.Tests;
public class EstimateSignatureFileHelperTests : IDisposable public class EstimateSignatureFileHelperTests : IDisposable
{ {
private readonly string _tempRoot; private readonly string _tempRoot;
private SiteSettings siteSettings;
public EstimateSignatureFileHelperTests() public EstimateSignatureFileHelperTests()
{ {
@ -28,7 +29,7 @@ public class EstimateSignatureFileHelperTests : IDisposable
Path.GetTempPath(), Path.GetTempPath(),
"yavsc-sig-tests-" + Guid.NewGuid().ToString("N")); "yavsc-sig-tests-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_tempRoot); Directory.CreateDirectory(_tempRoot);
AbstractFileSystemHelpers.UserFilesDirName = _tempRoot; this.siteSettings = new SiteSettings { Blog = _tempRoot };
} }
public void Dispose() public void Dispose()
@ -71,7 +72,7 @@ public class EstimateSignatureFileHelperTests : IDisposable
}; };
// Act // Act
var fi = Run(user, 123L, SignatureType.Pro, payload); var fi = Run(user, 123L, SignatureType.Pro, payload, this.siteSettings);
// Assert: file exists, sits under the user's root, and // Assert: file exists, sits under the user's root, and
// parses as a yavsc.signature/v1 envelope. // parses as a yavsc.signature/v1 envelope.
@ -98,7 +99,8 @@ public class EstimateSignatureFileHelperTests : IDisposable
var ct = TestContext.Current.CancellationToken; var ct = TestContext.Current.CancellationToken;
await Assert.ThrowsAsync<ArgumentNullException>(() => await Assert.ThrowsAsync<ArgumentNullException>(() =>
EstimateSignatureFileHelper.ReceiveEstimateSignatureAsync( EstimateSignatureFileHelper.ReceiveEstimateSignatureAsync(
user, 1L, SignatureType.Pro, payload: null!, token: ct)); user, 1L, SignatureType.Pro, payload: null!,
siteSettings, token: ct));
} }
[Fact] [Fact]
@ -109,19 +111,19 @@ public class EstimateSignatureFileHelperTests : IDisposable
var ct = TestContext.Current.CancellationToken; var ct = TestContext.Current.CancellationToken;
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() =>
EstimateSignatureFileHelper.ReceiveEstimateSignatureAsync( EstimateSignatureFileHelper.ReceiveEstimateSignatureAsync(
user, 0L, SignatureType.Pro, payload, token: ct)); user, 0L, SignatureType.Pro, payload, this.siteSettings, token: ct));
} }
// --- helpers ---------------------------------------------------- // --- helpers ----------------------------------------------------
private static FileReceivedInfo Run( private static FileReceivedInfo Run(
ClaimsPrincipal user, long estimateId, SignatureType type, SignaturePadPayload payload) ClaimsPrincipal user, long estimateId, SignatureType type, SignaturePadPayload payload, SiteSettings siteSettings)
{ {
// The helper is async; tests that don't care about the // The helper is async; tests that don't care about the
// result can call it sync via .GetAwaiter().GetResult() // result can call it sync via .GetAwaiter().GetResult()
// because we know it never throws in the happy path. // because we know it never throws in the happy path.
return EstimateSignatureFileHelper return EstimateSignatureFileHelper
.ReceiveEstimateSignatureAsync(user, estimateId, type, payload, CancellationToken.None) .ReceiveEstimateSignatureAsync(user, estimateId, type, payload, siteSettings, CancellationToken.None)
.GetAwaiter().GetResult(); .GetAwaiter().GetResult();
} }

View file

@ -9,6 +9,10 @@
"FavIcon": "/favicon.ico", "FavIcon": "/favicon.ico",
"Icon": "/images/yavsc.png", "Icon": "/images/yavsc.png",
"GitRepository": "testingrepo", "GitRepository": "testingrepo",
"Blog": "blog",
"Bills": "bills",
"TempDir": "temp",
"Avatars": "avatars",
"Owner": { "Owner": {
"Name": "Site Owner Name", "Name": "Site Owner Name",
"EMail": "your@email", "EMail": "your@email",

View file

@ -490,14 +490,17 @@ IHtmlLocalizerFactory htmlLocalizerFactory,
if (result.Succeeded) if (result.Succeeded)
{ {
_logger.LogInformation(3, "User created a new account with password."); _logger.LogInformation(3, "User created a new account with password.");
await _emailSender.SendEmailAsync(Config.SiteSetup.Owner.Name, Config.SiteSetup.Owner.EMail, await _emailSender.SendEmailAsync(
$"[{_siteSettings.Title}] Inscription avec mot de passe: {user.UserName} ", $"{user.Id}/{user.UserName}/{user.Email}"); _siteSettings.Owner.Name,
_siteSettings.Owner.EMail,
$"[{_siteSettings.Title}] Inscription avec mot de passe: {user.UserName} ",
$"{user.Id}/{user.UserName}/{user.Email}");
// TODO user.DiskQuota = Startup.SiteSetup.UserFiles.Quota; // TODO user.DiskQuota = Startup.SiteSetup.UserFiles.Quota;
// For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=532713 // For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=532713
// Send an email with this link // Send an email with this link
Uri authority = new Uri(Config.Authority); Uri authority = new Uri(_siteSettings.Authority);
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.Action("ConfirmEmail", "Account", var callbackUrl = Url.Action("ConfirmEmail", "Account",
@ -703,10 +706,15 @@ IHtmlLocalizerFactory htmlLocalizerFactory,
await _signInManager.SignInAsync(user, isPersistent: false); await _signInManager.SignInAsync(user, isPersistent: false);
await _emailSender.SendEmailAsync(Config.SiteSetup.Owner.Name, Config.SiteSetup.Owner.EMail, await _emailSender.SendEmailAsync(
$"[{_siteSettings.Title}] Inscription via {info.LoginProvider}: {user.UserName} ", $"{user.Id}/{user.UserName}/{user.Email}"); _siteSettings.Owner.Name,
_siteSettings.Owner.EMail,
$"[{_siteSettings.Title}] Inscription via {info.LoginProvider}: {user.UserName} ",
$"{user.Id}/{user.UserName}/{user.Email}");
_logger.LogInformation(6, "User created an account using {Name} provider.", info.LoginProvider); _logger.LogInformation(6,
"User created an account using {Name} provider.",
info.LoginProvider);
return Redirect(returnUrl); return Redirect(returnUrl);
} }

View file

@ -109,8 +109,9 @@ namespace Yavsc.Org.Controllers
{ {
if (ModelState.IsValid) if (ModelState.IsValid)
{ {
Models.Blog.BlogPost post = blogSpotService.Create(User.GetUserId(), var uid = User.GetUserId();
blogInput, Request.Form.Files); Models.Blog.BlogPost post = blogSpotService.Create(uid, blogInput);
blogSpotService.AttachFiles(Request.Form.Files, uid, post.Id);
return RedirectToAction("Index"); return RedirectToAction("Index");
} }
return View("Edit", blogInput); return View("Edit", blogInput);

View file

@ -8,6 +8,7 @@ namespace Yavsc.Controllers
using Helpers; using Helpers;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using Models; using Models;
using ViewModels.FrontOffice; using ViewModels.FrontOffice;
using Yavsc.Server.Helpers; using Yavsc.Server.Helpers;
@ -20,18 +21,21 @@ namespace Yavsc.Controllers
readonly ILogger _logger; readonly ILogger _logger;
readonly IStringLocalizer _SR; readonly IStringLocalizer _SR;
private readonly IBillingService _billing; private readonly IBillingService _billing;
private readonly SiteSettings _siteSettings;
public FrontOfficeController(ApplicationDbContext context, public FrontOfficeController(ApplicationDbContext context,
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
IBillingService billing, IBillingService billing,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IStringLocalizer<FrontOfficeController> SR) IStringLocalizer<FrontOfficeController> SR,
IOptions<SiteSettings> siteSettings)
{ {
_context = context; _context = context;
_userManager = userManager; _userManager = userManager;
_logger = loggerFactory.CreateLogger<FrontOfficeController>(); _logger = loggerFactory.CreateLogger<FrontOfficeController>();
_SR = SR; _SR = SR;
_billing = billing; _billing = billing;
_siteSettings = siteSettings.Value;
} }
public ActionResult Index() public ActionResult Index()
{ {
@ -95,8 +99,8 @@ namespace Yavsc.Controllers
[HttpGet] [HttpGet]
public IActionResult EstimatePdf(long id) public IActionResult EstimatePdf(long id)
{ {
ViewBag.TempDir = Config.SiteSetup.TempDir; ViewBag.TempDir = _siteSettings.TempDir;
ViewBag.BillsDir = AbstractFileSystemHelpers.UserBillsDirName; ViewBag.BillsDir = _siteSettings.Bills;
var estimate = _context.Estimates.Include(x => x.Query) var estimate = _context.Estimates.Include(x => x.Query)
.Include(x => x.Query.Client) .Include(x => x.Query.Client)
.Include(x => x.Query.PerformerProfile) .Include(x => x.Query.PerformerProfile)

View file

@ -1,20 +1,25 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Yavsc.Server.Helpers; using Yavsc.Server.Helpers;
namespace Yavsc.Controllers namespace Yavsc.Org.Controllers
{ {
public class FileSystemController : Controller public class FileSystemController : Controller
{ {
public FileSystemController() private SiteSettings _siteSettings;
public FileSystemController(IOptions<SiteSettings> siteSettings)
{ {
_siteSettings = siteSettings.Value;
} }
// Removed redundant empty constructor block
public IActionResult Index(string subdir="") public IActionResult Index(string subdir="")
{ {
if (subdir !=null) if (subdir !=null)
if (!subdir.IsValidYavscPath()) if (!subdir.IsValidYavscPath())
return new BadRequestResult(); return new BadRequestResult();
var files = AbstractFileSystemHelpers.GetUserFiles(User.GetUserId(), subdir); var files = FileSystemHelpers.GetUserFiles(_siteSettings, User.GetUserId(), subdir);
return View(files); return View(files);
} }
} }

View file

@ -1044,7 +1044,7 @@ public static class HostingExtensions
Startup.Configure(app, siteSettings, smtpSettings, Startup.Configure(app, siteSettings, smtpSettings,
payPalSettings, googleAuthSettings, localization, loggerFactory, payPalSettings, googleAuthSettings, localization, loggerFactory,
app.Environment.EnvironmentName); app.Environment.EnvironmentName);
app.ConfigureFileServerApp(); app.ConfigureFileServerApp(siteSettings.Value);
app.UseSession(); app.UseSession();
return app; return app;
} }
@ -1238,24 +1238,44 @@ ADD COLUMN IF NOT EXISTS ""Moderated"" boolean NOT NULL DEFAULT FALSE;");
#nullable disable #nullable disable
public static IApplicationBuilder ConfigureFileServerApp(this IApplicationBuilder app, public static IApplicationBuilder ConfigureFileServerApp(this IApplicationBuilder app,
SiteSettings settings,
bool enableDirectoryBrowsing = false) bool enableDirectoryBrowsing = false)
{ {
if (settings.GitRepository == null)
{
throw new InvalidOperationException("GitRepository path is not configured in SiteSettings.");
}
if (settings.Blog == null)
{
throw new InvalidOperationException("Blog path is not configured in SiteSettings.");
}
if (settings.Avatars == null)
{
throw new InvalidOperationException("Avatars path is not configured in SiteSettings.");
}
if (settings.Bills == null)
{
throw new InvalidOperationException("Bills path is not configured in SiteSettings.");
}
if (settings.TempDir == null)
{
throw new InvalidOperationException("TempDir path is not configured in SiteSettings.");
}
var userFilesDirInfo = new DirectoryInfo(Config.SiteSetup.Blog); var userFilesDirInfo = new DirectoryInfo(settings.Blog);
AbstractFileSystemHelpers.UserFilesDirName = userFilesDirInfo.FullName;
if (!userFilesDirInfo.Exists) userFilesDirInfo.Create(); if (!userFilesDirInfo.Exists) userFilesDirInfo.Create();
Config.UserFilesOptions = new FileServerOptions() Config.UserFilesOptions = new FileServerOptions()
{ {
FileProvider = new PhysicalFileProvider(AbstractFileSystemHelpers.UserFilesDirName), FileProvider = new PhysicalFileProvider(userFilesDirInfo.FullName),
RequestPath = PathString.FromUriComponent(Constants.UserFilesPath), RequestPath = PathString.FromUriComponent(Constants.UserFilesPath),
EnableDirectoryBrowsing = enableDirectoryBrowsing, EnableDirectoryBrowsing = enableDirectoryBrowsing,
}; };
Config.UserFilesOptions.EnableDefaultFiles = true; Config.UserFilesOptions.EnableDefaultFiles = true;
Config.UserFilesOptions.StaticFileOptions.ServeUnknownFileTypes = true; Config.UserFilesOptions.StaticFileOptions.ServeUnknownFileTypes = true;
var avatarsDirInfo = new DirectoryInfo(Config.SiteSetup.Avatars); var avatarsDirInfo = new DirectoryInfo(settings.Avatars);
if (!avatarsDirInfo.Exists) avatarsDirInfo.Create(); if (!avatarsDirInfo.Exists) avatarsDirInfo.Create();
Config.AvatarsDirName = avatarsDirInfo.FullName; Config.AvatarsDirName = avatarsDirInfo.FullName;
@ -1267,7 +1287,7 @@ ADD COLUMN IF NOT EXISTS ""Moderated"" boolean NOT NULL DEFAULT FALSE;");
}; };
var gitdirinfo = new DirectoryInfo(Config.SiteSetup.GitRepository); var gitdirinfo = new DirectoryInfo(settings.GitRepository);
Config.GitDirName = gitdirinfo.FullName; Config.GitDirName = gitdirinfo.FullName;
if (!gitdirinfo.Exists) gitdirinfo.Create(); if (!gitdirinfo.Exists) gitdirinfo.Create();
Config.GitOptions = new FileServerOptions() Config.GitOptions = new FileServerOptions()

View file

@ -115,11 +115,11 @@ namespace Yavsc.Helpers
return new TeXString(string.Join(lineSeparator,texlines)); return new TeXString(string.Join(lineSeparator,texlines));
} }
public static bool GenerateEstimatePdf(this PdfGenerationViewModel Model) public static bool GenerateEstimatePdf(this SiteSettings settings, PdfGenerationViewModel Model)
{ {
string errorMsg = null; string errorMsg = null;
var billdir = Model.DestDir; var billdir = Model.DestDir;
var tempdir = Config.SiteSetup.TempDir; var tempdir = settings.TempDir;
string name = Model.BaseFileName; string name = Model.BaseFileName;
string fullname = new FileInfo( string fullname = new FileInfo(
System.IO.Path.Combine(tempdir, name)).FullName; System.IO.Path.Combine(tempdir, name)).FullName;

View file

@ -2,6 +2,8 @@ using System.Diagnostics;
using System.Security.Claims; using System.Security.Claims;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Yavsc;
using Yavsc.Blogspot; using Yavsc.Blogspot;
using Yavsc.Models; using Yavsc.Models;
using Yavsc.Models.Access; using Yavsc.Models.Access;
@ -18,14 +20,17 @@ public class OldBlogSpotService
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
private readonly IFileSystemAuthManager fileSystemAuthManager; private readonly IFileSystemAuthManager fileSystemAuthManager;
private readonly SiteSettings siteSettings;
public OldBlogSpotService(ApplicationDbContext context, public OldBlogSpotService(ApplicationDbContext context,
IAuthorizationService authorizationService, IAuthorizationService authorizationService,
IFileSystemAuthManager fileSystemAuthManager) IFileSystemAuthManager fileSystemAuthManager,
IOptions<SiteSettings> siteSettings)
{ {
_authorizationService = authorizationService; _authorizationService = authorizationService;
_context = context; _context = context;
this.fileSystemAuthManager = fileSystemAuthManager; this.fileSystemAuthManager = fileSystemAuthManager;
this.siteSettings = siteSettings.Value;
} }
public Yavsc.Models.Blog.BlogPost Create(string userId, Yavsc.Models.Blog.BlogPost post, IFormFileCollection files) public Yavsc.Models.Blog.BlogPost Create(string userId, Yavsc.Models.Blog.BlogPost post, IFormFileCollection files)
@ -45,7 +50,7 @@ public class OldBlogSpotService
// Créer un répertoire pour les fichiers du blog // Créer un répertoire pour les fichiers du blog
string blogFilesSubdir = $"blogs/{post.Id}"; string blogFilesSubdir = $"blogs/{post.Id}";
string destDir = Path.Combine( string destDir = Path.Combine(
AbstractFileSystemHelpers.UserFilesDirName, siteSettings.Blog,
user.UserName, user.UserName,
blogFilesSubdir blogFilesSubdir
); );

View file

@ -21,13 +21,10 @@ public class Startup
{ {
Config.GoogleSettings = googleSettings.Value; Config.GoogleSettings = googleSettings.Value;
ResourcesHelpers.GlobalLocalizer = localizer; ResourcesHelpers.GlobalLocalizer = localizer;
Config.SiteSetup = siteSettings.Value;
Config.SmtpSetup = smtpSettings.Value; Config.SmtpSetup = smtpSettings.Value;
Config.Authority = siteSettings.Value.Authority; Config.Authority = siteSettings.Value.Authority;
string blogsDir = siteSettings.Value.Blog ?? throw new Exception("blogsDir is not set."); string blogsDir = siteSettings.Value.Blog ?? throw new Exception("blogsDir is not set.");
string billsDir = siteSettings.Value.Bills ?? throw new Exception("billsDir is not set."); string billsDir = siteSettings.Value.Bills ?? throw new Exception("billsDir is not set.");
AbstractFileSystemHelpers.UserFilesDirName = new DirectoryInfo(blogsDir).FullName;
AbstractFileSystemHelpers.UserBillsDirName = new DirectoryInfo(billsDir).FullName;
Config.Temp = siteSettings.Value.TempDir; Config.Temp = siteSettings.Value.TempDir;
Config.PayPalSettings = payPalSettings.Value; Config.PayPalSettings = payPalSettings.Value;
@ -35,7 +32,7 @@ public class Startup
// TODO implement an installation & upgrade procedure // TODO implement an installation & upgrade procedure
// Create required directories // Create required directories
foreach (string dir in new string[] { AbstractFileSystemHelpers.UserFilesDirName, AbstractFileSystemHelpers.UserBillsDirName, Config.Temp }) foreach (string dir in new string[] { blogsDir, billsDir, Config.Temp })
{ {
if (dir == null) if (dir == null)
{ {
@ -48,27 +45,27 @@ public class Startup
di.Create(); di.Create();
} }
} }
CheckApp(loggerFactory.CreateLogger<Startup>(), environmentName); CheckApp(siteSettings.Value, loggerFactory.CreateLogger<Startup>(), environmentName);
} }
public static void CheckApp(ILogger logger, string environmentName) public static void CheckApp(SiteSettings settings, ILogger logger, string environmentName)
{ {
var appData = Environment.GetEnvironmentVariable("APPDATA"); var appData = Environment.GetEnvironmentVariable("APPDATA");
if (appData == null) if (appData == null)
{ {
logger.LogWarning("AppData was not found in environment variables"); logger.LogWarning("AppData was not found in environment variables");
if (Config.SiteSetup.DataDir == null) { if (settings.DataDir == null) {
Config.SiteSetup.DataDir = "AppData"+environmentName; settings.DataDir = "AppData"+environmentName;
logger.LogInformation("Using: "+Config.SiteSetup.DataDir); logger.LogInformation("Using: "+settings.DataDir);
} else logger.LogInformation("Using value from settings: "+Config.SiteSetup.DataDir); } else logger.LogInformation("Using value from settings: "+settings.DataDir);
DirectoryInfo di = new DirectoryInfo(Config.SiteSetup.DataDir); DirectoryInfo di = new DirectoryInfo(settings.DataDir);
if (!di.Exists) if (!di.Exists)
{ {
di.Create(); di.Create();
logger.LogWarning("Created dir : "+di.FullName); logger.LogWarning("Created dir : "+di.FullName);
} }
logger.LogInformation("Using existing directory: "+di.FullName); logger.LogInformation("Using existing directory: "+di.FullName);
Environment.SetEnvironmentVariable("APPDATA", Config.SiteSetup.DataDir); Environment.SetEnvironmentVariable("APPDATA", settings.DataDir);
logger.LogWarning("It has been set to : "+Environment.GetEnvironmentVariable("APPDATA")); logger.LogWarning("It has been set to : "+Environment.GetEnvironmentVariable("APPDATA"));
} }

View file

@ -8,7 +8,7 @@ using Yavsc.ViewModels;
using Yavsc.ViewModels.Gen; using Yavsc.ViewModels.Gen;
using Yavsc.Services; using Yavsc.Services;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Yavsc.Server.Helpers; using Microsoft.Extensions.Options;
namespace Yavsc.ViewComponents namespace Yavsc.ViewComponents
{ {
@ -17,20 +17,29 @@ namespace Yavsc.ViewComponents
readonly ApplicationDbContext dbContext; readonly ApplicationDbContext dbContext;
readonly IBillingService billing; readonly IBillingService billing;
readonly IStringLocalizer<BillViewComponent> localizer; readonly IStringLocalizer<BillViewComponent> localizer;
private readonly SiteSettings siteSettings;
public BillViewComponent(ApplicationDbContext dbContext, public BillViewComponent(ApplicationDbContext dbContext,
IStringLocalizer<BillViewComponent> localizer, IStringLocalizer<BillViewComponent> localizer,
IBillingService billing) IBillingService billing,
IOptions<SiteSettings> siteSettings)
{ {
this.billing = billing; this.billing = billing;
this.dbContext = dbContext; this.dbContext = dbContext;
this.localizer = localizer; this.localizer = localizer;
this.siteSettings = siteSettings.Value;
} }
public async Task<IViewComponentResult> InvokeAsync(string code, IBillable billable, OutputFormat format, bool asBill) public async Task<IViewComponentResult> InvokeAsync(
string code,
IBillable billable,
OutputFormat format,
bool asBill,
SiteSettings settings
)
{ {
var di = new DirectoryInfo(Config.SiteSetup.Bills); var di = new DirectoryInfo(settings.Bills);
var dia = new DirectoryInfo(Config.SiteSetup.Avatars); var dia = new DirectoryInfo(settings.Avatars);
ViewBag.BillsDir = di.FullName; ViewBag.BillsDir = di.FullName;
ViewBag.AvatarsDir = dia.FullName; ViewBag.AvatarsDir = dia.FullName;
ViewBag.AsBill = asBill; // vrai pour une facture, sinon, c'est un devis ViewBag.AsBill = asBill; // vrai pour une facture, sinon, c'est un devis
@ -56,9 +65,9 @@ namespace Yavsc.ViewComponents
ViewBag.PerformerProfile = profile; ViewBag.PerformerProfile = profile;
ViewBag.ActivityLabel = (await dbContext.Activities.SingleAsync(a => a.Code == billable.ActivityCode)).Name; ViewBag.ActivityLabel = (await dbContext.Activities.SingleAsync(a => a.Code == billable.ActivityCode)).Name;
var proaddr = profile.OrganizationAddress.Address; var proAddr = profile.OrganizationAddress.Address;
ViewBag.PerformerOrganizationAddress = proaddr.SplitAddressToTeX() ; ViewBag.PerformerOrganizationAddress = proAddr.SplitAddressToTeX() ;
ViewBag.FooterPerformerOrganizationAddress = proaddr.SplitAddressToTeX(", "); ViewBag.FooterPerformerOrganizationAddress = proAddr.SplitAddressToTeX(", ");
ViewBag.PerformerAddress = performer.PostalAddress?.Address.SplitAddressToTeX() ; ViewBag.PerformerAddress = performer.PostalAddress?.Address.SplitAddressToTeX() ;
switch (format) { switch (format) {
@ -76,20 +85,21 @@ namespace Yavsc.ViewComponents
} }
ViewComponentContext.ViewContext.Writer = oldWriter; ViewComponentContext.ViewContext.Writer = oldWriter;
var genrtrData = new PdfGenerationViewModel var pdfGenerationViewModel = new PdfGenerationViewModel
{ {
Temp = Config.Temp, Temp = Config.Temp,
TeXSource = tex, TeXSource = tex,
DestDir = AbstractFileSystemHelpers.UserBillsDirName, DestDir = settings.Blog,
BaseFileName = billable.GetFileBaseName(billing) BaseFileName = billable.GetFileBaseName(billing)
}; };
if (genrtrData.GenerateEstimatePdf()) { if (settings.GenerateEstimatePdf(pdfGenerationViewModel)) {
return this.View(new { Generated = genrtrData.BaseFileName+".pdf" }); return this.View(new { Generated = pdfGenerationViewModel.BaseFileName+".pdf" });
} else { } else {
return View(new { Error = genrtrData.GenerationErrorMessage } ); return View(new { Error = pdfGenerationViewModel.GenerationErrorMessage } );
} }
} }
ViewBag.BillFileInfo = billable.GetBillInfo(billing); ViewBag.BillFileInfo = billable.GetBillInfo(billing, siteSettings);
ViewBag.Settings = settings;
return View("Default",billable); return View("Default",billable);
} }

View file

@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Yavsc.Models; using Yavsc.Models;
using Yavsc.Server.Helpers; using Yavsc.Server.Helpers;
using Yavsc.ViewModels.UserFiles; using Yavsc.ViewModels.UserFiles;
@ -11,24 +12,24 @@ namespace Yavsc.ViewComponents
{ {
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager; private readonly SignInManager<ApplicationUser> _signInManager;
public DirectoryViewComponent(UserManager<ApplicationUser> userManager, private readonly SiteSettings siteSettings;
SignInManager<ApplicationUser> signInManager)
public DirectoryViewComponent(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IOptions<SiteSettings> siteSettings)
{ {
_userManager = userManager; _userManager = userManager;
_signInManager = signInManager; _signInManager = signInManager;
this.siteSettings = siteSettings.Value;
} }
public async Task<IViewComponentResult> InvokeAsync(string dirname) public async Task<IViewComponentResult> InvokeAsync(string dirname)
{ {
string uid = ViewContext.HttpContext.User.GetUserId(); string uid = ViewContext.HttpContext.User.GetUserId();
return View(new UserDirectoryInfo(
IViewComponentResult result = null; siteSettings.Blog,
result = View(new UserDirectoryInfo(
AbstractFileSystemHelpers.UserFilesDirName,
uid, dirname)); uid, dirname));
return result;
} }
} }
} }

View file

@ -7,7 +7,7 @@
@model Yavsc.ViewModels.Gen.PdfGenerationViewModel @model Yavsc.ViewModels.Gen.PdfGenerationViewModel
@{ @{
Model.GenerateEstimatePdf(); ViewBag.Settings.GenerateEstimatePdf(Model);
} }
@if (Model.Generated) { @if (Model.Generated) {
<div> <div>

View file

@ -7,7 +7,7 @@
@model Yavsc.ViewModels.Gen.PdfGenerationViewModel @model Yavsc.ViewModels.Gen.PdfGenerationViewModel
@{ @{
Model.GenerateEstimatePdf(); SiteSettings.Value.GenerateEstimatePdf(Model);
} }
@if (Model.Generated) { @if (Model.Generated) {
<div> <div>

View file

@ -1,10 +1,10 @@
<nav class="navbar navbar-expand-sm navbar-dark bg-dark" aria-label="Yavsc" <nav class="navbar navbar-expand-sm navbar-dark bg-dark" aria-label="Yavsc"
style="background-image: url('@Config.SiteSetup.Banner'); style="background-image: url('@SiteSettings.Value.Banner');
background-position: 0%; background-position: 0%;
background-attachment: fixed;"> background-attachment: fixed;">
<a class="navbar-brand" href="/"> <a class="navbar-brand" href="/">
<img src="~/images/yavsc-logo.svg" alt="Yavsc" style="height:32px; margin-right:8px; display:inline-block; vertical-align:middle;" /> <img src="~/images/yavsc-logo.svg" alt="Yavsc" style="height:32px; margin-right:8px; display:inline-block; vertical-align:middle;" />
@Config.SiteSetup.Title @SiteSettings.Value.Title
</a> </a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" <button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbar" aria-controls="navbar" data-bs-target="#navbar" aria-controls="navbar"

View file

@ -33,7 +33,8 @@
"Bills": "bills", "Bills": "bills",
"Blog": "blog", "Blog": "blog",
"TempDir": "temp", "TempDir": "temp",
"DataDir": "data" "DataDir": "data",
"GitRepository": "git"
}, },
"Smtp": { "Smtp": {
"Host": "[YOURSMTPHOST]", "Host": "[YOURSMTPHOST]",

View file

@ -32,7 +32,6 @@ public static class Config
public static IConfigurationRoot? GoogleWebClientConfiguration { get; set; } public static IConfigurationRoot? GoogleWebClientConfiguration { get; set; }
public static GoogleServiceAccount? GServiceAccount { get; set; } public static GoogleServiceAccount? GServiceAccount { get; set; }
public static SiteSettings SiteSetup { get; set; } = new SiteSettings();
public static FileServerOptions? UserFilesOptions { get; set; } public static FileServerOptions? UserFilesOptions { get; set; }
public static FileServerOptions? GitOptions { get; set; } public static FileServerOptions? GitOptions { get; set; }
public static string AvatarsDirName { set; get; } = "Avatars"; public static string AvatarsDirName { set; get; } = "Avatars";

View file

@ -21,11 +21,12 @@ namespace Yavsc.Helpers
return bill; return bill;
} }
public static FileInfo GetBillInfo(this IBillable bill, IBillingService service) public static FileInfo GetBillInfo(this IBillable bill,
IBillingService service, SiteSettings siteSettings)
{ {
var suffix = bill.GetIsAcquitted() ? "-ack":null; var suffix = bill.GetIsAcquitted() ? "-ack":null;
var filename = bill.GetFileBaseName(service)+".pdf"; var filename = bill.GetFileBaseName(service)+".pdf";
return new FileInfo(Path.Combine(AbstractFileSystemHelpers.UserBillsDirName, filename)); return new FileInfo(Path.Combine(siteSettings.Bills, filename));
} }
} }
} }

View file

@ -64,6 +64,7 @@ public static class EstimateSignatureFileHelper
long estimateId, long estimateId,
SignatureType type, SignatureType type,
SignaturePadPayload payload, SignaturePadPayload payload,
SiteSettings siteSettings,
CancellationToken token = default) CancellationToken token = default)
{ {
if (user is null) throw new ArgumentNullException(nameof(user)); if (user is null) throw new ArgumentNullException(nameof(user));
@ -77,7 +78,7 @@ public static class EstimateSignatureFileHelper
// (not user-derived), so we skip the IsValidYavscPath // (not user-derived), so we skip the IsValidYavscPath
// check that ReceiveUserFile performs on user-supplied // check that ReceiveUserFile performs on user-supplied
// subpaths. // subpaths.
var root = user.EnsureDestinationDirectory(SignaturesSubdir); var root = user.EnsureDestinationDirectory(SignaturesSubdir, siteSettings);
var fileName = FileNameFormat(type, estimateId, DateTime.UtcNow.Ticks); var fileName = FileNameFormat(type, estimateId, DateTime.UtcNow.Ticks);
var fullPath = Path.Combine(root, fileName); var fullPath = Path.Combine(root, fileName);

View file

@ -13,6 +13,7 @@ using Yavsc.Exceptions;
using Yavsc.Abstract.Helpers; using Yavsc.Abstract.Helpers;
using ImageMagick; using ImageMagick;
using Yavsc.Server.Models.FileSystem; using Yavsc.Server.Models.FileSystem;
using Yavsc.ViewModels.UserFiles;
namespace Yavsc.Server.Helpers namespace Yavsc.Server.Helpers
{ {
public static class FileSystemHelpers public static class FileSystemHelpers
@ -27,10 +28,12 @@ namespace Yavsc.Server.Helpers
} }
} }
} }
public static async Task<FileReceivedInfo> ReceiveProSignatureAsync(this ClaimsPrincipal user, string billingCode, long estimateId, IFormFile formFile, string signType) public static async Task<FileReceivedInfo> ReceiveProSignatureAsync(this ClaimsPrincipal user, string billingCode, long estimateId, IFormFile formFile, string signType,
SiteSettings settings
)
{ {
var item = new FileReceivedInfo( var item = new FileReceivedInfo(
Config.SiteSetup.Bills, settings.Bills,
AbstractFileSystemHelpers.SignFileNameFormat("pro", billingCode, estimateId)); AbstractFileSystemHelpers.SignFileNameFormat("pro", billingCode, estimateId));
var fi = new FileInfo(item.FullName); var fi = new FileInfo(item.FullName);
@ -39,16 +42,11 @@ namespace Yavsc.Server.Helpers
return item; return item;
} }
public static string GetAvatarUri(this ApplicationUser user)
{
return $"/{Config.SiteSetup.Avatars}/{user.UserName}.png";
}
public static string EnsureDestinationDirectory( public static string EnsureDestinationDirectory(
this ClaimsPrincipal user, this ClaimsPrincipal user,
string subpath) string subpath, SiteSettings siteSettings)
{ {
var root = Path.Combine(AbstractFileSystemHelpers.UserFilesDirName, user.Identity.Name); var root = Path.Combine(siteSettings.Blog, user.Identity.Name);
if (!string.IsNullOrWhiteSpace(subpath)) if (!string.IsNullOrWhiteSpace(subpath))
{ {
if (!subpath.IsValidYavscPath()) if (!subpath.IsValidYavscPath())
@ -69,18 +67,18 @@ namespace Yavsc.Server.Helpers
/// <param name="user"></param> /// <param name="user"></param>
/// <param name="fileName"></param> /// <param name="fileName"></param>
public static void DeleteUserFile(this ApplicationUser user, string fileName) public static void DeleteUserFile(this ApplicationUser user, string fileName, SiteSettings siteSettings)
{ {
var root = Path.Combine(AbstractFileSystemHelpers.UserFilesDirName, user.UserName); var root = Path.Combine(siteSettings.Blog, user.UserName);
var fi = new FileInfo(Path.Combine(root, fileName)); var fi = new FileInfo(Path.Combine(root, fileName));
if (!fi.Exists) return; if (!fi.Exists) return;
fi.Delete(); fi.Delete();
user.DiskUsage -= fi.Length; user.DiskUsage -= fi.Length;
} }
public static FsOperationInfo DeleteUserDirOrFile(this ApplicationUser user, string dirName) public static FsOperationInfo DeleteUserDirOrFile(this ApplicationUser user, string dirName, SiteSettings siteSettings)
{ {
var root = Path.Combine(AbstractFileSystemHelpers.UserFilesDirName, user.UserName); var root = Path.Combine(siteSettings.Blog, user.UserName);
if (string.IsNullOrEmpty(dirName)) if (string.IsNullOrEmpty(dirName))
return new FsOperationInfo { Done = false, ErrorCode = ErrorCode.InvalidRequest, ErrorMessage = "specify a directory or file name" }; return new FsOperationInfo { Done = false, ErrorCode = ErrorCode.InvalidRequest, ErrorMessage = "specify a directory or file name" };
@ -101,9 +99,9 @@ namespace Yavsc.Server.Helpers
return new FsOperationInfo { Done = true }; return new FsOperationInfo { Done = true };
} }
public static FsOperationInfo MoveUserDir(this ApplicationUser user, string fromDirName, string toDirName) public static FsOperationInfo MoveUserDir(this ApplicationUser user, string fromDirName, string toDirName, SiteSettings siteSettings)
{ {
var root = Path.Combine(AbstractFileSystemHelpers.UserFilesDirName, user.UserName); var root = Path.Combine(siteSettings.Blog, user.UserName);
if (string.IsNullOrEmpty(fromDirName)) if (string.IsNullOrEmpty(fromDirName))
return new FsOperationInfo { Done = false, ErrorCode = ErrorCode.InvalidRequest, ErrorMessage = "specify a dir name " }; return new FsOperationInfo { Done = false, ErrorCode = ErrorCode.InvalidRequest, ErrorMessage = "specify a dir name " };
@ -127,28 +125,28 @@ namespace Yavsc.Server.Helpers
di.MoveTo(destPath); di.MoveTo(destPath);
return new FsOperationInfo { Done = true }; return new FsOperationInfo { Done = true };
} }
public static FsOperationInfo MoveUserFileToDir(this ApplicationUser user, string fileNameFrom, string fileNameDest) public static FsOperationInfo MoveUserFileToDir(this ApplicationUser user, string fileNameFrom, string fileNameDest, SiteSettings siteSettings)
{ {
var root = Path.Combine(AbstractFileSystemHelpers.UserFilesDirName, user.UserName); var root = Path.Combine(siteSettings.Blog, user.UserName);
var fi = new FileInfo(Path.Combine(root, fileNameFrom)); var fi = new FileInfo(Path.Combine(root, fileNameFrom));
if (!fi.Exists) return new FsOperationInfo { ErrorCode = ErrorCode.NotFound, ErrorMessage = "no file to move" }; if (!fi.Exists) return new FsOperationInfo { Done = false, ErrorCode = ErrorCode.NotFound, ErrorMessage = "no file to move" };
string dest; string dest;
if (!string.IsNullOrEmpty(fileNameDest)) dest = Path.Combine(root, fileNameDest); if (!string.IsNullOrEmpty(fileNameDest)) dest = Path.Combine(root, fileNameDest);
else dest = root; else dest = root;
var fo = new FileInfo(dest); var fo = new FileInfo(dest);
if (fo.Exists) return new FsOperationInfo { ErrorCode = ErrorCode.DestExists, ErrorMessage = "destination file name is an existing file" }; if (fo.Exists) return new FsOperationInfo { Done = false, ErrorCode = ErrorCode.DestExists, ErrorMessage = "destination file name is an existing file" };
var dout = new DirectoryInfo(dest); var dout = new DirectoryInfo(dest);
if (!dout.Exists) dout.Create(); if (!dout.Exists) dout.Create();
fi.MoveTo(Path.Combine(dout.FullName, fi.Name)); fi.MoveTo(Path.Combine(dout.FullName, fi.Name));
return new FsOperationInfo { Done = true }; return new FsOperationInfo { Done = true };
} }
public static FsOperationInfo MoveUserFile(this ApplicationUser user, string fileNameFrom, string fileNameDest) public static FsOperationInfo MoveUserFile(this ApplicationUser user, string fileNameFrom, string fileNameDest, SiteSettings siteSettings)
{ {
var root = Path.Combine(AbstractFileSystemHelpers.UserFilesDirName, user.UserName); var root = Path.Combine(siteSettings.Blog, user.UserName);
var fi = new FileInfo(Path.Combine(root, fileNameFrom)); var fi = new FileInfo(Path.Combine(root, fileNameFrom));
if (!fi.Exists) return new FsOperationInfo { ErrorCode = ErrorCode.NotFound, ErrorMessage = "no file to move" }; if (!fi.Exists) return new FsOperationInfo { Done = false, ErrorCode = ErrorCode.NotFound, ErrorMessage = "no file to move" };
var fo = new FileInfo(Path.Combine(root, fileNameDest)); var fo = new FileInfo(Path.Combine(root, fileNameDest));
if (fo.Exists) return new FsOperationInfo { ErrorCode = ErrorCode.DestExists, ErrorMessage = "destination file name is an existing file" }; if (fo.Exists) return new FsOperationInfo { Done = false, ErrorCode = ErrorCode.DestExists, ErrorMessage = "destination file name is an existing file" };
fi.MoveTo(fo.FullName); fi.MoveTo(fo.FullName);
return new FsOperationInfo { Done = true }; return new FsOperationInfo { Done = true };
} }
@ -245,7 +243,9 @@ namespace Yavsc.Server.Helpers
} }
public static FileReceivedInfo ReceiveAvatar(this ApplicationUser user, IFormFile formFile) public static FileReceivedInfo ReceiveAvatar(this ApplicationUser user,
IFormFile formFile,
SiteSettings settings)
{ {
if (user == null) throw new ArgumentNullException(nameof(user)); if (user == null) throw new ArgumentNullException(nameof(user));
if (formFile == null) throw new ArgumentNullException(nameof(formFile)); if (formFile == null) throw new ArgumentNullException(nameof(formFile));
@ -256,7 +256,7 @@ namespace Yavsc.Server.Helpers
avatarsRequestPath = Constants.AvatarsPath; avatarsRequestPath = Constants.AvatarsPath;
} }
var avatarsDirectory = Config.SiteSetup?.Avatars; var avatarsDirectory = settings.Avatars;
if (string.IsNullOrWhiteSpace(avatarsDirectory)) if (string.IsNullOrWhiteSpace(avatarsDirectory))
{ {
avatarsDirectory = "avatars"; avatarsDirectory = "avatars";
@ -306,5 +306,12 @@ namespace Yavsc.Server.Helpers
return $"{basename}-{flow.SequenceNumber}{ext}"; return $"{basename}-{flow.SequenceNumber}{ext}";
} }
public static UserDirectoryInfo GetUserFiles(SiteSettings settings, string userId, string subdir)
{
UserDirectoryInfo di = new UserDirectoryInfo(settings.Blog, userId, subdir);
return di;
}
} }
} }

View file

@ -5,6 +5,8 @@ using System.Security.Claims;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Yavsc;
using Yavsc.Blogspot; using Yavsc.Blogspot;
using Yavsc.Models; using Yavsc.Models;
using Yavsc.Models.Access; using Yavsc.Models.Access;
@ -19,36 +21,34 @@ public class BlogSpotService
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
private readonly IFileSystemAuthManager fileSystemAuthManager; private readonly IFileSystemAuthManager fileSystemAuthManager;
private readonly SiteSettings siteSettings;
public BlogSpotService( public BlogSpotService(
ApplicationDbContext context, ApplicationDbContext context,
IAuthorizationService authorizationService, IAuthorizationService authorizationService,
IFileSystemAuthManager fileSystemAuthManager) IFileSystemAuthManager fileSystemAuthManager,
IOptions<SiteSettings> siteSettings)
{ {
_authorizationService = authorizationService; _authorizationService = authorizationService;
_context = context; _context = context;
this.fileSystemAuthManager = fileSystemAuthManager; this.fileSystemAuthManager = fileSystemAuthManager;
this.siteSettings = siteSettings.Value;
} }
public BlogPost Create(string userId, BlogPost post, IFormFileCollection files) public void AttachFiles(IFormFileCollection files, string userId, long postId)
{ {
// Sauvegarder le post d'abord pour obtenir son ID
// Le createur vient de l'authentification, donc on ne le prend pas du post
post.AuthorId = userId;
_context.BlogSpot.Add(post);
_context.SaveChanges(userId);
// Traiter les fichiers attaches s'il y en a // Traiter les fichiers attaches s'il y en a
if (files != null && files.Count > 0) if (files != null && files.Count > 0)
{ {
var user = _context.Users.FirstOrDefault(u => u.Id == userId); var user = _context.Users.FirstOrDefault(u => u.Id == userId);
if (user != null) if (user == null)
{ throw new InvalidOperationException($"Utilisateur {userId} non trouvé.");
try try
{ {
string blogFilesSubdir = $"blogs/{post.Id}"; string blogFilesSubdir = $"blogs/{postId}";
string destDir = Path.Combine( string destDir = Path.Combine(
AbstractFileSystemHelpers.UserFilesDirName, siteSettings.Blog,
user.UserName, user.UserName,
blogFilesSubdir); blogFilesSubdir);
var di = new DirectoryInfo(destDir); var di = new DirectoryInfo(destDir);
@ -70,7 +70,7 @@ public class BlogSpotService
var attachment = new BlogAttachedFile var attachment = new BlogAttachedFile
{ {
PostId = post.Id, PostId = postId,
FileId = uploadedFile.Id FileId = uploadedFile.Id
}; };
_context.BlogAttachedFiles.Add(attachment); _context.BlogAttachedFiles.Add(attachment);
@ -85,6 +85,13 @@ public class BlogSpotService
} }
} }
public BlogPost Create(string userId, BlogPost post )
{
// Sauvegarder le post d'abord pour obtenir son ID
// Le createur vient de l'authentification, donc on ne le prend pas du post
post.AuthorId = userId;
_context.BlogSpot.Add(post);
_context.SaveChanges(userId);
return post; return post;
} }
@ -186,58 +193,6 @@ public class BlogSpotService
_context.SaveChanges(user.GetUserId()); _context.SaveChanges(user.GetUserId());
} }
public async Task Modify(ClaimsPrincipal user, BlogPost blog, IFormFileCollection files)
{
await Modify(user, blog);
if (files == null || files.Count == 0)
return;
var userId = user.GetUserId();
var userEntity = _context.Users.FirstOrDefault(u => u.Id == userId);
if (userEntity == null)
return;
try
{
string blogFilesSubdir = $"blogs/{blog.Id}";
string destDir = Path.Combine(
AbstractFileSystemHelpers.UserFilesDirName,
userEntity.UserName,
blogFilesSubdir);
var di = new DirectoryInfo(destDir);
if (!di.Exists) di.Create();
foreach (var formFile in files)
{
var fileInfo = userEntity.ReceiveUserFile(destDir, formFile);
if (fileInfo != null && !fileInfo.QuotaOffense)
{
var uploadedFile = new UploadedFile
{
Path = fileInfo.FileName,
ContentType = formFile.ContentType,
Length = formFile.Length
};
_context.UploadedFiles.Add(uploadedFile);
_context.SaveChanges(userId);
var attachment = new BlogAttachedFile
{
PostId = blog.Id,
FileId = uploadedFile.Id
};
_context.BlogAttachedFiles.Add(attachment);
}
}
_context.SaveChanges(userId);
}
catch (Exception ex)
{
Debug.WriteLine($"Erreur lors du traitement des fichiers : {ex.Message}");
}
}
public async Task<IEnumerable<IBlogPost>> Index(ClaimsPrincipal user, string id, int skip = 0, int take = 25) public async Task<IEnumerable<IBlogPost>> Index(ClaimsPrincipal user, string id, int skip = 0, int take = 25)
{ {

View file

@ -6,89 +6,72 @@ namespace Yavsc
{ {
public class SiteSettings public class SiteSettings
{ {
public string Title { get; set; } = "Yavsc"; public string Title { get; set; }
public string Slogan { get; set; } = ""; public string Slogan { get; set; }
public string Banner { get; set; } = ""; public string Banner { get; set; }
public string StyleSheet { get; set; } = "site.css"; public string StyleSheet { get; set; }
public string FavIcon { get; set; } = "favicon.ico"; public string FavIcon { get; set; }
public string Logo { get; set; } = "logo.png"; public string Logo { get; set; }
/// <summary> /// <summary>
/// Origins to allow via the CORS "default" policy. /// Origins to allow via the CORS "default" policy.
/// Each entry must be a full origin (scheme + host [+ port]), e.g. "https://app.example.com". /// Each entry must be a full origin (scheme + host [+ port]), e.g. "https://app.example.com".
/// </summary> /// </summary>
public IList<string> CorsAllowedOrigins { get; set; } = new List<string> public IList<string> CorsAllowedOrigins { get; set; }
{
"https://localhost:5001"
};
/// <summary> /// <summary>
/// External Url /// External Url
/// </summary> /// </summary>
/// <value></value> /// <value></value>
public string ExternalUrl { get; set; } = "http://lua.pschneider.fr"; public string ExternalUrl { get; set; }
/// <summary> /// <summary>
/// Base URL of the API fronting this site. /// Base URL of the API fronting this site.
/// </summary> /// </summary>
public string ApiUrl { get; set; } = ""; public string ApiUrl { get; set; }
/// <summary> /// <summary>
/// Must be a fqdn. /// Must be a fqdn.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public string Authority { get; set; } = "lua.pschneider.fr"; public string Authority { get; set; }
/// <summary> /// <summary>
/// Owner's email /// Owner's email
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public StaticContact Owner { get; set; } = new StaticContact public StaticContact Owner { get; set; }
{
EMail = "root@lua.pschneider.fr",
Name = "Root"
} ;
/// <summary> /// <summary>
/// Administrator's email /// Administrator's email
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public StaticContact Admin { get; set; } = new StaticContact public StaticContact Admin { get; set; }
{
EMail = "root@lua.pschneider.fr",
Name = "Root"
} ;
public string DataDir { get; set; } = "data"; public string DataDir { get; set; }
public string Avatars { get; set; } = "avatars"; public string Avatars { get; set; }
public long Quota { get; set; } public long Quota { get; set; }
public string Blog { get; set; } = "blogs"; public string Blog { get; set; }
public string Bills { get; set; } = "bills"; public string Bills { get; set; }
public string GitRepository { get; set; } = "sources"; public string GitRepository { get; set; }
public string BusinessName { get; set; } = "Yavsc";
public string? Street { get; set; }
public string? PostalCode { get; set; }
public string? CountryCode { get; set; }
public string HomeViewName { get; set; } = "Home";
/// <summary> /// <summary>
/// Specifies the directory where should be /// Specifies the directory where should be
/// generated pdf files using pandoc /// generated pdf files using pandoc
/// </summary> /// </summary>
/// <returns>The temporary directory to use</returns> /// <returns>The temporary directory to use</returns>
public string TempDir { get; set; } = "temp"; public string TempDir { get; set; }
/// <summary> /// <summary>
/// Disk usage user list maximum length in memory /// Disk usage user list maximum length in memory
/// </summary> /// </summary>
/// <value></value> /// <value></value>
public int DUUserListLen { get; set; } = 256; public int DUUserListLen { get; set; }
/// <summary> /// <summary>
/// Default acl file name /// Default acl file name
/// </summary> /// </summary>
/// <value></value> /// <value></value>
public string AccessListFileName { get; set; } = ".access"; public string AccessListFileName { get; set; }
} }
} }