Merge tag 'v5.0.1' into dev
This commit is contained in:
@ -76,7 +76,7 @@ namespace Oqtane.Controllers
|
||||
[Authorize(Roles = RoleNames.Host)]
|
||||
public Alias Put(int id, [FromBody] Alias alias)
|
||||
{
|
||||
if (ModelState.IsValid && _aliases.GetAlias(alias.AliasId, false) != null)
|
||||
if (ModelState.IsValid && alias.AliasId == id && _aliases.GetAlias(alias.AliasId, false) != null)
|
||||
{
|
||||
alias = _aliases.UpdateAlias(alias);
|
||||
_syncManager.AddSyncEvent(alias.TenantId, EntityNames.Alias, alias.AliasId, SyncEventActions.Update);
|
||||
|
@ -35,8 +35,8 @@ namespace Oqtane.Controllers
|
||||
private readonly ISyncManager _syncManager;
|
||||
private readonly ILogManager _logger;
|
||||
private readonly Alias _alias;
|
||||
|
||||
public FileController(IWebHostEnvironment environment, IFileRepository files, IFolderRepository folders, IUserPermissions userPermissions, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager)
|
||||
private readonly ISettingRepository _settingRepository;
|
||||
public FileController(IWebHostEnvironment environment, IFileRepository files, IFolderRepository folders, IUserPermissions userPermissions, ISettingRepository settingRepository, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager)
|
||||
{
|
||||
_environment = environment;
|
||||
_files = files;
|
||||
@ -45,6 +45,7 @@ namespace Oqtane.Controllers
|
||||
_syncManager = syncManager;
|
||||
_logger = logger;
|
||||
_alias = tenantManager.GetAlias();
|
||||
_settingRepository = settingRepository;
|
||||
}
|
||||
|
||||
// GET: api/<controller>?folder=x
|
||||
@ -207,7 +208,7 @@ namespace Oqtane.Controllers
|
||||
public Models.File Put(int id, [FromBody] Models.File file)
|
||||
{
|
||||
var File = _files.GetFile(file.FileId, false);
|
||||
if (ModelState.IsValid && file.Folder.SiteId == _alias.SiteId && File != null // ensure file exists
|
||||
if (ModelState.IsValid && file.Folder.SiteId == _alias.SiteId && file.FileId == id && File != null // ensure file exists
|
||||
&& _userPermissions.IsAuthorized(User, file.Folder.SiteId, EntityNames.Folder, File.FolderId, PermissionNames.Edit) // ensure user had edit rights to original folder
|
||||
&& _userPermissions.IsAuthorized(User, file.Folder.SiteId, EntityNames.Folder, file.FolderId, PermissionNames.Edit)) // ensure user has edit rights to new folder
|
||||
{
|
||||
@ -287,6 +288,8 @@ namespace Oqtane.Controllers
|
||||
folder = _folders.GetFolder(FolderId);
|
||||
}
|
||||
|
||||
var _UploadableFiles = (_settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "UploadableFiles")?.SettingValue ?? Constants.UploadableFiles) ?? Constants.UploadableFiles;
|
||||
|
||||
if (folder != null && folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.Edit, folder.PermissionList))
|
||||
{
|
||||
string folderPath = _folders.GetFolderPath(folder);
|
||||
@ -297,7 +300,7 @@ namespace Oqtane.Controllers
|
||||
name = url.Substring(url.LastIndexOf("/", StringComparison.Ordinal) + 1);
|
||||
}
|
||||
// check for allowable file extensions
|
||||
if (!Constants.UploadableFiles.Split(',').Contains(Path.GetExtension(name).ToLower().Replace(".", "")))
|
||||
if (!_UploadableFiles.Split(',').Contains(Path.GetExtension(name).ToLower().Replace(".", "")))
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Create, "File Could Not Be Downloaded From Url Due To Its File Extension {Url}", url);
|
||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.Conflict;
|
||||
@ -362,6 +365,10 @@ namespace Oqtane.Controllers
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the UploadableFiles extensions
|
||||
string uploadfilesSetting = _settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "UploadableFiles")?.SettingValue;
|
||||
string _UploadableFiles = uploadfilesSetting ?? Constants.UploadableFiles;
|
||||
|
||||
// ensure filename is valid
|
||||
string token = ".part_";
|
||||
if (!formfile.FileName.IsPathOrFileValid() || !formfile.FileName.Contains(token))
|
||||
@ -371,7 +378,7 @@ namespace Oqtane.Controllers
|
||||
|
||||
// check for allowable file extensions (ignore token)
|
||||
var extension = Path.GetExtension(formfile.FileName.Substring(0, formfile.FileName.IndexOf(token))).Replace(".", "");
|
||||
if (!Constants.UploadableFiles.Split(',').Contains(extension.ToLower()))
|
||||
if (!_UploadableFiles.Split(',').Contains(extension.ToLower()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -604,9 +611,11 @@ namespace Oqtane.Controllers
|
||||
public IActionResult GetImage(int id, int width, int height, string mode, string position, string background, string rotate, string recreate)
|
||||
{
|
||||
var file = _files.GetFile(id);
|
||||
|
||||
var _ImageFiles = (_settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "ImageFiles")?.SettingValue ?? Constants.ImageFiles) ?? Constants.ImageFiles;
|
||||
if (file != null && file.Folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.PermissionList))
|
||||
{
|
||||
if (Constants.ImageFiles.Split(',').Contains(file.Extension.ToLower()))
|
||||
if (_ImageFiles.Split(',').Contains(file.Extension.ToLower()))
|
||||
{
|
||||
var filepath = _files.GetFilePath(file);
|
||||
if (System.IO.File.Exists(filepath))
|
||||
@ -658,8 +667,15 @@ namespace Oqtane.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt {FileId}", id);
|
||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
if (file != null)
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt {FileId}", id);
|
||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
}
|
||||
else
|
||||
{
|
||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
}
|
||||
}
|
||||
|
||||
string errorPath = Path.Combine(GetFolderPath("wwwroot/images"), "error.png");
|
||||
@ -763,6 +779,7 @@ namespace Oqtane.Controllers
|
||||
private Models.File CreateFile(string filename, int folderid, string filepath)
|
||||
{
|
||||
var file = _files.GetFile(folderid, filename);
|
||||
var _ImageFiles = (_settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "ImageFiles")?.SettingValue ?? Constants.ImageFiles) ?? Constants.ImageFiles;
|
||||
|
||||
int size = 0;
|
||||
var folder = _folders.GetFolder(folderid, false);
|
||||
@ -789,7 +806,7 @@ namespace Oqtane.Controllers
|
||||
file.ImageHeight = 0;
|
||||
file.ImageWidth = 0;
|
||||
|
||||
if (Constants.ImageFiles.Split(',').Contains(file.Extension.ToLower()))
|
||||
if (_ImageFiles.Split(',').Contains(file.Extension.ToLower()))
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -204,7 +204,7 @@ namespace Oqtane.Controllers
|
||||
[Authorize(Roles = RoleNames.Registered)]
|
||||
public Folder Put(int id, [FromBody] Folder folder)
|
||||
{
|
||||
if (ModelState.IsValid && folder.SiteId == _alias.SiteId && _folders.GetFolder(folder.FolderId, false) != null && _userPermissions.IsAuthorized(User, folder.SiteId, EntityNames.Folder, folder.FolderId, PermissionNames.Edit))
|
||||
if (ModelState.IsValid && folder.SiteId == _alias.SiteId && folder.FolderId == id && _folders.GetFolder(folder.FolderId, false) != null && _userPermissions.IsAuthorized(User, folder.SiteId, EntityNames.Folder, folder.FolderId, PermissionNames.Edit))
|
||||
{
|
||||
if (folder.IsPathValid())
|
||||
{
|
||||
|
@ -30,11 +30,12 @@ namespace Oqtane.Controllers
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly IHttpContextAccessor _accessor;
|
||||
private readonly IAliasRepository _aliases;
|
||||
private readonly ISiteRepository _sites;
|
||||
private readonly ILogger<InstallationController> _filelogger;
|
||||
private readonly ITenantManager _tenantManager;
|
||||
private readonly IServerStateManager _serverState;
|
||||
|
||||
public InstallationController(IConfigManager configManager, IInstallationManager installationManager, IDatabaseManager databaseManager, ILocalizationManager localizationManager, IMemoryCache cache, IHttpContextAccessor accessor, IAliasRepository aliases, ILogger<InstallationController> filelogger, ITenantManager tenantManager, IServerStateManager serverState)
|
||||
public InstallationController(IConfigManager configManager, IInstallationManager installationManager, IDatabaseManager databaseManager, ILocalizationManager localizationManager, IMemoryCache cache, IHttpContextAccessor accessor, IAliasRepository aliases, ISiteRepository sites, ILogger<InstallationController> filelogger, ITenantManager tenantManager, IServerStateManager serverState)
|
||||
{
|
||||
_configManager = configManager;
|
||||
_installationManager = installationManager;
|
||||
@ -43,6 +44,7 @@ namespace Oqtane.Controllers
|
||||
_cache = cache;
|
||||
_accessor = accessor;
|
||||
_aliases = aliases;
|
||||
_sites = sites;
|
||||
_filelogger = filelogger;
|
||||
_tenantManager = tenantManager;
|
||||
_serverState = serverState;
|
||||
@ -108,6 +110,70 @@ namespace Oqtane.Controllers
|
||||
return GetAssemblyList().Select(item => item.HashedName).ToList();
|
||||
}
|
||||
|
||||
private List<ClientAssembly> GetAssemblyList()
|
||||
{
|
||||
var alias = _tenantManager.GetAlias();
|
||||
|
||||
return _cache.GetOrCreate($"assemblieslist:{alias.SiteKey}", entry =>
|
||||
{
|
||||
var assemblyList = new List<ClientAssembly>();
|
||||
|
||||
var site = _sites.GetSite(alias.SiteId);
|
||||
if (site != null && (site.Runtime == "WebAssembly" || site.HybridEnabled))
|
||||
{
|
||||
var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
|
||||
|
||||
// testmode setting is used for validating that the API is downloading the appropriate assemblies to the client
|
||||
bool hashfilename = true;
|
||||
if (_configManager.GetSetting($"{SettingKeys.TestModeKey}", "false") == "true")
|
||||
{
|
||||
hashfilename = false;
|
||||
}
|
||||
|
||||
// get site assemblies which should be downloaded to client
|
||||
var assemblies = _serverState.GetServerState(alias.SiteKey).Assemblies;
|
||||
|
||||
// populate assembly list
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
if (assembly != Constants.ClientId)
|
||||
{
|
||||
var filepath = Path.Combine(binFolder, assembly) + ".dll";
|
||||
if (System.IO.File.Exists(filepath))
|
||||
{
|
||||
assemblyList.Add(new ClientAssembly(Path.Combine(binFolder, assembly + ".dll"), hashfilename));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// insert satellite assemblies at beginning of list
|
||||
foreach (var culture in _localizationManager.GetInstalledCultures())
|
||||
{
|
||||
if (culture != Constants.DefaultCulture)
|
||||
{
|
||||
var assembliesFolderPath = Path.Combine(binFolder, culture);
|
||||
if (Directory.Exists(assembliesFolderPath))
|
||||
{
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
var filepath = Path.Combine(assembliesFolderPath, assembly) + ".resources.dll";
|
||||
if (System.IO.File.Exists(filepath))
|
||||
{
|
||||
assemblyList.Insert(0, new ClientAssembly(Path.Combine(assembliesFolderPath, assembly + ".resources.dll"), hashfilename));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_filelogger.LogError(Utilities.LogMessage(this, $"The Satellite Assembly Folder For {culture} Does Not Exist"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return assemblyList;
|
||||
});
|
||||
}
|
||||
|
||||
// GET api/<controller>/load?list=x,y
|
||||
[HttpGet("load")]
|
||||
public IActionResult Load(string list = "*")
|
||||
@ -115,126 +181,79 @@ namespace Oqtane.Controllers
|
||||
return File(GetAssemblies(list), System.Net.Mime.MediaTypeNames.Application.Octet, "oqtane.dll");
|
||||
}
|
||||
|
||||
private List<ClientAssembly> GetAssemblyList()
|
||||
{
|
||||
var siteKey = _tenantManager.GetAlias().SiteKey;
|
||||
|
||||
return _cache.GetOrCreate($"assemblieslist:{siteKey}", entry =>
|
||||
{
|
||||
var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
|
||||
var assemblyList = new List<ClientAssembly>();
|
||||
|
||||
// testmode setting is used for validating that the API is downloading the appropriate assemblies to the client
|
||||
bool hashfilename = true;
|
||||
if (_configManager.GetSetting($"{SettingKeys.TestModeKey}", "false") == "true")
|
||||
{
|
||||
hashfilename = false;
|
||||
}
|
||||
|
||||
// get site assemblies which should be downloaded to client
|
||||
var assemblies = _serverState.GetServerState(siteKey).Assemblies;
|
||||
|
||||
// populate assembly list
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
if (assembly != Constants.ClientId)
|
||||
{
|
||||
var filepath = Path.Combine(binFolder, assembly) + ".dll";
|
||||
if (System.IO.File.Exists(filepath))
|
||||
{
|
||||
assemblyList.Add(new ClientAssembly(Path.Combine(binFolder, assembly + ".dll"), hashfilename));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// insert satellite assemblies at beginning of list
|
||||
foreach (var culture in _localizationManager.GetInstalledCultures())
|
||||
{
|
||||
if (culture != Constants.DefaultCulture)
|
||||
{
|
||||
var assembliesFolderPath = Path.Combine(binFolder, culture);
|
||||
if (Directory.Exists(assembliesFolderPath))
|
||||
{
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
var filepath = Path.Combine(assembliesFolderPath, assembly) + ".resources.dll";
|
||||
if (System.IO.File.Exists(filepath))
|
||||
{
|
||||
assemblyList.Insert(0, new ClientAssembly(Path.Combine(assembliesFolderPath, assembly + ".resources.dll"), hashfilename));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_filelogger.LogError(Utilities.LogMessage(this, $"The Satellite Assembly Folder For {culture} Does Not Exist"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return assemblyList;
|
||||
});
|
||||
}
|
||||
|
||||
private byte[] GetAssemblies(string list)
|
||||
{
|
||||
var siteKey = _tenantManager.GetAlias().SiteKey;
|
||||
var alias = _tenantManager.GetAlias();
|
||||
|
||||
if (list == "*")
|
||||
{
|
||||
return _cache.GetOrCreate($"assemblies:{siteKey}", entry =>
|
||||
return _cache.GetOrCreate($"assemblies:{alias.SiteKey}", entry =>
|
||||
{
|
||||
return GetZIP(list);
|
||||
return GetZIP(list, alias);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
return GetZIP(list);
|
||||
return GetZIP(list, alias);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] GetZIP(string list)
|
||||
private byte[] GetZIP(string list, Alias alias)
|
||||
{
|
||||
var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
|
||||
|
||||
// get list of assemblies which should be downloaded to client
|
||||
List<ClientAssembly> assemblies = GetAssemblyList();
|
||||
if (list != "*")
|
||||
var site = _sites.GetSite(alias.SiteId);
|
||||
if (site != null && (site.Runtime == "WebAssembly" || site.HybridEnabled))
|
||||
{
|
||||
var filter = list.Split(',').ToList();
|
||||
assemblies.RemoveAll(item => !filter.Contains(item.HashedName));
|
||||
}
|
||||
var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
|
||||
|
||||
// create zip file containing assemblies and debug symbols
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
|
||||
// get list of assemblies which should be downloaded to client
|
||||
List<ClientAssembly> assemblies = GetAssemblyList();
|
||||
if (list != "*")
|
||||
{
|
||||
foreach (var assembly in assemblies)
|
||||
var filter = list.Split(',').ToList();
|
||||
assemblies.RemoveAll(item => !filter.Contains(item.HashedName));
|
||||
}
|
||||
|
||||
// create zip file containing assemblies and debug symbols
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
|
||||
{
|
||||
if (Path.GetFileNameWithoutExtension(assembly.FilePath) != Constants.ClientId)
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
if (System.IO.File.Exists(assembly.FilePath))
|
||||
if (Path.GetFileNameWithoutExtension(assembly.FilePath) != Constants.ClientId)
|
||||
{
|
||||
using (var filestream = new FileStream(assembly.FilePath, FileMode.Open, FileAccess.Read))
|
||||
using (var entrystream = archive.CreateEntry(assembly.HashedName).Open())
|
||||
if (System.IO.File.Exists(assembly.FilePath))
|
||||
{
|
||||
filestream.CopyTo(entrystream);
|
||||
using (var filestream = new FileStream(assembly.FilePath, FileMode.Open, FileAccess.Read))
|
||||
using (var entrystream = archive.CreateEntry(assembly.HashedName).Open())
|
||||
{
|
||||
filestream.CopyTo(entrystream);
|
||||
}
|
||||
}
|
||||
}
|
||||
var pdb = assembly.FilePath.Replace(".dll", ".pdb");
|
||||
if (System.IO.File.Exists(pdb))
|
||||
{
|
||||
using (var filestream = new FileStream(pdb, FileMode.Open, FileAccess.Read))
|
||||
using (var entrystream = archive.CreateEntry(assembly.HashedName.Replace(".dll", ".pdb")).Open())
|
||||
var pdb = assembly.FilePath.Replace(".dll", ".pdb");
|
||||
if (System.IO.File.Exists(pdb))
|
||||
{
|
||||
filestream.CopyTo(entrystream);
|
||||
using (var filestream = new FileStream(pdb, FileMode.Open, FileAccess.Read))
|
||||
using (var entrystream = archive.CreateEntry(assembly.HashedName.Replace(".dll", ".pdb")).Open())
|
||||
{
|
||||
filestream.CopyTo(entrystream);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return memoryStream.ToArray();
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// return empty zip
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
using (var zip = new ZipArchive(memoryStream, ZipArchiveMode.Create)) {}
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -67,7 +67,7 @@ namespace Oqtane.Controllers
|
||||
[Authorize(Roles = RoleNames.Host)]
|
||||
public Job Put(int id, [FromBody] Job job)
|
||||
{
|
||||
if (ModelState.IsValid && _jobs.GetJob(job.JobId, false) != null)
|
||||
if (ModelState.IsValid && job.JobId == id && _jobs.GetJob(job.JobId, false) != null)
|
||||
{
|
||||
job = _jobs.UpdateJob(job);
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Job Updated {Job}", job);
|
||||
|
@ -1,5 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -9,9 +12,6 @@ using Oqtane.Infrastructure;
|
||||
using Oqtane.Models;
|
||||
using Oqtane.Repository;
|
||||
using Oqtane.Shared;
|
||||
using System.Linq;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Oqtane.Controllers
|
||||
{
|
||||
@ -102,6 +102,24 @@ namespace Oqtane.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
[Authorize(Roles = RoleNames.Admin)]
|
||||
public void Put([FromBody] Language language)
|
||||
{
|
||||
if (ModelState.IsValid && language.SiteId == _alias.SiteId)
|
||||
{
|
||||
_languages.UpdateLanguage(language);
|
||||
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Language, language.LanguageId, SyncEventActions.Update);
|
||||
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Site, _alias.SiteId, SyncEventActions.Refresh);
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Language Updated {Language}", language);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Language Put Attempt {Language}", language);
|
||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Roles = RoleNames.Admin)]
|
||||
public Language Post([FromBody] Language language)
|
||||
|
@ -154,7 +154,7 @@ namespace Oqtane.Controllers
|
||||
{
|
||||
var _module = _modules.GetModule(module.ModuleId, false);
|
||||
|
||||
if (ModelState.IsValid && module.SiteId == _alias.SiteId && _module != null && _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Module, module.ModuleId, PermissionNames.Edit))
|
||||
if (ModelState.IsValid && module.SiteId == _alias.SiteId && module.ModuleId == id && _module != null && _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Module, module.ModuleId, PermissionNames.Edit))
|
||||
{
|
||||
module = _modules.UpdateModule(module);
|
||||
|
||||
|
@ -167,7 +167,7 @@ namespace Oqtane.Controllers
|
||||
[Authorize(Roles = RoleNames.Admin)]
|
||||
public void Put(int id, [FromBody] ModuleDefinition moduleDefinition)
|
||||
{
|
||||
if (ModelState.IsValid && moduleDefinition.SiteId == _alias.SiteId && _moduleDefinitions.GetModuleDefinition(moduleDefinition.ModuleDefinitionId, moduleDefinition.SiteId) != null)
|
||||
if (ModelState.IsValid && moduleDefinition.SiteId == _alias.SiteId && moduleDefinition.ModuleDefinitionId == id && _moduleDefinitions.GetModuleDefinition(moduleDefinition.ModuleDefinitionId, moduleDefinition.SiteId) != null)
|
||||
{
|
||||
_moduleDefinitions.UpdateModuleDefinition(moduleDefinition);
|
||||
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.ModuleDefinition, moduleDefinition.ModuleDefinitionId, SyncEventActions.Update);
|
||||
|
@ -161,6 +161,12 @@ namespace Oqtane.Controllers
|
||||
{
|
||||
if (ModelState.IsValid && notification.SiteId == _alias.SiteId && IsAuthorized(notification.FromUserId))
|
||||
{
|
||||
if (!User.IsInRole(RoleNames.Admin))
|
||||
{
|
||||
// content must be HTML encoded for non-admins to prevent HTML injection
|
||||
notification.Subject = WebUtility.HtmlEncode(notification.Subject);
|
||||
notification.Body = WebUtility.HtmlEncode(notification.Body);
|
||||
}
|
||||
notification = _notifications.AddNotification(notification);
|
||||
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Notification, notification.NotificationId, SyncEventActions.Create);
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Notification Added {NotificationId}", notification.NotificationId);
|
||||
@ -179,8 +185,14 @@ namespace Oqtane.Controllers
|
||||
[Authorize(Roles = RoleNames.Registered)]
|
||||
public Notification Put(int id, [FromBody] Notification notification)
|
||||
{
|
||||
if (ModelState.IsValid && notification.SiteId == _alias.SiteId && _notifications.GetNotification(notification.NotificationId, false) != null && (IsAuthorized(notification.FromUserId) || IsAuthorized(notification.ToUserId)))
|
||||
if (ModelState.IsValid && notification.SiteId == _alias.SiteId && notification.NotificationId == id && _notifications.GetNotification(notification.NotificationId, false) != null && (IsAuthorized(notification.FromUserId) || IsAuthorized(notification.ToUserId)))
|
||||
{
|
||||
if (!User.IsInRole(RoleNames.Admin))
|
||||
{
|
||||
// content must be HTML encoded for non-admins to prevent HTML injection
|
||||
notification.Subject = WebUtility.HtmlEncode(notification.Subject);
|
||||
notification.Body = WebUtility.HtmlEncode(notification.Body);
|
||||
}
|
||||
notification = _notifications.UpdateNotification(notification);
|
||||
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Notification, notification.NotificationId, SyncEventActions.Update);
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Notification Updated {NotificationId}", notification.NotificationId);
|
||||
|
@ -269,7 +269,7 @@ namespace Oqtane.Controllers
|
||||
// get current page
|
||||
var currentPage = _pages.GetPage(page.PageId, false);
|
||||
|
||||
if (ModelState.IsValid && page.SiteId == _alias.SiteId && currentPage != null && _userPermissions.IsAuthorized(User, page.SiteId, EntityNames.Page, page.PageId, PermissionNames.Edit))
|
||||
if (ModelState.IsValid && page.SiteId == _alias.SiteId && page.PageId == id && currentPage != null && _userPermissions.IsAuthorized(User, page.SiteId, EntityNames.Page, page.PageId, PermissionNames.Edit))
|
||||
{
|
||||
// get current page permissions
|
||||
var currentPermissions = _permissionRepository.GetPermissions(page.SiteId, EntityNames.Page, page.PageId).ToList();
|
||||
|
@ -109,7 +109,7 @@ namespace Oqtane.Controllers
|
||||
public PageModule Put(int id, [FromBody] PageModule pageModule)
|
||||
{
|
||||
var page = _pages.GetPage(pageModule.PageId);
|
||||
if (ModelState.IsValid && page != null && page.SiteId == _alias.SiteId && _pageModules.GetPageModule(pageModule.PageModuleId, false) != null && _userPermissions.IsAuthorized(User, page.SiteId, EntityNames.Page, pageModule.PageId, PermissionNames.Edit))
|
||||
if (ModelState.IsValid && page != null && page.SiteId == _alias.SiteId && pageModule.PageModuleId == id && _pageModules.GetPageModule(pageModule.PageModuleId, false) != null && _userPermissions.IsAuthorized(User, page.SiteId, EntityNames.Page, pageModule.PageId, PermissionNames.Edit))
|
||||
{
|
||||
pageModule = _pageModules.UpdatePageModule(pageModule);
|
||||
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.PageModule, pageModule.PageModuleId, SyncEventActions.Update);
|
||||
|
@ -94,7 +94,7 @@ namespace Oqtane.Controllers
|
||||
[Authorize(Policy = $"{EntityNames.Profile}:{PermissionNames.Write}:{RoleNames.Admin}")]
|
||||
public Profile Put(int id, [FromBody] Profile profile)
|
||||
{
|
||||
if (ModelState.IsValid && profile.SiteId == _alias.SiteId && _profiles.GetProfile(profile.ProfileId, false) != null)
|
||||
if (ModelState.IsValid && profile.SiteId == _alias.SiteId && profile.ProfileId == id && _profiles.GetProfile(profile.ProfileId, false) != null)
|
||||
{
|
||||
profile = _profiles.UpdateProfile(profile);
|
||||
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Profile, profile.ProfileId, SyncEventActions.Update);
|
||||
|
@ -98,7 +98,7 @@ namespace Oqtane.Controllers
|
||||
[Authorize(Policy = $"{EntityNames.Role}:{PermissionNames.Write}:{RoleNames.Admin}")]
|
||||
public Role Put(int id, [FromBody] Role role)
|
||||
{
|
||||
if (ModelState.IsValid && role.SiteId == _alias.SiteId && _roles.GetRole(role.RoleId, false) != null)
|
||||
if (ModelState.IsValid && role.SiteId == _alias.SiteId && role.RoleId == id && _roles.GetRole(role.RoleId, false) != null)
|
||||
{
|
||||
role = _roles.UpdateRole(role);
|
||||
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Role, role.RoleId, SyncEventActions.Update);
|
||||
|
@ -128,7 +128,7 @@ namespace Oqtane.Controllers
|
||||
[HttpPut("{id}")]
|
||||
public Setting Put(int id, [FromBody] Setting setting)
|
||||
{
|
||||
if (ModelState.IsValid && IsAuthorized(setting.EntityName, setting.EntityId, PermissionNames.Edit))
|
||||
if (ModelState.IsValid && setting.SettingId == id && IsAuthorized(setting.EntityName, setting.EntityId, PermissionNames.Edit))
|
||||
{
|
||||
setting = _settings.UpdateSetting(setting);
|
||||
AddSyncEvent(setting.EntityName, setting.SettingId, SyncEventActions.Update);
|
||||
|
@ -86,6 +86,12 @@ namespace Oqtane.Controllers
|
||||
.Where(item => !item.IsPrivate || User.IsInRole(RoleNames.Admin))
|
||||
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
|
||||
|
||||
// populate File Extensions
|
||||
site.ImageFiles = site.Settings.ContainsKey("ImageFiles") && !string.IsNullOrEmpty(site.Settings["ImageFiles"])
|
||||
? site.Settings["ImageFiles"] : Constants.ImageFiles;
|
||||
site.UploadableFiles = site.Settings.ContainsKey("UploadableFiles") && !string.IsNullOrEmpty(site.Settings["UploadableFiles"])
|
||||
? site.Settings["UploadableFiles"] : Constants.UploadableFiles;
|
||||
|
||||
// pages
|
||||
List<Setting> settings = _settings.GetSettings(EntityNames.Page).ToList();
|
||||
site.Pages = new List<Page>();
|
||||
@ -192,7 +198,7 @@ namespace Oqtane.Controllers
|
||||
public Site Put(int id, [FromBody] Site site)
|
||||
{
|
||||
var current = _sites.GetSite(site.SiteId, false);
|
||||
if (ModelState.IsValid && site.SiteId == _alias.SiteId && site.TenantId == _alias.TenantId && current != null)
|
||||
if (ModelState.IsValid && site.SiteId == _alias.SiteId && site.TenantId == _alias.TenantId && site.SiteId == id && current != null)
|
||||
{
|
||||
site = _sites.UpdateSite(site);
|
||||
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Site, site.SiteId, SyncEventActions.Update);
|
||||
|
@ -71,7 +71,7 @@ namespace Oqtane.Controllers
|
||||
[Authorize(Roles = RoleNames.Admin)]
|
||||
public void Put(int id, [FromBody] Theme theme)
|
||||
{
|
||||
if (ModelState.IsValid && theme.SiteId == _alias.SiteId && _themes.GetTheme(theme.ThemeId,theme.SiteId) != null)
|
||||
if (ModelState.IsValid && theme.SiteId == _alias.SiteId && theme.ThemeId == id && _themes.GetTheme(theme.ThemeId,theme.SiteId) != null)
|
||||
{
|
||||
_themes.UpdateTheme(theme);
|
||||
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Theme, theme.ThemeId, SyncEventActions.Update);
|
||||
|
@ -118,7 +118,7 @@ namespace Oqtane.Controllers
|
||||
[Authorize(Roles = RoleNames.Admin)]
|
||||
public UrlMapping Put(int id, [FromBody] UrlMapping urlMapping)
|
||||
{
|
||||
if (ModelState.IsValid && urlMapping.SiteId == _alias.SiteId && _urlMappings.GetUrlMapping(urlMapping.UrlMappingId, false) != null)
|
||||
if (ModelState.IsValid && urlMapping.SiteId == _alias.SiteId && urlMapping.UrlMappingId == id && _urlMappings.GetUrlMapping(urlMapping.UrlMappingId, false) != null)
|
||||
{
|
||||
urlMapping = _urlMappings.UpdateUrlMapping(urlMapping);
|
||||
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.UrlMapping, urlMapping.UrlMappingId, SyncEventActions.Update);
|
||||
|
@ -173,15 +173,16 @@ namespace Oqtane.Controllers
|
||||
[Authorize]
|
||||
public async Task<User> Put(int id, [FromBody] User user)
|
||||
{
|
||||
if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && _users.GetUser(user.UserId, false) != null
|
||||
if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && user.UserId == id && _users.GetUser(user.UserId, false) != null
|
||||
&& (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || User.Identity.Name == user.Username))
|
||||
{
|
||||
user.EmailConfirmed = User.IsInRole(RoleNames.Admin);
|
||||
user = await _userManager.UpdateUser(user);
|
||||
}
|
||||
else
|
||||
{
|
||||
user.Password = ""; // remove sensitive information
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Post Attempt {User}", user);
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Put Attempt {User}", user);
|
||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
user = null;
|
||||
}
|
||||
|
@ -149,7 +149,7 @@ namespace Oqtane.Controllers
|
||||
public UserRole Put(int id, [FromBody] UserRole userRole)
|
||||
{
|
||||
var role = _roles.GetRole(userRole.RoleId);
|
||||
if (ModelState.IsValid && role != null && SiteValid(role.SiteId) && RoleValid(role.Name) && _userRoles.GetUserRole(userRole.UserRoleId, false) != null)
|
||||
if (ModelState.IsValid && role != null && SiteValid(role.SiteId) && RoleValid(role.Name) && userRole.UserRoleId == id && _userRoles.GetUserRole(userRole.UserRoleId, false) != null)
|
||||
{
|
||||
userRole = _userRoles.UpdateUserRole(userRole);
|
||||
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.UserRole, userRole.UserRoleId, SyncEventActions.Update);
|
||||
|
54
Oqtane.Server/Extensions/ClaimsPrincipalExtensions.cs
Normal file
54
Oqtane.Server/Extensions/ClaimsPrincipalExtensions.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Oqtane.Extensions
|
||||
{
|
||||
public static class ClaimsPrincipalExtensions
|
||||
{
|
||||
public static string Username(this ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
if (claimsPrincipal.HasClaim(item => item.Type == ClaimTypes.Name))
|
||||
{
|
||||
return claimsPrincipal.Claims.FirstOrDefault(item => item.Type == ClaimTypes.Name).Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public static int UserId(this ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
if (claimsPrincipal.HasClaim(item => item.Type == ClaimTypes.NameIdentifier))
|
||||
{
|
||||
return int.Parse(claimsPrincipal.Claims.First(item => item.Type == ClaimTypes.NameIdentifier).Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public static string Roles(this ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var roles = "";
|
||||
foreach (var claim in claimsPrincipal.Claims.Where(item => item.Type == ClaimTypes.Role))
|
||||
{
|
||||
roles += ((roles == "") ? "" : ";") + claim.Value;
|
||||
}
|
||||
return roles;
|
||||
}
|
||||
|
||||
public static string SiteKey(this ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
if (claimsPrincipal.HasClaim(item => item.Type == "sitekey"))
|
||||
{
|
||||
return claimsPrincipal.Claims.FirstOrDefault(item => item.Type == "sitekey").Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -124,7 +124,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||
// note that ConfigureApplicationCookie internally uses an ApplicationScheme of "Identity.Application"
|
||||
services.ConfigureApplicationCookie(options =>
|
||||
{
|
||||
options.Cookie.HttpOnly = false;
|
||||
options.Cookie.HttpOnly = true;
|
||||
options.Cookie.SameSite = SameSiteMode.Strict;
|
||||
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
|
||||
options.Events.OnRedirectToLogin = context =>
|
||||
@ -179,7 +179,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||
options.Lockout.AllowedForNewUsers = false;
|
||||
|
||||
// SignIn settings
|
||||
options.SignIn.RequireConfirmedEmail = true;
|
||||
options.SignIn.RequireConfirmedEmail = true;
|
||||
options.SignIn.RequireConfirmedPhoneNumber = false;
|
||||
|
||||
// User settings
|
||||
|
@ -50,7 +50,6 @@ namespace Oqtane.Extensions
|
||||
options.SaveTokens = false;
|
||||
options.GetClaimsFromUserInfoEndpoint = true;
|
||||
options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-" + AuthenticationProviderTypes.OpenIDConnect : "/" + alias.Path + "/signin-" + AuthenticationProviderTypes.OpenIDConnect;
|
||||
options.ResponseType = sitesettings.GetValue("ExternalLogin:AuthResponseType", "code"); // authorization code flow
|
||||
options.ResponseMode = OpenIdConnectResponseMode.FormPost; // recommended as most secure
|
||||
|
||||
// cookie config is required to avoid Correlation Failed errors
|
||||
@ -62,6 +61,7 @@ namespace Oqtane.Extensions
|
||||
options.MetadataAddress = sitesettings.GetValue("ExternalLogin:MetadataUrl", "");
|
||||
options.ClientId = sitesettings.GetValue("ExternalLogin:ClientId", "");
|
||||
options.ClientSecret = sitesettings.GetValue("ExternalLogin:ClientSecret", "");
|
||||
options.ResponseType = sitesettings.GetValue("ExternalLogin:AuthResponseType", "code"); // default is authorization code flow
|
||||
options.UsePkce = bool.Parse(sitesettings.GetValue("ExternalLogin:PKCE", "false"));
|
||||
if (!string.IsNullOrEmpty(sitesettings.GetValue("ExternalLogin:RoleClaimType", "")))
|
||||
{
|
||||
@ -150,15 +150,16 @@ namespace Oqtane.Extensions
|
||||
private static async Task OnCreatingTicket(OAuthCreatingTicketContext context)
|
||||
{
|
||||
// OAuth 2.0
|
||||
var email = "";
|
||||
var id = "";
|
||||
var claims = "";
|
||||
var id = "";
|
||||
var name = "";
|
||||
var email = "";
|
||||
|
||||
if (context.Options.UserInformationEndpoint != "")
|
||||
{
|
||||
try
|
||||
{
|
||||
// call user information endpoint
|
||||
// call user information endpoint using access token
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
request.Headers.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version));
|
||||
@ -167,32 +168,49 @@ namespace Oqtane.Extensions
|
||||
response.EnsureSuccessStatusCode();
|
||||
claims = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// parse json output
|
||||
// get claim types
|
||||
var idClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", "");
|
||||
var nameClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:NameClaimType", "");
|
||||
var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", "");
|
||||
if (!claims.StartsWith("[") && !claims.EndsWith("]"))
|
||||
|
||||
// some user endpoints can return multiple objects (ie. GitHub) so convert single object to array (if necessary)
|
||||
var jsonclaims = claims;
|
||||
if (!jsonclaims.StartsWith("[") && !jsonclaims.EndsWith("]"))
|
||||
{
|
||||
claims = "[" + claims + "]"; // convert to json array
|
||||
jsonclaims = "[" + jsonclaims + "]";
|
||||
}
|
||||
JsonNode items = JsonNode.Parse(claims)!;
|
||||
|
||||
// parse claim values
|
||||
JsonNode items = JsonNode.Parse(jsonclaims)!;
|
||||
foreach (var item in items.AsArray())
|
||||
{
|
||||
if (item[emailClaimType] != null)
|
||||
name = "";
|
||||
email = "";
|
||||
|
||||
// id claim is required
|
||||
if (!string.IsNullOrEmpty(idClaimType) && item[idClaimType] != null)
|
||||
{
|
||||
if (EmailValid(item[emailClaimType].ToString(), context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", "")))
|
||||
id = item[idClaimType].ToString();
|
||||
|
||||
// name claim is optional
|
||||
if (!string.IsNullOrEmpty(nameClaimType) && item[nameClaimType] != null)
|
||||
{
|
||||
email = item[emailClaimType].ToString().ToLower();
|
||||
if (item[idClaimType] != null)
|
||||
name = item[nameClaimType].ToString();
|
||||
}
|
||||
|
||||
// email claim is optional
|
||||
if (!string.IsNullOrEmpty(emailClaimType) && item[emailClaimType] != null)
|
||||
{
|
||||
if (EmailValid(item[emailClaimType].ToString(), context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", "")))
|
||||
{
|
||||
id = item[idClaimType].ToString();
|
||||
email = item[emailClaimType].ToString().ToLower();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
id = email;
|
||||
if (!string.IsNullOrEmpty(id))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -203,7 +221,7 @@ namespace Oqtane.Extensions
|
||||
}
|
||||
|
||||
// validate user
|
||||
var identity = await ValidateUser(email, id, claims, context.HttpContext, context.Principal);
|
||||
var identity = await ValidateUser(id, name, email, claims, context.HttpContext, context.Principal);
|
||||
if (identity.Label == ExternalLoginStatus.Success)
|
||||
{
|
||||
identity.AddClaim(new Claim("access_token", context.AccessToken));
|
||||
@ -213,6 +231,14 @@ namespace Oqtane.Extensions
|
||||
// pass properties to OnTicketReceived
|
||||
context.Properties.SetParameter("status", identity.Label);
|
||||
context.Properties.SetParameter("redirecturl", context.Properties.RedirectUri);
|
||||
|
||||
// set cookie expiration
|
||||
string cookieExpStr = context.HttpContext.GetSiteSettings().GetValue("LoginOptions:CookieExpiration", "");
|
||||
if (!string.IsNullOrEmpty(cookieExpStr) && TimeSpan.TryParse(cookieExpStr, out TimeSpan cookieExpTS))
|
||||
{
|
||||
context.Properties.ExpiresUtc = DateTime.Now.Add(cookieExpTS);
|
||||
context.Properties.IsPersistent = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static Task OnTicketReceived(TicketReceivedContext context)
|
||||
@ -231,28 +257,46 @@ namespace Oqtane.Extensions
|
||||
private static async Task OnTokenValidated(TokenValidatedContext context)
|
||||
{
|
||||
// OpenID Connect
|
||||
var claims = "";
|
||||
var id = "";
|
||||
var name = "";
|
||||
var email = "";
|
||||
|
||||
// serialize claims
|
||||
foreach (var claim in context.Principal.Claims)
|
||||
{
|
||||
claims += "\"" + claim.Type + "\":\"" + claim.Value + "\",";
|
||||
}
|
||||
claims = "{" + claims.Substring(0, claims.Length - 1) + "}";
|
||||
|
||||
// get claim types
|
||||
var idClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", "");
|
||||
var id = context.Principal.FindFirstValue(idClaimType);
|
||||
var nameClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:NameClaimType", "");
|
||||
var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", "");
|
||||
var email = context.Principal.FindFirstValue(emailClaimType);
|
||||
var claims = string.Join(", ", context.Principal.Claims.Select(item => item.Type).ToArray());
|
||||
|
||||
// parse claim values - id claim is required
|
||||
id = context.Principal.FindFirstValue(idClaimType);
|
||||
|
||||
// name claim is optional
|
||||
if (!string.IsNullOrEmpty(nameClaimType) && context.Principal.FindFirstValue(nameClaimType) != null)
|
||||
{
|
||||
name = context.Principal.FindFirstValue(nameClaimType);
|
||||
}
|
||||
|
||||
// email claim is optional
|
||||
if (!string.IsNullOrEmpty(emailClaimType) && context.Principal.FindFirstValue(emailClaimType) != null)
|
||||
{
|
||||
if (EmailValid(context.Principal.FindFirstValue(emailClaimType), context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", "")))
|
||||
{
|
||||
email = context.Principal.FindFirstValue(emailClaimType);
|
||||
}
|
||||
}
|
||||
|
||||
// validate user
|
||||
var identity = await ValidateUser(email, id, claims, context.HttpContext, context.Principal);
|
||||
var identity = await ValidateUser(id, name, email, claims, context.HttpContext, context.Principal);
|
||||
if (identity.Label == ExternalLoginStatus.Success)
|
||||
{
|
||||
// external roles
|
||||
if (!string.IsNullOrEmpty(context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", "")))
|
||||
{
|
||||
foreach (var claim in context.Principal.Claims.Where(item => item.Type == ClaimTypes.Role))
|
||||
{
|
||||
if (!identity.Claims.Any(item => item.Type == ClaimTypes.Role && item.Value == claim.Value))
|
||||
{
|
||||
identity.AddClaim(new Claim(ClaimTypes.Role, claim.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// include access token
|
||||
identity.AddClaim(new Claim("access_token", context.SecurityToken.RawData));
|
||||
context.Principal = new ClaimsPrincipal(identity);
|
||||
}
|
||||
@ -284,12 +328,20 @@ namespace Oqtane.Extensions
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static async Task<ClaimsIdentity> ValidateUser(string email, string id, string claims, HttpContext httpContext, ClaimsPrincipal claimsPrincipal)
|
||||
private static async Task<ClaimsIdentity> ValidateUser(string id, string name, string email, string claims, HttpContext httpContext, ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var _logger = httpContext.RequestServices.GetRequiredService<ILogManager>();
|
||||
ClaimsIdentity identity = new ClaimsIdentity(Constants.AuthenticationScheme);
|
||||
// use identity.Label as a temporary location to store validation status information
|
||||
|
||||
// review claims feature (for testing - external login is disabled)
|
||||
if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:ReviewClaims", "false")))
|
||||
{
|
||||
_logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "Provider Returned The Following Claims: {Claims}", claims);
|
||||
identity.Label = ExternalLoginStatus.ReviewClaims;
|
||||
return identity;
|
||||
}
|
||||
|
||||
var providerType = httpContext.GetSiteSettings().GetValue("ExternalLogin:ProviderType", "");
|
||||
var providerName = httpContext.GetSiteSettings().GetValue("ExternalLogin:ProviderName", "");
|
||||
var alias = httpContext.GetAlias();
|
||||
@ -308,136 +360,158 @@ namespace Oqtane.Extensions
|
||||
}
|
||||
else
|
||||
{
|
||||
if (EmailValid(email, httpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", "")))
|
||||
bool duplicates = false;
|
||||
if (!string.IsNullOrEmpty(email))
|
||||
{
|
||||
bool duplicates = false;
|
||||
try
|
||||
{
|
||||
identityuser = await _identityUserManager.FindByEmailAsync(email);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// FindByEmailAsync will throw an error if the email matches multiple user accounts
|
||||
catch // FindByEmailAsync will throw an error if the email matches multiple user accounts
|
||||
{
|
||||
duplicates = true;
|
||||
}
|
||||
if (identityuser == null)
|
||||
}
|
||||
if (identityuser == null)
|
||||
{
|
||||
if (duplicates)
|
||||
{
|
||||
if (duplicates)
|
||||
identity.Label = ExternalLoginStatus.DuplicateEmail;
|
||||
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Multiple Users Exist With Email Address {Email}. Login Denied.", email);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:CreateUsers", "true")))
|
||||
{
|
||||
identity.Label = ExternalLoginStatus.DuplicateEmail;
|
||||
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Multiple Users Exist With Email Address {Email}. Login Denied.", email);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:CreateUsers", "true")))
|
||||
{
|
||||
identityuser = new IdentityUser();
|
||||
identityuser.UserName = email;
|
||||
identityuser.Email = email;
|
||||
identityuser.EmailConfirmed = true;
|
||||
var result = await _identityUserManager.CreateAsync(identityuser, DateTime.UtcNow.ToString("yyyy-MMM-dd-HH-mm-ss", CultureInfo.InvariantCulture));
|
||||
if (result.Succeeded)
|
||||
{
|
||||
user = new User
|
||||
{
|
||||
SiteId = alias.SiteId,
|
||||
Username = email,
|
||||
DisplayName = email,
|
||||
Email = email,
|
||||
LastLoginOn = null,
|
||||
LastIPAddress = ""
|
||||
};
|
||||
user = _users.AddUser(user);
|
||||
// user identifiers
|
||||
var username = "";
|
||||
var emailaddress = "";
|
||||
var displayname = "";
|
||||
bool emailconfirmed = false;
|
||||
|
||||
if (user != null)
|
||||
if (!string.IsNullOrEmpty(email)) // email claim provided
|
||||
{
|
||||
username = email;
|
||||
emailaddress = email;
|
||||
displayname = (!string.IsNullOrEmpty(name)) ? name : email;
|
||||
emailconfirmed = true;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(name)) // name claim provided
|
||||
{
|
||||
username = name.ToLower().Replace(" ", "") + DateTime.UtcNow.ToString("mmss");
|
||||
emailaddress = ""; // unknown - will need to be requested from user later
|
||||
displayname = name;
|
||||
}
|
||||
else // neither email nor name provided
|
||||
{
|
||||
username = Guid.NewGuid().ToString("N");
|
||||
emailaddress = ""; // unknown - will need to be requested from user later
|
||||
displayname = username;
|
||||
}
|
||||
|
||||
identityuser = new IdentityUser();
|
||||
identityuser.UserName = username;
|
||||
identityuser.Email = emailaddress;
|
||||
identityuser.EmailConfirmed = emailconfirmed;
|
||||
|
||||
// generate password based on random date and punctuation ie. Jan-23-1981+14:43:12!
|
||||
Random rnd = new Random();
|
||||
var date = DateTime.UtcNow.AddDays(-rnd.Next(50 * 365)).AddHours(rnd.Next(0, 24)).AddMinutes(rnd.Next(0, 60)).AddSeconds(rnd.Next(0, 60));
|
||||
var password = date.ToString("MMM-dd-yyyy+HH:mm:ss", CultureInfo.InvariantCulture) + (char)rnd.Next(33, 47);
|
||||
|
||||
var result = await _identityUserManager.CreateAsync(identityuser, password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
user = new User
|
||||
{
|
||||
SiteId = alias.SiteId,
|
||||
Username = username,
|
||||
DisplayName = displayname,
|
||||
Email = emailaddress,
|
||||
LastLoginOn = null,
|
||||
LastIPAddress = ""
|
||||
};
|
||||
user = _users.AddUser(user);
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(email))
|
||||
{
|
||||
var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>();
|
||||
string url = httpContext.Request.Scheme + "://" + alias.Name;
|
||||
string body = "You Recently Used An External Account To Sign In To Our Site.\n\n" + url + "\n\nThank You!";
|
||||
var notification = new Notification(user.SiteId, user, "User Account Notification", body);
|
||||
_notifications.AddNotification(notification);
|
||||
|
||||
// add user login
|
||||
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + user.SiteId.ToString(), id, providerName));
|
||||
|
||||
_logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "User Added {User}", user);
|
||||
}
|
||||
else
|
||||
{
|
||||
identity.Label = ExternalLoginStatus.UserNotCreated;
|
||||
_logger.Log(alias.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add User {Email}", email);
|
||||
}
|
||||
|
||||
// add user login
|
||||
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + user.SiteId.ToString(), id, providerName));
|
||||
|
||||
_logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "User Added {User}", user);
|
||||
}
|
||||
else
|
||||
{
|
||||
identity.Label = ExternalLoginStatus.UserNotCreated;
|
||||
_logger.Log(alias.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add Identity User {Email} {Error}", email, result.Errors.ToString());
|
||||
_logger.Log(alias.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add User {Email}", email);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
identity.Label = ExternalLoginStatus.UserDoesNotExist;
|
||||
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Creation Of New Users Is Disabled For This Site. User With Email Address {Email} Will First Need To Be Registered On The Site.", email);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var logins = await _identityUserManager.GetLoginsAsync(identityuser);
|
||||
var login = logins.FirstOrDefault(item => item.LoginProvider == (providerType + ":" + alias.SiteId.ToString()));
|
||||
if (login == null)
|
||||
{
|
||||
if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:VerifyUsers", "true")))
|
||||
{
|
||||
// external login using existing user account - verification required
|
||||
var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>();
|
||||
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
|
||||
string url = httpContext.Request.Scheme + "://" + alias.Name;
|
||||
url += $"/login?name={identityuser.UserName}&token={WebUtility.UrlEncode(token)}&key={WebUtility.UrlEncode(id)}";
|
||||
string body = $"You Recently Signed In To Our Site With {providerName} Using The Email Address {email}. ";
|
||||
body += "In Order To Complete The Linkage Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
|
||||
var notification = new Notification(alias.SiteId, email, email, "External Login Linkage", body);
|
||||
_notifications.AddNotification(notification);
|
||||
|
||||
identity.Label = ExternalLoginStatus.VerificationRequired;
|
||||
_logger.Log(alias.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Verification For Provider {Provider} Sent To {Email}", providerName, email);
|
||||
}
|
||||
else
|
||||
{
|
||||
// external login using existing user account - link automatically
|
||||
user = _users.GetUser(identityuser.UserName);
|
||||
user.SiteId = alias.SiteId;
|
||||
|
||||
var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>();
|
||||
string url = httpContext.Request.Scheme + "://" + alias.Name;
|
||||
string body = "You Recently Used An External Account To Sign In To Our Site.\n\n" + url + "\n\nThank You!";
|
||||
var notification = new Notification(user.SiteId, user, "User Account Notification", body);
|
||||
_notifications.AddNotification(notification);
|
||||
|
||||
// add user login
|
||||
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + user.SiteId.ToString(), id, providerName));
|
||||
|
||||
_logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Created For User {Username} And Provider {Provider}", user.Username, providerName);
|
||||
identity.Label = ExternalLoginStatus.UserNotCreated;
|
||||
_logger.Log(alias.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add Identity User {Email} {Error}", email, result.Errors.ToString());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// provider keys do not match
|
||||
identity.Label = ExternalLoginStatus.ProviderKeyMismatch;
|
||||
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Key Does Not Match For User {Username}. Login Denied.", identityuser.UserName);
|
||||
identity.Label = ExternalLoginStatus.UserDoesNotExist;
|
||||
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Creation Of New Users Is Disabled For This Site. User With Email Address {Email} Will First Need To Be Registered On The Site.", email);
|
||||
}
|
||||
}
|
||||
}
|
||||
else // email invalid
|
||||
else
|
||||
{
|
||||
identity.Label = ExternalLoginStatus.InvalidEmail;
|
||||
if (!string.IsNullOrEmpty(email))
|
||||
var logins = await _identityUserManager.GetLoginsAsync(identityuser);
|
||||
var login = logins.FirstOrDefault(item => item.LoginProvider == (providerType + ":" + alias.SiteId.ToString()));
|
||||
if (login == null)
|
||||
{
|
||||
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The Email Address {Email} Is Invalid Or Does Not Match The Domain Filter Criteria. Login Denied.", email);
|
||||
if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:VerifyUsers", "true")))
|
||||
{
|
||||
// external login using existing user account - verification required
|
||||
var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>();
|
||||
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
|
||||
string url = httpContext.Request.Scheme + "://" + alias.Name;
|
||||
url += $"/login?name={identityuser.UserName}&token={WebUtility.UrlEncode(token)}&key={WebUtility.UrlEncode(id)}";
|
||||
string body = $"You Recently Signed In To Our Site With {providerName} Using The Email Address {email}. ";
|
||||
body += "In Order To Complete The Linkage Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
|
||||
var notification = new Notification(alias.SiteId, email, email, "External Login Linkage", body);
|
||||
_notifications.AddNotification(notification);
|
||||
|
||||
identity.Label = ExternalLoginStatus.VerificationRequired;
|
||||
_logger.Log(alias.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Verification For Provider {Provider} Sent To {Email}", providerName, email);
|
||||
}
|
||||
else
|
||||
{
|
||||
// external login using existing user account - link automatically
|
||||
user = _users.GetUser(identityuser.UserName);
|
||||
user.SiteId = alias.SiteId;
|
||||
|
||||
var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>();
|
||||
string url = httpContext.Request.Scheme + "://" + alias.Name;
|
||||
string body = "You Recently Used An External Account To Sign In To Our Site.\n\n" + url + "\n\nThank You!";
|
||||
var notification = new Notification(user.SiteId, user, "User Account Notification", body);
|
||||
_notifications.AddNotification(notification);
|
||||
|
||||
// add user login
|
||||
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + user.SiteId.ToString(), id, providerName));
|
||||
|
||||
_logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Created For User {Username} And Provider {Provider}", user.Username, providerName);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return An Email Address To Uniquely Identify The User. The Email Claim Specified Was {EmailCLaimType} And Actual Claim Types Are {Claims}. Login Denied.", httpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", ""), claims);
|
||||
// provider keys do not match
|
||||
identity.Label = ExternalLoginStatus.ProviderKeyMismatch;
|
||||
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Key Does Not Match For User {Username}. Login Denied.", identityuser.UserName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -455,6 +529,25 @@ namespace Oqtane.Extensions
|
||||
user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString();
|
||||
_users.UpdateUser(user);
|
||||
|
||||
// external roles
|
||||
if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", "")))
|
||||
{
|
||||
if (claimsPrincipal.Claims.Any(item => item.Type == ClaimTypes.Role))
|
||||
{
|
||||
foreach (var claim in claimsPrincipal.Claims.Where(item => item.Type == ClaimTypes.Role))
|
||||
{
|
||||
if (!identity.Claims.Any(item => item.Type == ClaimTypes.Role && item.Value == claim.Value))
|
||||
{
|
||||
identity.AddClaim(new Claim(ClaimTypes.Role, claim.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The Role Claim {ClaimType} Does Not Exist. Please Use The Review Claims Feature To View The Claims Returned By Your Provider.", httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", ""));
|
||||
}
|
||||
}
|
||||
|
||||
// user profile claims
|
||||
if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", "")))
|
||||
{
|
||||
@ -493,7 +586,7 @@ namespace Oqtane.Extensions
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim {ClaimType} Does Not Exist. The Valid Claims Are {Claims}.", mapping.Split(":")[0], claims);
|
||||
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim {ClaimType} Does Not Exist. Please Use The Review Claims Feature To View The Claims Returned By Your Provider.", mapping.Split(":")[0]);
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -506,9 +599,10 @@ namespace Oqtane.Extensions
|
||||
_logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} Using Provider {Provider}", user.Username, providerName);
|
||||
}
|
||||
}
|
||||
else // id invalid
|
||||
else // claims invalid
|
||||
{
|
||||
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return An Identifier To Uniquely Identify The User. The Identifier Claim Specified Was {IdentifierCLaimType} And Actual Claim Types Are {Claims}. Login Denied.", httpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", ""), claims);
|
||||
identity.Label = ExternalLoginStatus.MissingClaims;
|
||||
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return All Of The Claims Types Specified Or Email Address Does Not Saitisfy Domain Filter. The Actual Claims Returned Were {Claims}. Login Was Denied.", claims);
|
||||
}
|
||||
|
||||
return identity;
|
||||
|
@ -105,12 +105,6 @@ namespace Oqtane.Infrastructure
|
||||
IsNewTenant = false
|
||||
};
|
||||
|
||||
// on upgrade install the associated Nuget package
|
||||
if (!string.IsNullOrEmpty(install.ConnectionString))
|
||||
{
|
||||
InstallDatabase(install);
|
||||
}
|
||||
|
||||
var installation = IsInstalled();
|
||||
if (!installation.Success)
|
||||
{
|
||||
@ -209,57 +203,6 @@ namespace Oqtane.Infrastructure
|
||||
return result;
|
||||
}
|
||||
|
||||
private Installation InstallDatabase(InstallConfig install)
|
||||
{
|
||||
var result = new Installation {Success = false, Message = string.Empty};
|
||||
|
||||
try
|
||||
{
|
||||
bool installPackages = false;
|
||||
|
||||
// iterate database packages in installation folder
|
||||
var packagesFolder = new DirectoryInfo(Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder));
|
||||
foreach (var package in packagesFolder.GetFiles("*.nupkg.bak"))
|
||||
{
|
||||
// determine if package needs to be upgraded or installed
|
||||
bool upgrade = System.IO.File.Exists(package.FullName.Replace(".nupkg.bak",".log"));
|
||||
if (upgrade || package.Name.StartsWith(Utilities.GetAssemblyName(install.DatabaseType)))
|
||||
{
|
||||
var packageName = Path.Combine(package.DirectoryName, package.Name);
|
||||
packageName = packageName.Substring(0, packageName.IndexOf(".bak"));
|
||||
package.MoveTo(packageName, true);
|
||||
installPackages = true;
|
||||
}
|
||||
}
|
||||
if (installPackages)
|
||||
{
|
||||
using (var scope = _serviceScopeFactory.CreateScope())
|
||||
{
|
||||
var installationManager = scope.ServiceProvider.GetRequiredService<IInstallationManager>();
|
||||
installationManager.InstallPackages();
|
||||
}
|
||||
}
|
||||
|
||||
// load the installation database type (if necessary)
|
||||
if (Type.GetType(install.DatabaseType) == null)
|
||||
{
|
||||
var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
|
||||
var assembliesFolder = new DirectoryInfo(assemblyPath);
|
||||
var assemblyFile = new FileInfo($"{assembliesFolder}/{Utilities.GetAssemblyName(install.DatabaseType)}.dll");
|
||||
AssemblyLoadContext.Default.LoadOqtaneAssembly(assemblyFile);
|
||||
}
|
||||
|
||||
result.Success = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Message = ex.ToString();
|
||||
_filelogger.LogError(Utilities.LogMessage(this, result.Message));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Installation CreateDatabase(InstallConfig install)
|
||||
{
|
||||
var result = new Installation { Success = false, Message = string.Empty };
|
||||
@ -268,8 +211,6 @@ namespace Oqtane.Infrastructure
|
||||
{
|
||||
try
|
||||
{
|
||||
InstallDatabase(install);
|
||||
|
||||
var databaseType = install.DatabaseType;
|
||||
|
||||
// get database type
|
||||
@ -436,7 +377,7 @@ namespace Oqtane.Infrastructure
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Message = "An Error Occurred Migrating A Tenant Database. This Is Usually Related To A Tenant Database Not Being In A Supported State. " + ex.ToString();
|
||||
result.Message = "An Error Occurred Migrating The Database For Tenant " + tenant.Name + ". This Is Usually Related To Database Permissions, Connection String Mappings, Or The Database Not Being In A Supported State. " + ex.ToString();
|
||||
_filelogger.LogError(Utilities.LogMessage(this, result.Message));
|
||||
}
|
||||
|
||||
@ -457,7 +398,7 @@ namespace Oqtane.Infrastructure
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Message = "An Error Occurred Executing Upgrade Logic. " + ex.ToString();
|
||||
result.Message = "An Error Occurred Executing Upgrade Logic On Tenant " + tenant.Name + ". " + ex.ToString();
|
||||
_filelogger.LogError(Utilities.LogMessage(this, result.Message));
|
||||
}
|
||||
}
|
||||
@ -527,7 +468,7 @@ namespace Oqtane.Infrastructure
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Message = "An Error Occurred Installing " + moduleDefinition.Name + " Version " + versions[i] + " - " + ex.ToString();
|
||||
result.Message = "An Error Occurred Installing " + moduleDefinition.Name + " Version " + versions[i] + " On Tenant " + tenant.Name + " - " + ex.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -614,6 +555,7 @@ namespace Oqtane.Infrastructure
|
||||
SiteTemplateType = install.SiteTemplate,
|
||||
Runtime = (!string.IsNullOrEmpty(install.Runtime)) ? install.Runtime : _configManager.GetSection("Runtime").Value,
|
||||
RenderMode = (!string.IsNullOrEmpty(install.RenderMode)) ? install.RenderMode : _configManager.GetSection("RenderMode").Value,
|
||||
HybridEnabled = false
|
||||
};
|
||||
site = sites.AddSite(site);
|
||||
|
||||
|
@ -15,10 +15,18 @@ namespace Oqtane.Infrastructure.EventSubscribers
|
||||
|
||||
public void EntityChanged(SyncEvent syncEvent)
|
||||
{
|
||||
// when site entities change (ie. site, pages, modules, etc...) a site refresh event is raised and the site cache item needs to be refreshed
|
||||
if (syncEvent.EntityName == EntityNames.Site && syncEvent.Action == SyncEventActions.Refresh)
|
||||
{
|
||||
_cache.Remove($"site:{syncEvent.TenantId}:{syncEvent.EntityId}");
|
||||
}
|
||||
|
||||
// when a site entity is updated the hosting model may have changed, so the client assemblies cache items need to be refreshed
|
||||
if (syncEvent.EntityName == EntityNames.Site && syncEvent.Action == SyncEventActions.Update)
|
||||
{
|
||||
_cache.Remove($"assemblieslist:{syncEvent.TenantId}:{syncEvent.EntityId}");
|
||||
_cache.Remove($"assemblies:{syncEvent.TenantId}:{syncEvent.EntityId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ using System.Xml;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Oqtane.Controllers;
|
||||
using Oqtane.Shared;
|
||||
// ReSharper disable AssignNullToNotNullAttribute
|
||||
|
||||
@ -41,51 +40,25 @@ namespace Oqtane.Infrastructure
|
||||
}
|
||||
}
|
||||
|
||||
// method must be static as it is called in ConfigureServices during Startup
|
||||
public static string InstallPackages(string webRootPath, string contentRootPath)
|
||||
{
|
||||
string errors = "";
|
||||
string binPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
|
||||
|
||||
string sourceFolder = Path.Combine(contentRootPath, "Packages");
|
||||
string sourceFolder = Path.Combine(contentRootPath, Constants.PackagesFolder);
|
||||
if (!Directory.Exists(sourceFolder))
|
||||
{
|
||||
Directory.CreateDirectory(sourceFolder);
|
||||
}
|
||||
|
||||
// move packages to secure /Packages folder
|
||||
foreach (var folderName in "Modules,Themes,Packages".Split(","))
|
||||
{
|
||||
string folder = Path.Combine(webRootPath, folderName);
|
||||
if (Directory.Exists(folder))
|
||||
{
|
||||
foreach (var file in Directory.GetFiles(folder, "*.nupkg*"))
|
||||
{
|
||||
var destinationFile = Path.Combine(sourceFolder, Path.GetFileName(file));
|
||||
if (File.Exists(destinationFile))
|
||||
{
|
||||
File.Delete(destinationFile);
|
||||
}
|
||||
// read assembly log
|
||||
var assemblyLogPath = Path.Combine(sourceFolder, "assemblies.log");
|
||||
var assemblies = GetAssemblyLog(assemblyLogPath);
|
||||
|
||||
if (destinationFile.ToLower().EndsWith(".nupkg.bak"))
|
||||
{
|
||||
// leave a copy in the current folder as it is distributed with the core framework
|
||||
File.Copy(file, destinationFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
// move to destination
|
||||
File.Move(file, destinationFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Directory.CreateDirectory(folder);
|
||||
}
|
||||
}
|
||||
|
||||
// iterate through Nuget packages in source folder
|
||||
foreach (string packagename in Directory.GetFiles(sourceFolder, "*.nupkg"))
|
||||
// install Nuget packages in secure Packages folder
|
||||
var packages = Directory.GetFiles(sourceFolder, "*.nupkg");
|
||||
foreach (string packagename in packages)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -154,10 +127,29 @@ namespace Oqtane.Infrastructure
|
||||
// ContentRootPath sometimes produces inconsistent path casing - so can't use string.Replace()
|
||||
filename = Regex.Replace(filename, Regex.Escape(contentRootPath), "", RegexOptions.IgnoreCase);
|
||||
assets.Add(filename);
|
||||
if (!manifest && Path.GetExtension(filename) == ".log")
|
||||
|
||||
// packages can include a manifest (rather than relying on the framework to dynamically create one)
|
||||
if (!manifest && filename.EndsWith(name + ".log"))
|
||||
{
|
||||
manifest = true;
|
||||
}
|
||||
|
||||
// register assembly
|
||||
if (Path.GetExtension(filename) == ".dll")
|
||||
{
|
||||
// if package version was not installed previously
|
||||
if (!File.Exists(Path.Combine(sourceFolder, name + ".log")))
|
||||
{
|
||||
if (assemblies.ContainsKey(Path.GetFileName(filename)))
|
||||
{
|
||||
assemblies[Path.GetFileName(filename)] += 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
assemblies.Add(Path.GetFileName(filename), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -187,6 +179,12 @@ namespace Oqtane.Infrastructure
|
||||
File.Delete(packagename);
|
||||
}
|
||||
|
||||
if (packages.Length != 0)
|
||||
{
|
||||
// save assembly log
|
||||
SetAssemblyLog(assemblyLogPath, assemblies);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
@ -232,6 +230,10 @@ namespace Oqtane.Infrastructure
|
||||
{
|
||||
if (!string.IsNullOrEmpty(PackageName))
|
||||
{
|
||||
// read assembly log
|
||||
var assemblyLogPath = Path.Combine(Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder), "assemblies.log");
|
||||
var assemblies = GetAssemblyLog(assemblyLogPath);
|
||||
|
||||
// get manifest with highest version
|
||||
string packagename = "";
|
||||
string[] packages = Directory.GetFiles(Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder), PackageName + "*.log");
|
||||
@ -249,17 +251,31 @@ namespace Oqtane.Infrastructure
|
||||
{
|
||||
// legacy support for assets that were stored as absolute paths
|
||||
string filepath = asset.StartsWith("\\") ? Path.Combine(_environment.ContentRootPath, asset.Substring(1)) : asset;
|
||||
if (File.Exists(filepath))
|
||||
|
||||
// delete assets
|
||||
if (Path.GetExtension(filepath) == ".dll")
|
||||
{
|
||||
// do not remove licensing assemblies - this is a temporary fix until a more robust dependency management solution is available
|
||||
if (!filepath.Contains("Oqtane.Licensing."))
|
||||
// use assembly log to determine if assembly is used in other packages
|
||||
if (assemblies.ContainsKey(Path.GetFileName(filepath)))
|
||||
{
|
||||
File.Delete(filepath);
|
||||
if (!Directory.EnumerateFiles(Path.GetDirectoryName(filepath)).Any())
|
||||
if (assemblies[Path.GetFileName(filepath)] == 1)
|
||||
{
|
||||
Directory.Delete(Path.GetDirectoryName(filepath), true);
|
||||
DeleteFile(filepath);
|
||||
assemblies.Remove(Path.GetFileName(filepath));
|
||||
}
|
||||
else
|
||||
{
|
||||
assemblies[Path.GetFileName(filepath)] -= 1;
|
||||
}
|
||||
}
|
||||
else // does not exist in assembly log
|
||||
{
|
||||
DeleteFile(filepath);
|
||||
}
|
||||
}
|
||||
else // not an assembly
|
||||
{
|
||||
DeleteFile(filepath);
|
||||
}
|
||||
}
|
||||
|
||||
@ -269,6 +285,9 @@ namespace Oqtane.Infrastructure
|
||||
File.Delete(asset);
|
||||
}
|
||||
|
||||
// save assembly log
|
||||
SetAssemblyLog(assemblyLogPath, assemblies);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -276,6 +295,76 @@ namespace Oqtane.Infrastructure
|
||||
return false;
|
||||
}
|
||||
|
||||
private void DeleteFile(string filepath)
|
||||
{
|
||||
if (File.Exists(filepath))
|
||||
{
|
||||
File.Delete(filepath);
|
||||
if (!Directory.EnumerateFiles(Path.GetDirectoryName(filepath)).Any())
|
||||
{
|
||||
Directory.Delete(Path.GetDirectoryName(filepath), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int RegisterAssemblies()
|
||||
{
|
||||
var assemblyLogPath = GetAssemblyLogPath();
|
||||
var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
|
||||
|
||||
var assemblies = GetAssemblyLog(assemblyLogPath);
|
||||
|
||||
// remove assemblies that no longer exist
|
||||
foreach (var dll in assemblies)
|
||||
{
|
||||
if (!File.Exists(Path.Combine(binFolder, dll.Key)))
|
||||
{
|
||||
assemblies.Remove(dll.Key);
|
||||
}
|
||||
}
|
||||
// add assemblies which are not registered
|
||||
foreach (var dll in Directory.GetFiles(binFolder, "*.dll"))
|
||||
{
|
||||
if (!assemblies.ContainsKey(Path.GetFileName(dll)))
|
||||
{
|
||||
assemblies.Add(Path.GetFileName(dll), 1);
|
||||
}
|
||||
}
|
||||
|
||||
SetAssemblyLog(assemblyLogPath, assemblies);
|
||||
|
||||
return assemblies.Count;
|
||||
}
|
||||
|
||||
private string GetAssemblyLogPath()
|
||||
{
|
||||
string packagesFolder = Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder);
|
||||
if (!Directory.Exists(packagesFolder))
|
||||
{
|
||||
Directory.CreateDirectory(packagesFolder);
|
||||
}
|
||||
return Path.Combine(packagesFolder, "assemblies.log");
|
||||
}
|
||||
|
||||
private static Dictionary<string, int> GetAssemblyLog(string assemblyLogPath)
|
||||
{
|
||||
Dictionary<string, int> assemblies = new Dictionary<string, int>();
|
||||
if (File.Exists(assemblyLogPath))
|
||||
{
|
||||
assemblies = JsonSerializer.Deserialize<Dictionary<string, int>>(File.ReadAllText(assemblyLogPath));
|
||||
}
|
||||
return assemblies;
|
||||
}
|
||||
|
||||
private static void SetAssemblyLog(string assemblyLogPath, Dictionary<string, int> assemblies)
|
||||
{
|
||||
if (File.Exists(assemblyLogPath))
|
||||
{
|
||||
File.Delete(assemblyLogPath);
|
||||
}
|
||||
File.WriteAllText(assemblyLogPath, JsonSerializer.Serialize(assemblies, new JsonSerializerOptions { WriteIndented = true }));
|
||||
}
|
||||
|
||||
public async Task UpgradeFramework()
|
||||
{
|
||||
string folder = Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder);
|
||||
|
@ -6,6 +6,7 @@ namespace Oqtane.Infrastructure
|
||||
{
|
||||
void InstallPackages();
|
||||
bool UninstallPackage(string PackageName);
|
||||
int RegisterAssemblies();
|
||||
Task UpgradeFramework();
|
||||
void RestartApplication();
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ namespace Oqtane.Infrastructure
|
||||
Alias GetAlias();
|
||||
Tenant GetTenant();
|
||||
void SetAlias(Alias alias);
|
||||
void SetAlias(int tenantId, int siteId);
|
||||
void SetTenant(int tenantId);
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ namespace Oqtane.Infrastructure
|
||||
var logRepository = provider.GetRequiredService<ILogRepository>();
|
||||
var visitorRepository = provider.GetRequiredService<IVisitorRepository>();
|
||||
var notificationRepository = provider.GetRequiredService<INotificationRepository>();
|
||||
var installationManager = provider.GetRequiredService<IInstallationManager>();
|
||||
|
||||
// iterate through sites for current tenant
|
||||
List<Site> sites = siteRepository.GetSites().ToList();
|
||||
@ -96,6 +97,17 @@ namespace Oqtane.Infrastructure
|
||||
}
|
||||
}
|
||||
|
||||
// register assemblies
|
||||
try
|
||||
{
|
||||
var assemblies = installationManager.RegisterAssemblies();
|
||||
log += assemblies.ToString() + " Assemblies Registered<br />";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log += $"Error Registering Assemblies - {ex.Message}<br />";
|
||||
}
|
||||
|
||||
return log;
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Oqtane.Repository;
|
||||
using Oqtane.Shared;
|
||||
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
|
||||
|
||||
namespace Oqtane.Infrastructure
|
||||
{
|
||||
@ -22,8 +23,7 @@ namespace Oqtane.Infrastructure
|
||||
var config = context.RequestServices.GetService(typeof(IConfigManager)) as IConfigManager;
|
||||
string path = context.Request.Path.ToString();
|
||||
|
||||
|
||||
if (config.IsInstalled() && !path.StartsWith("/_blazor"))
|
||||
if (config.IsInstalled() && !path.StartsWith("/_")) // ignore Blazor framework requests
|
||||
{
|
||||
// get alias (note that this also sets SiteState.Alias)
|
||||
var tenantManager = context.RequestServices.GetService(typeof(ITenantManager)) as ITenantManager;
|
||||
@ -57,9 +57,25 @@ namespace Oqtane.Infrastructure
|
||||
{
|
||||
if (path.StartsWith("/" + alias.Path) && (Constants.ReservedRoutes.Any(item => path.Contains("/" + item + "/"))))
|
||||
{
|
||||
context.Request.Path = path.Replace("/" + alias.Path, "");
|
||||
context.Request.Path = path.Substring(alias.Path.Length + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// handle sitemap.xml request
|
||||
if (context.Request.Path.ToString().Contains("/sitemap.xml") && !context.Request.Path.ToString().Contains("/pages"))
|
||||
{
|
||||
context.Request.Path = "/pages/sitemap.xml";
|
||||
}
|
||||
|
||||
// handle robots.txt root request (does not support subfolder aliases)
|
||||
if (context.Request.Path.StartsWithSegments("/robots.txt") && string.IsNullOrEmpty(alias.Path))
|
||||
{
|
||||
// allow all user agents and specify site map
|
||||
var robots = $"User-agent: *\n\nSitemap: {context.Request.Scheme}://{alias.Name}/sitemap.xml";
|
||||
context.Response.ContentType = "text/plain";
|
||||
await context.Response.WriteAsync(robots);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,13 +26,14 @@ namespace Oqtane.Infrastructure
|
||||
{
|
||||
Alias alias = null;
|
||||
|
||||
if (_siteState?.Alias != null && _siteState.Alias.AliasId != -1)
|
||||
// does not support mock Alias objects (GetTenant should be used to retrieve a TenantId)
|
||||
if (_siteState?.Alias != null && _siteState.Alias.AliasId != -1)
|
||||
{
|
||||
alias = _siteState.Alias;
|
||||
}
|
||||
else
|
||||
{
|
||||
// if there is http context
|
||||
// if there is HttpContext
|
||||
var httpcontext = _httpContextAccessor.HttpContext;
|
||||
if (httpcontext != null)
|
||||
{
|
||||
@ -78,15 +79,19 @@ namespace Oqtane.Infrastructure
|
||||
return null;
|
||||
}
|
||||
|
||||
// background processes can set the alias using the SiteState service
|
||||
public void SetAlias(Alias alias)
|
||||
{
|
||||
// background processes can set the alias using the SiteState service
|
||||
_siteState.Alias = alias;
|
||||
}
|
||||
|
||||
public void SetAlias(int tenantId, int siteId)
|
||||
{
|
||||
_siteState.Alias = _aliasRepository.GetAliases().ToList().FirstOrDefault(item => item.TenantId == tenantId && item.SiteId == siteId);
|
||||
}
|
||||
|
||||
public void SetTenant(int tenantId)
|
||||
{
|
||||
// background processes can set the alias using the SiteState service
|
||||
_siteState.Alias = new Alias { TenantId = tenantId, AliasId = -1, SiteId = -1 };
|
||||
}
|
||||
}
|
||||
|
@ -106,7 +106,7 @@ namespace Oqtane.Managers
|
||||
{
|
||||
if (string.IsNullOrEmpty(user.Password))
|
||||
{
|
||||
// create random interal password based on random date and punctuation ie. Jan-23-1981+14:43:12!
|
||||
// generate password based on random date and punctuation ie. Jan-23-1981+14:43:12!
|
||||
Random rnd = new Random();
|
||||
var date = DateTime.UtcNow.AddDays(-rnd.Next(50 * 365)).AddHours(rnd.Next(0, 24)).AddMinutes(rnd.Next(0, 60)).AddSeconds(rnd.Next(0, 60));
|
||||
user.Password = date.ToString("MMM-dd-yyyy+HH:mm:ss", CultureInfo.InvariantCulture) + (char)rnd.Next(33, 47);
|
||||
@ -152,7 +152,7 @@ namespace Oqtane.Managers
|
||||
{
|
||||
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 Complete The Registration Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
|
||||
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);
|
||||
}
|
||||
@ -205,8 +205,22 @@ namespace Oqtane.Managers
|
||||
if (user.Email != identityuser.Email)
|
||||
{
|
||||
await _identityUserManager.SetEmailAsync(identityuser, user.Email);
|
||||
var emailConfirmationToken = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
|
||||
await _identityUserManager.ConfirmEmailAsync(identityuser, emailConfirmationToken);
|
||||
|
||||
// if email address changed and user is not administrator, email verification is required for new email address
|
||||
if (!user.EmailConfirmed)
|
||||
{
|
||||
var alias = _tenantManager.GetAlias();
|
||||
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);
|
||||
}
|
||||
else
|
||||
{
|
||||
var emailConfirmationToken = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
|
||||
await _identityUserManager.ConfirmEmailAsync(identityuser, emailConfirmationToken);
|
||||
}
|
||||
}
|
||||
|
||||
user = _users.UpdateUser(user);
|
||||
@ -308,7 +322,7 @@ namespace Oqtane.Managers
|
||||
user = _users.GetUser(identityuser.UserName);
|
||||
if (user != null)
|
||||
{
|
||||
if (identityuser.EmailConfirmed)
|
||||
if (await _identityUserManager.IsEmailConfirmedAsync(identityuser))
|
||||
{
|
||||
user.IsAuthenticated = true;
|
||||
user.LastLoginOn = DateTime.UtcNow;
|
||||
@ -323,7 +337,7 @@ namespace Oqtane.Managers
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Not Verified {Username}", user.Username);
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Email Address Not Verified {Username}", user.Username);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,30 @@
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Oqtane.Databases.Interfaces;
|
||||
using Oqtane.Migrations.EntityBuilders;
|
||||
using Oqtane.Repository;
|
||||
|
||||
namespace Oqtane.Migrations.Tenant
|
||||
{
|
||||
[DbContext(typeof(TenantDBContext))]
|
||||
[Migration("Tenant.05.00.01.00")]
|
||||
public class AddSiteHybridEnabled : MultiDatabaseMigration
|
||||
{
|
||||
public AddSiteHybridEnabled(IDatabase database) : base(database)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase);
|
||||
siteEntityBuilder.AddBooleanColumn("HybridEnabled", true);
|
||||
siteEntityBuilder.UpdateColumn("HybridEnabled", "0", "bool", ""); // default to false
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase);
|
||||
siteEntityBuilder.DropColumn("HybridEnabled");
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Configurations>Debug;Release</Configurations>
|
||||
<Version>5.0.0</Version>
|
||||
<Version>5.0.1</Version>
|
||||
<Product>Oqtane</Product>
|
||||
<Authors>Shaun Walker</Authors>
|
||||
<Company>.NET Foundation</Company>
|
||||
@ -11,13 +11,14 @@
|
||||
<Copyright>.NET Foundation</Copyright>
|
||||
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
|
||||
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
|
||||
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0</PackageReleaseNotes>
|
||||
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1</PackageReleaseNotes>
|
||||
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
|
||||
<RepositoryType>Git</RepositoryType>
|
||||
<RootNamespace>Oqtane</RootNamespace>
|
||||
<IsPackable>true</IsPackable>
|
||||
<DefineConstants>$(DefineConstants);OQTANE;OQTANE3</DefineConstants>
|
||||
<PreserveCompilationContext>true</PreserveCompilationContext>
|
||||
<SatelliteResourceLanguages>none</SatelliteResourceLanguages>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="wwwroot\Modules\Templates\**" />
|
||||
@ -35,7 +36,6 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.0-preview3.23201.1" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
@ -43,9 +43,10 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.0" />
|
||||
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.7-pre20231110210158" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="8.0.0" />
|
||||
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.7" />
|
||||
<PackageReference Include="Oqtane.Licensing" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@ -55,10 +56,18 @@
|
||||
<ItemGroup>
|
||||
<ModuleTemplateFiles Include="$(ProjectDir)wwwroot\Modules\Templates\**\*.*" />
|
||||
<ThemeTemplateFiles Include="$(ProjectDir)wwwroot\Themes\Templates\**\*.*" />
|
||||
<MySQLFiles Include="$(OutputPath)Oqtane.Database.MySQL.dll;$(OutputPath)Oqtane.Database.MySQL.pdb;$(OutputPath)MySql.EntityFrameworkCore.dll;$(OutputPath)MySql.Data.dll" />
|
||||
<PostgreSQLFiles Include="$(OutputPath)Oqtane.Database.PostgreSQL.dll;$(OutputPath)Oqtane.Database.PostgreSQL.pdb;$(OutputPath)EFCore.NamingConventions.dll;$(OutputPath)Npgsql.EntityFrameworkCore.PostgreSQL.dll;$(OutputPath)Npgsql.dll" />
|
||||
<SqliteFiles Include="$(OutputPath)Oqtane.Database.Sqlite.dll;$(OutputPath)Oqtane.Database.Sqlite.pdb;$(OutputPath)Microsoft.EntityFrameworkCore.Sqlite.dll" />
|
||||
<SqlServerFiles Include="$(OutputPath)Oqtane.Database.SqlServer.dll;$(OutputPath)Oqtane.Database.SqlServer.pdb;$(OutputPath)Microsoft.EntityFrameworkCore.SqlServer.dll" />
|
||||
</ItemGroup>
|
||||
<Target Name="AddPayloadsFolder" AfterTargets="Publish">
|
||||
<Copy SourceFiles="@(ModuleTemplateFiles)" DestinationFiles="@(ModuleTemplateFiles->'$(PublishDir)wwwroot\Modules\Templates\%(RecursiveDir)%(Filename)%(Extension)')" SkipUnchangedFiles="false" />
|
||||
<Copy SourceFiles="@(ThemeTemplateFiles)" DestinationFiles="@(ThemeTemplateFiles->'$(PublishDir)wwwroot\Themes\Templates\%(RecursiveDir)%(Filename)%(Extension)')" SkipUnchangedFiles="false" />
|
||||
<Copy SourceFiles="@(MySQLFiles)" DestinationFiles="@(MySQLFiles->'$(PublishDir)%(Filename)%(Extension)')" SkipUnchangedFiles="false" />
|
||||
<Copy SourceFiles="@(PostgreSQLFiles)" DestinationFiles="@(PostgreSQLFiles->'$(PublishDir)%(Filename)%(Extension)')" SkipUnchangedFiles="false" />
|
||||
<Copy SourceFiles="@(SqliteFiles)" DestinationFiles="@(SqliteFiles->'$(PublishDir)%(Filename)%(Extension)')" SkipUnchangedFiles="false" />
|
||||
<Copy SourceFiles="@(SqlServerFiles)" DestinationFiles="@(SqlServerFiles->'$(PublishDir)%(Filename)%(Extension)')" SkipUnchangedFiles="false" />
|
||||
</Target>
|
||||
<ItemGroup>
|
||||
<!-- extends watching group to include *.dll files and exclude the ones cause an infinite loop -->
|
||||
|
@ -19,7 +19,7 @@ namespace Oqtane.Pages
|
||||
var providertype = HttpContext.GetSiteSettings().GetValue("ExternalLogin:ProviderType", "");
|
||||
if (providertype != "")
|
||||
{
|
||||
return new ChallengeResult(providertype, new AuthenticationProperties { RedirectUri = returnurl + (returnurl.Contains("?") ? "&" : "?") + "reload=post" });
|
||||
return new ChallengeResult(providertype, new AuthenticationProperties { RedirectUri = returnurl + (returnurl.Contains("?") ? "&" : "?") + "reload=post" });
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -53,7 +53,8 @@ namespace Oqtane.Pages
|
||||
{
|
||||
if (_userPermissions.IsAuthorized(null, PermissionNames.View, page.PermissionList) && page.IsNavigation)
|
||||
{
|
||||
sitemap.Add(new Sitemap { Url = _alias.Protocol + _alias.Name + Utilities.NavigateUrl(_alias.Path, page.Path, ""), ModifiedOn = DateTime.UtcNow });
|
||||
var rooturl = _alias.Protocol + (string.IsNullOrEmpty(_alias.Path) ? _alias.Name : _alias.Name.Substring(0, _alias.Name.IndexOf("/")));
|
||||
sitemap.Add(new Sitemap { Url = rooturl + Utilities.NavigateUrl(_alias.Path, page.Path, ""), ModifiedOn = DateTime.UtcNow });
|
||||
|
||||
foreach (var pageModule in pageModules.Where(item => item.PageId == page.PageId))
|
||||
{
|
||||
@ -72,7 +73,7 @@ namespace Oqtane.Pages
|
||||
var urls = ((ISitemap)moduleobject).GetUrls(_alias.Path, page.Path, pageModule.Module);
|
||||
foreach (var url in urls)
|
||||
{
|
||||
sitemap.Add(new Sitemap { Url = _alias.Protocol + _alias.Name + url.Url, ModifiedOn = DateTime.UtcNow });
|
||||
sitemap.Add(new Sitemap { Url = rooturl + url.Url, ModifiedOn = DateTime.UtcNow });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -4,9 +4,7 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.AspNetCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Oqtane.Infrastructure;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Oqtane.Shared;
|
||||
using Oqtane.Documentation;
|
||||
|
||||
namespace Oqtane.Server
|
||||
|
@ -45,13 +45,20 @@ namespace Oqtane.Repository
|
||||
Tenant tenant = _tenantManager.GetTenant();
|
||||
if (tenant != null)
|
||||
{
|
||||
_connectionString = _config.GetConnectionString(tenant.DBConnectionString)
|
||||
.Replace($"|{Constants.DataDirectory}|", AppDomain.CurrentDomain.GetData(Constants.DataDirectory)?.ToString());
|
||||
_databaseType = tenant.DBType;
|
||||
_connectionString = _config.GetConnectionString(tenant.DBConnectionString);
|
||||
if (_connectionString != null)
|
||||
{
|
||||
_connectionString = _connectionString.Replace($"|{Constants.DataDirectory}|", AppDomain.CurrentDomain.GetData(Constants.DataDirectory)?.ToString());
|
||||
_databaseType = tenant.DBType;
|
||||
}
|
||||
else
|
||||
{
|
||||
// tenant connection string does not exist in appsettings.json
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!String.IsNullOrEmpty(_databaseType))
|
||||
if (!string.IsNullOrEmpty(_databaseType))
|
||||
{
|
||||
var type = Type.GetType(_databaseType);
|
||||
ActiveDatabase = Activator.CreateInstance(type) as IDatabase;
|
||||
|
@ -9,6 +9,8 @@ namespace Oqtane.Repository
|
||||
|
||||
Language AddLanguage(Language language);
|
||||
|
||||
void UpdateLanguage(Language language);
|
||||
|
||||
Language GetLanguage(int languageId);
|
||||
|
||||
void DeleteLanguage(int languageId);
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Oqtane.Models;
|
||||
|
||||
namespace Oqtane.Repository
|
||||
@ -12,7 +13,7 @@ namespace Oqtane.Repository
|
||||
{
|
||||
_db = context;
|
||||
}
|
||||
|
||||
|
||||
public IEnumerable<Language> GetLanguages(int siteId)
|
||||
{
|
||||
return _db.Language.Where(l => l.SiteId == siteId);
|
||||
@ -35,6 +36,25 @@ namespace Oqtane.Repository
|
||||
return language;
|
||||
}
|
||||
|
||||
public void UpdateLanguage(Language language)
|
||||
{
|
||||
if (language.LanguageId != 0)
|
||||
{
|
||||
_db.Entry(language).State = EntityState.Modified;
|
||||
}
|
||||
if (language.IsDefault)
|
||||
{
|
||||
// Ensure all other languages are not set to default
|
||||
_db.Language
|
||||
.Where(l => l.SiteId == language.SiteId &&
|
||||
l.LanguageId != language.LanguageId)
|
||||
.ToList()
|
||||
.ForEach(l => l.IsDefault = false);
|
||||
}
|
||||
|
||||
_db.SaveChanges();
|
||||
}
|
||||
|
||||
public Language GetLanguage(int languageId)
|
||||
{
|
||||
return _db.Language.Find(languageId);
|
||||
|
@ -93,7 +93,15 @@ namespace Oqtane.Repository
|
||||
public int ExecuteNonQuery(string connectionString, string databaseType, string query)
|
||||
{
|
||||
var db = GetActiveDatabase(databaseType);
|
||||
return db.ExecuteNonQuery(GetConnectionString(connectionString), query);
|
||||
var connectionstring = GetConnectionString(connectionString);
|
||||
if (connectionstring != null)
|
||||
{
|
||||
return db.ExecuteNonQuery(GetConnectionString(connectionString), query);
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetScriptFromAssembly(Assembly assembly, string fileName)
|
||||
|
@ -18,7 +18,7 @@ namespace Oqtane.Security
|
||||
public string GenerateToken(Alias alias, ClaimsIdentity identity, string secret, string issuer, string audience, int lifetime)
|
||||
{
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var key = Encoding.ASCII.GetBytes(secret);
|
||||
var key = Encoding.ASCII.GetBytes(PadSecret(secret));
|
||||
var tokenDescriptor = new SecurityTokenDescriptor
|
||||
{
|
||||
Subject = new ClaimsIdentity(identity),
|
||||
@ -36,7 +36,7 @@ namespace Oqtane.Security
|
||||
if (!string.IsNullOrEmpty(token))
|
||||
{
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var key = Encoding.ASCII.GetBytes(secret);
|
||||
var key = Encoding.ASCII.GetBytes(PadSecret(secret));
|
||||
try
|
||||
{
|
||||
tokenHandler.ValidateToken(token, new TokenValidationParameters
|
||||
@ -66,5 +66,11 @@ namespace Oqtane.Security
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private string PadSecret(string secret)
|
||||
{
|
||||
// ensure secret is 256 bits
|
||||
return (secret.Length < 32) ? (secret + "????????????????????????????????").Substring(0, 32) : secret;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ using Oqtane.Models;
|
||||
using System.Collections.Generic;
|
||||
using Oqtane.Extensions;
|
||||
using Oqtane.Shared;
|
||||
using System.IO;
|
||||
|
||||
namespace Oqtane.Security
|
||||
{
|
||||
@ -17,9 +18,11 @@ namespace Oqtane.Security
|
||||
{
|
||||
if (context != null && context.Principal.Identity.IsAuthenticated && context.Principal.Identity.Name != null)
|
||||
{
|
||||
// check if framework is installed
|
||||
var config = context.HttpContext.RequestServices.GetService(typeof(IConfigManager)) as IConfigManager;
|
||||
if (config.IsInstalled())
|
||||
string path = context.Request.Path.ToString().ToLower();
|
||||
|
||||
// check if framework is installed
|
||||
if (config.IsInstalled() && !path.StartsWith("/_")) // ignore Blazor framework requests
|
||||
{
|
||||
// get current site
|
||||
var alias = context.HttpContext.GetAlias();
|
||||
@ -28,12 +31,11 @@ namespace Oqtane.Security
|
||||
var claims = context.Principal.Claims;
|
||||
|
||||
// check if principal has roles and matches current site
|
||||
if (!claims.Any(item => item.Type == ClaimTypes.Role) || claims.FirstOrDefault(item => item.Type == "sitekey")?.Value != alias.SiteKey)
|
||||
if (!claims.Any(item => item.Type == ClaimTypes.Role) || !claims.Any(item => item.Type == "sitekey" && item.Value == alias.SiteKey))
|
||||
{
|
||||
var userRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRepository)) as IUserRepository;
|
||||
var userRoleRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository;
|
||||
var _logger = context.HttpContext.RequestServices.GetService(typeof(ILogManager)) as ILogManager;
|
||||
string path = context.Request.Path.ToString().ToLower();
|
||||
|
||||
User user = userRepository.GetUser(context.Principal.Identity.Name);
|
||||
if (user != null)
|
||||
|
@ -71,6 +71,9 @@ namespace Oqtane
|
||||
{
|
||||
options.DetailedErrors = true;
|
||||
}
|
||||
})
|
||||
.AddHubOptions(options => {
|
||||
options.MaximumReceiveMessageSize = null; // no limit (for large amnounts of data ie. textarea components)
|
||||
});
|
||||
|
||||
// setup HttpClient for server side in a client side compatible fashion ( with auth cookie )
|
||||
|
@ -1,3 +1,4 @@
|
||||
del "*.nupkg"
|
||||
"..\..\[RootFolder]\oqtane.package\nuget.exe" pack [Owner].Module.[Module].nuspec
|
||||
XCOPY "*.nupkg" "..\..\[RootFolder]\Oqtane.Server\Packages\" /Y
|
||||
|
||||
|
@ -81,7 +81,7 @@ namespace [Owner].Module.[Module].Controllers
|
||||
[Authorize(Policy = PolicyNames.EditModule)]
|
||||
public Models.[Module] Put(int id, [FromBody] Models.[Module] [Module])
|
||||
{
|
||||
if (ModelState.IsValid && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId) && _[Module]Repository.Get[Module]([Module].[Module]Id, false) != null)
|
||||
if (ModelState.IsValid && [Module].[Module]Id == id && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId) && _[Module]Repository.Get[Module]([Module].[Module]Id, false) != null)
|
||||
{
|
||||
[Module] = _[Module]Repository.Update[Module]([Module]);
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Update, "[Module] Updated {[Module]}", [Module]);
|
||||
|
@ -4,6 +4,12 @@ body {
|
||||
padding-top: 7rem;
|
||||
}
|
||||
|
||||
/* App Logo */
|
||||
.app-logo .img-fluid {
|
||||
max-height: 90px;
|
||||
padding: 0 5px 0 5px;
|
||||
}
|
||||
|
||||
.table > :not(caption) > * > * {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
@ -4,6 +4,12 @@ body {
|
||||
padding-top: 7rem;
|
||||
}
|
||||
|
||||
/* App Logo */
|
||||
.app-logo .img-fluid {
|
||||
max-height: 90px;
|
||||
padding: 0 5px 0 5px;
|
||||
}
|
||||
|
||||
.table > :not(caption) > * > * {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
@ -1,2 +1,3 @@
|
||||
del "*.nupkg"
|
||||
"..\..\[RootFolder]\oqtane.package\nuget.exe" pack [Owner].Theme.[Theme].nuspec
|
||||
XCOPY "*.nupkg" "..\..\[RootFolder]\Oqtane.Server\wwwroot\Themes\" /Y
|
||||
|
Reference in New Issue
Block a user