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:
parent
744782df7a
commit
940cdcb349
|
@ -164,7 +164,7 @@ else
|
|||
_tenantid = (string)e.Value;
|
||||
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)
|
||||
{
|
||||
_isinitialized = tenant.IsInitialized;
|
||||
|
@ -273,8 +273,11 @@ else
|
|||
if (user != null)
|
||||
{
|
||||
Tenant tenant = _tenants.FirstOrDefault(item => item.TenantId == int.Parse(_tenantid));
|
||||
tenant.IsInitialized = true;
|
||||
await TenantService.UpdateTenantAsync(tenant);
|
||||
if (tenant != null)
|
||||
{
|
||||
tenant.IsInitialized = true;
|
||||
await TenantService.UpdateTenantAsync(tenant);
|
||||
}
|
||||
}
|
||||
}
|
||||
await Log(aliases[0], LogLevel.Information, "", null, "Site Created {Site}", site);
|
||||
|
|
|
@ -67,14 +67,6 @@
|
|||
<input type="password" class="form-control" @bind="@password" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label class="control-label">Schema: </label>
|
||||
</td>
|
||||
<td>
|
||||
<input class="form-control" @bind="@schema" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<button type="button" class="btn btn-success" @onclick="SaveTenant">Save</button>
|
||||
<NavLink class="btn btn-secondary" href="@NavigateUrl()">Cancel</NavLink>
|
||||
|
@ -88,7 +80,6 @@
|
|||
string database = "Oqtane-" + DateTime.UtcNow.ToString("yyyyMMddHHmm");
|
||||
string username = "";
|
||||
string password = "";
|
||||
string schema = "";
|
||||
string integratedsecurity = "display: none;";
|
||||
|
||||
private void SetIntegratedSecurity(ChangeEventArgs e)
|
||||
|
@ -109,32 +100,41 @@
|
|||
{
|
||||
ShowProgressIndicator();
|
||||
|
||||
string connectionstring = "";
|
||||
string connectionString = "";
|
||||
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
|
||||
{
|
||||
connectionstring = "Data Source=" + server + ";Initial Catalog=" + database + ";";
|
||||
connectionString = "Data Source=" + server + ";Initial Catalog=" + database + ";";
|
||||
if (integratedsecurity == "display: none;")
|
||||
{
|
||||
connectionstring += "Integrated Security=SSPI;";
|
||||
connectionString += "Integrated Security=SSPI;";
|
||||
}
|
||||
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)
|
||||
{
|
||||
Tenant tenant = new Tenant();
|
||||
tenant.Name = name;
|
||||
tenant.DBConnectionString = connectionstring;
|
||||
tenant.DBSchema = schema;
|
||||
tenant.IsInitialized = false;
|
||||
//TODO : Move to Database Manager
|
||||
Tenant tenant = new Tenant
|
||||
{
|
||||
Name = name,
|
||||
DBConnectionString = connectionString,
|
||||
IsInitialized = false
|
||||
};
|
||||
await TenantService.AddTenantAsync(tenant);
|
||||
await logger.LogInformation("Tenant Created {Tenant}", tenant);
|
||||
|
||||
|
|
|
@ -20,14 +20,7 @@
|
|||
<input class="form-control" @bind="@connectionstring" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label class="control-label">Schema: </label>
|
||||
</td>
|
||||
<td>
|
||||
<input class="form-control" @bind="@schema" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<button type="button" class="btn btn-success" @onclick="SaveTenant">Save</button>
|
||||
<NavLink class="btn btn-secondary" href="@NavigateUrl()">Cancel</NavLink>
|
||||
|
@ -38,7 +31,6 @@
|
|||
int tenantid;
|
||||
string name = "";
|
||||
string connectionstring = "";
|
||||
string schema = "";
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
|
@ -50,7 +42,6 @@
|
|||
{
|
||||
name = tenant.Name;
|
||||
connectionstring = tenant.DBConnectionString;
|
||||
schema = tenant.DBSchema;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -70,7 +61,6 @@
|
|||
{
|
||||
tenant.Name = name;
|
||||
tenant.DBConnectionString = connectionstring;
|
||||
tenant.DBSchema = schema;
|
||||
await TenantService.UpdateTenantAsync(tenant);
|
||||
await logger.LogInformation("Tenant Saved {TenantId}", tenantid);
|
||||
|
||||
|
|
|
@ -19,24 +19,21 @@ namespace Oqtane.Services
|
|||
_navigationManager = navigationManager;
|
||||
}
|
||||
|
||||
private string Apiurl
|
||||
{
|
||||
get { return CreateApiUrl(_siteState.Alias, _navigationManager.Uri, "Installation"); }
|
||||
}
|
||||
private string ApiUrl => CreateApiUrl(_siteState.Alias, _navigationManager.Uri, "Installation");
|
||||
|
||||
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()
|
||||
{
|
||||
return await _http.GetJsonAsync<Installation>(Apiurl + "/upgrade");
|
||||
return await _http.GetJsonAsync<Installation>(ApiUrl + "/upgrade");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
using Oqtane.Models;
|
||||
using System.Threading.Tasks;
|
||||
using Oqtane.Shared;
|
||||
|
||||
namespace Oqtane.Services
|
||||
{
|
||||
public interface IInstallationService
|
||||
{
|
||||
Task<Installation> IsInstalled();
|
||||
Task<Installation> Install(string connectionstring);
|
||||
Task<Installation> Install(InstallConfig config);
|
||||
Task<Installation> Upgrade();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,117 +7,117 @@
|
|||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="mx-auto text-center">
|
||||
<img src="oqtane.png" />
|
||||
<img src="oqtane.png"/>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="app-rule" />
|
||||
<hr class="app-rule"/>
|
||||
<div class="row justify-content-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;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<label class="control-label" style="font-weight: bold">Database Type: </label>
|
||||
</td>
|
||||
<td>
|
||||
<select class="custom-select" @bind="@_databaseType">
|
||||
<option value="LocalDB">Local Database</option>
|
||||
<option value="SQLServer">SQL Server</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label class="control-label" style="font-weight: bold">Server: </label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control" @bind="@_serverName" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label class="control-label" style="font-weight: bold">Database: </label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control" @bind="@_databaseName" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label class="control-label" style="font-weight: bold">Integrated Security: </label>
|
||||
</td>
|
||||
<td>
|
||||
<select class="custom-select" @onchange="SetIntegratedSecurity">
|
||||
<option value="true" selected>True</option>
|
||||
<option value="false">False</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="@_integratedSecurityDisplay">
|
||||
<td>
|
||||
<label class="control-label" style="font-weight: bold">Username: </label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control" @bind="@_username" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="@_integratedSecurityDisplay">
|
||||
<td>
|
||||
<label class="control-label" style="font-weight: bold">Password: </label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="password" class="form-control" @bind="@_password" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label class="control-label" style="font-weight: bold">Database Type: </label>
|
||||
</td>
|
||||
<td>
|
||||
<select class="custom-select" @bind="@_databaseType">
|
||||
<option value="LocalDB">Local Database</option>
|
||||
<option value="SQLServer">SQL Server</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label class="control-label" style="font-weight: bold">Server: </label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control" @bind="@_serverName"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label class="control-label" style="font-weight: bold">Database: </label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control" @bind="@_databaseName"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label class="control-label" style="font-weight: bold">Integrated Security: </label>
|
||||
</td>
|
||||
<td>
|
||||
<select class="custom-select" @onchange="SetIntegratedSecurity">
|
||||
<option value="true" selected>True</option>
|
||||
<option value="false">False</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="@_integratedSecurityDisplay">
|
||||
<td>
|
||||
<label class="control-label" style="font-weight: bold">Username: </label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control" @bind="@_username"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="@_integratedSecurityDisplay">
|
||||
<td>
|
||||
<label class="control-label" style="font-weight: bold">Password: </label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="password" class="form-control" @bind="@_password"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<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;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<label class="control-label" style="font-weight: bold">Username: </label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control" @bind="@_hostUsername" readonly />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label class="control-label" style="font-weight: bold">Password: </label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="password" class="form-control" @bind="@_hostPassword" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label class="control-label" style="font-weight: bold">Confirm: </label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="password" class="form-control" @bind="@_confirmPassword" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label class="control-label" style="font-weight: bold">Email: </label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control" @bind="@_hostEmail" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label class="control-label" style="font-weight: bold">Username: </label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control" @bind="@_hostUsername" readonly/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label class="control-label" style="font-weight: bold">Password: </label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="password" class="form-control" @bind="@_hostPassword"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label class="control-label" style="font-weight: bold">Confirm: </label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="password" class="form-control" @bind="@_confirmPassword"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label class="control-label" style="font-weight: bold">Email: </label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control" @bind="@_hostEmail"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="app-rule" />
|
||||
<hr class="app-rule"/>
|
||||
<div class="row">
|
||||
<div class="mx-auto text-center">
|
||||
<button type="button" class="btn btn-success" @onclick="Install">Install Now</button><br /><br />
|
||||
@((MarkupString)_message)
|
||||
<button type="button" class="btn btn-success" @onclick="Install">Install Now</button><br/><br/>
|
||||
@((MarkupString) _message)
|
||||
</div>
|
||||
<div class="app-progress-indicator" style="@_loadingDisplay"></div>
|
||||
</div>
|
||||
|
@ -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 = "<div class=\"alert alert-danger\" role=\"alert\">Please Enter All Fields And Ensure Passwords Match And Are Greater Than 5 Characters In Length</div>";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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/<controller>
|
||||
[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> ApplicationVersion { get; set; }
|
||||
public virtual DbSet<Tenant> Tenant { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,95 +75,107 @@ namespace Oqtane.Controllers
|
|||
[HttpPost]
|
||||
public async Task<User> 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<User> 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<Role> 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<Role> 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;
|
||||
}
|
||||
|
|
|
@ -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 )
|
||||
|
|
367
Oqtane.Server/Infrastructure/DatabaseManager.cs
Normal file
367
Oqtane.Server/Infrastructure/DatabaseManager.cs
Normal 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 + "]\"}]",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -27,14 +27,10 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Scripts\00.00.01.sql" />
|
||||
<None Remove="Scripts\Master.sql" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Scripts\Master.sql" />
|
||||
<EmbeddedResource Include="Scripts\00.00.01.sql" />
|
||||
<EmbeddedResource Include="Scripts\00.00.00.sql" />
|
||||
<EmbeddedResource Include="Scripts\Master.00.00.00.sql" />
|
||||
<EmbeddedResource Include="Scripts\Master.00.00.01.sql" />
|
||||
<EmbeddedResource Include="Scripts\Tenant.00.00.00.sql" />
|
||||
<EmbeddedResource Include="Scripts\Tenant.00.00.01.sql" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -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<IServiceScopeFactory>().CreateScope())
|
||||
{
|
||||
var manager = serviceScope.ServiceProvider.GetService<DatabaseManager>();
|
||||
manager.StartupMigration();
|
||||
}
|
||||
//DatabaseManager.StartupMigration();
|
||||
host.Run();
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
|
|
|
@ -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();
|
||||
|
|
23
Oqtane.Server/Repository/Context/InstallationContext.cs
Normal file
23
Oqtane.Server/Repository/Context/InstallationContext.cs
Normal 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; }
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Alias> aliases = aliasRepository.GetAliases().ToList(); // cached
|
||||
if (aliasId != -1)
|
||||
{
|
||||
IEnumerable<Alias> aliases = aliasRepository.GetAliases(); // cached
|
||||
IEnumerable<Tenant> 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<Tenant> tenants = tenantRepository.GetTenants(); // cached
|
||||
_tenant = tenants.FirstOrDefault(item => item.TenantId == _alias.TenantId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
2
Oqtane.Server/Scripts/Master.00.00.01.sql
Normal file
2
Oqtane.Server/Scripts/Master.00.00.01.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
alter table Tenant drop column DBSchema
|
||||
go
|
|
@ -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]
|
7
Oqtane.Server/SilentInstall.json
Normal file
7
Oqtane.Server/SilentInstall.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"Alias" : "",
|
||||
"DefaultConnection" : "",
|
||||
"HostUser" : "host",
|
||||
"Password" : "",
|
||||
"HostEmail" : ""
|
||||
}
|
|
@ -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<MasterDBContext>(options =>
|
||||
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")
|
||||
.Replace("|DataDirectory|", AppDomain.CurrentDomain.GetData("DataDirectory").ToString())
|
||||
.Replace("|DataDirectory|", AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString())
|
||||
));
|
||||
services.AddDbContext<TenantDBContext>(options => { });
|
||||
|
||||
|
@ -160,7 +163,8 @@ namespace Oqtane
|
|||
services.AddSingleton(Configuration);
|
||||
services.AddSingleton<IInstallationManager, InstallationManager>();
|
||||
services.AddSingleton<ISyncManager, SyncManager>();
|
||||
|
||||
services.AddSingleton<DatabaseManager>();
|
||||
|
||||
// register transient scoped core services
|
||||
services.AddTransient<IModuleDefinitionRepository, ModuleDefinitionRepository>();
|
||||
services.AddTransient<IThemeRepository, ThemeRepository>();
|
||||
|
@ -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.
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": ""
|
||||
},
|
||||
"Oqtane": {
|
||||
"DefaultAlias": "",
|
||||
"DefaultPassword": ""
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
|
|
12
Oqtane.Shared/Shared/InstallConfig.cs
Normal file
12
Oqtane.Shared/Shared/InstallConfig.cs
Normal 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; }
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user