-

+
-
+
-
Database Configuration
+
Database Configuration
-
Application Administrator
+
Application Administrator
-
+
-
- @((MarkupString)_message)
+
+ @((MarkupString) _message)
@@ -140,7 +140,7 @@
private void SetIntegratedSecurity(ChangeEventArgs e)
{
- if (Convert.ToBoolean((string)e.Value))
+ if (Convert.ToBoolean((string) e.Value))
{
_integratedSecurityDisplay = "display: none;";
}
@@ -172,10 +172,20 @@
else
{
connectionstring += "User ID=" + _username + ";Password=" + _password;
-
}
}
- Installation installation = await InstallationService.Install(connectionstring);
+
+ var config = new InstallConfig
+ {
+ ConnectionString = connectionstring,
+ HostUser = _hostUsername,
+ HostEmail = _hostEmail,
+ Password = _hostPassword,
+ IsMaster = true,
+ };
+
+ Installation installation = await InstallationService.Install(config);
+ //TODO: Should be moved to Database manager
if (installation.Success)
{
Site site = new Site();
@@ -208,4 +218,5 @@
_message = "
Please Enter All Fields And Ensure Passwords Match And Are Greater Than 5 Characters In Length
";
}
}
+
}
diff --git a/Oqtane.Server/Controllers/InstallationController.cs b/Oqtane.Server/Controllers/InstallationController.cs
index 763a570d..bb99f23f 100644
--- a/Oqtane.Server/Controllers/InstallationController.cs
+++ b/Oqtane.Server/Controllers/InstallationController.cs
@@ -1,16 +1,9 @@
-using DbUp;
-using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Oqtane.Models;
using Oqtane.Shared;
-using System;
-using System.Data.SqlClient;
-using System.IO;
-using System.Linq;
-using System.Reflection;
-using System.Threading;
+using Oqtane.Infrastructure;
using Oqtane.Infrastructure.Interfaces;
// ReSharper disable StringIndexOfIsCultureSpecific.1
@@ -22,155 +15,45 @@ namespace Oqtane.Controllers
{
private readonly IConfigurationRoot _config;
private readonly IInstallationManager _installationManager;
+ private readonly DatabaseManager _databaseManager;
- public InstallationController(IConfigurationRoot config, IInstallationManager installationManager)
+ public InstallationController(IConfigurationRoot config, IInstallationManager installationManager, DatabaseManager databaseManager)
{
_config = config;
_installationManager = installationManager;
+ _databaseManager = databaseManager;
}
// POST api/
[HttpPost]
- public Installation Post([FromBody] string connectionString)
+ public Installation Post([FromBody] InstallConfig config)
{
- var installation = new Installation { Success = false, Message = "" };
+ //TODO Security ????
+ var installation = new Installation {Success = false, Message = ""};
- if (ModelState.IsValid)
+ if (ModelState.IsValid && (!_databaseManager.IsInstalled || !config.IsMaster))
{
- bool master = false;
- string defaultconnectionstring = _config.GetConnectionString("DefaultConnection");
- if (string.IsNullOrEmpty(defaultconnectionstring) || connectionString == defaultconnectionstring)
- {
- master = true;
- }
+ bool master = config.IsMaster;
- bool exists = false;
- if (master)
- {
- exists = IsInstalled().Success;
- }
+ config.Alias = config.Alias ?? HttpContext.Request.Host.Value;
+ var result = DatabaseManager.InstallDatabase(config);
- if (!exists)
+ if (result.Success)
{
- string datadirectory = AppDomain.CurrentDomain.GetData("DataDirectory").ToString();
- connectionString = connectionString.Replace("|DataDirectory|", datadirectory);
-
- SqlConnection connection = new SqlConnection(connectionString);
- try
+ if (master)
{
- using (connection)
- {
- connection.Open();
- }
- exists = true;
- }
- catch
- {
- // database does not exist
+ _config.Reload();
}
- // 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)
- {
- installation.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 = "";
- if (master)
- {
- using (StreamReader reader = new StreamReader(Directory.GetCurrentDirectory() + "\\Scripts\\Master.sql"))
- {
- initializationScript = reader.ReadToEnd();
- }
- initializationScript = initializationScript.Replace("{ConnectionString}", connectionString.Replace(datadirectory, "|DataDirectory|"));
- 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)
- {
- installation.Message = result.Error.Message;
- }
- else
- {
- // update appsettings
- if (master)
- {
- 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();
- }
- installation.Success = true;
- }
- }
- }
- }
- else
- {
- installation.Message = "Application Is Already Installed";
+ installation.Success = true;
+ return installation;
}
+
+ installation.Message = result.Message;
+ return installation;
}
+
+ installation.Message = "Application Is Already Installed";
return installation;
}
@@ -178,133 +61,21 @@ namespace Oqtane.Controllers
[HttpGet("installed")]
public Installation IsInstalled()
{
- var installation = new Installation { Success = false, Message = "" };
+ var installation = new Installation {Success = false, Message = ""};
- string datadirectory = AppDomain.CurrentDomain.GetData("DataDirectory").ToString();
- string connectionString = _config.GetConnectionString("DefaultConnection");
- connectionString = connectionString.Replace("|DataDirectory|", datadirectory);
-
- if (!string.IsNullOrEmpty(connectionString))
- {
- SqlConnection connection = new SqlConnection(connectionString);
- try
- {
- using (connection)
- {
- connection.Open();
- }
- installation.Success = true;
- }
- catch
- {
- // database does not exist
- installation.Message = "Database Does Not Exist";
- }
- }
- else
- {
- installation.Message = "Connection String Has Not Been Specified In Oqtane.Server\\appsettings.json";
- }
-
- if (installation.Success)
- {
- var dbUpgradeConfig = DeployChanges.To.SqlDatabase(connectionString)
- .WithScript(new DbUp.Engine.SqlScript("Master.sql", ""));
- var dbUpgrade = dbUpgradeConfig.Build();
- installation.Success = !dbUpgrade.IsUpgradeRequired();
- if (!installation.Success)
- {
- installation.Message = "Master Installation Scripts Have Not Been Executed";
- }
- else
- {
- using (var db = new InstallationContext(connectionString))
- {
- ApplicationVersion version = db.ApplicationVersion.ToList().LastOrDefault();
- if (version == null || version.Version != Constants.Version)
- {
- version = new ApplicationVersion();
- version.Version = Constants.Version;
- version.CreatedOn = DateTime.UtcNow;
- db.ApplicationVersion.Add(version);
- db.SaveChanges();
- }
- }
-
- Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies()
- .Where(item => item.FullName.Contains(".Module.")).ToArray();
-
- // get tenants
- using (var db = new InstallationContext(connectionString))
- {
- foreach (Tenant tenant in db.Tenant.ToList())
- {
- connectionString = tenant.DBConnectionString;
- connectionString = connectionString.Replace("|DataDirectory|", datadirectory);
-
- // upgrade framework
- dbUpgradeConfig = DeployChanges.To.SqlDatabase(connectionString)
- .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly());
- 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 (Assembly assembly in assemblies)
- {
- InstallModule(assembly, connectionString);
- }
- }
- }
- }
- }
+ installation.Success = _databaseManager.IsInstalled;
+ installation.Message = _databaseManager.Message;
return installation;
}
- private void InstallModule(Assembly assembly, string connectionstring)
- {
- var dbUpgradeConfig = DeployChanges.To.SqlDatabase(connectionstring)
- .WithScriptsEmbeddedInAssembly(assembly); // scripts must be included as Embedded Resources
- 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
- }
- }
- }
-
[HttpGet("upgrade")]
[Authorize(Roles = Constants.HostRole)]
public Installation Upgrade()
{
- var installation = new Installation { Success = true, Message = "" };
+ var installation = new Installation {Success = true, Message = ""};
_installationManager.UpgradeFramework();
return installation;
}
}
-
- public class InstallationContext : DbContext
- {
- private readonly string _connectionString;
-
- public InstallationContext(string connectionString)
- {
- _connectionString = connectionString;
- }
-
- protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
- => optionsBuilder.UseSqlServer(_connectionString);
-
- public virtual DbSet ApplicationVersion { get; set; }
- public virtual DbSet Tenant { get; set; }
- }
}
diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs
index 19d0aaf9..d2bc94db 100644
--- a/Oqtane.Server/Controllers/UserController.cs
+++ b/Oqtane.Server/Controllers/UserController.cs
@@ -75,95 +75,107 @@ namespace Oqtane.Controllers
[HttpPost]
public async Task Post([FromBody] User user)
{
- User newUser = null;
-
if (ModelState.IsValid)
{
- // users created by non-administrators must be verified
- bool verified = !(!User.IsInRole(Constants.AdminRole) && user.Username != Constants.HostUser);
+ var newUser = await CreateUser(user);
+ return newUser;
+ }
- IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
- if (identityuser == null)
+ return null;
+ }
+
+ //TODO shoud be moved to another layer
+ private async Task CreateUser(User user)
+ {
+ User newUser = null;
+ // users created by non-administrators must be verified
+ bool verified = !(!User.IsInRole(Constants.AdminRole) && user.Username != Constants.HostUser);
+
+ IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
+ if (identityuser == null)
+ {
+ identityuser = new IdentityUser();
+ identityuser.UserName = user.Username;
+ identityuser.Email = user.Email;
+ identityuser.EmailConfirmed = verified;
+ var result = await _identityUserManager.CreateAsync(identityuser, user.Password);
+ if (result.Succeeded)
{
- identityuser = new IdentityUser();
- identityuser.UserName = user.Username;
- identityuser.Email = user.Email;
- identityuser.EmailConfirmed = verified;
- var result = await _identityUserManager.CreateAsync(identityuser, user.Password);
- if (result.Succeeded)
+ user.LastLoginOn = null;
+ user.LastIPAddress = "";
+ newUser = _users.AddUser(user);
+ if (!verified)
{
- user.LastLoginOn = null;
- user.LastIPAddress = "";
- newUser = _users.AddUser(user);
- if (!verified)
- {
- Notification notification = new Notification();
- notification.SiteId = user.SiteId;
- notification.FromUserId = null;
- notification.ToUserId = newUser.UserId;
- notification.ToEmail = "";
- notification.Subject = "User Account Verification";
- string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
- string url = HttpContext.Request.Scheme + "://" + _tenants.GetAlias().Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
- notification.Body = "Dear " + user.DisplayName + ",\n\nIn Order To Complete The Registration Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
- notification.ParentId = null;
- notification.CreatedOn = DateTime.UtcNow;
- notification.IsDelivered = false;
- notification.DeliveredOn = null;
- _notifications.AddNotification(notification);
- }
-
- // assign to host role if this is the host user ( initial installation )
- if (user.Username == Constants.HostUser)
- {
- int hostroleid = _roles.GetRoles(user.SiteId, true).Where(item => item.Name == Constants.HostRole).FirstOrDefault().RoleId;
- UserRole userrole = new UserRole();
- userrole.UserId = newUser.UserId;
- userrole.RoleId = hostroleid;
- userrole.EffectiveDate = null;
- userrole.ExpiryDate = null;
- _userRoles.AddUserRole(userrole);
- }
-
- // add folder for user
- Folder folder = _folders.GetFolder(user.SiteId, "Users\\");
- if (folder != null)
- {
- _folders.AddFolder(new Folder { SiteId = folder.SiteId, ParentId = folder.FolderId, Name = "My Folder", Path = folder.Path + newUser.UserId.ToString() + "\\", Order = 1, IsSystem = true,
- Permissions = "[{\"PermissionName\":\"Browse\",\"Permissions\":\"[" + newUser.UserId.ToString() + "]\"},{\"PermissionName\":\"View\",\"Permissions\":\"All Users\"},{\"PermissionName\":\"Edit\",\"Permissions\":\"[" + newUser.UserId.ToString() + "]\"}]" });
- }
+ Notification notification = new Notification();
+ notification.SiteId = user.SiteId;
+ notification.FromUserId = null;
+ notification.ToUserId = newUser.UserId;
+ notification.ToEmail = "";
+ notification.Subject = "User Account Verification";
+ string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
+ string url = HttpContext.Request.Scheme + "://" + _tenants.GetAlias().Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
+ notification.Body = "Dear " + user.DisplayName + ",\n\nIn Order To Complete The Registration Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
+ notification.ParentId = null;
+ notification.CreatedOn = DateTime.UtcNow;
+ notification.IsDelivered = false;
+ notification.DeliveredOn = null;
+ _notifications.AddNotification(notification);
}
- }
- else
- {
- var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, false);
- if (result.Succeeded)
- {
- newUser = _users.GetUser(user.Username);
- }
- }
- if (newUser != null && user.Username != Constants.HostUser)
- {
- // add auto assigned roles to user for site
- List roles = _roles.GetRoles(user.SiteId).Where(item => item.IsAutoAssigned).ToList();
- foreach (Role role in roles)
+ // assign to host role if this is the host user ( initial installation )
+ if (user.Username == Constants.HostUser)
{
+ int hostroleid = _roles.GetRoles(user.SiteId, true).Where(item => item.Name == Constants.HostRole).FirstOrDefault().RoleId;
UserRole userrole = new UserRole();
userrole.UserId = newUser.UserId;
- userrole.RoleId = role.RoleId;
+ userrole.RoleId = hostroleid;
userrole.EffectiveDate = null;
userrole.ExpiryDate = null;
_userRoles.AddUserRole(userrole);
}
- }
- if (newUser != null)
- {
- newUser.Password = ""; // remove sensitive information
- _logger.Log(user.SiteId, LogLevel.Information, this, LogFunction.Create, "User Added {User}", newUser);
+ // add folder for user
+ Folder folder = _folders.GetFolder(user.SiteId, "Users\\");
+ if (folder != null)
+ {
+ _folders.AddFolder(new Folder
+ {
+ SiteId = folder.SiteId, ParentId = folder.FolderId, Name = "My Folder", Path = folder.Path + newUser.UserId.ToString() + "\\", Order = 1, IsSystem = true,
+ Permissions = "[{\"PermissionName\":\"Browse\",\"Permissions\":\"[" + newUser.UserId.ToString() + "]\"},{\"PermissionName\":\"View\",\"Permissions\":\"All Users\"},{\"PermissionName\":\"Edit\",\"Permissions\":\"[" +
+ newUser.UserId.ToString() + "]\"}]"
+ });
+ }
}
}
+ else
+ {
+ var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, false);
+ if (result.Succeeded)
+ {
+ newUser = _users.GetUser(user.Username);
+ }
+ }
+
+ if (newUser != null && user.Username != Constants.HostUser)
+ {
+ // add auto assigned roles to user for site
+ List roles = _roles.GetRoles(user.SiteId).Where(item => item.IsAutoAssigned).ToList();
+ foreach (Role role in roles)
+ {
+ UserRole userrole = new UserRole();
+ userrole.UserId = newUser.UserId;
+ userrole.RoleId = role.RoleId;
+ userrole.EffectiveDate = null;
+ userrole.ExpiryDate = null;
+ _userRoles.AddUserRole(userrole);
+ }
+ }
+
+ if (newUser != null)
+ {
+ newUser.Password = ""; // remove sensitive information
+ _logger.Log(user.SiteId, LogLevel.Information, this, LogFunction.Create, "User Added {User}", newUser);
+ }
return newUser;
}
diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs
index e2bf622e..aef47901 100644
--- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs
+++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs
@@ -115,7 +115,7 @@ namespace Microsoft.Extensions.DependencyInjection
foreach (var file in assembliesFolder.EnumerateFiles($"*.{pattern}.*.dll"))
{
// check if assembly is already loaded
- var assembly = Assemblies.FirstOrDefault(a => a.Location == file.FullName);
+ var assembly = Assemblies.FirstOrDefault(a =>!a.IsDynamic && a.Location == file.FullName);
if (assembly == null)
{
// load assembly from stream to prevent locking file ( as long as dependencies are in /bin they will load as well )
diff --git a/Oqtane.Server/Infrastructure/DatabaseManager.cs b/Oqtane.Server/Infrastructure/DatabaseManager.cs
new file mode 100644
index 00000000..bccbf5b8
--- /dev/null
+++ b/Oqtane.Server/Infrastructure/DatabaseManager.cs
@@ -0,0 +1,367 @@
+using System;
+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.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Newtonsoft.Json;
+using Oqtane.Controllers;
+using Oqtane.Models;
+using Oqtane.Repository;
+using Oqtane.Shared;
+using File = System.IO.File;
+
+namespace Oqtane.Infrastructure
+{
+ public class DatabaseManager
+ {
+ private readonly IConfigurationRoot _config;
+ private readonly IServiceScopeFactory _serviceScopeFactory;
+ private bool _isInstalled;
+
+ public DatabaseManager(IConfigurationRoot config, IServiceScopeFactory serviceScopeFactory)
+ {
+ _config = config;
+ _serviceScopeFactory = serviceScopeFactory;
+ }
+
+ public string Message { get; set; }
+
+ public bool IsInstalled
+ {
+ get
+ {
+ if (!_isInstalled) _isInstalled = CheckInstallState();
+
+ return _isInstalled;
+ }
+ set => _isInstalled = value;
+ }
+
+ private bool CheckInstallState()
+ {
+ var defaultConnectionString = _config.GetConnectionString("DefaultConnection");
+ var result = !string.IsNullOrEmpty(defaultConnectionString);
+ if (result)
+ {
+ using (var scope = _serviceScopeFactory.CreateScope())
+ {
+ var dbContext = scope.ServiceProvider.GetRequiredService();
+ result = dbContext.Database.CanConnect();
+ }
+ 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"));
+
+ result = !dbUpgradeConfig.Build().IsUpgradeRequired();
+ if (!result) Message = "Master Installation Scripts Have Not Been Executed";
+ }
+ else
+ {
+ Message = "Database is not avaiable";
+ }
+ }
+ else
+ {
+ Message = "Connection string is empty";
+ }
+
+ return result;
+ }
+
+
+ public static string NormalizeConnectionString(string connectionString, string dataDirectory)
+ {
+ connectionString = connectionString
+ .Replace("|DataDirectory|", dataDirectory);
+ //.Replace(@"\", @"\\");
+ return connectionString;
+ }
+
+ 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))
+ {
+ result = new Installation
+ {
+ Success = false,
+ Message = "Connection string is empty",
+ };
+ return result;
+ }
+
+ result = MasterMigration(connectionString, alias, result, installConfig.IsMaster);
+ if (installConfig.IsMaster && result.Success)
+ {
+ WriteVersionInfo(connectionString);
+ TenantMigration(connectionString, dataDirectory);
+ UpdateOqtaneSettings(connectionString);
+ AddOrUpdateAppSetting("Oqtane:DefaultAlias", alias);
+ }
+
+ 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};
+
+ 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))
+ {
+ dbc.Database.EnsureCreated();
+ }
+ }
+ catch (Exception e)
+ {
+ result = new Installation
+ {
+ Success = false,
+ Message = e.Message,
+ };
+ Console.WriteLine(e);
+ return result;
+ }
+
+ var dbUpgradeConfig = DeployChanges
+ .To
+ .SqlDatabase(connectionString)
+ .WithVariable("ConnectionString", connectionString)
+ .WithVariable("Alias", alias)
+ .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), s => s.Contains("Master") && master || s.Contains("Tenant"))
+ ;
+
+ 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;
+ }
+
+ return result;
+ }
+
+ private static void ModuleMigration(Assembly assembly, string connectionString)
+ {
+ var dbUpgradeConfig = DeployChanges.To.SqlDatabase(connectionString)
+ .WithScriptsEmbeddedInAssembly(assembly); // scripts must be included as Embedded Resources
+ 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
+ }
+ }
+ }
+
+ private static void WriteVersionInfo(string connectionString)
+ {
+ using (var db = new InstallationContext(connectionString))
+ {
+ var version = db.ApplicationVersion.ToList().LastOrDefault();
+ if (version == null || version.Version != Constants.Version)
+ {
+ version = new ApplicationVersion {Version = Constants.Version, CreatedOn = DateTime.UtcNow};
+ db.ApplicationVersion.Add(version);
+ db.SaveChanges();
+ }
+ }
+ }
+
+ private static void TenantMigration(string connectionString, string dataDirectory)
+ {
+ var assemblies = AppDomain.CurrentDomain.GetAssemblies()
+ .Where(item => item.FullName != null && item.FullName.Contains(".Module.")).ToArray();
+
+ // get tenants
+ using (var db = new InstallationContext(connectionString))
+ {
+ foreach (var tenant in db.Tenant.ToList())
+ {
+ 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);
+ }
+ }
+ }
+
+ public static void UpdateOqtaneSettings(string connectionString)
+ {
+ AddOrUpdateAppSetting("ConnectionStrings:DefaultConnection", connectionString);
+ //AddOrUpdateAppSetting("Oqtane:DefaultAlias", connectionString);
+ }
+
+
+ public static void AddOrUpdateAppSetting(string sectionPathKey, T value)
+ {
+ try
+ {
+ var filePath = Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json");
+ var json = File.ReadAllText(filePath);
+ dynamic jsonObj = JsonConvert.DeserializeObject(json);
+
+ SetValueRecursively(sectionPathKey, jsonObj, value);
+
+ string output = JsonConvert.SerializeObject(jsonObj, Formatting.Indented);
+ File.WriteAllText(filePath, output);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("Error writing app settings | {0}", ex);
+ }
+ }
+
+ private static void SetValueRecursively(string sectionPathKey, dynamic jsonObj, T value)
+ {
+ // split the string at the first ':' character
+ var remainingSections = sectionPathKey.Split(":", 2);
+
+ var currentSection = remainingSections[0];
+ if (remainingSections.Length > 1)
+ {
+ // continue with the procress, moving down the tree
+ var nextSection = remainingSections[1];
+ SetValueRecursively(nextSection, jsonObj[currentSection], value);
+ }
+ else
+ {
+ // we've got to the end of the tree, set the value
+ jsonObj[currentSection] = value;
+ }
+ }
+
+ public void StartupMigration()
+ {
+ var defaultConnectionString = _config.GetConnectionString("DefaultConnection");
+ var defaultAlias = _config.GetSection("Oqtane").GetValue("DefaultAlias", string.Empty);
+
+ // if no values specified, fallback to IDE installer
+ if (string.IsNullOrEmpty(defaultConnectionString) || string.IsNullOrEmpty(defaultAlias))
+ {
+ IsInstalled = false;
+ return;
+ }
+
+ var result = MasterMigration(defaultConnectionString, defaultAlias, null, true);
+ IsInstalled = result.Success;
+ if (_isInstalled)
+ BuildDefaultSite();
+ }
+
+ public void BuildDefaultSite()
+ {
+ 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 site = new Site
+ {
+ TenantId = -1,
+ Name = "Default Site",
+ LogoFileId = null,
+ DefaultThemeType = Constants.DefaultTheme,
+ DefaultLayoutType = Constants.DefaultLayout,
+ DefaultContainerType = Constants.DefaultContainer,
+ };
+ site = siteRepository.AddSite(site);
+
+ var user = new User
+ {
+ SiteId = site.SiteId,
+ Username = Constants.HostUser,
+ //TODO Decide default password or throw exception ??
+ Password = _config.GetSection("Oqtane").GetValue("DefaultPassword", "oQtane123"),
+ Email = _config.GetSection("Oqtane").GetValue("DefaultEmail", "nobody@cortonso.com"),
+ DisplayName = Constants.HostUser,
+ };
+ CreateHostUser(folders, userRoles, roles, users, identityUserManager, user);
+ }
+ }
+
+
+ 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, "Users\\");
+ if (folder != null)
+ folderRepository.AddFolder(new Folder
+ {
+ SiteId = folder.SiteId, ParentId = folder.FolderId, Name = "My Folder", Path = folder.Path + newUser.UserId + "\\", Order = 1, IsSystem = true,
+ Permissions = "[{\"PermissionName\":\"Browse\",\"Permissions\":\"[" + newUser.UserId + "]\"},{\"PermissionName\":\"View\",\"Permissions\":\"All Users\"},{\"PermissionName\":\"Edit\",\"Permissions\":\"[" +
+ newUser.UserId + "]\"}]",
+ });
+ }
+ }
+ }
+}
diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj
index 32847187..7471c460 100644
--- a/Oqtane.Server/Oqtane.Server.csproj
+++ b/Oqtane.Server/Oqtane.Server.csproj
@@ -27,14 +27,10 @@
-
-
-
-
-
-
-
-
+
+
+
+
diff --git a/Oqtane.Server/Program.cs b/Oqtane.Server/Program.cs
index b440e758..6402cf5e 100644
--- a/Oqtane.Server/Program.cs
+++ b/Oqtane.Server/Program.cs
@@ -4,6 +4,8 @@ using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Blazor.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore;
+using Microsoft.Extensions.DependencyInjection;
+using Oqtane.Infrastructure;
namespace Oqtane.Server
{
@@ -12,7 +14,14 @@ namespace Oqtane.Server
#if DEBUG || RELEASE
public static void Main(string[] args)
{
- CreateHostBuilder(args).Build().Run();
+ var host = CreateHostBuilder(args).Build();
+ using (var serviceScope = host.Services.GetRequiredService().CreateScope())
+ {
+ var manager = serviceScope.ServiceProvider.GetService();
+ manager.StartupMigration();
+ }
+ //DatabaseManager.StartupMigration();
+ host.Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
diff --git a/Oqtane.Server/Repository/Context/DBContextBase.cs b/Oqtane.Server/Repository/Context/DBContextBase.cs
index e6aceed9..247006bd 100644
--- a/Oqtane.Server/Repository/Context/DBContextBase.cs
+++ b/Oqtane.Server/Repository/Context/DBContextBase.cs
@@ -22,21 +22,11 @@ namespace Oqtane.Repository
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(_tenant.DBConnectionString
- .Replace("|DataDirectory|", AppDomain.CurrentDomain.GetData("DataDirectory").ToString())
+ .Replace("|DataDirectory|", AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString())
);
base.OnConfiguring(optionsBuilder);
}
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
- base.OnModelCreating(modelBuilder);
-
- if (_tenant.DBSchema != "")
- {
- modelBuilder.HasDefaultSchema(_tenant.DBSchema);
- }
- }
-
public override int SaveChanges()
{
ChangeTracker.DetectChanges();
diff --git a/Oqtane.Server/Repository/Context/InstallationContext.cs b/Oqtane.Server/Repository/Context/InstallationContext.cs
new file mode 100644
index 00000000..7b971a16
--- /dev/null
+++ b/Oqtane.Server/Repository/Context/InstallationContext.cs
@@ -0,0 +1,23 @@
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.EntityFrameworkCore;
+using Oqtane.Models;
+
+namespace Oqtane.Repository
+{
+
+ public class InstallationContext : DbContext
+ {
+ private readonly string _connectionString;
+
+ public InstallationContext(string connectionString)
+ {
+ _connectionString = connectionString;
+ }
+
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ => optionsBuilder.UseSqlServer(_connectionString);
+
+ public virtual DbSet ApplicationVersion { get; set; }
+ public virtual DbSet Tenant { get; set; }
+ }
+}
diff --git a/Oqtane.Server/Repository/SiteTemplateRepository.cs b/Oqtane.Server/Repository/SiteTemplateRepository.cs
index 4d3b5f7b..c88934fd 100644
--- a/Oqtane.Server/Repository/SiteTemplateRepository.cs
+++ b/Oqtane.Server/Repository/SiteTemplateRepository.cs
@@ -41,7 +41,7 @@ namespace Oqtane.Repository
var siteTemplateObject = ActivatorUtilities.CreateInstance(_serviceProvider, siteTemplateType);
siteTemplate = new SiteTemplate
{
- Name = (string)siteTemplateType.GetProperty("Name").GetValue(siteTemplateObject),
+ Name = (string)siteTemplateType.GetProperty("Name")?.GetValue(siteTemplateObject),
TypeName = siteTemplateType.AssemblyQualifiedName
};
siteTemplates.Add(siteTemplate);
diff --git a/Oqtane.Server/Repository/TenantRepository.cs b/Oqtane.Server/Repository/TenantRepository.cs
index 19acf9fa..4af682d6 100644
--- a/Oqtane.Server/Repository/TenantRepository.cs
+++ b/Oqtane.Server/Repository/TenantRepository.cs
@@ -49,10 +49,14 @@ namespace Oqtane.Repository
}
public void DeleteTenant(int tenantId)
- {
+ {
Tenant tenant = _db.Tenant.Find(tenantId);
- _db.Tenant.Remove(tenant);
- _db.SaveChanges();
+ if (tenant != null)
+ {
+ _db.Tenant.Remove(tenant);
+ _db.SaveChanges();
+ }
+
_cache.Remove("tenants");
}
}
diff --git a/Oqtane.Server/Repository/TenantResolver.cs b/Oqtane.Server/Repository/TenantResolver.cs
index 9760c4e6..8a5b85c4 100644
--- a/Oqtane.Server/Repository/TenantResolver.cs
+++ b/Oqtane.Server/Repository/TenantResolver.cs
@@ -29,43 +29,41 @@ namespace Oqtane.Repository
{
aliasName = accessor.HttpContext.Request.Host.Value;
string path = accessor.HttpContext.Request.Path.Value;
- string[] segments = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
+ 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
+ else // background processes can pass in an alias using the SiteState service
{
- if (siteState != null)
- {
- aliasId = siteState.Alias.AliasId;
- }
+ aliasId = siteState?.Alias?.AliasId ?? -1;
}
// get the alias and tenant
- if (aliasId != -1 || aliasName != "")
+ IEnumerable aliases = aliasRepository.GetAliases().ToList(); // cached
+ if (aliasId != -1)
{
- IEnumerable aliases = aliasRepository.GetAliases(); // cached
- IEnumerable tenants = tenantRepository.GetTenants(); // cached
+ _alias = aliases.FirstOrDefault(item => item.AliasId == aliasId);
+ }
+ 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);
+ }
- if (aliasId != -1)
- {
- _alias = aliases.FirstOrDefault(item => item.AliasId == aliasId);
- }
- else
- {
- _alias = aliases.FirstOrDefault(item => item.Name == aliasName);
- }
- if (_alias != null)
- {
- _tenant = tenants.FirstOrDefault(item => item.TenantId == _alias.TenantId);
- }
+ if (_alias != null)
+ {
+ IEnumerable tenants = tenantRepository.GetTenants(); // cached
+ _tenant = tenants.FirstOrDefault(item => item.TenantId == _alias.TenantId);
}
}
diff --git a/Oqtane.Server/Scripts/Master.sql b/Oqtane.Server/Scripts/Master.00.00.00.sql
similarity index 96%
rename from Oqtane.Server/Scripts/Master.sql
rename to Oqtane.Server/Scripts/Master.00.00.00.sql
index 45e474c7..e1b64042 100644
--- a/Oqtane.Server/Scripts/Master.sql
+++ b/Oqtane.Server/Scripts/Master.00.00.00.sql
@@ -123,7 +123,7 @@ 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'', 1, '', getdate(), '', getdate())
+VALUES (1, N'Master', N'$ConnectionString$', N'', 1, '', getdate(), '', getdate())
GO
SET IDENTITY_INSERT [dbo].[Tenant] OFF
GO
@@ -131,7 +131,7 @@ 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())
+VALUES (1, N'$Alias$', 1, 1, '', getdate(), '', getdate())
GO
SET IDENTITY_INSERT [dbo].[Alias] OFF
GO
diff --git a/Oqtane.Server/Scripts/Master.00.00.01.sql b/Oqtane.Server/Scripts/Master.00.00.01.sql
new file mode 100644
index 00000000..02943e96
--- /dev/null
+++ b/Oqtane.Server/Scripts/Master.00.00.01.sql
@@ -0,0 +1,2 @@
+alter table Tenant drop column DBSchema
+go
diff --git a/Oqtane.Server/Scripts/00.00.00.sql b/Oqtane.Server/Scripts/Tenant.00.00.00.sql
similarity index 96%
rename from Oqtane.Server/Scripts/00.00.00.sql
rename to Oqtane.Server/Scripts/Tenant.00.00.00.sql
index 666fe978..5157b401 100644
--- a/Oqtane.Server/Scripts/00.00.00.sql
+++ b/Oqtane.Server/Scripts/Tenant.00.00.00.sql
@@ -404,7 +404,7 @@ Create indexes
*/
-CREATE UNIQUE NONCLUSTERED INDEX IX_Setting ON dbo.Setting
+CREATE UNIQUE NONCLUSTERED INDEX IX_Setting ON [dbo].Setting
(
EntityName,
EntityId,
@@ -412,13 +412,13 @@ CREATE UNIQUE NONCLUSTERED INDEX IX_Setting ON dbo.Setting
) ON [PRIMARY]
GO
-CREATE UNIQUE NONCLUSTERED INDEX IX_User ON dbo.[User]
+CREATE UNIQUE NONCLUSTERED INDEX IX_User ON [dbo].[User]
(
Username
) ON [PRIMARY]
GO
-CREATE UNIQUE NONCLUSTERED INDEX IX_Permission ON dbo.Permission
+CREATE UNIQUE NONCLUSTERED INDEX IX_Permission ON [dbo].Permission
(
SiteId,
EntityName,
@@ -429,7 +429,7 @@ CREATE UNIQUE NONCLUSTERED INDEX IX_Permission ON dbo.Permission
) ON [PRIMARY]
GO
-CREATE UNIQUE NONCLUSTERED INDEX IX_Page ON dbo.Page
+CREATE UNIQUE NONCLUSTERED INDEX IX_Page ON [dbo].Page
(
SiteId,
[Path],
@@ -437,14 +437,14 @@ CREATE UNIQUE NONCLUSTERED INDEX IX_Page ON dbo.Page
) ON [PRIMARY]
GO
-CREATE UNIQUE NONCLUSTERED INDEX IX_UserRole ON dbo.UserRole
+CREATE UNIQUE NONCLUSTERED INDEX IX_UserRole ON [dbo].UserRole
(
RoleId,
UserId
) ON [PRIMARY]
GO
-CREATE UNIQUE NONCLUSTERED INDEX IX_Folder ON dbo.Folder
+CREATE UNIQUE NONCLUSTERED INDEX IX_Folder ON [dbo].Folder
(
SiteId,
[Path]
diff --git a/Oqtane.Server/Scripts/00.00.01.sql b/Oqtane.Server/Scripts/Tenant.00.00.01.sql
similarity index 100%
rename from Oqtane.Server/Scripts/00.00.01.sql
rename to Oqtane.Server/Scripts/Tenant.00.00.01.sql
diff --git a/Oqtane.Server/SilentInstall.json b/Oqtane.Server/SilentInstall.json
new file mode 100644
index 00000000..cf3e04b4
--- /dev/null
+++ b/Oqtane.Server/SilentInstall.json
@@ -0,0 +1,7 @@
+{
+ "Alias" : "",
+ "DefaultConnection" : "",
+ "HostUser" : "host",
+ "Password" : "",
+ "HostEmail" : ""
+}
diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs
index 07a47ee7..190e4adb 100644
--- a/Oqtane.Server/Startup.cs
+++ b/Oqtane.Server/Startup.cs
@@ -19,9 +19,12 @@ using Oqtane.Infrastructure.Interfaces;
using Oqtane.Repository;
using Oqtane.Security;
using Oqtane.Services;
-// DO NOT REMOVE - needed for client-side Blazor
using Oqtane.Shared;
+
+#if WASM
+// DO NOT REMOVE - needed for client-side Blazor
using Microsoft.AspNetCore.ResponseCompression;
+#endif
namespace Oqtane
{
@@ -113,7 +116,7 @@ namespace Oqtane
services.AddDbContext(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")
- .Replace("|DataDirectory|", AppDomain.CurrentDomain.GetData("DataDirectory").ToString())
+ .Replace("|DataDirectory|", AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString())
));
services.AddDbContext(options => { });
@@ -160,7 +163,8 @@ namespace Oqtane
services.AddSingleton(Configuration);
services.AddSingleton();
services.AddSingleton();
-
+ services.AddSingleton();
+
// register transient scoped core services
services.AddTransient();
services.AddTransient();
@@ -203,6 +207,8 @@ namespace Oqtane
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Oqtane", Version = "v1" });
});
+
+
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
diff --git a/Oqtane.Server/appsettings.json b/Oqtane.Server/appsettings.json
index 7f07c90a..9e226a4d 100644
--- a/Oqtane.Server/appsettings.json
+++ b/Oqtane.Server/appsettings.json
@@ -1,5 +1,9 @@
{
"ConnectionStrings": {
"DefaultConnection": ""
+ },
+ "Oqtane": {
+ "DefaultAlias": "",
+ "DefaultPassword": ""
}
}
diff --git a/Oqtane.Shared/Models/Tenant.cs b/Oqtane.Shared/Models/Tenant.cs
index bf6c4627..3f0a518a 100644
--- a/Oqtane.Shared/Models/Tenant.cs
+++ b/Oqtane.Shared/Models/Tenant.cs
@@ -7,9 +7,7 @@ namespace Oqtane.Models
public int TenantId { get; set; }
public string Name { get; set; }
public string DBConnectionString { get; set; }
- public string DBSchema { 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/InstallConfig.cs b/Oqtane.Shared/Shared/InstallConfig.cs
new file mode 100644
index 00000000..f7f065b4
--- /dev/null
+++ b/Oqtane.Shared/Shared/InstallConfig.cs
@@ -0,0 +1,12 @@
+namespace Oqtane.Shared
+{
+ 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 HostEmail { get; set; }
+ public bool IsMaster { get; set; }
+ }
+}