461 lines
19 KiB
C#
461 lines
19 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Data;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using DbUp;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Newtonsoft.Json;
|
|
using Oqtane.Extensions;
|
|
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 void StartupMigration()
|
|
{
|
|
var defaultConnectionString = _config.GetConnectionString(SettingKeys.ConnectionStringKey);
|
|
var defaultAlias = GetInstallationConfig(SettingKeys.DefaultAliasKey, string.Empty);
|
|
|
|
// if no values specified, fallback to IDE installer
|
|
if (string.IsNullOrEmpty(defaultConnectionString))
|
|
{
|
|
IsInstalled = false;
|
|
return;
|
|
}
|
|
|
|
var freshInstall = !IsMasterInstalled(defaultConnectionString);
|
|
var password = GetInstallationConfig(SettingKeys.HostPasswordKey, String.Empty);
|
|
var email = GetInstallationConfig(SettingKeys.HostEmailKey, String.Empty);
|
|
if (freshInstall && (string.IsNullOrEmpty(password) || string.IsNullOrEmpty(email) || string.IsNullOrEmpty(defaultAlias)))
|
|
{
|
|
IsInstalled = false;
|
|
Message = "Incomplete startup install configuration";
|
|
return;
|
|
}
|
|
|
|
var result = MasterMigration(defaultConnectionString, defaultAlias, null, true);
|
|
IsInstalled = result.Success;
|
|
|
|
if (result.Success)
|
|
{
|
|
var dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString();
|
|
WriteVersionInfo(defaultConnectionString);
|
|
TenantMigration(defaultConnectionString, dataDirectory);
|
|
}
|
|
|
|
if (_isInstalled && !IsDefaultSiteInstalled(defaultConnectionString))
|
|
{
|
|
BuildDefaultSite(password,email);
|
|
|
|
}
|
|
}
|
|
|
|
public bool IsInstalled
|
|
{
|
|
get
|
|
{
|
|
if (!_isInstalled) _isInstalled = CheckInstallState();
|
|
|
|
return _isInstalled;
|
|
}
|
|
set => _isInstalled = value;
|
|
}
|
|
|
|
private bool CheckInstallState()
|
|
{
|
|
var defaultConnectionString = _config.GetConnectionString(SettingKeys.ConnectionStringKey);
|
|
var 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 available";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Message = "Connection string is empty";
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
public static string NormalizeConnectionString(string connectionString, string dataDirectory)
|
|
{
|
|
connectionString = connectionString
|
|
.Replace("|DataDirectory|", dataDirectory);
|
|
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 = MasterMigration(connectionString, alias, result, installConfig.IsMaster);
|
|
if (installConfig.IsMaster && result.Success)
|
|
{
|
|
WriteVersionInfo(connectionString);
|
|
TenantMigration(connectionString, dataDirectory);
|
|
UpdateConnectionStringSetting(connectionString);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
result = new Installation
|
|
{
|
|
Success = false,
|
|
Message = "Connection string is empty",
|
|
};
|
|
return result;
|
|
}
|
|
|
|
private static Installation MasterMigration(string connectionString, string alias, Installation result, bool master)
|
|
{
|
|
if (result == null) result = new Installation {Success = false, Message = string.Empty};
|
|
|
|
bool firstInstall;
|
|
try
|
|
{
|
|
// create empty database if does not exists
|
|
// dbup database creation does not work correctly on localdb databases
|
|
using (var dbc = new DbContext(new DbContextOptionsBuilder().UseSqlServer(connectionString).Options))
|
|
{
|
|
dbc.Database.EnsureCreated();
|
|
//check for vanilla db
|
|
firstInstall = !TableExists(dbc, "SchemaVersions");
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
result.Message = e.Message;
|
|
Console.WriteLine(e);
|
|
return result;
|
|
}
|
|
// when alias is not specified on first install, fallback to ide
|
|
if (firstInstall && string.IsNullOrEmpty(alias)) return result;
|
|
|
|
var dbUpgradeConfig = DeployChanges
|
|
.To
|
|
.SqlDatabase(connectionString)
|
|
.WithVariable("ConnectionString", connectionString)
|
|
.WithVariable("Alias", alias)
|
|
.WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), s => master || !s.Contains("Master."));
|
|
|
|
var dbUpgrade = dbUpgradeConfig.Build();
|
|
if (!dbUpgrade.IsUpgradeRequired())
|
|
{
|
|
result.Success = true;
|
|
result.Message = string.Empty;
|
|
return result;
|
|
}
|
|
|
|
var upgradeResult = dbUpgrade.PerformUpgrade();
|
|
if (!upgradeResult.Successful)
|
|
{
|
|
Console.WriteLine(upgradeResult.Error.Message);
|
|
result.Message = upgradeResult.Error.Message;
|
|
}
|
|
else
|
|
{
|
|
result.Success = true;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static void ModuleMigration(Assembly assembly, string connectionString)
|
|
{
|
|
|
|
Console.WriteLine($"Migrating assembly {assembly.FullName}");
|
|
var dbUpgradeConfig = DeployChanges.To.SqlDatabase(connectionString)
|
|
.WithScriptsEmbeddedInAssembly(assembly, s => !s.ToLower().Contains("uninstall.sql")); // scripts must be included as Embedded Resources
|
|
var dbUpgrade = dbUpgradeConfig.Build();
|
|
if (dbUpgrade.IsUpgradeRequired())
|
|
{
|
|
var result = 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)
|
|
{
|
|
Console.WriteLine("Tenant migration");
|
|
var assemblies = AppDomain.CurrentDomain.GetAssemblies()
|
|
.Where(item => item.FullName != null && item.FullName.ToLower().Contains(".module.")).ToArray();
|
|
|
|
// get tenants
|
|
using (var db = new InstallationContext(connectionString))
|
|
{
|
|
foreach (var tenant in db.Tenant.ToList())
|
|
{
|
|
Console.WriteLine($"Migrating tenant {tenant.Name}");
|
|
connectionString = NormalizeConnectionString(tenant.DBConnectionString, dataDirectory);
|
|
// upgrade framework
|
|
var dbUpgradeConfig = DeployChanges.To.SqlDatabase(connectionString)
|
|
.WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), s => s.Contains("Tenant"));
|
|
var dbUpgrade = dbUpgradeConfig.Build();
|
|
if (dbUpgrade.IsUpgradeRequired())
|
|
{
|
|
var result = dbUpgrade.PerformUpgrade();
|
|
if (!result.Successful)
|
|
{
|
|
// TODO: log result.Error.Message - problem is logger is not available here
|
|
}
|
|
}
|
|
|
|
// iterate through Oqtane module assemblies and execute any database scripts
|
|
foreach (var assembly in assemblies) ModuleMigration(assembly, connectionString);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void UpdateConnectionStringSetting(string connectionString)
|
|
{
|
|
AddOrUpdateAppSetting($"ConnectionStrings:{SettingKeys.ConnectionStringKey}", 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 process, 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;
|
|
}
|
|
}
|
|
|
|
private void BuildDefaultSite(string password, string email)
|
|
{
|
|
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 tenants = scope.ServiceProvider.GetRequiredService<ITenantRepository>();
|
|
|
|
var tenant = tenants.GetTenants().First();
|
|
|
|
var site = new Site
|
|
{
|
|
TenantId = tenant.TenantId,
|
|
Name = "Default Site",
|
|
LogoFileId = null,
|
|
DefaultThemeType = GetInstallationConfig(SettingKeys.DefaultThemeKey, Constants.DefaultTheme),
|
|
DefaultLayoutType = GetInstallationConfig(SettingKeys.DefaultLayoutKey, Constants.DefaultLayout),
|
|
DefaultContainerType = GetInstallationConfig(SettingKeys.DefaultContainerKey, Constants.DefaultContainer),
|
|
SiteTemplateType = GetInstallationConfig(SettingKeys.SiteTemplateKey, Constants.DefaultSiteTemplate),
|
|
};
|
|
site = siteRepository.AddSite(site);
|
|
|
|
var user = new User
|
|
{
|
|
SiteId = site.SiteId,
|
|
Username = GetInstallationConfig(SettingKeys.HostUserKey, Constants.HostUser),
|
|
Password = password,
|
|
Email = email,
|
|
DisplayName = GetInstallationConfig(SettingKeys.HostUserKey, Constants.HostUser),
|
|
};
|
|
CreateHostUser(folders, userRoles, roles, users, identityUserManager, user);
|
|
tenant.IsInitialized = true;
|
|
tenants.UpdateTenant(tenant);
|
|
}
|
|
}
|
|
|
|
private string GetInstallationConfig(string key, string defaultValue)
|
|
{
|
|
var value = _config.GetSection(SettingKeys.InstallationSection).GetValue(key, defaultValue);
|
|
// double fallback to default value - allow hold sample keys in config
|
|
if (string.IsNullOrEmpty(value)) value = defaultValue;
|
|
return value;
|
|
}
|
|
|
|
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 = Path.Combine(folder.Path, newUser.UserId.ToString()),
|
|
Order = 1,
|
|
IsSystem = true,
|
|
Permissions = new List<Permission>
|
|
{
|
|
new Permission(PermissionNames.Browse, newUser.UserId, true),
|
|
new Permission(PermissionNames.View, Constants.AllUsersRole, true),
|
|
new Permission(PermissionNames.Edit, newUser.UserId, true),
|
|
}.EncodePermissions(),
|
|
});
|
|
}
|
|
}
|
|
|
|
private static bool IsDefaultSiteInstalled(string connectionString)
|
|
{
|
|
using (var db = new InstallationContext(connectionString))
|
|
{
|
|
return db.Tenant.Any(t => t.IsInitialized);
|
|
}
|
|
}
|
|
|
|
private static bool IsMasterInstalled(string connectionString)
|
|
{
|
|
using (var db = new InstallationContext(connectionString))
|
|
{
|
|
//check if DbUp was initialized
|
|
return TableExists(db, "SchemaVersions");
|
|
}
|
|
}
|
|
|
|
public static bool TableExists(DbContext context, string tableName)
|
|
{
|
|
return TableExists(context, "dbo", tableName);
|
|
}
|
|
|
|
public static bool TableExists(DbContext context, string schema, string tableName)
|
|
{
|
|
var connection = context.Database.GetDbConnection();
|
|
|
|
if (connection.State.Equals(ConnectionState.Closed))
|
|
connection.Open();
|
|
|
|
using (var command = connection.CreateCommand())
|
|
{
|
|
command.CommandText = @"
|
|
SELECT 1 FROM INFORMATION_SCHEMA.TABLES
|
|
WHERE TABLE_SCHEMA = @Schema
|
|
AND TABLE_NAME = @TableName";
|
|
|
|
var schemaParam = command.CreateParameter();
|
|
schemaParam.ParameterName = "@Schema";
|
|
schemaParam.Value = schema;
|
|
command.Parameters.Add(schemaParam);
|
|
|
|
var tableNameParam = command.CreateParameter();
|
|
tableNameParam.ParameterName = "@TableName";
|
|
tableNameParam.Value = tableName;
|
|
command.Parameters.Add(tableNameParam);
|
|
|
|
return command.ExecuteScalar() != null;
|
|
}
|
|
}
|
|
}
|
|
}
|