From 34538dd945fa4bb02f789a0f71f4131d19d6c40d Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Thu, 30 Apr 2020 13:58:04 -0400 Subject: [PATCH] install/upgrade refactoring to consolidate all use cases and implement IInstallable interface for modules, moved tenant creation to site management UI, fixed z-order issues in Blazor theme, enhanced JS Interop methods to support integrity and crossorigin --- .../Templates/External/Client/Index.razor | 3 +- .../Templates/External/Client/ModuleInfo.cs | 3 +- .../Server/Manager/[Module]Manager.cs | 18 +- ...1.00.00.sql => [Owner].[Module].1.0.0.sql} | 0 ...all.sql => [Owner].[Module].Uninstall.sql} | 0 .../[Owner].[Module]s.Module.Server.csproj | 8 +- .../Modules/[Module]/Index.razor | 3 +- .../Modules/[Module]/ModuleInfo.cs | 3 +- .../[Module]/Manager/[Module]Manager.cs | 18 +- ...1.00.00.sql => [Owner].[Module].1.0.0.sql} | 0 ...all.sql => [Owner].[Module].Uninstall.sql} | 0 Oqtane.Client/Modules/Admin/Sites/Add.razor | 319 +++++--- Oqtane.Client/Modules/Admin/Tenants/Add.razor | 156 ---- .../Modules/Admin/Tenants/Edit.razor | 6 +- .../Modules/Admin/Tenants/Index.razor | 25 +- Oqtane.Client/UI/Installer.razor | 199 ++--- Oqtane.Client/UI/Interop.cs | 8 +- Oqtane.Client/UI/ThemeBuilder.razor | 6 +- .../Controllers/InstallationController.cs | 42 +- .../Controllers/ModuleDefinitionController.cs | 23 +- .../Infrastructure/DatabaseManager.cs | 759 ++++++++++-------- .../Interfaces/IDatabaseManager.cs | 12 + .../Infrastructure/Interfaces/IInstallable.cs | 8 +- Oqtane.Server/Infrastructure/LogManager.cs | 10 +- .../HtmlText/Manager/HtmlTextManager.cs | 8 +- Oqtane.Server/Oqtane.Server.csproj | 5 - Oqtane.Server/Program.cs | 5 +- .../Repository/Context/InstallationContext.cs | 6 +- .../Interfaces/IModuleDefinitionRepository.cs | 1 + .../Repository/Interfaces/ISqlRepository.cs | 2 +- .../Repository/ModuleDefinitionRepository.cs | 38 +- Oqtane.Server/Repository/ModuleRepository.cs | 24 +- Oqtane.Server/Repository/SiteRepository.cs | 4 +- Oqtane.Server/Repository/SqlRepository.cs | 41 +- Oqtane.Server/Repository/TenantResolver.cs | 77 +- Oqtane.Server/Scripts/Master.00.00.00.sql | 30 - Oqtane.Server/Scripts/Master.00.00.01.sql | 7 +- Oqtane.Server/Security/UserPermissions.cs | 9 +- Oqtane.Server/Startup.cs | 2 +- .../Oqtane.Themes.BlazorTheme/Theme.css | 31 +- Oqtane.Server/wwwroot/js/interop.js | 28 +- Oqtane.Shared/Models/Tenant.cs | 1 - Oqtane.Shared/Shared/Constants.cs | 1 + Oqtane.Shared/Shared/InstallConfig.cs | 14 +- 44 files changed, 1051 insertions(+), 912 deletions(-) rename Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Server/Scripts/{01.00.00.sql => [Owner].[Module].1.0.0.sql} (100%) rename Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Server/Scripts/{Uninstall.sql => [Owner].[Module].Uninstall.sql} (100%) rename Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Server/Modules/[Module]/Scripts/{01.00.00.sql => [Owner].[Module].1.0.0.sql} (100%) rename Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Server/Modules/[Module]/Scripts/{Uninstall.sql => [Owner].[Module].Uninstall.sql} (100%) delete mode 100644 Oqtane.Client/Modules/Admin/Tenants/Add.razor create mode 100644 Oqtane.Server/Infrastructure/Interfaces/IDatabaseManager.cs diff --git a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Client/Index.razor b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Client/Index.razor index 9318628b..68c31a34 100644 --- a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Client/Index.razor +++ b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Client/Index.razor @@ -62,7 +62,8 @@ else - Repository\I[Module]Repository.cs - interface for defining repository methods
- Repository\[Module]Respository.cs - implements repository interface methods for data access using EF Core
- Repository\[Module]Context.cs - provides a DB Context for data access
-- Scripts\01.00.00.sql - database schema definition

+- Scripts\[Module].1.0.0.sql - database schema definition script

+- Scripts\[Module].Uninstall.sql - database uninstall script

[RootPath]Shared\
- [Owner].[Module]s.Module.Shared.csproj - shared project
- Models\[Module].cs - model definition

diff --git a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Client/ModuleInfo.cs b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Client/ModuleInfo.cs index ed7c765b..a95a461e 100644 --- a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Client/ModuleInfo.cs +++ b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Client/ModuleInfo.cs @@ -11,7 +11,8 @@ namespace [Owner].[Module]s.Modules Description = "[Module]", Version = "1.0.0", Dependencies = "[Owner].[Module]s.Module.Shared", - ServerManagerType = "[ServerManagerType]" + ServerManagerType = "[ServerManagerType]", + ReleaseVersions = "1.0.0" }; } } diff --git a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Server/Manager/[Module]Manager.cs b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Server/Manager/[Module]Manager.cs index 7265d4eb..0b987600 100644 --- a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Server/Manager/[Module]Manager.cs +++ b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Server/Manager/[Module]Manager.cs @@ -3,18 +3,32 @@ using System.Linq; using System.Text.Json; using Oqtane.Modules; using Oqtane.Models; +using Oqtane.Infrastructure; +using Oqtane.Repository; using [Owner].[Module]s.Models; using [Owner].[Module]s.Repository; namespace [Owner].[Module]s.Manager { - public class [Module]Manager : IPortable + public class [Module]Manager : IInstallable, IPortable { private I[Module]Repository _[Module]s; + private ISqlRepository _sql; - public [Module]Manager(I[Module]Repository [Module]s) + public [Module]Manager(I[Module]Repository [Module]s, ISqlRepository sql) { _[Module]s = [Module]s; + _sql = sql; + } + + public bool Install(Tenant tenant, string version) + { + return _sql.ExecuteScript(tenant, GetType().Assembly, "[Owner].[Module]." + version + ".sql"); + } + + public bool Uninstall(Tenant tenant) + { + return _sql.ExecuteScript(tenant, GetType().Assembly, "[Owner].[Module].Uninstall.sql"); } public string ExportModule(Module module) diff --git a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Server/Scripts/01.00.00.sql b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Server/Scripts/[Owner].[Module].1.0.0.sql similarity index 100% rename from Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Server/Scripts/01.00.00.sql rename to Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Server/Scripts/[Owner].[Module].1.0.0.sql diff --git a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Server/Scripts/Uninstall.sql b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Server/Scripts/[Owner].[Module].Uninstall.sql similarity index 100% rename from Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Server/Scripts/Uninstall.sql rename to Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Server/Scripts/[Owner].[Module].Uninstall.sql diff --git a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Server/[Owner].[Module]s.Module.Server.csproj b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Server/[Owner].[Module]s.Module.Server.csproj index 884263a6..d9ec6aee 100644 --- a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Server/[Owner].[Module]s.Module.Server.csproj +++ b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Server/[Owner].[Module]s.Module.Server.csproj @@ -13,13 +13,13 @@ - - + + - - + + diff --git a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Client/Modules/[Module]/Index.razor b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Client/Modules/[Module]/Index.razor index 90536f4a..15cf068b 100644 --- a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Client/Modules/[Module]/Index.razor +++ b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Client/Modules/[Module]/Index.razor @@ -54,7 +54,8 @@ else - Repository\I[Module]Repository.cs - interface for defining repository methods
- Repository\[Module]Respository.cs - implements repository interface methods for data access using EF Core
- Repository\[Module]Context.cs - provides a DB Context for data access
-- Scripts\01.00.00.sql - database schema definition

+- Scripts\[Module].1.0.0.sql - database schema definition script

+- Scripts\[Module].Uninstall.sql - database uninstall script

[RootPath]Oqtane.Shared\Modules\[Module]\
- Models\[Module].cs - model definition

diff --git a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Client/Modules/[Module]/ModuleInfo.cs b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Client/Modules/[Module]/ModuleInfo.cs index ed7c765b..a95a461e 100644 --- a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Client/Modules/[Module]/ModuleInfo.cs +++ b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Client/Modules/[Module]/ModuleInfo.cs @@ -11,7 +11,8 @@ namespace [Owner].[Module]s.Modules Description = "[Module]", Version = "1.0.0", Dependencies = "[Owner].[Module]s.Module.Shared", - ServerManagerType = "[ServerManagerType]" + ServerManagerType = "[ServerManagerType]", + ReleaseVersions = "1.0.0" }; } } diff --git a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Server/Modules/[Module]/Manager/[Module]Manager.cs b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Server/Modules/[Module]/Manager/[Module]Manager.cs index 7265d4eb..0b987600 100644 --- a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Server/Modules/[Module]/Manager/[Module]Manager.cs +++ b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Server/Modules/[Module]/Manager/[Module]Manager.cs @@ -3,18 +3,32 @@ using System.Linq; using System.Text.Json; using Oqtane.Modules; using Oqtane.Models; +using Oqtane.Infrastructure; +using Oqtane.Repository; using [Owner].[Module]s.Models; using [Owner].[Module]s.Repository; namespace [Owner].[Module]s.Manager { - public class [Module]Manager : IPortable + public class [Module]Manager : IInstallable, IPortable { private I[Module]Repository _[Module]s; + private ISqlRepository _sql; - public [Module]Manager(I[Module]Repository [Module]s) + public [Module]Manager(I[Module]Repository [Module]s, ISqlRepository sql) { _[Module]s = [Module]s; + _sql = sql; + } + + public bool Install(Tenant tenant, string version) + { + return _sql.ExecuteScript(tenant, GetType().Assembly, "[Owner].[Module]." + version + ".sql"); + } + + public bool Uninstall(Tenant tenant) + { + return _sql.ExecuteScript(tenant, GetType().Assembly, "[Owner].[Module].Uninstall.sql"); } public string ExportModule(Module module) diff --git a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Server/Modules/[Module]/Scripts/01.00.00.sql b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Server/Modules/[Module]/Scripts/[Owner].[Module].1.0.0.sql similarity index 100% rename from Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Server/Modules/[Module]/Scripts/01.00.00.sql rename to Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Server/Modules/[Module]/Scripts/[Owner].[Module].1.0.0.sql diff --git a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Server/Modules/[Module]/Scripts/Uninstall.sql b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Server/Modules/[Module]/Scripts/[Owner].[Module].Uninstall.sql similarity index 100% rename from Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Server/Modules/[Module]/Scripts/Uninstall.sql rename to Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Server/Modules/[Module]/Scripts/[Owner].[Module].Uninstall.sql diff --git a/Oqtane.Client/Modules/Admin/Sites/Add.razor b/Oqtane.Client/Modules/Admin/Sites/Add.razor index 7c18431c..40ac6f39 100644 --- a/Oqtane.Client/Modules/Admin/Sites/Add.razor +++ b/Oqtane.Client/Modules/Admin/Sites/Add.razor @@ -5,8 +5,9 @@ @inject IAliasService AliasService @inject ISiteService SiteService @inject IThemeService ThemeService -@inject ISiteTemplateService SiteTemplateService +@inject ISiteTemplateService SiteTemplateService @inject IUserService UserService +@inject IInstallationService InstallationService @if (_tenants == null) { @@ -17,21 +18,7 @@ else - - - - - @if (!_isinitialized) + + + + + @if (_tenantid == "+") { + + + + + + + + + + + + + + + + + + + + + + + + @if (!_integratedsecurity) + { + + + + + + + + + } @@ -116,7 +188,7 @@ else } @@ -132,16 +204,24 @@ else private List _siteTemplates; private List _themeList; private List _tenants; - private string _tenantid = "-1"; + private string _tenantid = "-"; + + private string _tenantname = string.Empty; + private string _databasetype = "LocalDB"; + private string _server = "(LocalDb)\\MSSQLLocalDB"; + private string _database = "Oqtane-" + DateTime.UtcNow.ToString("yyyyMMddHHmm"); + private string _username = string.Empty; + private string _password = string.Empty; + private bool _integratedsecurity = true; + private string _hostusername = Constants.HostUser; + private string _hostpassword = string.Empty; + private string _name = string.Empty; private string _urls = string.Empty; private string _themetype = string.Empty; private string _layouttype = string.Empty; private string _containertype = string.Empty; private string _sitetemplatetype = string.Empty; - private bool _isinitialized = true; - private string _username = string.Empty; - private string _password = string.Empty; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; @@ -153,29 +233,29 @@ else _themes = ThemeService.GetThemeTypes(_themeList); _containers = ThemeService.GetContainerTypes(_themeList); _siteTemplates = await SiteTemplateService.GetSiteTemplatesAsync(); - _username = Constants.HostUser; } - private async void TenantChanged(ChangeEventArgs e) + private void TenantChanged(ChangeEventArgs e) { - try + _tenantid = (string)e.Value; + if (string.IsNullOrEmpty(_tenantname)) { - _tenantid = (string)e.Value; - if (_tenantid != "-1") - { - var tenant = _tenants.FirstOrDefault(item => item.TenantId == int.Parse(_tenantid)); - if (tenant != null) - { - _isinitialized = tenant.IsInitialized; - StateHasChanged(); - } - } + _tenantname = _name; } - catch (Exception ex) + StateHasChanged(); + } + + private void SetIntegratedSecurity(ChangeEventArgs e) + { + if (Convert.ToBoolean((string)e.Value)) { - await logger.LogError(ex, "Error Loading Tenant {TenantId} {Error}", _tenantid, ex.Message); - AddModuleMessage("Error Loading Tenant", MessageType.Error); + _integratedsecurity = true; } + else + { + _integratedsecurity = false; + } + StateHasChanged(); } private async void ThemeChanged(ChangeEventArgs e) @@ -191,7 +271,7 @@ else { _panelayouts = new Dictionary(); } - + StateHasChanged(); } catch (Exception ex) @@ -203,105 +283,116 @@ else private async Task SaveSite() { - if (_tenantid != "-1" && _name != string.Empty && _urls != string.Empty && !string.IsNullOrEmpty(_themetype) && (_panelayouts.Count == 0 || !string.IsNullOrEmpty(_layouttype)) && !string.IsNullOrEmpty(_containertype) && !string.IsNullOrEmpty(_sitetemplatetype)) + if (_tenantid != "-" && _name != string.Empty && _urls != string.Empty && !string.IsNullOrEmpty(_themetype) && (_panelayouts.Count == 0 || !string.IsNullOrEmpty(_layouttype)) && !string.IsNullOrEmpty(_containertype) && !string.IsNullOrEmpty(_sitetemplatetype)) { - var unique = true; + var duplicates = new List(); var aliases = await AliasService.GetAliasesAsync(); foreach (string name in _urls.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { if (aliases.Exists(item => item.Name == name)) { - unique = false; + duplicates.Add(name); } } - - if (unique) + + if (duplicates.Count == 0) { - var isvalid = true; + InstallConfig config = new InstallConfig(); - if (!_isinitialized) + if (_tenantid == "+") { - var user = new User(); - user.SiteId = PageState.Site.SiteId; - user.Username = _username; - user.Password = _password; - user = await UserService.LoginUserAsync(user, false, false); - isvalid = user.IsAuthenticated; - } - - if (isvalid) - { - ShowProgressIndicator(); - - aliases = new List(); - _urls = _urls.Replace("\n", ","); - - foreach (string name in _urls.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) - { - var alias = new Alias(); - alias.Name = name; - alias.TenantId = int.Parse(_tenantid); - alias.SiteId = -1; - alias = await AliasService.AddAliasAsync(alias); - aliases.Add(alias); - } - - var site = new Site(); - site.TenantId = int.Parse(_tenantid); - site.Name = _name; - site.LogoFileId = null; - site.FaviconFileId = null; - site.DefaultThemeType = _themetype; - site.DefaultLayoutType = (_layouttype == null ? string.Empty : _layouttype); - site.DefaultContainerType = _containertype; - site.PwaIsEnabled = false; - site.PwaAppIconFileId = null; - site.PwaSplashIconFileId = null; - site.AllowRegistration = false; - site.SiteTemplateType = _sitetemplatetype; - site = await SiteService.AddSiteAsync(site, aliases[0]); - - foreach (Alias alias in aliases) - { - alias.SiteId = site.SiteId; - await AliasService.UpdateAliasAsync(alias); - } - - if (!_isinitialized) + if (!string.IsNullOrEmpty(_tenantname) && _tenants.FirstOrDefault(item => item.Name == _tenantname) == null) { + // validate host credentials var user = new User(); - user.SiteId = site.SiteId; - user.Username = _username; - user.Password = _password; - user.Email = PageState.User.Email; - user.DisplayName = PageState.User.DisplayName; - user = await UserService.AddUserAsync(user, aliases[0]); - - if (user != null) + user.SiteId = PageState.Site.SiteId; + user.Username = Constants.HostUser; + user.Password = _hostpassword; + user = await UserService.LoginUserAsync(user, false, false); + if (user.IsAuthenticated) { - var tenant = _tenants.FirstOrDefault(item => item.TenantId == int.Parse(_tenantid)); - if (tenant != null) + if (!string.IsNullOrEmpty(_server) && !string.IsNullOrEmpty(_database)) { - tenant.IsInitialized = true; - await TenantService.UpdateTenantAsync(tenant); + var connectionString = string.Empty; + if (_databasetype == "LocalDB") + { + connectionString = "Data Source=" + _server + ";AttachDbFilename=|DataDirectory|\\" + _database + ".mdf;Initial Catalog=" + _database + ";Integrated Security=SSPI;"; + } + else + { + connectionString = "Data Source=" + _server + ";Initial Catalog=" + _database + ";"; + + if (_integratedsecurity) + { + connectionString += "Integrated Security=SSPI;"; + } + else + { + connectionString += "User ID=" + _username + ";Password=" + _password; + } + } + + config.ConnectionString = connectionString; + config.HostPassword = _hostpassword; + config.HostEmail = user.Email; + config.HostName = user.DisplayName; + config.TenantName = _tenantname; + config.IsNewTenant = true; + } + else + { + AddModuleMessage("You Must Specify A Server And Database", MessageType.Error); } } + else + { + AddModuleMessage("Invalid Host Password", MessageType.Error); + } + } + else + { + AddModuleMessage("Tenant Name Is Missing Or Already Exists", MessageType.Error); } - - await Log(aliases[0], LogLevel.Information, string.Empty, null, "Site Created {Site}", site); - - var uri = new Uri(NavigationManager.Uri); - NavigationManager.NavigateTo(uri.Scheme + "://" + aliases[0].Name, true); } else { - await logger.LogError("Invalid Password Entered For Host {Username}", _username); - AddModuleMessage("Invalid Host Password", MessageType.Error); + var tenant = _tenants.FirstOrDefault(item => item.TenantId == int.Parse(_tenantid)); + if (tenant != null) + { + config.TenantName = tenant.Name; + config.ConnectionString= tenant.DBConnectionString; + config.IsNewTenant = false; + } + } + + if (!string.IsNullOrEmpty(config.TenantName)) + { + config.SiteName = _name; + config.Aliases = _urls.Replace("\n", ","); + config.DefaultTheme = _themetype; + config.DefaultLayout = _layouttype; + config.DefaultContainer = _containertype; + config.SiteTemplate = _sitetemplatetype; + + ShowProgressIndicator(); + + var installation = await InstallationService.Install(config); + if (installation.Success) + { + var aliasname = config.Aliases.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)[0]; + var uri = new Uri(NavigationManager.Uri); + NavigationManager.NavigateTo(uri.Scheme + "://" + aliasname, true); + } + else + { + await logger.LogError("Error Creating Site {Error}", installation.Message); + AddModuleMessage(installation.Message, MessageType.Error); + } } } else { - AddModuleMessage("An Alias Specified Has Already Been Used For Another Site", MessageType.Warning); + AddModuleMessage(string.Join(", ", duplicates.ToArray()) + " Already Used For Another Site", MessageType.Warning); } } else diff --git a/Oqtane.Client/Modules/Admin/Tenants/Add.razor b/Oqtane.Client/Modules/Admin/Tenants/Add.razor deleted file mode 100644 index 3fedf8fb..00000000 --- a/Oqtane.Client/Modules/Admin/Tenants/Add.razor +++ /dev/null @@ -1,156 +0,0 @@ -@namespace Oqtane.Modules.Admin.Tenants -@inherits ModuleBase -@inject NavigationManager NavigationManager -@inject ITenantService TenantService -@inject IInstallationService InstallationService - -
- - - -
- + @@ -101,14 +88,99 @@ else
+ + + +
+
+
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
- +
- +
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - -
- - - -
- - - -
- - - -
- - - -
- - - -
- -Cancel - -@code { - private string name = string.Empty; - private string type = "LocalDB"; - private string server = "(LocalDb)\\MSSQLLocalDB"; - private string database = "Oqtane-" + DateTime.UtcNow.ToString("yyyyMMddHHmm"); - private string username = string.Empty; - private string password = string.Empty; - private string schema = string.Empty; - private string integratedsecurity = "display: none;"; - - public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; - - private void SetIntegratedSecurity(ChangeEventArgs e) - { - if (Convert.ToBoolean((string)e.Value)) - { - integratedsecurity = "display: none;"; - } - else - { - integratedsecurity = string.Empty; - } - } - - private async Task SaveTenant() - { - if (!string.IsNullOrEmpty(name)) - { - ShowProgressIndicator(); - - var connectionString = string.Empty; - if (type == "LocalDB") - { - connectionString = "Data Source=" + server + ";AttachDbFilename=|DataDirectory|\\" + database + ".mdf;Initial Catalog=" + database + ";Integrated Security=SSPI;"; - } - else - { - connectionString = "Data Source=" + server + ";Initial Catalog=" + database + ";"; - - if (integratedsecurity == "display: none;") - { - connectionString += "Integrated Security=SSPI;"; - } - else - { - connectionString += "User ID=" + username + ";Password=" + password; - - } - } - - var config = new InstallConfig - { - IsMaster = false, - ConnectionString = connectionString, - }; - - var installation = await InstallationService.Install(config); - if (installation.Success) - { - //TODO : Move to Database Manager - var tenant = new Tenant - { - Name = name, - DBConnectionString = connectionString, - IsInitialized = false - }; - await TenantService.AddTenantAsync(tenant); - await logger.LogInformation("Tenant Created {Tenant}", tenant); - - NavigationManager.NavigateTo(NavigateUrl()); - } - else - { - await logger.LogError("Error Creating Tenant {Error}", installation.Message); - AddModuleMessage(installation.Message, MessageType.Error); - } - } - else - { - AddModuleMessage("You Must Provide A Name For The Tenant", MessageType.Warning); - } - } -} diff --git a/Oqtane.Client/Modules/Admin/Tenants/Edit.razor b/Oqtane.Client/Modules/Admin/Tenants/Edit.razor index f8a06db1..94ee91d6 100644 --- a/Oqtane.Client/Modules/Admin/Tenants/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Tenants/Edit.razor @@ -6,7 +6,7 @@ diff --git a/Oqtane.Client/Modules/Admin/Tenants/Index.razor b/Oqtane.Client/Modules/Admin/Tenants/Index.razor index 018c1f68..ead272dc 100644 --- a/Oqtane.Client/Modules/Admin/Tenants/Index.razor +++ b/Oqtane.Client/Modules/Admin/Tenants/Index.razor @@ -1,6 +1,7 @@ @namespace Oqtane.Modules.Admin.Tenants @inherits ModuleBase @inject ITenantService TenantService +@inject IAliasService AliasService @if (tenants == null) { @@ -8,8 +9,6 @@ } else { - -
@@ -39,9 +38,25 @@ else { try { - await TenantService.DeleteTenantAsync(Tenant.TenantId); - await logger.LogInformation("Tenant Deleted {Tenant}", Tenant); - StateHasChanged(); + string message = string.Empty; + var aliases = await AliasService.GetAliasesAsync(); + foreach (var alias in aliases) + { + if (alias.TenantId == Tenant.TenantId) + { + message += ", " + alias.Name; + } + } + if (string.IsNullOrEmpty(message)) + { + await TenantService.DeleteTenantAsync(Tenant.TenantId); + await logger.LogInformation("Tenant Deleted {Tenant}", Tenant); + StateHasChanged(); + } + else + { + AddModuleMessage("Tenant Cannot Be Deleted Until The Following Sites Are Deleted: " + message.Substring(2), MessageType.Warning); + } } catch (Exception ex) { diff --git a/Oqtane.Client/UI/Installer.razor b/Oqtane.Client/UI/Installer.razor index c20459b3..c3afd00d 100644 --- a/Oqtane.Client/UI/Installer.razor +++ b/Oqtane.Client/UI/Installer.razor @@ -7,116 +7,116 @@
- +
-
+
-

Database Configuration


+

Database Configuration


- + @if (name == Constants.MasterTenant) @@ -21,10 +21,10 @@
- + - +
 
- - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + +
- - - -
- - - -
- - - -
- - - -
- - - -
- - - -
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
-

Application Administrator


+

Application Administrator


- - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + +
- - - -
- - - -
- - - -
- - - -
+ + + +
+ + + +
+ + + +
+ + + +
-
+
-

+

@((MarkupString) _message)
@@ -146,7 +146,7 @@ private async Task Install() { - if (_hostUsername != "" && _hostPassword.Length >= 6 && _hostPassword == _confirmPassword && _hostEmail != "") + if (_serverName != "" && _databaseName != "" && _hostUsername != "" && _hostPassword.Length >= 6 && _hostPassword == _confirmPassword && _hostEmail != "") { _loadingDisplay = ""; StateHasChanged(); @@ -169,19 +169,24 @@ } } + Uri uri = new Uri(NavigationManager.Uri); + var config = new InstallConfig { ConnectionString = connectionstring, - HostUser = _hostUsername, + Aliases = uri.Authority, HostEmail = _hostEmail, - Password = _hostPassword, - IsMaster = true, + HostPassword = _hostPassword, + HostName = Constants.HostUser, + TenantName = Constants.MasterTenant, + IsNewTenant = true, + SiteName = Constants.DefaultSite }; var installation = await InstallationService.Install(config); if (installation.Success) { - NavigationManager.NavigateTo("", true); + NavigationManager.NavigateTo(uri.Scheme + "://" + uri.Authority, true); } else { diff --git a/Oqtane.Client/UI/Interop.cs b/Oqtane.Client/UI/Interop.cs index b57d1444..73aaf029 100644 --- a/Oqtane.Client/UI/Interop.cs +++ b/Oqtane.Client/UI/Interop.cs @@ -73,13 +73,13 @@ namespace Oqtane.UI } } - public Task IncludeLink(string id, string rel, string url, string type) + public Task IncludeLink(string id, string rel, string url, string type, string integrity, string crossorigin) { try { _jsRuntime.InvokeAsync( "interop.includeLink", - id, rel, url, type); + id, rel, url, type, integrity, crossorigin); return Task.CompletedTask; } catch @@ -88,13 +88,13 @@ namespace Oqtane.UI } } - public Task IncludeScript(string id, string src, string content, string location) + public Task IncludeScript(string id, string src, string content, string location, string integrity, string crossorigin) { try { _jsRuntime.InvokeAsync( "interop.includeScript", - id, src, content, location); + id, src, content, location, integrity, crossorigin); return Task.CompletedTask; } catch diff --git a/Oqtane.Client/UI/ThemeBuilder.razor b/Oqtane.Client/UI/ThemeBuilder.razor index 4f5d76f2..1649ac89 100644 --- a/Oqtane.Client/UI/ThemeBuilder.razor +++ b/Oqtane.Client/UI/ThemeBuilder.razor @@ -22,7 +22,7 @@ } if (PageState.Site.FaviconFileId != null) { - await interop.IncludeLink("fav-icon", "shortcut icon", Utilities.ContentUrl(PageState.Alias.Path, PageState.Site.FaviconFileId.Value), "image/x-icon"); + await interop.IncludeLink("fav-icon", "shortcut icon", Utilities.ContentUrl(PageState.Alias.Path, PageState.Site.FaviconFileId.Value), "image/x-icon", "", ""); } if (PageState.Site.PwaIsEnabled) { @@ -74,7 +74,7 @@ "document.getElementById('pwa-manifest').setAttribute('href', url); " + "} " + ", 1000);"; - await interop.IncludeScript("pwa-manifestscript", "", manifest, "body"); + await interop.IncludeScript("pwa-manifestscript", "", manifest, "body", "", ""); // service worker must be in root of site string serviceworker = "if ('serviceWorker' in navigator) { " + @@ -84,6 +84,6 @@ "console.log('ServiceWorker Registration Failed ', err); " + "}); " + "}"; - await interop.IncludeScript("pwa-serviceworker", "", serviceworker, "body"); + await interop.IncludeScript("pwa-serviceworker", "", serviceworker, "body", "", ""); } } diff --git a/Oqtane.Server/Controllers/InstallationController.cs b/Oqtane.Server/Controllers/InstallationController.cs index 713d065a..b930db48 100644 --- a/Oqtane.Server/Controllers/InstallationController.cs +++ b/Oqtane.Server/Controllers/InstallationController.cs @@ -5,8 +5,6 @@ using Oqtane.Models; using Oqtane.Shared; using Oqtane.Infrastructure; -// ReSharper disable StringIndexOfIsCultureSpecific.1 - namespace Oqtane.Controllers { [Route("{site}/api/[controller]")] @@ -14,9 +12,9 @@ namespace Oqtane.Controllers { private readonly IConfigurationRoot _config; private readonly IInstallationManager _installationManager; - private readonly DatabaseManager _databaseManager; + private readonly IDatabaseManager _databaseManager; - public InstallationController(IConfigurationRoot config, IInstallationManager installationManager, DatabaseManager databaseManager) + public InstallationController(IConfigurationRoot config, IInstallationManager installationManager, IDatabaseManager databaseManager) { _config = config; _installationManager = installationManager; @@ -27,33 +25,17 @@ namespace Oqtane.Controllers [HttpPost] public Installation Post([FromBody] InstallConfig config) { - //TODO Security ???? var installation = new Installation {Success = false, Message = ""}; - if (ModelState.IsValid && (!_databaseManager.IsInstalled || !config.IsMaster)) + if (ModelState.IsValid && (User.IsInRole(Constants.HostRole) || string.IsNullOrEmpty(_config.GetConnectionString(SettingKeys.ConnectionStringKey)))) { - bool master = config.IsMaster; - - config.Alias = config.Alias ?? HttpContext.Request.Host.Value; - var result = DatabaseManager.InstallDatabase(config); - - if (result.Success) - { - if (master) - { - _config.Reload(); - } - - _databaseManager.BuildDefaultSite(config.Password, config.HostEmail); - installation.Success = true; - return installation; - } - - installation.Message = result.Message; - return installation; + installation = _databaseManager.Install(config); + } + else + { + installation.Message = "Installation Not Authorized"; } - installation.Message = "Application Is Already Installed"; return installation; } @@ -61,12 +43,8 @@ namespace Oqtane.Controllers [HttpGet("installed")] public Installation IsInstalled() { - var installation = new Installation {Success = false, Message = ""}; - - installation.Success = _databaseManager.IsInstalled; - installation.Message = _databaseManager.Message; - - return installation; + bool isInstalled = _databaseManager.IsInstalled(); + return new Installation {Success = isInstalled, Message = string.Empty}; } [HttpGet("upgrade")] diff --git a/Oqtane.Server/Controllers/ModuleDefinitionController.cs b/Oqtane.Server/Controllers/ModuleDefinitionController.cs index 4ba5d613..673e42b3 100644 --- a/Oqtane.Server/Controllers/ModuleDefinitionController.cs +++ b/Oqtane.Server/Controllers/ModuleDefinitionController.cs @@ -22,16 +22,18 @@ namespace Oqtane.Controllers { private readonly IModuleDefinitionRepository _moduleDefinitions; private readonly IModuleRepository _modules; + private readonly ITenantRepository _tenants; private readonly IUserPermissions _userPermissions; private readonly IInstallationManager _installationManager; private readonly IWebHostEnvironment _environment; private readonly IServiceProvider _serviceProvider; private readonly ILogManager _logger; - public ModuleDefinitionController(IModuleDefinitionRepository moduleDefinitions, IModuleRepository modules, IUserPermissions userPermissions, IInstallationManager installationManager, IWebHostEnvironment environment, IServiceProvider serviceProvider, ILogManager logger) + public ModuleDefinitionController(IModuleDefinitionRepository moduleDefinitions, IModuleRepository modules,ITenantRepository tenants, IUserPermissions userPermissions, IInstallationManager installationManager, IWebHostEnvironment environment, IServiceProvider serviceProvider, ILogManager logger) { _moduleDefinitions = moduleDefinitions; _modules = modules; + _tenants = tenants; _userPermissions = userPermissions; _installationManager = installationManager; _environment = environment; @@ -104,8 +106,18 @@ namespace Oqtane.Controllers Type moduletype = Type.GetType(moduledefinition.ServerManagerType); if (moduletype != null && moduletype.GetInterface("IInstallable") != null) { - var moduleobject = ActivatorUtilities.CreateInstance(_serviceProvider, moduletype); - ((IInstallable)moduleobject).Uninstall(); + foreach (Tenant tenant in _tenants.GetTenants()) + { + try + { + var moduleobject = ActivatorUtilities.CreateInstance(_serviceProvider, moduletype); + ((IInstallable)moduleobject).Uninstall(tenant); + } + catch + { + // an error occurred executing the uninstall + } + } } } @@ -190,6 +202,11 @@ namespace Oqtane.Controllers module.ModuleDefinitionName = moduleDefinition.ModuleDefinitionName; _modules.UpdateModule(module); + if (moduleDefinition.Template == "internal") + { + // need logic to add embedded scripts to Oqtane.Server.csproj - also you need to remove them on uninstall + } + _installationManager.RestartApplication(); } } diff --git a/Oqtane.Server/Infrastructure/DatabaseManager.cs b/Oqtane.Server/Infrastructure/DatabaseManager.cs index a896a337..7fd44c66 100644 --- a/Oqtane.Server/Infrastructure/DatabaseManager.cs +++ b/Oqtane.Server/Infrastructure/DatabaseManager.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; using System.Data; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; using DbUp; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; @@ -15,193 +15,172 @@ using Oqtane.Extensions; using Oqtane.Models; using Oqtane.Repository; using Oqtane.Shared; +using Oqtane.Enums; using File = System.IO.File; namespace Oqtane.Infrastructure { - public class DatabaseManager + public class DatabaseManager : IDatabaseManager { private readonly IConfigurationRoot _config; private readonly IServiceScopeFactory _serviceScopeFactory; - private bool _isInstalled; + private readonly IMemoryCache _cache; - public DatabaseManager(IConfigurationRoot config, IServiceScopeFactory serviceScopeFactory) + public DatabaseManager(IConfigurationRoot config, IServiceScopeFactory serviceScopeFactory, IMemoryCache cache) { _config = config; _serviceScopeFactory = serviceScopeFactory; + _cache = cache; } - public string Message { get; set; } - - public void StartupMigration() + public bool IsInstalled() { - var defaultConnectionString = _config.GetConnectionString(SettingKeys.ConnectionStringKey); - var defaultAlias = GetInstallationConfig(SettingKeys.DefaultAliasKey, string.Empty); - var dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString(); - - //create data directory if does not exists - if (!Directory.Exists(dataDirectory)) Directory.CreateDirectory(dataDirectory); - - // if no values specified, fallback to IDE installer - if (string.IsNullOrEmpty(defaultConnectionString)) - { - IsInstalled = false; - return; - } - - var freshInstall = !IsMasterInstalled(defaultConnectionString); - var password = GetInstallationConfig(SettingKeys.HostPasswordKey, String.Empty); - var email = GetInstallationConfig(SettingKeys.HostEmailKey, String.Empty); - if (freshInstall && (string.IsNullOrEmpty(password) || string.IsNullOrEmpty(email) || string.IsNullOrEmpty(defaultAlias))) - { - IsInstalled = false; - Message = "Incomplete startup install configuration"; - return; - } - - var result = MasterMigration(defaultConnectionString, defaultAlias, null, true); - IsInstalled = result.Success; - - if (result.Success) - { - WriteVersionInfo(defaultConnectionString); - TenantMigration(defaultConnectionString, dataDirectory); - } - - if (_isInstalled && !IsDefaultSiteInstalled(defaultConnectionString)) - { - BuildDefaultSite(password,email); - } - } - - public bool IsInstalled - { - get - { - if (!_isInstalled) _isInstalled = CheckInstallState(); - - return _isInstalled; - } - set => _isInstalled = value; - } - - private bool CheckInstallState() - { - var defaultConnectionString = _config.GetConnectionString(SettingKeys.ConnectionStringKey); + var defaultConnectionString = NormalizeConnectionString(_config.GetConnectionString(SettingKeys.ConnectionStringKey)); var result = !string.IsNullOrEmpty(defaultConnectionString); if (result) { using (var scope = _serviceScopeFactory.CreateScope()) { - var dbContext = scope.ServiceProvider.GetRequiredService(); - result = dbContext.Database.CanConnect(); + var db = scope.ServiceProvider.GetRequiredService(); + result = db.Database.CanConnect(); + if (result) + { + try + { + result = db.Tenant.Any(); + } + catch + { + result = false; + } + } } - if (result) - { - //I think this is obsolete now and not accurate, maybe check presence of some table, Version ??? - var dbUpgradeConfig = DeployChanges - .To - .SqlDatabase(defaultConnectionString) - .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), s => s.Contains("Master")); + } + return result; + } - result = !dbUpgradeConfig.Build().IsUpgradeRequired(); - if (!result) Message = "Master Installation Scripts Have Not Been Executed"; + public Installation Install() + { + return Install(null); + } + + public Installation Install(InstallConfig install) + { + var result = new Installation { Success = false, Message = string.Empty }; + + // get configuration + if (install == null) + { + // startup or silent installation + install = new InstallConfig { ConnectionString = _config.GetConnectionString(SettingKeys.ConnectionStringKey), TenantName = Constants.MasterTenant, IsNewTenant = false }; + + if (!IsInstalled()) + { + install.Aliases = GetInstallationConfig(SettingKeys.DefaultAliasKey, string.Empty); + install.HostPassword = GetInstallationConfig(SettingKeys.HostPasswordKey, string.Empty); + install.HostEmail = GetInstallationConfig(SettingKeys.HostEmailKey, string.Empty); + + if (!string.IsNullOrEmpty(install.ConnectionString) && !string.IsNullOrEmpty(install.Aliases) && !string.IsNullOrEmpty(install.HostPassword) && !string.IsNullOrEmpty(install.HostEmail)) + { + // silent install + install.HostName = Constants.HostUser; + install.SiteTemplate = GetInstallationConfig(SettingKeys.SiteTemplateKey, Constants.DefaultSiteTemplate); + install.DefaultTheme = GetInstallationConfig(SettingKeys.DefaultThemeKey, Constants.DefaultTheme); + install.DefaultLayout = GetInstallationConfig(SettingKeys.DefaultLayoutKey, Constants.DefaultLayout); + install.DefaultContainer = GetInstallationConfig(SettingKeys.DefaultContainerKey, Constants.DefaultContainer); + install.SiteName = Constants.DefaultSite; + install.IsNewTenant = true; + } + else + { + // silent installation is missing required information + install.ConnectionString = ""; + } + } + } + else + { + // install wizard or add new site + if (!string.IsNullOrEmpty(install.Aliases)) + { + if (string.IsNullOrEmpty(install.SiteTemplate)) + { + install.SiteTemplate = GetInstallationConfig(SettingKeys.SiteTemplateKey, Constants.DefaultSiteTemplate); + } + if (string.IsNullOrEmpty(install.DefaultTheme)) + { + install.DefaultTheme = GetInstallationConfig(SettingKeys.DefaultThemeKey, Constants.DefaultTheme); + if (string.IsNullOrEmpty(install.DefaultLayout)) + { + install.DefaultLayout = GetInstallationConfig(SettingKeys.DefaultLayoutKey, Constants.DefaultLayout); + } + } + if (string.IsNullOrEmpty(install.DefaultContainer)) + { + install.DefaultContainer = GetInstallationConfig(SettingKeys.DefaultContainerKey, Constants.DefaultContainer); + } } else { - Message = "Database is not available"; + result.Message = "Invalid Installation Configuration"; + install.ConnectionString = ""; } } - else + + // proceed with installation/migration + if (!string.IsNullOrEmpty(install.ConnectionString)) { - Message = "Connection string is empty"; + result = CreateDatabase(install); + if (result.Success) + { + result = MigrateMaster(install); + if (result.Success) + { + result = CreateTenant(install); + if (result.Success) + { + result = MigrateTenants(install); + if (result.Success) + { + result = MigrateModules(install); + if (result.Success) + { + result = CreateSite(install); + } + } + } + } + } } + return result; } - - public static string NormalizeConnectionString(string connectionString, string dataDirectory) + private Installation CreateDatabase(InstallConfig install) { - connectionString = connectionString - .Replace("|DataDirectory|", dataDirectory); - return connectionString; - } + var result = new Installation { Success = false, Message = string.Empty }; - public static Installation InstallDatabase([NotNull] InstallConfig installConfig) - { - var dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString(); - var result = new Installation {Success = false, Message = ""}; - - var alias = installConfig.Alias; - var connectionString = NormalizeConnectionString(installConfig.ConnectionString, dataDirectory); - - if (!string.IsNullOrEmpty(connectionString) && !string.IsNullOrEmpty(alias)) + if (install.IsNewTenant) { - result = MasterMigration(connectionString, alias, result, installConfig.IsMaster); - if (installConfig.IsMaster && result.Success) + try { - WriteVersionInfo(connectionString); - TenantMigration(connectionString, dataDirectory); - UpdateConnectionStringSetting(connectionString); + //create data directory if does not exist + var dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString(); + if (!Directory.Exists(dataDirectory)) Directory.CreateDirectory(dataDirectory); + + using (var dbc = new DbContext(new DbContextOptionsBuilder().UseSqlServer(NormalizeConnectionString(install.ConnectionString)).Options)) + { + // create empty database if it does not exist + dbc.Database.EnsureCreated(); + result.Success = true; + } } - return result; - } - - result = new Installation - { - Success = false, - Message = "Connection string is empty", - }; - return result; - } - - private static Installation MasterMigration(string connectionString, string alias, Installation result, bool master) - { - if (result == null) result = new Installation {Success = false, Message = string.Empty}; - - bool firstInstall; - try - { - // create empty database if does not exists - // dbup database creation does not work correctly on localdb databases - using (var dbc = new DbContext(new DbContextOptionsBuilder().UseSqlServer(connectionString).Options)) + catch (Exception ex) { - dbc.Database.EnsureCreated(); - //check for vanilla db - firstInstall = !TableExists(dbc, "SchemaVersions"); + result.Message = ex.Message; } } - catch (Exception e) - { - result.Message = e.Message; - Console.WriteLine(e); - return result; - } - // when alias is not specified on first install, fallback to ide - if (firstInstall && string.IsNullOrEmpty(alias)) return result; - - var dbUpgradeConfig = DeployChanges - .To - .SqlDatabase(connectionString) - .WithVariable("ConnectionString", connectionString) - .WithVariable("Alias", alias) - .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), s => s.Contains("Master.")); - - var dbUpgrade = dbUpgradeConfig.Build(); - if (!dbUpgrade.IsUpgradeRequired()) - { - result.Success = true; - result.Message = string.Empty; - return result; - } - - var upgradeResult = dbUpgrade.PerformUpgrade(); - if (!upgradeResult.Successful) - { - Console.WriteLine(upgradeResult.Error.Message); - result.Message = upgradeResult.Error.Message; - } else { result.Success = true; @@ -210,74 +189,326 @@ namespace Oqtane.Infrastructure return result; } - private static void ModuleMigration(Assembly assembly, string connectionString) + private Installation MigrateMaster(InstallConfig install) { - Console.WriteLine($"Migrating assembly {assembly.FullName}"); - var dbUpgradeConfig = DeployChanges.To.SqlDatabase(connectionString) - .WithScriptsEmbeddedInAssembly(assembly, s => !s.ToLower().Contains("uninstall.sql")); // scripts must be included as Embedded Resources - var dbUpgrade = dbUpgradeConfig.Build(); - if (dbUpgrade.IsUpgradeRequired()) + var result = new Installation { Success = false, Message = string.Empty }; + + if (install.TenantName == Constants.MasterTenant) { - var result = dbUpgrade.PerformUpgrade(); - if (!result.Successful) + var upgradeConfig = DeployChanges + .To + .SqlDatabase(NormalizeConnectionString(install.ConnectionString)) + .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), s => s.Contains("Master.")); + + var upgrade = upgradeConfig.Build(); + if (upgrade.IsUpgradeRequired()) { - // TODO: log result.Error.Message - problem is logger is not available here + var upgradeResult = upgrade.PerformUpgrade(); + result.Success = upgradeResult.Successful; + if (!result.Success) + { + result.Message = upgradeResult.Error.Message; + } + } + else + { + result.Success = true; + } + + if (result.Success) + { + CreateApplicationVersion(install.ConnectionString); + UpdateConnectionString(install.ConnectionString); } } + else + { + result.Success = true; + } + + return result; } - private static void WriteVersionInfo(string connectionString) + private Installation CreateTenant(InstallConfig install) { - using (var db = new InstallationContext(connectionString)) + var result = new Installation { Success = false, Message = string.Empty }; + + if (!string.IsNullOrEmpty(install.TenantName) && !string.IsNullOrEmpty(install.Aliases)) { - var version = db.ApplicationVersion.ToList().LastOrDefault(); - if (version == null || version.Version != Constants.Version) + using (var db = new InstallationContext(NormalizeConnectionString(_config.GetConnectionString(SettingKeys.ConnectionStringKey)))) { - version = new ApplicationVersion {Version = Constants.Version, CreatedOn = DateTime.UtcNow}; + Tenant tenant; + if (install.IsNewTenant) + { + tenant = new Tenant { Name = install.TenantName, DBConnectionString = DenormalizeConnectionString(install.ConnectionString), CreatedBy = "", CreatedOn = DateTime.UtcNow, ModifiedBy = "", ModifiedOn = DateTime.UtcNow }; + db.Tenant.Add(tenant); + db.SaveChanges(); + _cache.Remove("tenants"); + + if (install.TenantName == Constants.MasterTenant) + { + var job = new Job { Name = "Notification Job", JobType = "Oqtane.Infrastructure.NotificationJob, Oqtane.Server", Frequency = "m", Interval = 1, StartDate = null, EndDate = null, IsEnabled = false, IsStarted = false, IsExecuting = false, NextExecution = null, RetentionHistory = 10, CreatedBy = "", CreatedOn = DateTime.UtcNow, ModifiedBy = "", ModifiedOn = DateTime.UtcNow }; + db.Job.Add(job); + db.SaveChanges(); + _cache.Remove("jobs"); + } + } + else + { + tenant = db.Tenant.FirstOrDefault(item => item.Name == install.TenantName); + } + + foreach (string aliasname in install.Aliases.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) + { + var alias = new Alias { Name = aliasname, TenantId = tenant.TenantId, SiteId = -1, CreatedBy = "", CreatedOn = DateTime.UtcNow, ModifiedBy = "", ModifiedOn = DateTime.UtcNow }; + db.Alias.Add(alias); + db.SaveChanges(); + } + _cache.Remove("aliases"); + } + } + + result.Success = true; + + return result; + } + + private Installation MigrateTenants(InstallConfig install) + { + var result = new Installation { Success = false, Message = string.Empty }; + + using (var db = new InstallationContext(NormalizeConnectionString(_config.GetConnectionString(SettingKeys.ConnectionStringKey)))) + { + foreach (var tenant in db.Tenant.ToList()) + { + var upgradeConfig = DeployChanges.To.SqlDatabase(NormalizeConnectionString(tenant.DBConnectionString)) + .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), s => s.Contains("Tenant")); + + var upgrade = upgradeConfig.Build(); + if (upgrade.IsUpgradeRequired()) + { + var upgradeResult = upgrade.PerformUpgrade(); + result.Success = upgradeResult.Successful; + if (!result.Success) + { + result.Message = upgradeResult.Error.Message; + } + } + } + } + + if (string.IsNullOrEmpty(result.Message)) + { + result.Success = true; + } + + return result; + } + + private Installation MigrateModules(InstallConfig install) + { + var result = new Installation { Success = false, Message = string.Empty }; + + using (var scope = _serviceScopeFactory.CreateScope()) + { + var moduledefinitions = scope.ServiceProvider.GetRequiredService(); + foreach (var moduledefinition in moduledefinitions.GetModuleDefinitions()) + { + if (!string.IsNullOrEmpty(moduledefinition.ServerManagerType) && !string.IsNullOrEmpty(moduledefinition.ReleaseVersions)) + { + string[] versions = moduledefinition.ReleaseVersions.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + using (var db = new InstallationContext(NormalizeConnectionString(_config.GetConnectionString(SettingKeys.ConnectionStringKey)))) + { + foreach (var tenant in db.Tenant.ToList()) + { + int index = Array.FindIndex(versions, item => item == moduledefinition.Version); + if (tenant.Name == install.TenantName && install.TenantName != Constants.MasterTenant) + { + index = -1; + } + if (index != (versions.Length - 1)) + { + if (index == -1) index = 0; + for (int i = index; i < versions.Length; i++) + { + Type moduletype = Type.GetType(moduledefinition.ServerManagerType); + if (moduletype != null && moduletype.GetInterface("IInstallable") != null) + { + try + { + var moduleobject = ActivatorUtilities.CreateInstance(scope.ServiceProvider, moduletype); + ((IInstallable)moduleobject).Install(tenant, versions[i]); + } + catch (Exception ex) + { + result.Message = "An Error Occurred Installing " + moduledefinition.Name + " - " + ex.Message.ToString(); + } + } + } + } + } + if (moduledefinition.Version != versions[versions.Length - 1]) + { + moduledefinition.Version = versions[versions.Length - 1]; + db.Entry(moduledefinition).State = EntityState.Modified; + db.SaveChanges(); + } + } + } + } + } + + if (string.IsNullOrEmpty(result.Message)) + { + result.Success = true; + } + + return result; + } + + private Installation CreateSite(InstallConfig install) + { + var result = new Installation { Success = false, Message = string.Empty }; + + if (!string.IsNullOrEmpty(install.TenantName) && !string.IsNullOrEmpty(install.Aliases) && !string.IsNullOrEmpty(install.SiteName)) + { + using (var scope = _serviceScopeFactory.CreateScope()) + { + // use the SiteState to set the Alias explicitly so the tenant can be resolved + var aliases = scope.ServiceProvider.GetRequiredService(); + string firstalias = install.Aliases.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)[0]; + var alias = aliases.GetAliases().FirstOrDefault(item => item.Name == firstalias); + var siteState = scope.ServiceProvider.GetRequiredService(); + siteState.Alias = alias; + + var sites = scope.ServiceProvider.GetRequiredService(); + var site = sites.GetSites().FirstOrDefault(item => item.Name == install.SiteName); + if (site == null) + { + var tenants = scope.ServiceProvider.GetRequiredService(); + var users = scope.ServiceProvider.GetRequiredService(); + var roles = scope.ServiceProvider.GetRequiredService(); + var userroles = scope.ServiceProvider.GetRequiredService(); + var folders = scope.ServiceProvider.GetRequiredService(); + var log = scope.ServiceProvider.GetRequiredService(); + var identityUserManager = scope.ServiceProvider.GetRequiredService>(); + + var tenant = tenants.GetTenants().FirstOrDefault(item => item.Name == install.TenantName); + + site = new Site + { + TenantId = tenant.TenantId, + Name = install.SiteName, + LogoFileId = null, + DefaultThemeType = install.DefaultTheme, + DefaultLayoutType = install.DefaultLayout, + DefaultContainerType = install.DefaultContainer, + SiteTemplateType = install.SiteTemplate + }; + site = sites.AddSite(site); + + IdentityUser identityUser = identityUserManager.FindByNameAsync(Constants.HostUser).GetAwaiter().GetResult(); + if (identityUser == null) + { + identityUser = new IdentityUser { UserName = Constants.HostUser, Email = install.HostEmail, EmailConfirmed = true }; + var create = identityUserManager.CreateAsync(identityUser, install.HostPassword).GetAwaiter().GetResult(); + if (create.Succeeded) + { + var user = new User + { + SiteId = site.SiteId, + Username = Constants.HostUser, + Password = install.HostPassword, + Email = install.HostEmail, + DisplayName = install.HostName, + LastIPAddress = "", + LastLoginOn = null + }; + + user = users.AddUser(user); + var hostRoleId = roles.GetRoles(user.SiteId, true).FirstOrDefault(item => item.Name == Constants.HostRole)?.RoleId ?? 0; + var userRole = new UserRole { UserId = user.UserId, RoleId = hostRoleId, EffectiveDate = null, ExpiryDate = null }; + userroles.AddUserRole(userRole); + + // add user folder + var folder = folders.GetFolder(user.SiteId, Utilities.PathCombine("Users", "\\")); + if (folder != null) + { + folders.AddFolder(new Folder + { + SiteId = folder.SiteId, + ParentId = folder.FolderId, + Name = "My Folder", + Path = Utilities.PathCombine(folder.Path, user.UserId.ToString(), "\\"), + Order = 1, + IsSystem = true, + Permissions = new List + { + new Permission(PermissionNames.Browse, user.UserId, true), + new Permission(PermissionNames.View, Constants.AllUsersRole, true), + new Permission(PermissionNames.Edit, user.UserId, true), + }.EncodePermissions(), + }); + } + } + } + + foreach (string aliasname in install.Aliases.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) + { + alias = aliases.GetAliases().FirstOrDefault(item => item.Name == aliasname); + alias.SiteId = site.SiteId; + aliases.UpdateAlias(alias); + } + + log.Log(site.SiteId, LogLevel.Trace, this, LogFunction.Create, "Site Created {Site}", site); + } + } + } + + result.Success = true; + + return result; + } + + private void CreateApplicationVersion(string connectionString) + { + using (var db = new InstallationContext(NormalizeConnectionString(connectionString))) + { + var version = db.ApplicationVersion.FirstOrDefault(item => item.Version == Constants.Version); + if (version == null) + { + version = new ApplicationVersion { Version = Constants.Version, CreatedOn = DateTime.UtcNow }; db.ApplicationVersion.Add(version); db.SaveChanges(); } } } - - private static void TenantMigration(string connectionString, string dataDirectory) + + private string NormalizeConnectionString(string connectionString) { - Console.WriteLine("Tenant migration"); - var assemblies = AppDomain.CurrentDomain.GetAssemblies() - .Where(item => item.FullName != null && item.FullName.ToLower().Contains(".module.")).ToArray(); + var dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString(); + connectionString = connectionString.Replace("|DataDirectory|", dataDirectory); + return connectionString; + } - // get tenants - using (var db = new InstallationContext(connectionString)) + private string DenormalizeConnectionString(string connectionString) + { + var dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString(); + connectionString = connectionString.Replace(dataDirectory, "|DataDirectory|"); + return connectionString; + } + + public void UpdateConnectionString(string connectionString) + { + connectionString = DenormalizeConnectionString(connectionString); + if (_config.GetConnectionString(SettingKeys.ConnectionStringKey) != connectionString) { - foreach (var tenant in db.Tenant.ToList()) - { - Console.WriteLine($"Migrating tenant {tenant.Name}"); - connectionString = NormalizeConnectionString(tenant.DBConnectionString, dataDirectory); - // upgrade framework - var dbUpgradeConfig = DeployChanges.To.SqlDatabase(connectionString) - .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), s => s.Contains("Tenant")); - var dbUpgrade = dbUpgradeConfig.Build(); - if (dbUpgrade.IsUpgradeRequired()) - { - var result = dbUpgrade.PerformUpgrade(); - if (!result.Successful) - { - // TODO: log result.Error.Message - problem is logger is not available here - } - } - - // iterate through Oqtane module assemblies and execute any database scripts - foreach (var assembly in assemblies) ModuleMigration(assembly, connectionString); - } + AddOrUpdateAppSetting($"ConnectionStrings:{SettingKeys.ConnectionStringKey}", connectionString); + _config.Reload(); } } - public static void UpdateConnectionStringSetting(string connectionString) - { - AddOrUpdateAppSetting($"ConnectionStrings:{SettingKeys.ConnectionStringKey}", connectionString); - } - - public static void AddOrUpdateAppSetting(string sectionPathKey, T value) + public void AddOrUpdateAppSetting(string sectionPathKey, T value) { try { @@ -296,7 +527,7 @@ namespace Oqtane.Infrastructure } } - private static void SetValueRecursively(string sectionPathKey, dynamic jsonObj, T value) + private void SetValueRecursively(string sectionPathKey, dynamic jsonObj, T value) { // split the string at the first ':' character var remainingSections = sectionPathKey.Split(":", 2); @@ -315,51 +546,6 @@ namespace Oqtane.Infrastructure } } - public void BuildDefaultSite(string password, string email) - { - using (var scope = _serviceScopeFactory.CreateScope()) - { - //Gather required services - var siteRepository = scope.ServiceProvider.GetRequiredService(); - - // Build default site only if no site present - if (siteRepository.GetSites().Any()) return; - - var users = scope.ServiceProvider.GetRequiredService(); - var roles = scope.ServiceProvider.GetRequiredService(); - var userRoles = scope.ServiceProvider.GetRequiredService(); - var folders = scope.ServiceProvider.GetRequiredService(); - var identityUserManager = scope.ServiceProvider.GetRequiredService>(); - var tenants = scope.ServiceProvider.GetRequiredService(); - - var tenant = tenants.GetTenants().First(); - - var site = new Site - { - TenantId = tenant.TenantId, - Name = "Default Site", - LogoFileId = null, - DefaultThemeType = GetInstallationConfig(SettingKeys.DefaultThemeKey, Constants.DefaultTheme), - DefaultLayoutType = GetInstallationConfig(SettingKeys.DefaultLayoutKey, Constants.DefaultLayout), - DefaultContainerType = GetInstallationConfig(SettingKeys.DefaultContainerKey, Constants.DefaultContainer), - SiteTemplateType = GetInstallationConfig(SettingKeys.SiteTemplateKey, Constants.DefaultSiteTemplate), - }; - site = siteRepository.AddSite(site); - - var user = new User - { - SiteId = site.SiteId, - Username = Constants.HostUser, - Password = password, - Email = email, - DisplayName = Constants.HostUser - }; - CreateHostUser(folders, userRoles, roles, users, identityUserManager, user); - tenant.IsInitialized = true; - tenants.UpdateTenant(tenant); - } - } - private string GetInstallationConfig(string key, string defaultValue) { var value = _config.GetSection(SettingKeys.InstallationSection).GetValue(key, defaultValue); @@ -368,96 +554,5 @@ namespace Oqtane.Infrastructure return value; } - private static void CreateHostUser(IFolderRepository folderRepository, IUserRoleRepository userRoleRepository, IRoleRepository roleRepository, IUserRepository userRepository, UserManager identityUserManager, User user) - { - var identityUser = new IdentityUser {UserName = user.Username, Email = user.Email, EmailConfirmed = true}; - var result = identityUserManager.CreateAsync(identityUser, user.Password).GetAwaiter().GetResult(); - - if (result.Succeeded) - { - user.LastLoginOn = null; - user.LastIPAddress = ""; - var newUser = userRepository.AddUser(user); - - // assign to host role if this is the host user ( initial installation ) - if (user.Username == Constants.HostUser) - { - var hostRoleId = roleRepository.GetRoles(user.SiteId, true).FirstOrDefault(item => item.Name == Constants.HostRole)?.RoleId ?? 0; - var userRole = new UserRole {UserId = newUser.UserId, RoleId = hostRoleId, EffectiveDate = null, ExpiryDate = null}; - userRoleRepository.AddUserRole(userRole); - } - - // add folder for user - var folder = folderRepository.GetFolder(user.SiteId, Utilities.PathCombine("Users","\\")); - if (folder != null) - folderRepository.AddFolder(new Folder - { - SiteId = folder.SiteId, - ParentId = folder.FolderId, - Name = "My Folder", - Path = Utilities.PathCombine(folder.Path, newUser.UserId.ToString(),"\\"), - Order = 1, - IsSystem = true, - Permissions = new List - { - new Permission(PermissionNames.Browse, newUser.UserId, true), - new Permission(PermissionNames.View, Constants.AllUsersRole, true), - new Permission(PermissionNames.Edit, newUser.UserId, true), - }.EncodePermissions(), - }); - } - } - - private static bool IsDefaultSiteInstalled(string connectionString) - { - using (var db = new InstallationContext(connectionString)) - { - return db.Tenant.Any(t => t.IsInitialized); - } - } - - private static bool IsMasterInstalled(string connectionString) - { - using (var db = new InstallationContext(connectionString)) - { - - //check if DbUp was initialized - return TableExists(db, "SchemaVersions"); - } - } - - public static bool TableExists(DbContext context, string tableName) - { - return TableExists(context, "dbo", tableName); - } - - public static bool TableExists(DbContext context, string schema, string tableName) - { - if (!context.Database.CanConnect()) return false; - var connection = context.Database.GetDbConnection(); - - if (connection.State.Equals(ConnectionState.Closed)) - connection.Open(); - - using (var command = connection.CreateCommand()) - { - command.CommandText = @" - SELECT 1 FROM INFORMATION_SCHEMA.TABLES - WHERE TABLE_SCHEMA = @Schema - AND TABLE_NAME = @TableName"; - - var schemaParam = command.CreateParameter(); - schemaParam.ParameterName = "@Schema"; - schemaParam.Value = schema; - command.Parameters.Add(schemaParam); - - var tableNameParam = command.CreateParameter(); - tableNameParam.ParameterName = "@TableName"; - tableNameParam.Value = tableName; - command.Parameters.Add(tableNameParam); - - return command.ExecuteScalar() != null; - } - } } } diff --git a/Oqtane.Server/Infrastructure/Interfaces/IDatabaseManager.cs b/Oqtane.Server/Infrastructure/Interfaces/IDatabaseManager.cs new file mode 100644 index 00000000..ffb0ff9c --- /dev/null +++ b/Oqtane.Server/Infrastructure/Interfaces/IDatabaseManager.cs @@ -0,0 +1,12 @@ +using Oqtane.Models; +using Oqtane.Shared; + +namespace Oqtane.Infrastructure +{ + public interface IDatabaseManager + { + bool IsInstalled(); + Installation Install(); + Installation Install(InstallConfig install); + } +} diff --git a/Oqtane.Server/Infrastructure/Interfaces/IInstallable.cs b/Oqtane.Server/Infrastructure/Interfaces/IInstallable.cs index c71de4f6..baef7184 100644 --- a/Oqtane.Server/Infrastructure/Interfaces/IInstallable.cs +++ b/Oqtane.Server/Infrastructure/Interfaces/IInstallable.cs @@ -1,8 +1,10 @@ -namespace Oqtane.Infrastructure +using Oqtane.Models; + +namespace Oqtane.Infrastructure { public interface IInstallable { - bool Install(string version); - bool Uninstall(); + bool Install(Tenant tenant, string version); + bool Uninstall(Tenant tenant); } } diff --git a/Oqtane.Server/Infrastructure/LogManager.cs b/Oqtane.Server/Infrastructure/LogManager.cs index b46bc8dc..52a4736a 100644 --- a/Oqtane.Server/Infrastructure/LogManager.cs +++ b/Oqtane.Server/Infrastructure/LogManager.cs @@ -69,10 +69,14 @@ namespace Oqtane.Infrastructure { log.UserId = user.UserId; } - HttpRequest request = _accessor.HttpContext.Request; - if (request != null) + log.Url = ""; + if (_accessor.HttpContext != null) { - log.Url = $"{request.Scheme}://{request.Host}{request.Path}{request.QueryString}"; + HttpRequest request = _accessor.HttpContext.Request; + if (request != null) + { + log.Url = $"{request.Scheme}://{request.Host}{request.Path}{request.QueryString}"; + } } Type type = Type.GetType(@class.ToString()); diff --git a/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs b/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs index 4d38e250..7e068076 100644 --- a/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs +++ b/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs @@ -18,14 +18,14 @@ namespace Oqtane.Modules.HtmlText.Manager _sql = sql; } - public bool Install(string version) + public bool Install(Tenant tenant, string version) { - return _sql.ExecuteEmbeddedScript(GetType().Assembly, "HtmlText." + version + ".sql"); + return _sql.ExecuteScript(tenant, GetType().Assembly, "HtmlText." + version + ".sql"); } - public bool Uninstall() + public bool Uninstall(Tenant tenant) { - return _sql.ExecuteEmbeddedScript(GetType().Assembly, "HtmlText.Uninstall.sql"); + return _sql.ExecuteScript(tenant, GetType().Assembly, "HtmlText.Uninstall.sql"); } public string ExportModule(Module module) diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index 95471ca0..347e9fc0 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -17,11 +17,6 @@ Oqtane - - - - - diff --git a/Oqtane.Server/Program.cs b/Oqtane.Server/Program.cs index 08d1fa06..c4b5492f 100644 --- a/Oqtane.Server/Program.cs +++ b/Oqtane.Server/Program.cs @@ -12,11 +12,10 @@ namespace Oqtane.Server public static void Main(string[] args) { var host = BuildWebHost(args); - // execute any database migrations for the framework or extensions using (var serviceScope = host.Services.GetRequiredService().CreateScope()) { - var databaseManager = serviceScope.ServiceProvider.GetService(); - databaseManager.StartupMigration(); + var databaseManager = serviceScope.ServiceProvider.GetService(); + databaseManager.Install(); } host.Run(); } diff --git a/Oqtane.Server/Repository/Context/InstallationContext.cs b/Oqtane.Server/Repository/Context/InstallationContext.cs index 7b971a16..18694939 100644 --- a/Oqtane.Server/Repository/Context/InstallationContext.cs +++ b/Oqtane.Server/Repository/Context/InstallationContext.cs @@ -17,7 +17,11 @@ namespace Oqtane.Repository protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseSqlServer(_connectionString); - public virtual DbSet ApplicationVersion { get; set; } + public virtual DbSet Alias { get; set; } public virtual DbSet Tenant { get; set; } + public virtual DbSet ModuleDefinition { get; set; } + public virtual DbSet Job { get; set; } + + public virtual DbSet ApplicationVersion { get; set; } } } diff --git a/Oqtane.Server/Repository/Interfaces/IModuleDefinitionRepository.cs b/Oqtane.Server/Repository/Interfaces/IModuleDefinitionRepository.cs index 32c59549..dd516461 100644 --- a/Oqtane.Server/Repository/Interfaces/IModuleDefinitionRepository.cs +++ b/Oqtane.Server/Repository/Interfaces/IModuleDefinitionRepository.cs @@ -5,6 +5,7 @@ namespace Oqtane.Repository { public interface IModuleDefinitionRepository { + IEnumerable GetModuleDefinitions(); IEnumerable GetModuleDefinitions(int sideId); ModuleDefinition GetModuleDefinition(int moduleDefinitionId, int sideId); void UpdateModuleDefinition(ModuleDefinition moduleDefinition); diff --git a/Oqtane.Server/Repository/Interfaces/ISqlRepository.cs b/Oqtane.Server/Repository/Interfaces/ISqlRepository.cs index 037e9416..a1402e3b 100644 --- a/Oqtane.Server/Repository/Interfaces/ISqlRepository.cs +++ b/Oqtane.Server/Repository/Interfaces/ISqlRepository.cs @@ -6,8 +6,8 @@ namespace Oqtane.Repository { public interface ISqlRepository { - bool ExecuteEmbeddedScript(Assembly assembly, string script); void ExecuteScript(Tenant tenant, string script); + bool ExecuteScript(Tenant tenant, Assembly assembly, string filename); int ExecuteNonQuery(Tenant tenant, string query); SqlDataReader ExecuteReader(Tenant tenant, string query); } diff --git a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs index 6db44ac0..117c7ae7 100644 --- a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs +++ b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs @@ -25,6 +25,11 @@ namespace Oqtane.Repository _permissions = permissions; } + public IEnumerable GetModuleDefinitions() + { + return LoadModuleDefinitions(-1); // used only during startup + } + public IEnumerable GetModuleDefinitions(int siteId) { return LoadModuleDefinitions(siteId); @@ -72,8 +77,12 @@ namespace Oqtane.Repository } List moduleDefinitions = _moduleDefinitions; - // get module definition permissions for site - List permissions = _permissions.GetPermissions(siteId, EntityNames.ModuleDefinition).ToList(); + List permissions = new List(); + if (siteId != -1) + { + // get module definition permissions for site + permissions = _permissions.GetPermissions(siteId, EntityNames.ModuleDefinition).ToList(); + } // get module definitions in database List moduledefs = _db.ModuleDefinition.ToList(); @@ -88,7 +97,10 @@ namespace Oqtane.Repository moduledef = new ModuleDefinition { ModuleDefinitionName = moduledefinition.ModuleDefinitionName }; _db.ModuleDefinition.Add(moduledef); _db.SaveChanges(); - _permissions.UpdatePermissions(siteId, EntityNames.ModuleDefinition, moduledef.ModuleDefinitionId, moduledefinition.Permissions); + if (siteId != -1) + { + _permissions.UpdatePermissions(siteId, EntityNames.ModuleDefinition, moduledef.ModuleDefinitionId, moduledefinition.Permissions); + } } else { @@ -109,13 +121,16 @@ namespace Oqtane.Repository { moduledefinition.Version = moduledef.Version; } - if (permissions.Count == 0) + if (siteId != -1) { - _permissions.UpdatePermissions(siteId, EntityNames.ModuleDefinition, moduledef.ModuleDefinitionId, moduledefinition.Permissions); - } - else - { - moduledefinition.Permissions = permissions.Where(item => item.EntityId == moduledef.ModuleDefinitionId).EncodePermissions(); + if (permissions.Count == 0) + { + _permissions.UpdatePermissions(siteId, EntityNames.ModuleDefinition, moduledef.ModuleDefinitionId, moduledefinition.Permissions); + } + else + { + moduledefinition.Permissions = permissions.Where(item => item.EntityId == moduledef.ModuleDefinitionId).EncodePermissions(); + } } // remove module definition from list as it is already synced moduledefs.Remove(moduledef); @@ -131,7 +146,10 @@ namespace Oqtane.Repository // any remaining module definitions are orphans foreach (ModuleDefinition moduledefinition in moduledefs) { - _permissions.DeletePermissions(siteId, EntityNames.ModuleDefinition, moduledefinition.ModuleDefinitionId); + if (siteId != -1) + { + _permissions.DeletePermissions(siteId, EntityNames.ModuleDefinition, moduledefinition.ModuleDefinitionId); + } _db.ModuleDefinition.Remove(moduledefinition); // delete _db.SaveChanges(); } diff --git a/Oqtane.Server/Repository/ModuleRepository.cs b/Oqtane.Server/Repository/ModuleRepository.cs index 7f38a1bf..a66517fc 100644 --- a/Oqtane.Server/Repository/ModuleRepository.cs +++ b/Oqtane.Server/Repository/ModuleRepository.cs @@ -87,8 +87,15 @@ namespace Oqtane.Repository Type moduletype = Type.GetType(moduledefinition.ServerManagerType); if (moduletype != null && moduletype.GetInterface("IPortable") != null) { - var moduleobject = ActivatorUtilities.CreateInstance(_serviceProvider, moduletype); - modulecontent.Content = ((IPortable) moduleobject).ExportModule(module); + try + { + var moduleobject = ActivatorUtilities.CreateInstance(_serviceProvider, moduletype); + modulecontent.Content = ((IPortable)moduleobject).ExportModule(module); + } + catch + { + // error in IPortable implementation + } } } @@ -124,9 +131,16 @@ namespace Oqtane.Repository Type moduletype = Type.GetType(moduledefinition.ServerManagerType); if (moduletype != null && moduletype.GetInterface("IPortable") != null) { - var moduleobject = ActivatorUtilities.CreateInstance(_serviceProvider, moduletype); - ((IPortable) moduleobject).ImportModule(module, modulecontent.Content, modulecontent.Version); - success = true; + try + { + var moduleobject = ActivatorUtilities.CreateInstance(_serviceProvider, moduletype); + ((IPortable)moduleobject).ImportModule(module, modulecontent.Content, modulecontent.Version); + success = true; + } + catch + { + // error in IPortable implementation + } } } } diff --git a/Oqtane.Server/Repository/SiteRepository.cs b/Oqtane.Server/Repository/SiteRepository.cs index 91cbccbf..0e39a6bf 100644 --- a/Oqtane.Server/Repository/SiteRepository.cs +++ b/Oqtane.Server/Repository/SiteRepository.cs @@ -784,14 +784,14 @@ namespace Oqtane.Repository Type moduletype = Type.GetType(moduledefinition.ServerManagerType); if (moduletype != null && moduletype.GetInterface("IPortable") != null) { - var moduleobject = ActivatorUtilities.CreateInstance(_serviceProvider, moduletype); try { + var moduleobject = ActivatorUtilities.CreateInstance(_serviceProvider, moduletype); ((IPortable)moduleobject).ImportModule(module, pagetemplatemodule.Content, moduledefinition.Version); } catch { - // error in module import + // error in IPortable implementation } } } diff --git a/Oqtane.Server/Repository/SqlRepository.cs b/Oqtane.Server/Repository/SqlRepository.cs index 0e5c2f3c..fcc4caf3 100644 --- a/Oqtane.Server/Repository/SqlRepository.cs +++ b/Oqtane.Server/Repository/SqlRepository.cs @@ -10,18 +10,21 @@ namespace Oqtane.Repository { public class SqlRepository : ISqlRepository { - private readonly ITenantRepository _tenants; - public SqlRepository(ITenantRepository tenants) + public void ExecuteScript(Tenant tenant, string script) { - _tenants = tenants; + // execute script in curent tenant + foreach (string query in script.Split("GO", StringSplitOptions.RemoveEmptyEntries)) + { + ExecuteNonQuery(tenant, query); + } } - public bool ExecuteEmbeddedScript(Assembly assembly, string filename) + public bool ExecuteScript(Tenant tenant, Assembly assembly, string filename) { // script must be included as an Embedded Resource within an assembly bool success = true; - string uninstallScript = ""; + string script = ""; if (assembly != null) { @@ -33,39 +36,27 @@ namespace Oqtane.Repository { using (var reader = new StreamReader(resourceStream)) { - uninstallScript = reader.ReadToEnd(); + script = reader.ReadToEnd(); } } } } - if (!string.IsNullOrEmpty(uninstallScript)) + if (!string.IsNullOrEmpty(script)) { - foreach (Tenant tenant in _tenants.GetTenants()) + try { - try - { - ExecuteScript(tenant, uninstallScript); - } - catch - { - success = false; - } + ExecuteScript(tenant, script); + } + catch + { + success = false; } } return success; } - public void ExecuteScript(Tenant tenant, string script) - { - // execute script in curent tenant - foreach (string query in script.Split("GO", StringSplitOptions.RemoveEmptyEntries)) - { - ExecuteNonQuery(tenant, query); - } - } - public int ExecuteNonQuery(Tenant tenant, string query) { SqlConnection conn = new SqlConnection(FormatConnectionString(tenant.DBConnectionString)); diff --git a/Oqtane.Server/Repository/TenantResolver.cs b/Oqtane.Server/Repository/TenantResolver.cs index 8a5b85c4..f5035a65 100644 --- a/Oqtane.Server/Repository/TenantResolver.cs +++ b/Oqtane.Server/Repository/TenantResolver.cs @@ -17,47 +17,48 @@ namespace Oqtane.Repository int aliasId = -1; string aliasName = ""; - // get alias identifier based on request context - if (accessor.HttpContext != null) + if (siteState != null && siteState.Alias != null) { - // check if an alias is passed as a querystring parameter ( for cross tenant access ) - if (accessor.HttpContext.Request.Query.ContainsKey("aliasid")) - { - aliasId = int.Parse(accessor.HttpContext.Request.Query["aliasid"]); - } - else // get the alias from the request url - { - aliasName = accessor.HttpContext.Request.Host.Value; - string path = accessor.HttpContext.Request.Path.Value; - string[] segments = path.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries); - if (segments.Length > 1 && segments[1] == "api" && segments[0] != "~") - { - aliasName += "/" + segments[0]; - } - - if (aliasName.EndsWith("/")) - { - aliasName = aliasName.Substring(0, aliasName.Length - 1); - } - } - } - else // background processes can pass in an alias using the SiteState service - { - aliasId = siteState?.Alias?.AliasId ?? -1; - } - - // get the alias and tenant - IEnumerable aliases = aliasRepository.GetAliases().ToList(); // cached - if (aliasId != -1) - { - _alias = aliases.FirstOrDefault(item => item.AliasId == aliasId); + // background processes can pass in an alias using the SiteState service + _alias = siteState.Alias; } else - { - - _alias = aliases.FirstOrDefault(item => item.Name == aliasName - //if here is only one alias and other methods fail, take it (case of startup install) - || aliases.Count() == 1); + { + // get alias identifier based on request context + if (accessor.HttpContext != null) + { + // check if an alias is passed as a querystring parameter ( for cross tenant access ) + if (accessor.HttpContext.Request.Query.ContainsKey("aliasid")) + { + aliasId = int.Parse(accessor.HttpContext.Request.Query["aliasid"]); + } + else // get the alias from the request url + { + aliasName = accessor.HttpContext.Request.Host.Value; + string path = accessor.HttpContext.Request.Path.Value; + string[] segments = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + if (segments.Length > 1 && segments[1] == "api" && segments[0] != "~") + { + aliasName += "/" + segments[0]; + } + + if (aliasName.EndsWith("/")) + { + aliasName = aliasName.Substring(0, aliasName.Length - 1); + } + } + } + + // get the alias + IEnumerable aliases = aliasRepository.GetAliases().ToList(); // cached + if (aliasId != -1) + { + _alias = aliases.FirstOrDefault(item => item.AliasId == aliasId); + } + else + { + _alias = aliases.FirstOrDefault(item => item.Name == aliasName || aliases.Count() == 1); + } } if (_alias != null) diff --git a/Oqtane.Server/Scripts/Master.00.00.00.sql b/Oqtane.Server/Scripts/Master.00.00.00.sql index 0b10fb01..fcfdd3ef 100644 --- a/Oqtane.Server/Scripts/Master.00.00.00.sql +++ b/Oqtane.Server/Scripts/Master.00.00.00.sql @@ -7,8 +7,6 @@ CREATE TABLE [dbo].[Tenant]( [TenantId] [int] IDENTITY(1,1) NOT NULL, [Name] [nvarchar](100) NOT NULL, [DBConnectionString] [nvarchar](1024) NOT NULL, - [DBSchema] [nvarchar](50) NOT NULL, - [IsInitialized] [bit] NOT NULL, [CreatedBy] [nvarchar](256) NOT NULL, [CreatedOn] [datetime] NOT NULL, [ModifiedBy] [nvarchar](256) NOT NULL, @@ -119,32 +117,4 @@ REFERENCES [dbo].[Job] ([JobId]) ON DELETE CASCADE GO -/* - -Create seed data - -*/ -SET IDENTITY_INSERT [dbo].[Tenant] ON -GO -INSERT [dbo].[Tenant] ([TenantId], [Name], [DBConnectionString], [DBSchema], [IsInitialized], [CreatedBy], [CreatedOn], [ModifiedBy], [ModifiedOn]) -VALUES (1, N'Master', N'$ConnectionString$', N'', 0, '', getdate(), '', getdate()) -GO -SET IDENTITY_INSERT [dbo].[Tenant] OFF -GO - -SET IDENTITY_INSERT [dbo].[Alias] ON -GO -INSERT [dbo].[Alias] ([AliasId], [Name], [TenantId], [SiteId], [CreatedBy], [CreatedOn], [ModifiedBy], [ModifiedOn]) -VALUES (1, N'$Alias$', 1, 1, '', getdate(), '', getdate()) -GO -SET IDENTITY_INSERT [dbo].[Alias] OFF -GO - -SET IDENTITY_INSERT [dbo].[Job] ON -GO -INSERT [dbo].[Job] ([JobId], [Name], [JobType], [Frequency], [Interval], [StartDate], [EndDate], [IsEnabled], [IsStarted], [IsExecuting], [NextExecution], [RetentionHistory], [CreatedBy], [CreatedOn], [ModifiedBy], [ModifiedOn]) -VALUES (1, N'Notification Job', N'Oqtane.Infrastructure.NotificationJob, Oqtane.Server', N'm', 1, null, null, 0, 0, 0, null, 10, '', getdate(), '', getdate()) -GO -SET IDENTITY_INSERT [dbo].[Job] OFF -GO diff --git a/Oqtane.Server/Scripts/Master.00.00.01.sql b/Oqtane.Server/Scripts/Master.00.00.01.sql index 02943e96..49ec882c 100644 --- a/Oqtane.Server/Scripts/Master.00.00.01.sql +++ b/Oqtane.Server/Scripts/Master.00.00.01.sql @@ -1,2 +1,5 @@ -alter table Tenant drop column DBSchema -go +/* + +schema updates + +*/ diff --git a/Oqtane.Server/Security/UserPermissions.cs b/Oqtane.Server/Security/UserPermissions.cs index 14a13199..1e89c4cd 100644 --- a/Oqtane.Server/Security/UserPermissions.cs +++ b/Oqtane.Server/Security/UserPermissions.cs @@ -55,7 +55,14 @@ namespace Oqtane.Security public User GetUser() { - return GetUser(_accessor.HttpContext.User); + if (_accessor.HttpContext != null) + { + return GetUser(_accessor.HttpContext.User); + } + else + { + return null; + } } } } diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index 52599d6e..9e24a6fa 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -155,7 +155,7 @@ namespace Oqtane services.AddSingleton(Configuration); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); // install any modules or themes ( this needs to occur BEFORE the assemblies are loaded into the app domain ) InstallationManager.UnpackPackages("Modules,Themes", _webRoot); diff --git a/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.BlazorTheme/Theme.css b/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.BlazorTheme/Theme.css index 114c2584..c69fc61d 100644 --- a/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.BlazorTheme/Theme.css +++ b/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.BlazorTheme/Theme.css @@ -16,7 +16,6 @@ .main .top-row { background-color: #e6e6e6; border-bottom: 1px solid #d6d5d5; - z-index: 9999; } .sidebar { @@ -42,6 +41,10 @@ margin-bottom: 0; } +.app-controlpanel { + z-index: 9999; +} + .app-menu .nav-item { font-size: 0.9rem; padding-bottom: 0.5rem; @@ -97,19 +100,21 @@ position: fixed; left: 275px; top: 0; - z-index: 1; + z-index: 3 } - + .sidebar { width: 250px; height: 100vh; position: sticky; top: 0; + z-index: 1 } .main .top-row { position: sticky; top: 0; + z-index: 2 } .main > div { @@ -154,10 +159,23 @@ } @media (max-width: 767px) { + .breadcrumbs { + position: fixed; + top: 150px; + width: 100%; + left: 0; + z-index: 1; + } + .sidebar { margin-top: 3.5rem; position: fixed; width: 100%; + z-index: 2; + } + + .main .top-row { + z-index: 2; } .main > .top-row.px-4 { @@ -172,13 +190,6 @@ margin-left: auto; } - .breadcrumbs { - position: fixed; - top: 150px; - width: 100%; - left: 0; - } - .main > .container { margin-top: 200px; } diff --git a/Oqtane.Server/wwwroot/js/interop.js b/Oqtane.Server/wwwroot/js/interop.js index ec289bbe..aa0529a0 100644 --- a/Oqtane.Server/wwwroot/js/interop.js +++ b/Oqtane.Server/wwwroot/js/interop.js @@ -48,7 +48,7 @@ window.interop = { } } }, - includeLink: function (id, rel, url, type) { + includeLink: function (id, rel, url, type, integrity, crossorigin) { var link; if (id !== "") { link = document.getElementById(id); @@ -66,6 +66,12 @@ window.interop = { if (type !== "") { link.type = type; } + if (integrity !== "") { + link.integrity = integrity; + } + if (crossorigin !== "") { + link.crossorigin = crossorigin; + } document.head.appendChild(link); } else { @@ -78,9 +84,15 @@ window.interop = { if (type !== "" && link.type !== type) { link.setAttribute('type', type); } + if (integrity !== "" && link.integrity !== integrity) { + link.setAttribute('integrity', integrity); + } + if (crossorigin !== "" && link.crossorigin !== crossorigin) { + link.setAttribute('crossorigin', crossorigin); + } } }, - includeScript: function (id, src, content, location) { + includeScript: function (id, src, content, location, integrity, crossorigin) { var script; if (id !== "") { script = document.getElementById(id); @@ -92,6 +104,12 @@ window.interop = { } if (src !== "") { script.src = src; + if (integrity !== "") { + script.integrity = integrity; + } + if (crossorigin !== "") { + script.crossorigin = crossorigin; + } } else { script.innerHTML = content; @@ -108,6 +126,12 @@ window.interop = { if (script.src !== src) { script.src = src; } + if (integrity !== "" && script.integrity !== integrity) { + script.setAttribute('integrity', integrity); + } + if (crossorigin !== "" && script.crossorigin !== crossorigin) { + script.setAttribute('crossorigin', crossorigin); + } } else { if (script.innerHTML !== content) { diff --git a/Oqtane.Shared/Models/Tenant.cs b/Oqtane.Shared/Models/Tenant.cs index 3f0a518a..ed392544 100644 --- a/Oqtane.Shared/Models/Tenant.cs +++ b/Oqtane.Shared/Models/Tenant.cs @@ -7,7 +7,6 @@ namespace Oqtane.Models public int TenantId { get; set; } public string Name { get; set; } public string DBConnectionString { get; set; } - public bool IsInitialized { get; set; } public string CreatedBy { get; set; } public DateTime CreatedOn { get; set; } public string ModifiedBy { get; set; } diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index eacaa4c6..f4edd1ce 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -33,6 +33,7 @@ public const string HostUser = "host"; public const string MasterTenant = "Master"; + public const string DefaultSite = "Default Site"; public const string AllUsersRole = "All Users"; public const string HostRole = "Host Users"; diff --git a/Oqtane.Shared/Shared/InstallConfig.cs b/Oqtane.Shared/Shared/InstallConfig.cs index f7f065b4..2ab74c74 100644 --- a/Oqtane.Shared/Shared/InstallConfig.cs +++ b/Oqtane.Shared/Shared/InstallConfig.cs @@ -2,11 +2,17 @@ { public class InstallConfig { - public string Alias { get; set; } public string ConnectionString { get; set; } - public string HostUser { get; set; } - public string Password { get; set; } + public string Aliases { get; set; } + public string TenantName { get; set; } + public bool IsNewTenant { get; set; } + public string SiteName { get; set; } + public string HostPassword { get; set; } public string HostEmail { get; set; } - public bool IsMaster { get; set; } + public string HostName { get; set; } + public string SiteTemplate { get; set; } + public string DefaultTheme { get; set; } + public string DefaultLayout { get; set; } + public string DefaultContainer { get; set; } } }