diff --git a/Oqtane.Client/App.razor b/Oqtane.Client/App.razor index 3446ccbf..6ad2be28 100644 --- a/Oqtane.Client/App.razor +++ b/Oqtane.Client/App.razor @@ -1,15 +1,31 @@ @using Oqtane.Shared @using Oqtane.Client.Shared +@using Oqtane.Services +@inject IInstallationService InstallationService - - - - - +@if (!Installed) +{ + +} +else +{ + + + + + +} @code { + private bool Installed = false; private PageState PageState { get; set; } + protected override async Task OnInitAsync() + { + var response = await InstallationService.IsInstalled(); + Installed = response.Success; + } + private void ChangeState(PageState pagestate) { PageState = pagestate; diff --git a/Oqtane.Client/Modules/Admin/Login/Index.razor b/Oqtane.Client/Modules/Admin/Login/Index.razor index 3071d436..c381e244 100644 --- a/Oqtane.Client/Modules/Admin/Login/Index.razor +++ b/Oqtane.Client/Modules/Admin/Login/Index.razor @@ -44,7 +44,7 @@ @code { public override SecurityAccessLevelEnum SecurityAccessLevel { get { return SecurityAccessLevelEnum.Anonymous; } } -public string Message { get; set; } = "
Use host/password For Demo Access
"; +public string Message { get; set; } = ""; public string Username { get; set; } = ""; public string Password { get; set; } = ""; public bool Remember { get; set; } = false; diff --git a/Oqtane.Client/Modules/HtmlText/Edit.razor b/Oqtane.Client/Modules/HtmlText/Edit.razor index 4bdd8aaf..63c4b2f3 100644 --- a/Oqtane.Client/Modules/HtmlText/Edit.razor +++ b/Oqtane.Client/Modules/HtmlText/Edit.razor @@ -33,7 +33,7 @@ protected override async Task OnInitAsync() { - HtmlTextService htmltextservice = new HtmlTextService(http, sitestate); + HtmlTextService htmltextservice = new HtmlTextService(http, sitestate, UriHelper); List htmltextlist = await htmltextservice.GetHtmlTextAsync(ModuleState.ModuleId); if (htmltextlist != null) { @@ -44,7 +44,7 @@ private async Task SaveContent() { - HtmlTextService htmltextservice = new HtmlTextService(http, sitestate); + HtmlTextService htmltextservice = new HtmlTextService(http, sitestate, UriHelper); if (htmltext != null) { htmltext.Content = content; diff --git a/Oqtane.Client/Modules/HtmlText/Index.razor b/Oqtane.Client/Modules/HtmlText/Index.razor index cc679988..90d86153 100644 --- a/Oqtane.Client/Modules/HtmlText/Index.razor +++ b/Oqtane.Client/Modules/HtmlText/Index.razor @@ -5,6 +5,7 @@ @using Oqtane.Client.Modules.Controls @using Oqtane.Shared; @inherits ModuleBase +@inject IUriHelper UriHelper @inject HttpClient http @inject SiteState sitestate @@ -17,7 +18,7 @@ protected override async Task OnInitAsync() { - HtmlTextService htmltextservice = new HtmlTextService(http, sitestate); + HtmlTextService htmltextservice = new HtmlTextService(http, sitestate, UriHelper); List htmltext = await htmltextservice.GetHtmlTextAsync(ModuleState.ModuleId); if (htmltext != null) { diff --git a/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs b/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs index c6f2443b..e8257b00 100644 --- a/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs +++ b/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs @@ -13,16 +13,18 @@ namespace Oqtane.Client.Modules.HtmlText.Services { private readonly HttpClient http; private readonly SiteState sitestate; + private readonly IUriHelper urihelper; - public HtmlTextService(HttpClient http, SiteState sitestate) + public HtmlTextService(HttpClient http, SiteState sitestate, IUriHelper urihelper) { this.http = http; this.sitestate = sitestate; + this.urihelper = urihelper; } private string apiurl { - get { return CreateApiUrl(sitestate.Alias, "HtmlText"); } + get { return CreateApiUrl(sitestate.Alias, urihelper.GetAbsoluteUri(), "HtmlText"); } } public async Task> GetHtmlTextAsync(int ModuleId) diff --git a/Oqtane.Client/Services/AliasService.cs b/Oqtane.Client/Services/AliasService.cs index 88aa9683..1d29f68a 100644 --- a/Oqtane.Client/Services/AliasService.cs +++ b/Oqtane.Client/Services/AliasService.cs @@ -11,17 +11,19 @@ namespace Oqtane.Services public class AliasService : ServiceBase, IAliasService { private readonly HttpClient http; + private readonly SiteState sitestate; private readonly IUriHelper urihelper; - public AliasService(HttpClient http, IUriHelper urihelper) + public AliasService(HttpClient http, SiteState sitestate, IUriHelper urihelper) { this.http = http; + this.sitestate = sitestate; this.urihelper = urihelper; } private string apiurl { - get { return CreateApiUrl(urihelper.GetAbsoluteUri(), "Alias"); } + get { return CreateApiUrl(sitestate.Alias, urihelper.GetAbsoluteUri(), "Alias"); } } public async Task> GetAliasesAsync() diff --git a/Oqtane.Client/Services/IInstallationService.cs b/Oqtane.Client/Services/IInstallationService.cs new file mode 100644 index 00000000..37f71fef --- /dev/null +++ b/Oqtane.Client/Services/IInstallationService.cs @@ -0,0 +1,12 @@ +using Oqtane.Models; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Oqtane.Services +{ + public interface IInstallationService + { + Task IsInstalled(); + Task Install(string connectionstring); + } +} diff --git a/Oqtane.Client/Services/InstallationService.cs b/Oqtane.Client/Services/InstallationService.cs new file mode 100644 index 00000000..399140ef --- /dev/null +++ b/Oqtane.Client/Services/InstallationService.cs @@ -0,0 +1,39 @@ +using Oqtane.Models; +using System.Threading.Tasks; +using System.Net.Http; +using System.Linq; +using Microsoft.AspNetCore.Components; +using System.Collections.Generic; +using Oqtane.Shared; + +namespace Oqtane.Services +{ + public class InstallationService : ServiceBase, IInstallationService + { + private readonly HttpClient http; + private readonly SiteState sitestate; + private readonly IUriHelper urihelper; + + public InstallationService(HttpClient http, SiteState sitestate, IUriHelper urihelper) + { + this.http = http; + this.sitestate = sitestate; + this.urihelper = urihelper; + } + + private string apiurl + { + get { return CreateApiUrl(sitestate.Alias, urihelper.GetAbsoluteUri(), "Installation"); } + } + + public async Task IsInstalled() + { + return await http.GetJsonAsync(apiurl + "/installed"); + } + + public async Task Install(string connectionstring) + { + return await http.PostJsonAsync(apiurl, connectionstring); + } + } +} diff --git a/Oqtane.Client/Services/ModuleDefinitionService.cs b/Oqtane.Client/Services/ModuleDefinitionService.cs index 75b06b2f..31fff1ba 100644 --- a/Oqtane.Client/Services/ModuleDefinitionService.cs +++ b/Oqtane.Client/Services/ModuleDefinitionService.cs @@ -14,16 +14,18 @@ namespace Oqtane.Services { private readonly HttpClient http; private readonly SiteState sitestate; + private readonly IUriHelper urihelper; - public ModuleDefinitionService(HttpClient http, SiteState sitestate) + public ModuleDefinitionService(HttpClient http, SiteState sitestate, IUriHelper urihelper) { this.http = http; this.sitestate = sitestate; + this.urihelper = urihelper; } private string apiurl { - get { return CreateApiUrl(sitestate.Alias, "ModuleDefinition"); } + get { return CreateApiUrl(sitestate.Alias, urihelper.GetAbsoluteUri(), "ModuleDefinition"); } } public async Task> GetModuleDefinitionsAsync() diff --git a/Oqtane.Client/Services/ModuleService.cs b/Oqtane.Client/Services/ModuleService.cs index ede01dc5..60f22eeb 100644 --- a/Oqtane.Client/Services/ModuleService.cs +++ b/Oqtane.Client/Services/ModuleService.cs @@ -12,16 +12,18 @@ namespace Oqtane.Services { private readonly HttpClient http; private readonly SiteState sitestate; + private readonly IUriHelper urihelper; - public ModuleService(HttpClient http, SiteState sitestate) + public ModuleService(HttpClient http, SiteState sitestate, IUriHelper urihelper) { this.http = http; this.sitestate = sitestate; + this.urihelper = urihelper; } private string apiurl { - get { return CreateApiUrl(sitestate.Alias, "Module"); } + get { return CreateApiUrl(sitestate.Alias, urihelper.GetAbsoluteUri(), "Module"); } } public async Task> GetModulesAsync(int PageId) diff --git a/Oqtane.Client/Services/PageModuleService.cs b/Oqtane.Client/Services/PageModuleService.cs index 240b4c44..8a65de2d 100644 --- a/Oqtane.Client/Services/PageModuleService.cs +++ b/Oqtane.Client/Services/PageModuleService.cs @@ -12,16 +12,18 @@ namespace Oqtane.Services { private readonly HttpClient http; private readonly SiteState sitestate; + private readonly IUriHelper urihelper; - public PageModuleService(HttpClient http, SiteState sitestate) + public PageModuleService(HttpClient http, SiteState sitestate, IUriHelper urihelper) { this.http = http; this.sitestate = sitestate; + this.urihelper = urihelper; } private string apiurl { - get { return CreateApiUrl(sitestate.Alias, "PageModule"); } + get { return CreateApiUrl(sitestate.Alias, "PageModule", urihelper.GetAbsoluteUri()); } } public async Task> GetPageModulesAsync() diff --git a/Oqtane.Client/Services/PageService.cs b/Oqtane.Client/Services/PageService.cs index fb54c32c..d0124c79 100644 --- a/Oqtane.Client/Services/PageService.cs +++ b/Oqtane.Client/Services/PageService.cs @@ -12,16 +12,18 @@ namespace Oqtane.Services { private readonly HttpClient http; private readonly SiteState sitestate; + private readonly IUriHelper urihelper; - public PageService(HttpClient http, SiteState sitestate) + public PageService(HttpClient http, SiteState sitestate, IUriHelper urihelper) { this.http = http; this.sitestate = sitestate; + this.urihelper = urihelper; } private string apiurl { - get { return CreateApiUrl(sitestate.Alias, "Page"); } + get { return CreateApiUrl(sitestate.Alias, urihelper.GetAbsoluteUri(), "Page"); } } public async Task> GetPagesAsync(int SiteId) diff --git a/Oqtane.Client/Services/ServiceBase.cs b/Oqtane.Client/Services/ServiceBase.cs index ca7c588f..b970c373 100644 --- a/Oqtane.Client/Services/ServiceBase.cs +++ b/Oqtane.Client/Services/ServiceBase.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.AspNetCore.Components; using Oqtane.Models; using Oqtane.Shared; @@ -6,21 +7,24 @@ namespace Oqtane.Services { public class ServiceBase { - // method for alias agnostic api call - public string CreateApiUrl(string absoluteUri, string serviceName) - { - Uri uri = new Uri(absoluteUri); - string apiurl = uri.Scheme + "://" + uri.Authority + "/~/api/" + serviceName; - return apiurl; - } - // method for alias specific api call - public string CreateApiUrl(Alias alias, string serviceName) + public string CreateApiUrl(Alias alias, string absoluteUri, string serviceName) { - string apiurl = alias.Url + "/"; - if (alias.Path == "") + string apiurl = ""; + if (alias != null) { - apiurl += "~/"; + // build a url which passes the alias that may include a subfolder for multi-tenancy + apiurl = alias.Url + "/"; + if (alias.Path == "") + { + apiurl += "~/"; + } + } + else + { + // build a url which ignores any subfolder for multi-tenancy + Uri uri = new Uri(absoluteUri); + apiurl = uri.Scheme + "://" + uri.Authority + "/~/"; } apiurl += "api/" + serviceName; return apiurl; diff --git a/Oqtane.Client/Services/SiteService.cs b/Oqtane.Client/Services/SiteService.cs index 519155a5..83cfad34 100644 --- a/Oqtane.Client/Services/SiteService.cs +++ b/Oqtane.Client/Services/SiteService.cs @@ -12,16 +12,18 @@ namespace Oqtane.Services { private readonly HttpClient http; private readonly SiteState sitestate; + private readonly IUriHelper urihelper; - public SiteService(HttpClient http, SiteState sitestate) + public SiteService(HttpClient http, SiteState sitestate, IUriHelper urihelper) { this.http = http; this.sitestate = sitestate; + this.urihelper = urihelper; } private string apiurl { - get { return CreateApiUrl(sitestate.Alias, "Site"); } + get { return CreateApiUrl(sitestate.Alias, urihelper.GetAbsoluteUri(), "Site"); } } public async Task> GetSitesAsync() diff --git a/Oqtane.Client/Services/TenantService.cs b/Oqtane.Client/Services/TenantService.cs index 27054504..4da871fb 100644 --- a/Oqtane.Client/Services/TenantService.cs +++ b/Oqtane.Client/Services/TenantService.cs @@ -12,16 +12,18 @@ namespace Oqtane.Services { private readonly HttpClient http; private readonly SiteState sitestate; + private readonly IUriHelper urihelper; - public TenantService(HttpClient http, SiteState sitestate) + public TenantService(HttpClient http, SiteState sitestate, IUriHelper urihelper) { this.http = http; this.sitestate = sitestate; + this.urihelper = urihelper; } private string apiurl { - get { return CreateApiUrl(sitestate.Alias, "Tenant"); } + get { return CreateApiUrl(sitestate.Alias, urihelper.GetAbsoluteUri(), "Tenant"); } } public async Task> GetTenantsAsync() diff --git a/Oqtane.Client/Services/ThemeService.cs b/Oqtane.Client/Services/ThemeService.cs index b6c2201e..a38a9e28 100644 --- a/Oqtane.Client/Services/ThemeService.cs +++ b/Oqtane.Client/Services/ThemeService.cs @@ -14,16 +14,18 @@ namespace Oqtane.Services { private readonly HttpClient http; private readonly SiteState sitestate; + private readonly IUriHelper urihelper; - public ThemeService(HttpClient http, SiteState sitestate) + public ThemeService(HttpClient http, SiteState sitestate, IUriHelper urihelper) { this.http = http; this.sitestate = sitestate; + this.urihelper = urihelper; } private string apiurl { - get { return CreateApiUrl(sitestate.Alias, "Theme"); } + get { return CreateApiUrl(sitestate.Alias, urihelper.GetAbsoluteUri(), "Theme"); } } public async Task> GetThemesAsync() diff --git a/Oqtane.Client/Services/UserService.cs b/Oqtane.Client/Services/UserService.cs index d83dc260..eeb19433 100644 --- a/Oqtane.Client/Services/UserService.cs +++ b/Oqtane.Client/Services/UserService.cs @@ -24,7 +24,7 @@ namespace Oqtane.Services private string apiurl { - get { return CreateApiUrl(sitestate.Alias, "User"); } + get { return CreateApiUrl(sitestate.Alias, urihelper.GetAbsoluteUri(), "User"); } } public async Task> GetUsersAsync() diff --git a/Oqtane.Client/Shared/Installer.razor b/Oqtane.Client/Shared/Installer.razor new file mode 100644 index 00000000..321f8d3d --- /dev/null +++ b/Oqtane.Client/Shared/Installer.razor @@ -0,0 +1,187 @@ +@using Oqtane.Services +@using Oqtane.Models +@inject IUriHelper UriHelper +@inject IInstallationService InstallationService +@inject IUserService UserService + +
+
+
+ +
+
+
+

Database Configuration

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+
+
+
+

Application Administrator

+
+
+ + + + + + + + + + + +
+ + + +
+ + + +
+
+
+
+
+

+ @((MarkupString)@Message) +
+
+
+
+ +@code { + +private string DatabaseType = "LocalDB"; +private string ServerName = "(LocalDb)\\MSSQLLocalDB"; +private string DatabaseName = "Oqtane-" + DateTime.Now.ToString("yyyyMMddHHmm"); +private bool IntegratedSecurity = true; +private string Username = ""; +private string Password = ""; +private string HostUsername = "host"; +private string HostPassword = ""; +private string Message = ""; + +private string IntegratedSecurityDisplay = "display:none;"; +private string LoadingDisplay = "display:none;"; + +private void SetIntegratedSecurity(UIChangeEventArgs e) +{ + if (Convert.ToBoolean(e.Value)) + { + IntegratedSecurityDisplay = "display:none;"; + } + else + { + IntegratedSecurityDisplay = ""; + } +} + +private async Task Install() +{ + if (HostPassword.Length >= 6) + { + LoadingDisplay = ""; + StateHasChanged(); + + string connectionstring = ""; + if (DatabaseType == "LocalDB") + { + connectionstring = "Data Source=" + ServerName + ";AttachDbFilename=|DataDirectory|\\" + DatabaseName + ".mdf;Initial Catalog=" + DatabaseName + ";Integrated Security=SSPI;"; + } + else + { + connectionstring = "Data Source=" + ServerName + ";Initial Catalog=" + DatabaseName + ";"; + if (IntegratedSecurityDisplay == "display:none;") + { + connectionstring += "Integrated Security=SSPI;"; + } + else + { + connectionstring += "User ID=" + Username + ";Password=" + Password; + + } + } + GenericResponse response = await InstallationService.Install(connectionstring); + if (response.Success) + { + User user = new User(); + user.Username = HostUsername; + user.DisplayName = HostUsername; + user.Password = HostPassword; + user.IsSuperUser = true; + user.Roles = ""; + await UserService.AddUserAsync(user); + UriHelper.NavigateTo("", true); + } + else + { + Message = "
" + response.Message + "
"; + LoadingDisplay = "display:none;"; + } + } + else + { + Message = "
Password Must Be 6 Characters Or Greater
"; + } +} +} diff --git a/Oqtane.Client/Startup.cs b/Oqtane.Client/Startup.cs index b817e046..1c64ca52 100644 --- a/Oqtane.Client/Startup.cs +++ b/Oqtane.Client/Startup.cs @@ -36,6 +36,7 @@ namespace Oqtane.Client // register scoped core services services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Oqtane.Client/wwwroot/css/site.css b/Oqtane.Client/wwwroot/css/site.css index 6774c2e9..193ec6f0 100644 --- a/Oqtane.Client/wwwroot/css/site.css +++ b/Oqtane.Client/wwwroot/css/site.css @@ -246,3 +246,13 @@ app { text-align: center; color: gray; } + +.loading { + background: rgba(0,0,0,0.2) url('../loading.gif') no-repeat 50% 50%; + width: 100%; + height: 100%; + position: fixed; + top: 0; + left: 0; + z-index: 999; +} \ No newline at end of file diff --git a/Oqtane.Client/wwwroot/js/interop.js b/Oqtane.Client/wwwroot/js/interop.js index a322ca40..db61ecbe 100644 --- a/Oqtane.Client/wwwroot/js/interop.js +++ b/Oqtane.Client/wwwroot/js/interop.js @@ -20,6 +20,14 @@ window.interop = { } return ""; }, + getElementByName: function (name) { + var elements = document.getElementsByName(name); + if (elements.length) { + return elements[0].value; + } else { + return ""; + } + }, addCSS: function (fileName) { var head = document.head; var link = document.createElement("link"); diff --git a/Oqtane.Client/wwwroot/loading.gif b/Oqtane.Client/wwwroot/loading.gif new file mode 100644 index 00000000..cc70a7a8 Binary files /dev/null and b/Oqtane.Client/wwwroot/loading.gif differ diff --git a/Oqtane.Server/Controllers/InstallationController.cs b/Oqtane.Server/Controllers/InstallationController.cs new file mode 100644 index 00000000..3297c572 --- /dev/null +++ b/Oqtane.Server/Controllers/InstallationController.cs @@ -0,0 +1,192 @@ +using DbUp; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Oqtane.Models; +using System; +using System.Data.SqlClient; +using System.IO; +using System.Reflection; +using System.Threading; + +namespace Oqtane.Controllers +{ + [Route("{site}/api/[controller]")] + public class InstallationController : Controller + { + private readonly IConfigurationRoot _config; + + public InstallationController(IConfigurationRoot config) + { + _config = config; + } + + // POST api/ + [HttpPost] + public GenericResponse Post([FromBody] string connectionString) + { + var response = new GenericResponse { Success = false, Message = "" }; + + if (ModelState.IsValid) + { + bool exists = IsInstalled().Success; + + if (!exists) + { + string datadirectory = AppDomain.CurrentDomain.GetData("DataDirectory").ToString(); + connectionString = connectionString.Replace("|DataDirectory|", datadirectory); + + SqlConnection connection = new SqlConnection(connectionString); + try + { + using (connection) + { + connection.Open(); + } + exists = true; + } + catch + { + // database does not exist + } + + // try to create database if it does not exist + if (!exists) + { + string masterConnectionString = ""; + string databaseName = ""; + string[] fragments = connectionString.Split(';', StringSplitOptions.RemoveEmptyEntries); + foreach (string fragment in fragments) + { + if (fragment.ToLower().Contains("initial catalog=") || fragment.ToLower().Contains("database=")) + { + databaseName = fragment.Substring(fragment.IndexOf("=") + 1); + } + else + { + if (!fragment.ToLower().Contains("attachdbfilename=")) + { + masterConnectionString += fragment + ";"; + } + } + } + connection = new SqlConnection(masterConnectionString); + try + { + using (connection) + { + connection.Open(); + SqlCommand command; + if (connectionString.ToLower().Contains("attachdbfilename=")) // LocalDB + { + command = new SqlCommand("CREATE DATABASE [" + databaseName + "] ON ( NAME = '" + databaseName + "', FILENAME = '" + datadirectory + "\\" + databaseName + ".mdf')", connection); + } + else + { + command = new SqlCommand("CREATE DATABASE [" + databaseName + "]", connection); + } + command.ExecuteNonQuery(); + exists = true; + } + } + catch (Exception ex) + { + response.Message = "Can Not Create Database - " + ex.Message; + } + + // sleep to allow SQL server to attach new database + Thread.Sleep(5000); + } + + if (exists) + { + // get master initialization script and update connectionstring and alias in seed data + string initializationScript = ""; + using (StreamReader reader = new StreamReader(Directory.GetCurrentDirectory() + "\\Scripts\\Master.sql")) + { + initializationScript = reader.ReadToEnd(); + } + initializationScript = initializationScript.Replace("{ConnectionString}", connectionString); + initializationScript = initializationScript.Replace("{Alias}", HttpContext.Request.Host.Value); + + var dbUpgradeConfig = DeployChanges.To.SqlDatabase(connectionString) + .WithScript(new DbUp.Engine.SqlScript("Master.sql", initializationScript)) + .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly()); // tenant scripts should be added to /Scripts folder as Embedded Resources + var dbUpgrade = dbUpgradeConfig.Build(); + if (dbUpgrade.IsUpgradeRequired()) + { + var result = dbUpgrade.PerformUpgrade(); + if (!result.Successful) + { + response.Message = result.Error.Message; + } + else + { + // update appsettings + string config = ""; + using (StreamReader reader = new StreamReader(Directory.GetCurrentDirectory() + "\\appsettings.json")) + { + config = reader.ReadToEnd(); + } + connectionString = connectionString.Replace(datadirectory, "|DataDirectory|"); + connectionString = connectionString.Replace(@"\", @"\\"); + config = config.Replace("DefaultConnection\": \"", "DefaultConnection\": \"" + connectionString); + using (StreamWriter writer = new StreamWriter(Directory.GetCurrentDirectory() + "\\appsettings.json")) + { + writer.WriteLine(config); + } + _config.Reload(); + response.Success = true; + } + } + } + } + else + { + response.Message = "Application Is Already Installed"; + } + } + return response; + } + + // GET api//installed + [HttpGet("installed")] + public GenericResponse IsInstalled() + { + var response = new GenericResponse { Success = false, Message = "" }; + + string datadirectory = AppDomain.CurrentDomain.GetData("DataDirectory").ToString(); + string connectionString = _config.GetConnectionString("DefaultConnection"); + connectionString = connectionString.Replace("|DataDirectory|", datadirectory); + + SqlConnection connection = new SqlConnection(connectionString); + try + { + using (connection) + { + connection.Open(); + } + response.Success = true; + } + catch + { + // database does not exist + response.Message = "Database Does Not Exist"; + } + + if (response.Success) + { + var dbUpgradeConfig = DeployChanges.To.SqlDatabase(connectionString) + .WithScript(new DbUp.Engine.SqlScript("Master.sql", "")); + var dbUpgrade = dbUpgradeConfig.Build(); + response.Success = !dbUpgrade.IsUpgradeRequired(); + if (!response.Success) + { + response.Message = "Scripts Have Not Been Run"; + } + } + + return response; + } + } +} diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index 759515ee..86f1fa8a 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -86,20 +86,9 @@ namespace Oqtane.Controllers [HttpPost("login")] public async Task Login([FromBody] User user) { - // TODO: seed host user - this logic should be moved to installation - IdentityUser identityuser = await identityUserManager.FindByNameAsync("host"); - if (identityuser == null) - { - var result = await identityUserManager.CreateAsync(new IdentityUser { UserName = "host", Email = "host" }, "password"); - if (result.Succeeded) - { - users.AddUser(new Models.User { Username = "host", DisplayName = "host", IsSuperUser = true, Roles = "" }); - } - } - if (ModelState.IsValid) { - identityuser = await identityUserManager.FindByNameAsync(user.Username); + IdentityUser identityuser = await identityUserManager.FindByNameAsync(user.Username); if (identityuser != null) { var result = await identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, false); diff --git a/Oqtane.Server/Filters/UpgradeFilter.cs b/Oqtane.Server/Filters/UpgradeFilter.cs deleted file mode 100644 index b08203aa..00000000 --- a/Oqtane.Server/Filters/UpgradeFilter.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using System.Reflection; -using DbUp; -using System.Data.SqlClient; -using System.Threading; -using System.IO; -using Microsoft.AspNetCore.Identity; - -namespace Oqtane.Filters -{ - public class UpgradeFilter : IStartupFilter - { - private readonly IConfiguration _config; - - public UpgradeFilter(IConfiguration config) - { - _config = config; - } - - public Action Configure(Action next) - { - string datadirectory = AppDomain.CurrentDomain.GetData("DataDirectory").ToString(); - string connectionString = _config.GetConnectionString("DefaultConnection"); - connectionString = connectionString.Replace("|DataDirectory|", datadirectory); - - // check if database exists - SqlConnection connection = new SqlConnection(connectionString); - bool databaseExists; - try - { - using (connection) - { - connection.Open(); - } - databaseExists = true; - } - catch - { - databaseExists = false; - } - - // create database if it does not exist - if (!databaseExists) - { - string masterConnectionString = ""; - string databaseName = ""; - string[] fragments = connectionString.Split(';', StringSplitOptions.RemoveEmptyEntries); - foreach(string fragment in fragments) - { - if (fragment.ToLower().Contains("initial catalog=") || fragment.ToLower().Contains("database=")) - { - databaseName = fragment.Substring(fragment.IndexOf("=") + 1); - } - else - { - if (!fragment.ToLower().Contains("attachdbfilename=")) - { - masterConnectionString += fragment + ";"; - } - } - } - connection = new SqlConnection(masterConnectionString); - try - { - using (connection) - { - connection.Open(); - SqlCommand command; - if (connectionString.ToLower().Contains("attachdbfilename=")) // LocalDB - { - command = new SqlCommand("CREATE DATABASE [" + databaseName + "] ON ( NAME = '" + databaseName + "', FILENAME = '" + datadirectory + "\\" + databaseName + ".mdf')", connection); - } - else - { - command = new SqlCommand("CREATE DATABASE [" + databaseName + "]", connection); - } - command.ExecuteNonQuery(); - } - } - catch (Exception ex) - { - throw ex; - } - - // sleep to allow SQL server to attach new database - Thread.Sleep(5000); - } - - // get master initialization script and update connectionstring in seed data - string initializationScript = ""; - using (StreamReader reader = new StreamReader(Directory.GetCurrentDirectory() + "\\Scripts\\Master.sql")) - { - initializationScript = reader.ReadToEnd(); - } - initializationScript = initializationScript.Replace("{ConnectionString}", connectionString); - - // handle upgrade scripts - var dbUpgradeConfig = DeployChanges.To.SqlDatabase(connectionString) - .WithScript(new DbUp.Engine.SqlScript("Master.sql", initializationScript)) - .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly()); // upgrade scripts should be added to /Scripts folder as Embedded Resources - var dbUpgrade = dbUpgradeConfig.Build(); - if (dbUpgrade.IsUpgradeRequired()) - { - var result = dbUpgrade.PerformUpgrade(); - if (!result.Successful) - { - throw new Exception(); - } - } - - return next; - } - } -} diff --git a/Oqtane.Server/Program.cs b/Oqtane.Server/Program.cs index 8e6d1632..180d79f3 100644 --- a/Oqtane.Server/Program.cs +++ b/Oqtane.Server/Program.cs @@ -1,11 +1,8 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; using Microsoft.AspNetCore.Blazor.Hosting; -using Microsoft.AspNetCore; using Microsoft.Extensions.Configuration; -using System.IO; -using System.Text; -using System; +using Microsoft.AspNetCore; namespace Oqtane.Server { @@ -14,7 +11,6 @@ namespace Oqtane.Server #if DEBUG || RELEASE public static void Main(string[] args) { - PrepareConfiguration(); CreateHostBuilder(args).Build().Run(); } @@ -29,7 +25,6 @@ namespace Oqtane.Server #if WASM public static void Main(string[] args) { - PrepareConfiguration(); BuildWebHost(args).Run(); } @@ -42,24 +37,5 @@ namespace Oqtane.Server .Build(); #endif - private static void PrepareConfiguration() - { - string config = ""; - using (StreamReader reader = new StreamReader(Directory.GetCurrentDirectory() + "\\appsettings.json")) - { - config = reader.ReadToEnd(); - } - // if using LocalDB create a unique database name - if (config.Contains("AttachDbFilename=|DataDirectory|\\\\Oqtane.mdf")) - { - string timestamp = DateTime.Now.ToString("yyyyMMddHHmm"); - config = config.Replace("Initial Catalog=Oqtane", "Initial Catalog=Oqtane-" + timestamp) - .Replace("AttachDbFilename=|DataDirectory|\\\\Oqtane.mdf", "AttachDbFilename=|DataDirectory|\\\\Oqtane-" + timestamp + ".mdf"); - using (StreamWriter writer = new StreamWriter(Directory.GetCurrentDirectory() + "\\appsettings.json")) - { - writer.WriteLine(config); - } - } - } } } diff --git a/Oqtane.Server/Repository/AliasRepository.cs b/Oqtane.Server/Repository/AliasRepository.cs index 0cc3b365..ca9f3059 100644 --- a/Oqtane.Server/Repository/AliasRepository.cs +++ b/Oqtane.Server/Repository/AliasRepository.cs @@ -9,10 +9,10 @@ namespace Oqtane.Repository { public class AliasRepository : IAliasRepository { - private HostContext db; + private MasterContext db; private readonly IMemoryCache _cache; - public AliasRepository(HostContext context, IMemoryCache cache) + public AliasRepository(MasterContext context, IMemoryCache cache) { db = context; _cache = cache; diff --git a/Oqtane.Server/Repository/HostContext.cs b/Oqtane.Server/Repository/MasterContext.cs similarity index 62% rename from Oqtane.Server/Repository/HostContext.cs rename to Oqtane.Server/Repository/MasterContext.cs index f0f77023..21dca1dd 100644 --- a/Oqtane.Server/Repository/HostContext.cs +++ b/Oqtane.Server/Repository/MasterContext.cs @@ -3,9 +3,9 @@ using Oqtane.Models; namespace Oqtane.Repository { - public class HostContext : DbContext + public class MasterContext : DbContext { - public HostContext(DbContextOptions options) : base(options) { } + public MasterContext(DbContextOptions options) : base(options) { } public virtual DbSet Alias { get; set; } public virtual DbSet Tenant { get; set; } diff --git a/Oqtane.Server/Repository/TenantRepository.cs b/Oqtane.Server/Repository/TenantRepository.cs index 9ee2c188..da7c6be9 100644 --- a/Oqtane.Server/Repository/TenantRepository.cs +++ b/Oqtane.Server/Repository/TenantRepository.cs @@ -10,10 +10,10 @@ namespace Oqtane.Repository { public class TenantRepository : ITenantRepository { - private HostContext db; + private MasterContext db; private readonly IMemoryCache _cache; - public TenantRepository(HostContext context, IMemoryCache cache) + public TenantRepository(MasterContext context, IMemoryCache cache) { db = context; _cache = cache; diff --git a/Oqtane.Server/Repository/TenantResolver.cs b/Oqtane.Server/Repository/TenantResolver.cs index 4685f9cd..bd78f450 100644 --- a/Oqtane.Server/Repository/TenantResolver.cs +++ b/Oqtane.Server/Repository/TenantResolver.cs @@ -1,20 +1,18 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Oqtane.Models; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Caching.Memory; namespace Oqtane.Repository { public class TenantResolver : ITenantResolver { - private HostContext db; + private MasterContext db; private readonly string aliasname; private readonly IAliasRepository _aliasrepository; private readonly ITenantRepository _tenantrepository; - public TenantResolver(HostContext context, IHttpContextAccessor accessor, IAliasRepository aliasrepository, ITenantRepository tenantrepository) + public TenantResolver(MasterContext context, IHttpContextAccessor accessor, IAliasRepository aliasrepository, ITenantRepository tenantrepository) { db = context; _aliasrepository = aliasrepository; diff --git a/Oqtane.Server/Scripts/Master.sql b/Oqtane.Server/Scripts/Master.sql index 1d76ad91..9c2cb0b4 100644 --- a/Oqtane.Server/Scripts/Master.sql +++ b/Oqtane.Server/Scripts/Master.sql @@ -54,10 +54,10 @@ GO SET IDENTITY_INSERT [dbo].[Alias] ON GO INSERT [dbo].[Alias] ([AliasId], [Name], [TenantId], [SiteId]) -VALUES (1, N'localhost:44357', 1, 1) +VALUES (1, N'{Alias}', 1, 1) GO INSERT [dbo].[Alias] ([AliasId], [Name], [TenantId], [SiteId]) -VALUES (2, N'localhost:44357/site2', 1, 2) +VALUES (2, N'{Alias}/site2', 1, 2) GO SET IDENTITY_INSERT [dbo].[Alias] OFF GO diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index ab2de131..d071f537 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -11,7 +11,6 @@ using System.Reflection; using Microsoft.Extensions.Hosting; using Oqtane.Modules; using Oqtane.Repository; -using Oqtane.Filters; using System.IO; using System.Runtime.Loader; using Oqtane.Services; @@ -25,7 +24,7 @@ namespace Oqtane.Server { public class Startup { - public IConfiguration Configuration { get; } + public IConfigurationRoot Configuration { get; } public Startup(IWebHostEnvironment env) { var builder = new ConfigurationBuilder() @@ -68,6 +67,7 @@ namespace Oqtane.Server // register scoped core services services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -101,7 +101,7 @@ namespace Oqtane.Server services.AddSingleton(); - services.AddDbContext(options => + services.AddDbContext(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection") .Replace("|DataDirectory|", AppDomain.CurrentDomain.GetData("DataDirectory").ToString()) )); @@ -143,10 +143,8 @@ namespace Oqtane.Server services.AddMvc().AddNewtonsoftJson(); - // register database install/upgrade filter - services.AddTransient(); - // register singleton scoped core services + services.AddSingleton(Configuration); services.AddSingleton(); services.AddSingleton(); @@ -237,7 +235,7 @@ namespace Oqtane.Server { services.AddSingleton(); - services.AddDbContext(options => + services.AddDbContext(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection") .Replace("|DataDirectory|", AppDomain.CurrentDomain.GetData("DataDirectory").ToString()) )); @@ -279,10 +277,8 @@ namespace Oqtane.Server services.AddMvc().AddNewtonsoftJson(); - // register database install/upgrade filter - services.AddTransient(); - // register singleton scoped core services + services.AddSingleton(Configuration); services.AddSingleton(); services.AddSingleton(); diff --git a/Oqtane.Server/appsettings.json b/Oqtane.Server/appsettings.json index 9e23f3b1..8138a005 100644 --- a/Oqtane.Server/appsettings.json +++ b/Oqtane.Server/appsettings.json @@ -1,5 +1,19 @@ { "ConnectionStrings": { - "DefaultConnection": "Data Source=(LocalDb)\\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\\Oqtane.mdf;Initial Catalog=Oqtane;Integrated Security=SSPI;" + "DefaultConnection": "" } } + + + + + + + + + + + + + + diff --git a/Oqtane.Server/wwwroot/css/site.css b/Oqtane.Server/wwwroot/css/site.css index 6774c2e9..193ec6f0 100644 --- a/Oqtane.Server/wwwroot/css/site.css +++ b/Oqtane.Server/wwwroot/css/site.css @@ -246,3 +246,13 @@ app { text-align: center; color: gray; } + +.loading { + background: rgba(0,0,0,0.2) url('../loading.gif') no-repeat 50% 50%; + width: 100%; + height: 100%; + position: fixed; + top: 0; + left: 0; + z-index: 999; +} \ No newline at end of file diff --git a/Oqtane.Server/wwwroot/loading.gif b/Oqtane.Server/wwwroot/loading.gif new file mode 100644 index 00000000..cc70a7a8 Binary files /dev/null and b/Oqtane.Server/wwwroot/loading.gif differ diff --git a/Oqtane.Shared/Models/GenericResponse.cs b/Oqtane.Shared/Models/GenericResponse.cs new file mode 100644 index 00000000..2281d79f --- /dev/null +++ b/Oqtane.Shared/Models/GenericResponse.cs @@ -0,0 +1,8 @@ +namespace Oqtane.Models +{ + public class GenericResponse + { + public bool Success { get; set; } + public string Message { get; set; } + } +}