Database Manager

done:
+ master.sql as resource
+ implemented incremental database changes also for Master
+ dbUp sql script variables implemented
+ improved database handling and creation code
+ simpified database creation
+ almost all Database and Tenant creation moved to DatabaseManager.cs (rest code marked with TODO)
+ Unattended install of master can be performed by settings in appsettings.json
+ Improved IsInstalled checking
+ Removed DBSchema field from Tenant
+ Default database and site creation moved to Program.Main
This commit is contained in:
Pavel Vesely 2020-03-25 15:30:16 +01:00
parent 744782df7a
commit 940cdcb349
26 changed files with 726 additions and 525 deletions

View File

@ -164,7 +164,7 @@ else
_tenantid = (string)e.Value; _tenantid = (string)e.Value;
if (_tenantid != "-1") if (_tenantid != "-1")
{ {
Tenant tenant = _tenants.Where(item => item.TenantId == int.Parse(_tenantid)).FirstOrDefault(); Tenant tenant = _tenants.FirstOrDefault(item => item.TenantId == int.Parse(_tenantid));
if (tenant != null) if (tenant != null)
{ {
_isinitialized = tenant.IsInitialized; _isinitialized = tenant.IsInitialized;
@ -273,10 +273,13 @@ else
if (user != null) if (user != null)
{ {
Tenant tenant = _tenants.FirstOrDefault(item => item.TenantId == int.Parse(_tenantid)); Tenant tenant = _tenants.FirstOrDefault(item => item.TenantId == int.Parse(_tenantid));
if (tenant != null)
{
tenant.IsInitialized = true; tenant.IsInitialized = true;
await TenantService.UpdateTenantAsync(tenant); await TenantService.UpdateTenantAsync(tenant);
} }
} }
}
await Log(aliases[0], LogLevel.Information, "", null, "Site Created {Site}", site); await Log(aliases[0], LogLevel.Information, "", null, "Site Created {Site}", site);
Uri uri = new Uri(NavigationManager.Uri); Uri uri = new Uri(NavigationManager.Uri);

View File

@ -67,14 +67,6 @@
<input type="password" class="form-control" @bind="@password" /> <input type="password" class="form-control" @bind="@password" />
</td> </td>
</tr> </tr>
<tr>
<td>
<label class="control-label">Schema: </label>
</td>
<td>
<input class="form-control" @bind="@schema" />
</td>
</tr>
</table> </table>
<button type="button" class="btn btn-success" @onclick="SaveTenant">Save</button> <button type="button" class="btn btn-success" @onclick="SaveTenant">Save</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">Cancel</NavLink> <NavLink class="btn btn-secondary" href="@NavigateUrl()">Cancel</NavLink>
@ -88,7 +80,6 @@
string database = "Oqtane-" + DateTime.UtcNow.ToString("yyyyMMddHHmm"); string database = "Oqtane-" + DateTime.UtcNow.ToString("yyyyMMddHHmm");
string username = ""; string username = "";
string password = ""; string password = "";
string schema = "";
string integratedsecurity = "display: none;"; string integratedsecurity = "display: none;";
private void SetIntegratedSecurity(ChangeEventArgs e) private void SetIntegratedSecurity(ChangeEventArgs e)
@ -109,32 +100,41 @@
{ {
ShowProgressIndicator(); ShowProgressIndicator();
string connectionstring = ""; string connectionString = "";
if (type == "LocalDB") if (type == "LocalDB")
{ {
connectionstring = "Data Source=" + server + ";AttachDbFilename=|DataDirectory|\\" + database + ".mdf;Initial Catalog=" + database + ";Integrated Security=SSPI;"; connectionString = "Data Source=" + server + ";AttachDbFilename=|DataDirectory|\\" + database + ".mdf;Initial Catalog=" + database + ";Integrated Security=SSPI;";
} }
else else
{ {
connectionstring = "Data Source=" + server + ";Initial Catalog=" + database + ";"; connectionString = "Data Source=" + server + ";Initial Catalog=" + database + ";";
if (integratedsecurity == "display: none;") if (integratedsecurity == "display: none;")
{ {
connectionstring += "Integrated Security=SSPI;"; connectionString += "Integrated Security=SSPI;";
} }
else else
{ {
connectionstring += "User ID=" + username + ";Password=" + password; connectionString += "User ID=" + username + ";Password=" + password;
} }
} }
Installation installation = await InstallationService.Install(connectionstring);
var config = new InstallConfig
{
IsMaster = false,
ConnectionString = connectionString,
};
Installation installation = await InstallationService.Install(config);
if (installation.Success) if (installation.Success)
{ {
Tenant tenant = new Tenant(); //TODO : Move to Database Manager
tenant.Name = name; Tenant tenant = new Tenant
tenant.DBConnectionString = connectionstring; {
tenant.DBSchema = schema; Name = name,
tenant.IsInitialized = false; DBConnectionString = connectionString,
IsInitialized = false
};
await TenantService.AddTenantAsync(tenant); await TenantService.AddTenantAsync(tenant);
await logger.LogInformation("Tenant Created {Tenant}", tenant); await logger.LogInformation("Tenant Created {Tenant}", tenant);

View File

@ -20,14 +20,7 @@
<input class="form-control" @bind="@connectionstring" /> <input class="form-control" @bind="@connectionstring" />
</td> </td>
</tr> </tr>
<tr>
<td>
<label class="control-label">Schema: </label>
</td>
<td>
<input class="form-control" @bind="@schema" />
</td>
</tr>
</table> </table>
<button type="button" class="btn btn-success" @onclick="SaveTenant">Save</button> <button type="button" class="btn btn-success" @onclick="SaveTenant">Save</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">Cancel</NavLink> <NavLink class="btn btn-secondary" href="@NavigateUrl()">Cancel</NavLink>
@ -38,7 +31,6 @@
int tenantid; int tenantid;
string name = ""; string name = "";
string connectionstring = ""; string connectionstring = "";
string schema = "";
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
@ -50,7 +42,6 @@
{ {
name = tenant.Name; name = tenant.Name;
connectionstring = tenant.DBConnectionString; connectionstring = tenant.DBConnectionString;
schema = tenant.DBSchema;
} }
} }
catch (Exception ex) catch (Exception ex)
@ -70,7 +61,6 @@
{ {
tenant.Name = name; tenant.Name = name;
tenant.DBConnectionString = connectionstring; tenant.DBConnectionString = connectionstring;
tenant.DBSchema = schema;
await TenantService.UpdateTenantAsync(tenant); await TenantService.UpdateTenantAsync(tenant);
await logger.LogInformation("Tenant Saved {TenantId}", tenantid); await logger.LogInformation("Tenant Saved {TenantId}", tenantid);

View File

@ -19,24 +19,21 @@ namespace Oqtane.Services
_navigationManager = navigationManager; _navigationManager = navigationManager;
} }
private string Apiurl private string ApiUrl => CreateApiUrl(_siteState.Alias, _navigationManager.Uri, "Installation");
{
get { return CreateApiUrl(_siteState.Alias, _navigationManager.Uri, "Installation"); }
}
public async Task<Installation> IsInstalled() public async Task<Installation> IsInstalled()
{ {
return await _http.GetJsonAsync<Installation>(Apiurl + "/installed"); return await _http.GetJsonAsync<Installation>(ApiUrl + "/installed");
} }
public async Task<Installation> Install(string connectionstring) public async Task<Installation> Install(InstallConfig config)
{ {
return await _http.PostJsonAsync<Installation>(Apiurl, connectionstring); return await _http.PostJsonAsync<Installation>(ApiUrl, config);
} }
public async Task<Installation> Upgrade() public async Task<Installation> Upgrade()
{ {
return await _http.GetJsonAsync<Installation>(Apiurl + "/upgrade"); return await _http.GetJsonAsync<Installation>(ApiUrl + "/upgrade");
} }
} }
} }

View File

@ -1,12 +1,13 @@
using Oqtane.Models; using Oqtane.Models;
using System.Threading.Tasks; using System.Threading.Tasks;
using Oqtane.Shared;
namespace Oqtane.Services namespace Oqtane.Services
{ {
public interface IInstallationService public interface IInstallationService
{ {
Task<Installation> IsInstalled(); Task<Installation> IsInstalled();
Task<Installation> Install(string connectionstring); Task<Installation> Install(InstallConfig config);
Task<Installation> Upgrade(); Task<Installation> Upgrade();
} }
} }

View File

@ -7,13 +7,13 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="mx-auto text-center"> <div class="mx-auto text-center">
<img src="oqtane.png" /> <img src="oqtane.png"/>
</div> </div>
</div> </div>
<hr class="app-rule" /> <hr class="app-rule"/>
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col text-center"> <div class="col text-center">
<h2>Database Configuration</h2><br /> <h2>Database Configuration</h2><br/>
<table class="form-group" cellpadding="4" cellspacing="4" style="margin: auto;"> <table class="form-group" cellpadding="4" cellspacing="4" style="margin: auto;">
<tbody> <tbody>
<tr> <tr>
@ -32,7 +32,7 @@
<label class="control-label" style="font-weight: bold">Server: </label> <label class="control-label" style="font-weight: bold">Server: </label>
</td> </td>
<td> <td>
<input type="text" class="form-control" @bind="@_serverName" /> <input type="text" class="form-control" @bind="@_serverName"/>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -40,7 +40,7 @@
<label class="control-label" style="font-weight: bold">Database: </label> <label class="control-label" style="font-weight: bold">Database: </label>
</td> </td>
<td> <td>
<input type="text" class="form-control" @bind="@_databaseName" /> <input type="text" class="form-control" @bind="@_databaseName"/>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -59,7 +59,7 @@
<label class="control-label" style="font-weight: bold">Username: </label> <label class="control-label" style="font-weight: bold">Username: </label>
</td> </td>
<td> <td>
<input type="text" class="form-control" @bind="@_username" /> <input type="text" class="form-control" @bind="@_username"/>
</td> </td>
</tr> </tr>
<tr style="@_integratedSecurityDisplay"> <tr style="@_integratedSecurityDisplay">
@ -67,14 +67,14 @@
<label class="control-label" style="font-weight: bold">Password: </label> <label class="control-label" style="font-weight: bold">Password: </label>
</td> </td>
<td> <td>
<input type="password" class="form-control" @bind="@_password" /> <input type="password" class="form-control" @bind="@_password"/>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="col text-center"> <div class="col text-center">
<h2>Application Administrator</h2><br /> <h2>Application Administrator</h2><br/>
<table class="form-group" cellpadding="4" cellspacing="4" style="margin: auto;"> <table class="form-group" cellpadding="4" cellspacing="4" style="margin: auto;">
<tbody> <tbody>
<tr> <tr>
@ -82,7 +82,7 @@
<label class="control-label" style="font-weight: bold">Username: </label> <label class="control-label" style="font-weight: bold">Username: </label>
</td> </td>
<td> <td>
<input type="text" class="form-control" @bind="@_hostUsername" readonly /> <input type="text" class="form-control" @bind="@_hostUsername" readonly/>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -90,7 +90,7 @@
<label class="control-label" style="font-weight: bold">Password: </label> <label class="control-label" style="font-weight: bold">Password: </label>
</td> </td>
<td> <td>
<input type="password" class="form-control" @bind="@_hostPassword" /> <input type="password" class="form-control" @bind="@_hostPassword"/>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -98,7 +98,7 @@
<label class="control-label" style="font-weight: bold">Confirm: </label> <label class="control-label" style="font-weight: bold">Confirm: </label>
</td> </td>
<td> <td>
<input type="password" class="form-control" @bind="@_confirmPassword" /> <input type="password" class="form-control" @bind="@_confirmPassword"/>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -106,18 +106,18 @@
<label class="control-label" style="font-weight: bold">Email: </label> <label class="control-label" style="font-weight: bold">Email: </label>
</td> </td>
<td> <td>
<input type="text" class="form-control" @bind="@_hostEmail" /> <input type="text" class="form-control" @bind="@_hostEmail"/>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
<hr class="app-rule" /> <hr class="app-rule"/>
<div class="row"> <div class="row">
<div class="mx-auto text-center"> <div class="mx-auto text-center">
<button type="button" class="btn btn-success" @onclick="Install">Install Now</button><br /><br /> <button type="button" class="btn btn-success" @onclick="Install">Install Now</button><br/><br/>
@((MarkupString)_message) @((MarkupString) _message)
</div> </div>
<div class="app-progress-indicator" style="@_loadingDisplay"></div> <div class="app-progress-indicator" style="@_loadingDisplay"></div>
</div> </div>
@ -140,7 +140,7 @@
private void SetIntegratedSecurity(ChangeEventArgs e) private void SetIntegratedSecurity(ChangeEventArgs e)
{ {
if (Convert.ToBoolean((string)e.Value)) if (Convert.ToBoolean((string) e.Value))
{ {
_integratedSecurityDisplay = "display: none;"; _integratedSecurityDisplay = "display: none;";
} }
@ -172,10 +172,20 @@
else else
{ {
connectionstring += "User ID=" + _username + ";Password=" + _password; connectionstring += "User ID=" + _username + ";Password=" + _password;
}
}
} var config = new InstallConfig
} {
Installation installation = await InstallationService.Install(connectionstring); 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) if (installation.Success)
{ {
Site site = new Site(); Site site = new Site();
@ -208,4 +218,5 @@
_message = "<div class=\"alert alert-danger\" role=\"alert\">Please Enter All Fields And Ensure Passwords Match And Are Greater Than 5 Characters In Length</div>"; _message = "<div class=\"alert alert-danger\" role=\"alert\">Please Enter All Fields And Ensure Passwords Match And Are Greater Than 5 Characters In Length</div>";
} }
} }
} }

View File

@ -1,16 +1,9 @@
using DbUp; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Oqtane.Models; using Oqtane.Models;
using Oqtane.Shared; using Oqtane.Shared;
using System; using Oqtane.Infrastructure;
using System.Data.SqlClient;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using Oqtane.Infrastructure.Interfaces; using Oqtane.Infrastructure.Interfaces;
// ReSharper disable StringIndexOfIsCultureSpecific.1 // ReSharper disable StringIndexOfIsCultureSpecific.1
@ -22,155 +15,45 @@ namespace Oqtane.Controllers
{ {
private readonly IConfigurationRoot _config; private readonly IConfigurationRoot _config;
private readonly IInstallationManager _installationManager; private readonly IInstallationManager _installationManager;
private readonly DatabaseManager _databaseManager;
public InstallationController(IConfigurationRoot config, IInstallationManager installationManager) public InstallationController(IConfigurationRoot config, IInstallationManager installationManager, DatabaseManager databaseManager)
{ {
_config = config; _config = config;
_installationManager = installationManager; _installationManager = installationManager;
_databaseManager = databaseManager;
} }
// POST api/<controller> // POST api/<controller>
[HttpPost] [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; bool master = config.IsMaster;
string defaultconnectionstring = _config.GetConnectionString("DefaultConnection");
if (string.IsNullOrEmpty(defaultconnectionstring) || connectionString == defaultconnectionstring)
{
master = true;
}
bool exists = false; config.Alias = config.Alias ?? HttpContext.Request.Host.Value;
var result = DatabaseManager.InstallDatabase(config);
if (result.Success)
{
if (master) if (master)
{ {
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)
{
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(); _config.Reload();
} }
installation.Success = true; installation.Success = true;
return installation;
} }
installation.Message = result.Message;
return installation;
} }
}
}
else
{
installation.Message = "Application Is Already Installed"; installation.Message = "Application Is Already Installed";
}
}
return installation; return installation;
} }
@ -178,133 +61,21 @@ namespace Oqtane.Controllers
[HttpGet("installed")] [HttpGet("installed")]
public Installation IsInstalled() public Installation IsInstalled()
{ {
var installation = new Installation { Success = false, Message = "" }; var installation = new Installation {Success = false, Message = ""};
string datadirectory = AppDomain.CurrentDomain.GetData("DataDirectory").ToString(); installation.Success = _databaseManager.IsInstalled;
string connectionString = _config.GetConnectionString("DefaultConnection"); installation.Message = _databaseManager.Message;
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);
}
}
}
}
}
return installation; 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")] [HttpGet("upgrade")]
[Authorize(Roles = Constants.HostRole)] [Authorize(Roles = Constants.HostRole)]
public Installation Upgrade() public Installation Upgrade()
{ {
var installation = new Installation { Success = true, Message = "" }; var installation = new Installation {Success = true, Message = ""};
_installationManager.UpgradeFramework(); _installationManager.UpgradeFramework();
return installation; 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> ApplicationVersion { get; set; }
public virtual DbSet<Tenant> Tenant { get; set; }
}
} }

View File

@ -75,10 +75,19 @@ namespace Oqtane.Controllers
[HttpPost] [HttpPost]
public async Task<User> Post([FromBody] User user) public async Task<User> Post([FromBody] User user)
{ {
User newUser = null;
if (ModelState.IsValid) if (ModelState.IsValid)
{ {
var newUser = await CreateUser(user);
return newUser;
}
return null;
}
//TODO shoud be moved to another layer
private async Task<User> CreateUser(User user)
{
User newUser = null;
// users created by non-administrators must be verified // users created by non-administrators must be verified
bool verified = !(!User.IsInRole(Constants.AdminRole) && user.Username != Constants.HostUser); bool verified = !(!User.IsInRole(Constants.AdminRole) && user.Username != Constants.HostUser);
@ -129,8 +138,12 @@ namespace Oqtane.Controllers
Folder folder = _folders.GetFolder(user.SiteId, "Users\\"); Folder folder = _folders.GetFolder(user.SiteId, "Users\\");
if (folder != null) 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, _folders.AddFolder(new Folder
Permissions = "[{\"PermissionName\":\"Browse\",\"Permissions\":\"[" + newUser.UserId.ToString() + "]\"},{\"PermissionName\":\"View\",\"Permissions\":\"All Users\"},{\"PermissionName\":\"Edit\",\"Permissions\":\"[" + newUser.UserId.ToString() + "]\"}]" }); {
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() + "]\"}]"
});
} }
} }
} }
@ -163,7 +176,6 @@ namespace Oqtane.Controllers
newUser.Password = ""; // remove sensitive information newUser.Password = ""; // remove sensitive information
_logger.Log(user.SiteId, LogLevel.Information, this, LogFunction.Create, "User Added {User}", newUser); _logger.Log(user.SiteId, LogLevel.Information, this, LogFunction.Create, "User Added {User}", newUser);
} }
}
return newUser; return newUser;
} }

View File

@ -115,7 +115,7 @@ namespace Microsoft.Extensions.DependencyInjection
foreach (var file in assembliesFolder.EnumerateFiles($"*.{pattern}.*.dll")) foreach (var file in assembliesFolder.EnumerateFiles($"*.{pattern}.*.dll"))
{ {
// check if assembly is already loaded // 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) if (assembly == null)
{ {
// load assembly from stream to prevent locking file ( as long as dependencies are in /bin they will load as well ) // load assembly from stream to prevent locking file ( as long as dependencies are in /bin they will load as well )

View File

@ -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<MasterDBContext>();
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<T>(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<T>(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<ISiteRepository>();
// Build default site only if no site present
if (siteRepository.GetSites().Any()) return;
var users = scope.ServiceProvider.GetRequiredService<IUserRepository>();
var roles = scope.ServiceProvider.GetRequiredService<IRoleRepository>();
var userRoles = scope.ServiceProvider.GetRequiredService<IUserRoleRepository>();
var folders = scope.ServiceProvider.GetRequiredService<IFolderRepository>();
var identityUserManager = scope.ServiceProvider.GetRequiredService<UserManager<IdentityUser>>();
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<IdentityUser> 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 + "]\"}]",
});
}
}
}
}

View File

@ -27,14 +27,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Remove="Scripts\00.00.01.sql" /> <EmbeddedResource Include="Scripts\Master.00.00.00.sql" />
<None Remove="Scripts\Master.sql" /> <EmbeddedResource Include="Scripts\Master.00.00.01.sql" />
</ItemGroup> <EmbeddedResource Include="Scripts\Tenant.00.00.00.sql" />
<EmbeddedResource Include="Scripts\Tenant.00.00.01.sql" />
<ItemGroup>
<Content Include="Scripts\Master.sql" />
<EmbeddedResource Include="Scripts\00.00.01.sql" />
<EmbeddedResource Include="Scripts\00.00.00.sql" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -4,6 +4,8 @@ using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Blazor.Hosting; using Microsoft.AspNetCore.Blazor.Hosting;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore; using Microsoft.AspNetCore;
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Infrastructure;
namespace Oqtane.Server namespace Oqtane.Server
{ {
@ -12,7 +14,14 @@ namespace Oqtane.Server
#if DEBUG || RELEASE #if DEBUG || RELEASE
public static void Main(string[] args) public static void Main(string[] args)
{ {
CreateHostBuilder(args).Build().Run(); var host = CreateHostBuilder(args).Build();
using (var serviceScope = host.Services.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
var manager = serviceScope.ServiceProvider.GetService<DatabaseManager>();
manager.StartupMigration();
}
//DatabaseManager.StartupMigration();
host.Run();
} }
public static IHostBuilder CreateHostBuilder(string[] args) => public static IHostBuilder CreateHostBuilder(string[] args) =>

View File

@ -22,21 +22,11 @@ namespace Oqtane.Repository
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {
optionsBuilder.UseSqlServer(_tenant.DBConnectionString optionsBuilder.UseSqlServer(_tenant.DBConnectionString
.Replace("|DataDirectory|", AppDomain.CurrentDomain.GetData("DataDirectory").ToString()) .Replace("|DataDirectory|", AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString())
); );
base.OnConfiguring(optionsBuilder); base.OnConfiguring(optionsBuilder);
} }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
if (_tenant.DBSchema != "")
{
modelBuilder.HasDefaultSchema(_tenant.DBSchema);
}
}
public override int SaveChanges() public override int SaveChanges()
{ {
ChangeTracker.DetectChanges(); ChangeTracker.DetectChanges();

View File

@ -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> ApplicationVersion { get; set; }
public virtual DbSet<Tenant> Tenant { get; set; }
}
}

View File

@ -41,7 +41,7 @@ namespace Oqtane.Repository
var siteTemplateObject = ActivatorUtilities.CreateInstance(_serviceProvider, siteTemplateType); var siteTemplateObject = ActivatorUtilities.CreateInstance(_serviceProvider, siteTemplateType);
siteTemplate = new SiteTemplate siteTemplate = new SiteTemplate
{ {
Name = (string)siteTemplateType.GetProperty("Name").GetValue(siteTemplateObject), Name = (string)siteTemplateType.GetProperty("Name")?.GetValue(siteTemplateObject),
TypeName = siteTemplateType.AssemblyQualifiedName TypeName = siteTemplateType.AssemblyQualifiedName
}; };
siteTemplates.Add(siteTemplate); siteTemplates.Add(siteTemplate);

View File

@ -51,8 +51,12 @@ namespace Oqtane.Repository
public void DeleteTenant(int tenantId) public void DeleteTenant(int tenantId)
{ {
Tenant tenant = _db.Tenant.Find(tenantId); Tenant tenant = _db.Tenant.Find(tenantId);
if (tenant != null)
{
_db.Tenant.Remove(tenant); _db.Tenant.Remove(tenant);
_db.SaveChanges(); _db.SaveChanges();
}
_cache.Remove("tenants"); _cache.Remove("tenants");
} }
} }

View File

@ -29,11 +29,12 @@ namespace Oqtane.Repository
{ {
aliasName = accessor.HttpContext.Request.Host.Value; aliasName = accessor.HttpContext.Request.Host.Value;
string path = accessor.HttpContext.Request.Path.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] != "~") if (segments.Length > 1 && segments[1] == "api" && segments[0] != "~")
{ {
aliasName += "/" + segments[0]; aliasName += "/" + segments[0];
} }
if (aliasName.EndsWith("/")) if (aliasName.EndsWith("/"))
{ {
aliasName = aliasName.Substring(0, aliasName.Length - 1); aliasName = aliasName.Substring(0, aliasName.Length - 1);
@ -42,32 +43,29 @@ namespace Oqtane.Repository
} }
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 ?? -1;
{
aliasId = siteState.Alias.AliasId;
}
} }
// get the alias and tenant // get the alias and tenant
if (aliasId != -1 || aliasName != "") IEnumerable<Alias> aliases = aliasRepository.GetAliases().ToList(); // cached
{
IEnumerable<Alias> aliases = aliasRepository.GetAliases(); // cached
IEnumerable<Tenant> tenants = tenantRepository.GetTenants(); // cached
if (aliasId != -1) if (aliasId != -1)
{ {
_alias = aliases.FirstOrDefault(item => item.AliasId == aliasId); _alias = aliases.FirstOrDefault(item => item.AliasId == aliasId);
} }
else else
{ {
_alias = aliases.FirstOrDefault(item => item.Name == aliasName);
_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 (_alias != null) if (_alias != null)
{ {
IEnumerable<Tenant> tenants = tenantRepository.GetTenants(); // cached
_tenant = tenants.FirstOrDefault(item => item.TenantId == _alias.TenantId); _tenant = tenants.FirstOrDefault(item => item.TenantId == _alias.TenantId);
} }
} }
}
public Alias GetAlias() public Alias GetAlias()
{ {

View File

@ -123,7 +123,7 @@ Create seed data
SET IDENTITY_INSERT [dbo].[Tenant] ON SET IDENTITY_INSERT [dbo].[Tenant] ON
GO GO
INSERT [dbo].[Tenant] ([TenantId], [Name], [DBConnectionString], [DBSchema], [IsInitialized], [CreatedBy], [CreatedOn], [ModifiedBy], [ModifiedOn]) 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 GO
SET IDENTITY_INSERT [dbo].[Tenant] OFF SET IDENTITY_INSERT [dbo].[Tenant] OFF
GO GO
@ -131,7 +131,7 @@ GO
SET IDENTITY_INSERT [dbo].[Alias] ON SET IDENTITY_INSERT [dbo].[Alias] ON
GO GO
INSERT [dbo].[Alias] ([AliasId], [Name], [TenantId], [SiteId], [CreatedBy], [CreatedOn], [ModifiedBy], [ModifiedOn]) 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 GO
SET IDENTITY_INSERT [dbo].[Alias] OFF SET IDENTITY_INSERT [dbo].[Alias] OFF
GO GO

View File

@ -0,0 +1,2 @@
alter table Tenant drop column DBSchema
go

View File

@ -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, EntityName,
EntityId, EntityId,
@ -412,13 +412,13 @@ CREATE UNIQUE NONCLUSTERED INDEX IX_Setting ON dbo.Setting
) ON [PRIMARY] ) ON [PRIMARY]
GO GO
CREATE UNIQUE NONCLUSTERED INDEX IX_User ON dbo.[User] CREATE UNIQUE NONCLUSTERED INDEX IX_User ON [dbo].[User]
( (
Username Username
) ON [PRIMARY] ) ON [PRIMARY]
GO GO
CREATE UNIQUE NONCLUSTERED INDEX IX_Permission ON dbo.Permission CREATE UNIQUE NONCLUSTERED INDEX IX_Permission ON [dbo].Permission
( (
SiteId, SiteId,
EntityName, EntityName,
@ -429,7 +429,7 @@ CREATE UNIQUE NONCLUSTERED INDEX IX_Permission ON dbo.Permission
) ON [PRIMARY] ) ON [PRIMARY]
GO GO
CREATE UNIQUE NONCLUSTERED INDEX IX_Page ON dbo.Page CREATE UNIQUE NONCLUSTERED INDEX IX_Page ON [dbo].Page
( (
SiteId, SiteId,
[Path], [Path],
@ -437,14 +437,14 @@ CREATE UNIQUE NONCLUSTERED INDEX IX_Page ON dbo.Page
) ON [PRIMARY] ) ON [PRIMARY]
GO GO
CREATE UNIQUE NONCLUSTERED INDEX IX_UserRole ON dbo.UserRole CREATE UNIQUE NONCLUSTERED INDEX IX_UserRole ON [dbo].UserRole
( (
RoleId, RoleId,
UserId UserId
) ON [PRIMARY] ) ON [PRIMARY]
GO GO
CREATE UNIQUE NONCLUSTERED INDEX IX_Folder ON dbo.Folder CREATE UNIQUE NONCLUSTERED INDEX IX_Folder ON [dbo].Folder
( (
SiteId, SiteId,
[Path] [Path]

View File

@ -0,0 +1,7 @@
{
"Alias" : "",
"DefaultConnection" : "",
"HostUser" : "host",
"Password" : "",
"HostEmail" : ""
}

View File

@ -19,9 +19,12 @@ using Oqtane.Infrastructure.Interfaces;
using Oqtane.Repository; using Oqtane.Repository;
using Oqtane.Security; using Oqtane.Security;
using Oqtane.Services; using Oqtane.Services;
// DO NOT REMOVE - needed for client-side Blazor
using Oqtane.Shared; using Oqtane.Shared;
#if WASM
// DO NOT REMOVE - needed for client-side Blazor
using Microsoft.AspNetCore.ResponseCompression; using Microsoft.AspNetCore.ResponseCompression;
#endif
namespace Oqtane namespace Oqtane
{ {
@ -113,7 +116,7 @@ namespace Oqtane
services.AddDbContext<MasterDBContext>(options => services.AddDbContext<MasterDBContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection") options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")
.Replace("|DataDirectory|", AppDomain.CurrentDomain.GetData("DataDirectory").ToString()) .Replace("|DataDirectory|", AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString())
)); ));
services.AddDbContext<TenantDBContext>(options => { }); services.AddDbContext<TenantDBContext>(options => { });
@ -160,6 +163,7 @@ namespace Oqtane
services.AddSingleton(Configuration); services.AddSingleton(Configuration);
services.AddSingleton<IInstallationManager, InstallationManager>(); services.AddSingleton<IInstallationManager, InstallationManager>();
services.AddSingleton<ISyncManager, SyncManager>(); services.AddSingleton<ISyncManager, SyncManager>();
services.AddSingleton<DatabaseManager>();
// register transient scoped core services // register transient scoped core services
services.AddTransient<IModuleDefinitionRepository, ModuleDefinitionRepository>(); services.AddTransient<IModuleDefinitionRepository, ModuleDefinitionRepository>();
@ -203,6 +207,8 @@ namespace Oqtane
{ {
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Oqtane", Version = "v1" }); 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. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

View File

@ -1,5 +1,9 @@
{ {
"ConnectionStrings": { "ConnectionStrings": {
"DefaultConnection": "" "DefaultConnection": ""
},
"Oqtane": {
"DefaultAlias": "",
"DefaultPassword": ""
} }
} }

View File

@ -7,9 +7,7 @@ namespace Oqtane.Models
public int TenantId { get; set; } public int TenantId { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string DBConnectionString { get; set; } public string DBConnectionString { get; set; }
public string DBSchema { get; set; }
public bool IsInitialized { get; set; } public bool IsInitialized { get; set; }
public string CreatedBy { get; set; } public string CreatedBy { get; set; }
public DateTime CreatedOn { get; set; } public DateTime CreatedOn { get; set; }
public string ModifiedBy { get; set; } public string ModifiedBy { get; set; }

View File

@ -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; }
}
}