Merge remote-tracking branch 'upstream/dev' into Bootstrap

This commit is contained in:
Leigh Pointer
2025-05-30 16:06:19 +02:00
30 changed files with 502 additions and 243 deletions

View File

@ -22,7 +22,6 @@ using Microsoft.AspNetCore.Cors;
using System.IO.Compression;
using Oqtane.Services;
using Microsoft.Extensions.Primitives;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.Net.Http.Headers;
// ReSharper disable StringIndexOfIsCultureSpecific.1

View File

@ -9,6 +9,7 @@ using Oqtane.Infrastructure;
using Oqtane.Repository;
using Oqtane.Security;
using System.Net;
using System.IO;
namespace Oqtane.Controllers
{
@ -20,18 +21,22 @@ namespace Oqtane.Controllers
private readonly IPageRepository _pages;
private readonly IModuleDefinitionRepository _moduleDefinitions;
private readonly ISettingRepository _settings;
private readonly IFolderRepository _folders;
private readonly IFileRepository _files;
private readonly IUserPermissions _userPermissions;
private readonly ISyncManager _syncManager;
private readonly ILogManager _logger;
private readonly Alias _alias;
public ModuleController(IModuleRepository modules, IPageModuleRepository pageModules, IPageRepository pages, IModuleDefinitionRepository moduleDefinitions, ISettingRepository settings, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger)
public ModuleController(IModuleRepository modules, IPageModuleRepository pageModules, IPageRepository pages, IModuleDefinitionRepository moduleDefinitions, ISettingRepository settings, IFolderRepository folders, IFileRepository files, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger)
{
_modules = modules;
_pageModules = pageModules;
_pages = pages;
_moduleDefinitions = moduleDefinitions;
_settings = settings;
_folders = folders;
_files = files;
_userPermissions = userPermissions;
_syncManager = syncManager;
_logger = logger;
@ -248,6 +253,61 @@ namespace Oqtane.Controllers
return content;
}
// POST api/<controller>/export?moduleid=x&pageid=y&folderid=z&filename=a
[HttpPost("export")]
[Authorize(Roles = RoleNames.Registered)]
public int Export(int moduleid, int pageid, int folderid, string filename)
{
var fileid = -1;
var module = _modules.GetModule(moduleid);
if (module != null && module.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Page, pageid, PermissionNames.Edit) &&
_userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Folder, folderid, PermissionNames.Edit) && !string.IsNullOrEmpty(filename))
{
// get content
var content = _modules.ExportModule(moduleid);
// get folder
var folder = _folders.GetFolder(folderid, false);
string folderPath = _folders.GetFolderPath(folder);
if (!Directory.Exists(folderPath))
{
Directory.CreateDirectory(folderPath);
}
// create json file
filename = Utilities.GetFriendlyUrl(Path.GetFileNameWithoutExtension(filename)) + ".json";
string filepath = Path.Combine(folderPath, filename);
if (System.IO.File.Exists(filepath))
{
System.IO.File.Delete(filepath);
}
System.IO.File.WriteAllText(filepath, content);
// register file
var file = _files.GetFile(folderid, filename);
if (file == null)
{
file = new Models.File { FolderId = folderid, Name = filename, Extension = "json", Size = (int)new FileInfo(filepath).Length, ImageWidth = 0, ImageHeight = 0 };
_files.AddFile(file);
}
else
{
file.Size = (int)new FileInfo(filepath).Length;
_files.UpdateFile(file);
}
fileid = file.FileId;
_logger.Log(LogLevel.Information, this, LogFunction.Read, "Content Exported For Module {ModuleId} To Folder {FolderId}", moduleid, folderid);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Export Attempt For Module {Module} To Folder {FolderId}", moduleid, folderid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
return fileid;
}
// POST api/<controller>/import?moduleid=x&pageid=y
[HttpPost("import")]
[Authorize(Roles = RoleNames.Registered)]

View File

@ -24,26 +24,50 @@ namespace Oqtane.Controllers
private readonly IPageModuleRepository _pageModules;
private readonly IUserPermissions _userPermissions;
private readonly ISyncManager _syncManager;
private readonly IAliasAccessor _aliasAccessor;
private readonly IOptionsMonitorCache<CookieAuthenticationOptions> _cookieCache;
private readonly IOptionsMonitorCache<OpenIdConnectOptions> _oidcCache;
private readonly IOptionsMonitorCache<OAuthOptions> _oauthCache;
private readonly IOptionsMonitorCache<IdentityOptions> _identityCache;
private readonly IOptions<CookieAuthenticationOptions> _cookieOptions;
private readonly IOptionsSnapshot<CookieAuthenticationOptions> _cookieOptionsSnapshot;
private readonly IOptionsMonitorCache<CookieAuthenticationOptions> _cookieOptionsMonitorCache;
private readonly IOptions<OpenIdConnectOptions> _oidcOptions;
private readonly IOptionsSnapshot<OpenIdConnectOptions> _oidcOptionsSnapshot;
private readonly IOptionsMonitorCache<OpenIdConnectOptions> _oidcOptionsMonitorCache;
private readonly IOptions<OAuthOptions> _oauthOptions;
private readonly IOptionsSnapshot<OAuthOptions> _oauthOptionsSnapshot;
private readonly IOptionsMonitorCache<OAuthOptions> _oauthOptionsMonitorCache;
private readonly IOptions<IdentityOptions> _identityOptions;
private readonly IOptionsSnapshot<IdentityOptions> _identityOptionsSnapshot;
private readonly IOptionsMonitorCache<IdentityOptions> _identityOptionsMonitorCache;
private readonly ILogManager _logger;
private readonly Alias _alias;
private readonly string _visitorCookie;
public SettingController(ISettingRepository settings, IPageModuleRepository pageModules, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, IAliasAccessor aliasAccessor, IOptionsMonitorCache<CookieAuthenticationOptions> cookieCache, IOptionsMonitorCache<OpenIdConnectOptions> oidcCache, IOptionsMonitorCache<OAuthOptions> oauthCache, IOptionsMonitorCache<IdentityOptions> identityCache, ILogManager logger)
public SettingController(ISettingRepository settings, IPageModuleRepository pageModules, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager,
IOptions<CookieAuthenticationOptions> cookieOptions, IOptionsSnapshot<CookieAuthenticationOptions> cookieOptionsSnapshot, IOptionsMonitorCache<CookieAuthenticationOptions> cookieOptionsMonitorCache,
IOptions<OpenIdConnectOptions> oidcOptions, IOptionsSnapshot<OpenIdConnectOptions> oidcOptionsSnapshot, IOptionsMonitorCache<OpenIdConnectOptions> oidcOptionsMonitorCache,
IOptions<OAuthOptions> oauthOptions, IOptionsSnapshot<OAuthOptions> oauthOptionsSnapshot, IOptionsMonitorCache<OAuthOptions> oauthOptionsMonitorCache,
IOptions<IdentityOptions> identityOptions, IOptionsSnapshot<IdentityOptions> identityOptionsSnapshot, IOptionsMonitorCache<IdentityOptions> identityOptionsMonitorCache,
ILogManager logger)
{
_settings = settings;
_pageModules = pageModules;
_userPermissions = userPermissions;
_syncManager = syncManager;
_aliasAccessor = aliasAccessor;
_cookieCache = cookieCache;
_oidcCache = oidcCache;
_oauthCache = oauthCache;
_identityCache = identityCache;
_cookieOptions = cookieOptions;
_cookieOptionsSnapshot = cookieOptionsSnapshot;
_cookieOptionsMonitorCache = cookieOptionsMonitorCache;
_oidcOptions = oidcOptions;
_oidcOptionsSnapshot = oidcOptionsSnapshot;
_oidcOptionsMonitorCache = oidcOptionsMonitorCache;
_oauthOptions = oauthOptions;
_oauthOptionsSnapshot = oauthOptionsSnapshot;
_oauthOptionsMonitorCache = oauthOptionsMonitorCache;
_identityOptions = identityOptions;
_identityOptionsSnapshot = identityOptionsSnapshot;
_identityOptionsMonitorCache = identityOptionsMonitorCache;
_logger = logger;
_alias = tenantManager.GetAlias();
_visitorCookie = Constants.VisitorCookiePrefix + _alias.SiteId.ToString();
@ -210,21 +234,21 @@ namespace Oqtane.Controllers
[Authorize(Roles = RoleNames.Admin)]
public void Clear()
{
// clear SiteOptionsCache for each option type
var cookieCache = new SiteOptionsCache<CookieAuthenticationOptions>(_aliasAccessor);
cookieCache.Clear();
var oidcCache = new SiteOptionsCache<OpenIdConnectOptions>(_aliasAccessor);
oidcCache.Clear();
var oauthCache = new SiteOptionsCache<OAuthOptions>(_aliasAccessor);
oauthCache.Clear();
var identityCache = new SiteOptionsCache<IdentityOptions>(_aliasAccessor);
identityCache.Clear();
(_cookieOptions as SiteOptionsManager<CookieAuthenticationOptions>).Reset();
(_cookieOptionsSnapshot as SiteOptionsManager<CookieAuthenticationOptions>).Reset();
_cookieOptionsMonitorCache.Clear();
// clear IOptionsMonitorCache for each option type
_cookieCache.Clear();
_oidcCache.Clear();
_oauthCache.Clear();
_identityCache.Clear();
(_oidcOptions as SiteOptionsManager<OpenIdConnectOptions>).Reset();
(_oidcOptionsSnapshot as SiteOptionsManager<OpenIdConnectOptions>).Reset();
_oidcOptionsMonitorCache.Clear();
(_oauthOptions as SiteOptionsManager<OAuthOptions>).Reset();
(_oauthOptionsSnapshot as SiteOptionsManager<OAuthOptions>).Reset();
_oauthOptionsMonitorCache.Clear();
(_identityOptions as SiteOptionsManager<IdentityOptions>).Reset();
(_identityOptionsSnapshot as SiteOptionsManager<IdentityOptions>).Reset();
_identityOptionsMonitorCache.Clear();
_logger.Log(LogLevel.Information, this, LogFunction.Other, "Site Options Cache Cleared");
}

View File

@ -131,7 +131,7 @@ namespace Oqtane.Controllers
filtered.TwoFactorCode = "";
filtered.SecurityStamp = "";
// include private properties if authenticated user is accessing their own user account os is an administrator
// include private properties if authenticated user is accessing their own user account or is an administrator
if (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == user.UserId)
{
filtered.Email = user.Email;
@ -140,6 +140,7 @@ namespace Oqtane.Controllers
filtered.LastLoginOn = user.LastLoginOn;
filtered.LastIPAddress = user.LastIPAddress;
filtered.TwoFactorRequired = user.TwoFactorRequired;
filtered.EmailConfirmed = user.EmailConfirmed;
filtered.Roles = user.Roles;
filtered.CreatedBy = user.CreatedBy;
filtered.CreatedOn = user.CreatedOn;
@ -200,10 +201,15 @@ namespace Oqtane.Controllers
[Authorize]
public async Task<User> Put(int id, [FromBody] User user)
{
if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && user.UserId == id && _users.GetUser(user.UserId, false) != null
var existing = _userManager.GetUser(user.UserId, user.SiteId);
if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && user.UserId == id && existing != null
&& (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || User.Identity.Name == user.Username))
{
user.EmailConfirmed = User.IsInRole(RoleNames.Admin);
// only authorized users can update the email confirmation
if (!_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin))
{
user.EmailConfirmed = existing.EmailConfirmed;
}
user = await _userManager.UpdateUser(user);
}
else

View File

@ -1,5 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Models;
using Microsoft.AspNetCore.Identity;
using System;

View File

@ -19,7 +19,6 @@ namespace Oqtane.Infrastructure
{
var cache = map.GetOrAdd(GetKey(), new OptionsCache<TOptions>());
cache.Clear();
}
public TOptions GetOrAdd(string name, Func<TOptions> createOptions)

View File

@ -65,7 +65,12 @@ namespace Oqtane.Managers
{
user.SiteId = siteid;
user.Roles = GetUserRoles(user.UserId, user.SiteId);
user.SecurityStamp = _identityUserManager.FindByNameAsync(user.Username).GetAwaiter().GetResult()?.SecurityStamp;
var identityuser = _identityUserManager.FindByNameAsync(user.Username).GetAwaiter().GetResult();
if (identityuser != null)
{
user.SecurityStamp = identityuser.SecurityStamp;
user.EmailConfirmed = identityuser.EmailConfirmed;
}
user.Settings = _settings.GetSettings(EntityNames.User, user.UserId)
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
}
@ -245,22 +250,30 @@ namespace Oqtane.Managers
{
identityuser.Email = user.Email;
await _identityUserManager.UpdateAsync(identityuser); // security stamp not updated
// if email address changed and it is not confirmed, verification is required for new email address
if (!user.EmailConfirmed)
{
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nIn Order To Verify The Email Address Associated To Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, user, "User Account Verification", body);
_notifications.AddNotification(notification);
}
}
if (user.EmailConfirmed)
{
var emailConfirmationToken = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
await _identityUserManager.ConfirmEmailAsync(identityuser, emailConfirmationToken);
if (!identityuser.EmailConfirmed)
{
var emailConfirmationToken = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
await _identityUserManager.ConfirmEmailAsync(identityuser, emailConfirmationToken);
string body = "Dear " + user.DisplayName + ",\n\nThe Email Address For Your User Account Has Been Verified. You Can Now Login With Your Username And Password.";
var notification = new Notification(user.SiteId, user, "User Account Verification", body);
_notifications.AddNotification(notification);
}
}
else
{
identityuser.EmailConfirmed = false;
await _identityUserManager.UpdateAsync(identityuser); // security stamp not updated
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nIn Order To Verify The Email Address Associated To Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, user, "User Account Verification", body);
_notifications.AddNotification(notification);
}
user = _users.UpdateUser(user);

View File

@ -48,7 +48,7 @@
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.11" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.8" />
<PackageReference Include="HtmlAgilityPack" Version="1.12.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Oqtane.Client\Oqtane.Client.csproj" />