Merge pull request #2112 from oqtane/dev

3.1.0 release
This commit is contained in:
Shaun Walker 2022-04-05 08:51:10 -04:00 committed by GitHub
commit cfbcc41543
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
158 changed files with 4198 additions and 1137 deletions

View File

@ -45,6 +45,9 @@
[Parameter] [Parameter]
public string RemoteIPAddress { get; set; } public string RemoteIPAddress { get; set; }
[Parameter]
public string AuthorizationToken { get; set; }
private bool _initialized = false; private bool _initialized = false;
private string _display = "display: none;"; private string _display = "display: none;";
private Installation _installation = new Installation { Success = false, Message = "" }; private Installation _installation = new Installation { Success = false, Message = "" };
@ -55,7 +58,7 @@
{ {
SiteState.RemoteIPAddress = RemoteIPAddress; SiteState.RemoteIPAddress = RemoteIPAddress;
SiteState.AntiForgeryToken = AntiForgeryToken; SiteState.AntiForgeryToken = AntiForgeryToken;
InstallationService.SetAntiForgeryTokenHeader(AntiForgeryToken); SiteState.AuthorizationToken = AuthorizationToken;
_installation = await InstallationService.IsInstalled(); _installation = await InstallationService.IsInstalled();
if (_installation.Alias != null) if (_installation.Alias != null)

View File

@ -121,90 +121,97 @@
{ {
_databaseName = "LocalDB"; _databaseName = "LocalDB";
} }
LoadDatabaseConfigComponent(); LoadDatabaseConfigComponent();
} }
private void DatabaseChanged(ChangeEventArgs eventArgs) private void DatabaseChanged(ChangeEventArgs eventArgs)
{ {
try try
{ {
_databaseName = (string)eventArgs.Value; _databaseName = (string)eventArgs.Value;
LoadDatabaseConfigComponent(); LoadDatabaseConfigComponent();
} }
catch catch
{ {
_message = Localizer["Error.DbConfig.Load"]; _message = Localizer["Error.DbConfig.Load"];
} }
} }
private void LoadDatabaseConfigComponent() private void LoadDatabaseConfigComponent()
{ {
var database = _databases.SingleOrDefault(d => d.Name == _databaseName); var database = _databases.SingleOrDefault(d => d.Name == _databaseName);
if (database != null) if (database != null)
{ {
_databaseConfigType = Type.GetType(database.ControlType); _databaseConfigType = Type.GetType(database.ControlType);
DatabaseConfigComponent = builder => DatabaseConfigComponent = builder =>
{ {
builder.OpenComponent(0, _databaseConfigType); builder.OpenComponent(0, _databaseConfigType);
builder.AddComponentReferenceCapture(1, inst => { _databaseConfig = Convert.ChangeType(inst, _databaseConfigType); }); builder.AddComponentReferenceCapture(1, inst => { _databaseConfig = Convert.ChangeType(inst, _databaseConfigType); });
builder.CloseComponent(); builder.CloseComponent();
}; };
} }
} }
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
if (firstRender) if (firstRender)
{ {
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
await interop.IncludeLink("", "stylesheet", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css", "text/css", "sha512-GQGU0fMMi238uA+a/bdWJfpUGKUkBdgfFdgBm72SUQ6BeyWjoY/ton0tEjH+OSH9iP4Dfh+7HM0I9f5eR0L/4w==", "anonymous", ""); await interop.IncludeLink("", "stylesheet", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css", "text/css", "sha512-GQGU0fMMi238uA+a/bdWJfpUGKUkBdgfFdgBm72SUQ6BeyWjoY/ton0tEjH+OSH9iP4Dfh+7HM0I9f5eR0L/4w==", "anonymous", "");
await interop.IncludeScript("", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js", "sha512-pax4MlgXjHEPfCwcJLQhigY7+N8rt6bVvWLFyUMuxShv170X53TRzGPmPkZmGBhk+jikR8WBM4yl7A9WMHHqvg==", "anonymous", "", "head", ""); await interop.IncludeScript("", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js", "sha512-pax4MlgXjHEPfCwcJLQhigY7+N8rt6bVvWLFyUMuxShv170X53TRzGPmPkZmGBhk+jikR8WBM4yl7A9WMHHqvg==", "anonymous", "", "head");
} }
} }
private async Task Install() private async Task Install()
{ {
var connectionString = String.Empty; var connectionString = String.Empty;
if (_databaseConfig is IDatabaseConfigControl databaseConfigControl) if (_databaseConfig is IDatabaseConfigControl databaseConfigControl)
{ {
connectionString = databaseConfigControl.GetConnectionString(); connectionString = databaseConfigControl.GetConnectionString();
} }
if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && _hostPassword.Length >= 6 && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@")) if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && !string.IsNullOrEmpty(_hostPassword) && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@"))
{ {
_loadingDisplay = ""; if (await UserService.ValidatePasswordAsync(_hostPassword))
StateHasChanged(); {
_loadingDisplay = "";
StateHasChanged();
Uri uri = new Uri(NavigationManager.Uri); Uri uri = new Uri(NavigationManager.Uri);
var database = _databases.SingleOrDefault(d => d.Name == _databaseName); var database = _databases.SingleOrDefault(d => d.Name == _databaseName);
var config = new InstallConfig var config = new InstallConfig
{ {
DatabaseType = database.DBType, DatabaseType = database.DBType,
ConnectionString = connectionString, ConnectionString = connectionString,
Aliases = uri.Authority, Aliases = uri.Authority,
HostUsername = _hostUsername, HostUsername = _hostUsername,
HostPassword = _hostPassword, HostPassword = _hostPassword,
HostEmail = _hostEmail, HostEmail = _hostEmail,
HostName = _hostUsername, HostName = _hostUsername,
TenantName = TenantNames.Master, TenantName = TenantNames.Master,
IsNewTenant = true, IsNewTenant = true,
SiteName = Constants.DefaultSite, SiteName = Constants.DefaultSite,
Register = _register Register = _register
}; };
var installation = await InstallationService.Install(config); var installation = await InstallationService.Install(config);
if (installation.Success) if (installation.Success)
{ {
NavigationManager.NavigateTo(uri.Scheme + "://" + uri.Authority, true); NavigationManager.NavigateTo(uri.Scheme + "://" + uri.Authority, true);
} }
else else
{ {
_message = installation.Message; _message = installation.Message;
_loadingDisplay = "display: none;"; _loadingDisplay = "display: none;";
} }
}
else
{
_message = Localizer["Message.Password.Invalid"];
}
} }
else else
{ {

View File

@ -27,6 +27,6 @@
protected override void OnInitialized() protected override void OnInitialized()
{ {
var admin = PageState.Pages.FirstOrDefault(item => item.Path == "admin"); var admin = PageState.Pages.FirstOrDefault(item => item.Path == "admin");
_pages = PageState.Pages.Where(item => item.ParentId == admin?.PageId && !item.IsDeleted).ToList(); _pages = PageState.Pages.Where(item => item.ParentId == admin?.PageId).ToList();
} }
} }

View File

@ -7,10 +7,6 @@
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_message != string.Empty)
{
<ModuleMessage Message="@_message" Type="@_type" />
}
<AuthorizeView Roles="@RoleNames.Registered"> <AuthorizeView Roles="@RoleNames.Registered">
<Authorizing> <Authorizing>
<text>...</text> <text>...</text>
@ -19,185 +15,285 @@
<div>@Localizer["Info.SignedIn"]</div> <div>@Localizer["Info.SignedIn"]</div>
</Authorized> </Authorized>
<NotAuthorized> <NotAuthorized>
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate> @if (!twofactor)
<div class="container Oqtane-Modules-Admin-Login" @onkeypress="@(e => KeyPressed(e))"> {
<div class="form-group"> <form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<label for="Username" class="control-label">@SharedLocalizer["Username"] </label> <div class="Oqtane-Modules-Admin-Login" @onkeypress="@(e => KeyPressed(e))">
<input type="text" @ref="username" name="Username" class="form-control username" placeholder="Username" @bind="@_username" id="Username" required /> @if (_allowexternallogin)
</div> {
<div class="form-group"> <button type="button" class="btn btn-primary" @onclick="ExternalLogin">@Localizer["Use"] @PageState.Site.Settings["ExternalLogin:ProviderName"]</button>
<label for="Password" class="control-label">@SharedLocalizer["Password"] </label> <br /><br />
<input type="password" name="Password" class="form-control password" placeholder="Password" @bind="@_password" id="Password" required /> }
</div> @if (_allowsitelogin)
<div class="form-group"> {
<div class="form-check form-check-inline"> <div class="form-group">
<label class="form-check-label" for="Remember">@Localizer["RememberMe"]</label>&nbsp; <Label Class="control-label" For="username" HelpText="Please enter your Username" ResourceKey="Username">Username:</Label>
<input type="checkbox" class="form-check-input" name="Remember" @bind="@_remember" id="Remember" /> <input id="username" type="text" @ref="username" class="form-control" placeholder="@Localizer["Username.Placeholder"]" @bind="@_username" required />
</div> </div>
</div> <div class="form-group mt-2">
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button> <Label Class="control-label" For="password" HelpText="Please enter your Password" ResourceKey="Password">Password:</Label>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button> <div class="input-group">
<br /><br /> <input id="password" type="@_passwordtype" name="Password" class="form-control" placeholder="@Localizer["Password.Placeholder"]" @bind="@_password" required />
<button type="button" class="btn btn-secondary" @onclick="Forgot">@Localizer["ForgotPassword"]</button> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div> </div>
</form> </div>
</NotAuthorized> <div class="form-group mt-2">
<div class="form-check">
<input id="remember" type="checkbox" class="form-check-input" @bind="@_remember" />
<Label Class="control-label" For="remember" HelpText="Specify if you would like to be signed back in automatically the next time you visit this site" ResourceKey="Remember">Remember Me?</Label>
</div>
</div>
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
<br /><br />
<button type="button" class="btn btn-secondary" @onclick="Forgot">@Localizer["ForgotPassword"]</button>
}
</div>
</form>
}
else
{
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container Oqtane-Modules-Admin-Login">
<div class="form-group">
<Label Class="control-label" For="code" HelpText="Please enter the secure verification code which was sent to you by email" ResourceKey="Code">Verification Code:</Label>
<input id="code" class="form-control" @bind="@_code" placeholder="@Localizer["Code.Placeholder"]" maxlength="6" required />
</div>
<br />
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
<button type="button" class="btn btn-secondary" @onclick="Reset">@SharedLocalizer["Cancel"]</button>
</div>
</form>
}
</NotAuthorized>
</AuthorizeView> </AuthorizeView>
@code { @code {
private string _returnUrl = string.Empty; private bool _allowsitelogin = true;
private string _message = string.Empty; private bool _allowexternallogin = false;
private MessageType _type = MessageType.Info; private ElementReference login;
private string _username = string.Empty; private bool validated = false;
private string _password = string.Empty; private bool twofactor = false;
private bool _remember = false; private string _username = string.Empty;
private bool validated = false; private ElementReference username;
private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private bool _remember = false;
private string _code = string.Empty;
private ElementReference login; private string _returnUrl = string.Empty;
private ElementReference username;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
public override List<Resource> Resources => new List<Resource>() public override List<Resource> Resources => new List<Resource>()
{ {
new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" } new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" }
}; };
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
if (PageState.QueryString.ContainsKey("returnurl")) try
{ {
_returnUrl = PageState.QueryString["returnurl"]; _togglepassword = Localizer["ShowPassword"];
}
if (PageState.QueryString.ContainsKey("name")) if (PageState.Site.Settings.ContainsKey("LoginOptions:AllowSiteLogin") && !string.IsNullOrEmpty(PageState.Site.Settings["LoginOptions:AllowSiteLogin"]))
{ {
_username = PageState.QueryString["name"]; _allowsitelogin = bool.Parse(PageState.Site.Settings["LoginOptions:AllowSiteLogin"]);
} }
if (PageState.QueryString.ContainsKey("token")) if (PageState.Site.Settings.ContainsKey("ExternalLogin:ProviderType") && !string.IsNullOrEmpty(PageState.Site.Settings["ExternalLogin:ProviderType"]))
{ {
var user = new User(); _allowexternallogin = true;
user.SiteId = PageState.Site.SiteId; }
user.Username = _username;
user = await UserService.VerifyEmailAsync(user, PageState.QueryString["token"]);
if (user != null) if (PageState.QueryString.ContainsKey("returnurl"))
{ {
await logger.LogInformation(LogFunction.Security, "Email Verified For For Username {Username}", _username); _returnUrl = PageState.QueryString["returnurl"];
_message = Localizer["Success.Account.Verified"]; }
}
else
{
await logger.LogError(LogFunction.Security, "Email Verification Failed For Username {Username}", _username);
_message = Localizer["Message.Account.NotVerfied"];
_type = MessageType.Warning;
}
}
}
protected override async Task OnAfterRenderAsync(bool firstRender) if (PageState.QueryString.ContainsKey("name"))
{ {
if (firstRender) _username = PageState.QueryString["name"];
{ }
if(PageState.User == null)
{
await username.FocusAsync();
}
}
}
private async Task Login() if (PageState.QueryString.ContainsKey("token"))
{ {
validated = true; var user = new User();
var interop = new Interop(JSRuntime); user.SiteId = PageState.Site.SiteId;
if (await interop.FormValid(login)) user.Username = _username;
{ user = await UserService.VerifyEmailAsync(user, PageState.QueryString["token"]);
if (PageState.Runtime == Oqtane.Shared.Runtime.Server)
{
var user = new User();
user.SiteId = PageState.Site.SiteId;
user.Username = _username;
user.Password = _password;
user = await UserService.LoginUserAsync(user, false, false);
if (user.IsAuthenticated) if (user != null)
{ {
await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username); await logger.LogInformation(LogFunction.Security, "Email Verified For For Username {Username}", _username);
// server-side Blazor needs to post to the Login page so that the cookies are set correctly AddModuleMessage(Localizer["Success.Account.Verified"], MessageType.Info);
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = _returnUrl }; }
string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/"); else
await interop.SubmitForm(url, fields); {
} await logger.LogError(LogFunction.Security, "Email Verification Failed For Username {Username}", _username);
else AddModuleMessage(Localizer["Message.Account.NotVerfied"], MessageType.Warning);
{ }
await logger.LogError(LogFunction.Security, "Login Failed For Username {Username}", _username); }
AddModuleMessage(Localizer["Error.Login.Fail"], MessageType.Error); }
} catch (Exception ex)
} {
else await logger.LogError(ex, "Error Loading Login {Error}", ex.Message);
{ AddModuleMessage(Localizer["Error.LoadLogin"], MessageType.Error);
// client-side Blazor }
var user = new User(); }
user.SiteId = PageState.Site.SiteId;
user.Username = _username;
user.Password = _password;
user = await UserService.LoginUserAsync(user, true, _remember);
if (user.IsAuthenticated)
{
await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username);
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider));
authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(NavigateUrl(_returnUrl, true));
}
else
{
await logger.LogError(LogFunction.Security, "Login Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Error.Login.Fail"], MessageType.Error);
}
}
}
else
{
AddModuleMessage(Localizer["Message.Required.UserInfo"], MessageType.Warning);
}
}
private void Cancel() protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
NavigationManager.NavigateTo(_returnUrl); if (firstRender && PageState.User == null)
} {
await username.FocusAsync();
}
}
private async Task Forgot() private async Task Login()
{ {
if (_username != string.Empty) try
{ {
var user = await UserService.GetUserAsync(_username, PageState.Site.SiteId); validated = true;
if (user != null) var interop = new Interop(JSRuntime);
{ if (await interop.FormValid(login))
await UserService.ForgotPasswordAsync(user); {
await logger.LogInformation(LogFunction.Security, "Password Reset Notification Sent For Username {Username}", _username); var user = new User { SiteId = PageState.Site.SiteId, Username = _username, Password = _password};
_message = Localizer["Message.ForgotUser"];
}
else
{
_message = Localizer["Message.UserDoesNotExist"];
_type = MessageType.Warning;
}
}
else
{
_message = Localizer["Message.ForgotPassword"];
}
StateHasChanged(); if (!twofactor)
} {
user = await UserService.LoginUserAsync(user, false, false);
}
else
{
user = await UserService.VerifyTwoFactorAsync(user, _code);
}
if (user.IsAuthenticated)
{
await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username);
if (PageState.Runtime == Oqtane.Shared.Runtime.Server)
{
// server-side Blazor needs to post to the Login page so that the cookies are set correctly
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = _returnUrl };
string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/");
await interop.SubmitForm(url, fields);
}
else
{
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider));
authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(NavigateUrl(_returnUrl, true));
}
}
else
{
if (user.TwoFactorRequired)
{
twofactor = true;
validated = false;
AddModuleMessage(Localizer["Message.TwoFactor"], MessageType.Info);
}
else
{
if (!twofactor)
{
await logger.LogInformation(LogFunction.Security, "Login Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Error.Login.Fail"], MessageType.Error);
}
else
{
await logger.LogInformation(LogFunction.Security, "Two Factor Verification Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Error.TwoFactor.Fail"], MessageType.Error);
}
}
}
}
else
{
AddModuleMessage(Localizer["Message.Required.UserInfo"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Performing Login {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Login"], MessageType.Error);
}
}
private void Cancel()
{
NavigationManager.NavigateTo(_returnUrl);
}
private async Task Forgot()
{
try
{
if (_username != string.Empty)
{
var user = await UserService.GetUserAsync(_username, PageState.Site.SiteId);
if (user != null)
{
await UserService.ForgotPasswordAsync(user);
await logger.LogInformation(LogFunction.Security, "Password Reset Notification Sent For Username {Username}", _username);
AddModuleMessage(Localizer["Message.ForgotUser"], MessageType.Info);
}
else
{
AddModuleMessage(Localizer["Message.UserDoesNotExist"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["Message.ForgotPassword"], MessageType.Info);
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Resetting Password {Error}", ex.Message);
AddModuleMessage(Localizer["Error.ResetPassword"], MessageType.Error);
}
}
private void Reset()
{
twofactor = false;
_username = "";
_password = "";
ClearModuleMessage();
StateHasChanged();
}
private async Task KeyPressed(KeyboardEventArgs e)
{
if (e.Code == "Enter" || e.Code == "NumpadEnter")
{
await Login();
}
}
private void TogglePassword()
{
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = Localizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = Localizer["ShowPassword"];
}
}
private void ExternalLogin()
{
NavigationManager.NavigateTo(Utilities.TenantUrl(PageState.Alias, "/pages/external?returnurl=" + _returnUrl), true);
}
private async Task KeyPressed(KeyboardEventArgs e)
{
if (e.Code == "Enter" || e.Code == "NumpadEnter")
{
await Login();
}
}
} }

View File

@ -155,7 +155,7 @@
} }
} }
if (log.PageId != null && log.ModuleId != null) if (log.PageId != null && log.ModuleId != null && log.ModuleId != -1)
{ {
var pagemodule = await PageModuleService.GetPageModuleAsync(log.PageId.Value, log.ModuleId.Value); var pagemodule = await PageModuleService.GetPageModuleAsync(log.PageId.Value, log.ModuleId.Value);
if (pagemodule != null) if (pagemodule != null)

View File

@ -252,7 +252,7 @@
_title = page.Title; _title = page.Title;
_meta = page.Meta; _meta = page.Meta;
_path = page.Path; _path = page.Path;
_pageModules = PageState.Modules.Where(m => m.PageId == page.PageId && m.IsDeleted == false).ToList(); _pageModules = PageState.Modules.Where(m => m.PageId == page.PageId).ToList();
if (string.IsNullOrEmpty(_path)) if (string.IsNullOrEmpty(_path))
{ {

View File

@ -13,6 +13,7 @@
<Header> <Header>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th> <th>@SharedLocalizer["Name"]</th>
</Header> </Header>
<Row> <Row>

View File

@ -90,9 +90,10 @@ else
{ {
SiteId = PageState.Site.SiteId, SiteId = PageState.Site.SiteId,
Username = _username, Username = _username,
DisplayName = (_displayname == string.Empty ? _username : _displayname), Password = _password,
Email = _email, Email = _email,
Password = _password DisplayName = (_displayname == string.Empty ? _username : _displayname),
PhotoFileId = null
}; };
user = await UserService.AddUserAsync(user); user = await UserService.AddUserAsync(user);

View File

@ -120,7 +120,10 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Enter the password for your SMTP account" ResourceKey="SmtpPassword">Password: </Label> <Label Class="col-sm-3" For="password" HelpText="Enter the password for your SMTP account" ResourceKey="SmtpPassword">Password: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="password" type="password" class="form-control" @bind="@_smtppassword" /> <div class="input-group">
<input id="password" type="@_smtppasswordtype" class="form-control" @bind="@_smtppassword" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleSMTPPassword">@_togglesmtppassword</button>
</div>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -129,6 +132,12 @@
<input id="sender" class="form-control" @bind="@_smtpsender" /> <input id="sender" class="form-control" @bind="@_smtpsender" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of days of notifications to retain" ResourceKey="Retention">Retention (Days): </Label>
<div class="col-sm-9">
<input id="retention" class="form-control" @bind="@_retention" />
</div>
</div>
<button type="button" class="btn btn-secondary" @onclick="SendEmail">@Localizer["Smtp.TestConfig"]</button> <button type="button" class="btn btn-secondary" @onclick="SendEmail">@Localizer["Smtp.TestConfig"]</button>
<br /><br /> <br /><br />
</div> </div>
@ -260,7 +269,10 @@
private string _smtpssl = "False"; private string _smtpssl = "False";
private string _smtpusername = string.Empty; private string _smtpusername = string.Empty;
private string _smtppassword = string.Empty; private string _smtppassword = string.Empty;
private string _smtppasswordtype = "password";
private string _togglesmtppassword = string.Empty;
private string _smtpsender = string.Empty; private string _smtpsender = string.Empty;
private string _retention = string.Empty;
private string _pwaisenabled; private string _pwaisenabled;
private int _pwaappiconfileid = -1; private int _pwaappiconfileid = -1;
private FileManager _pwaappiconfilemanager; private FileManager _pwaappiconfilemanager;
@ -329,7 +341,9 @@
_smtpssl = SettingService.GetSetting(settings, "SMTPSSL", "False"); _smtpssl = SettingService.GetSetting(settings, "SMTPSSL", "False");
_smtpusername = SettingService.GetSetting(settings, "SMTPUsername", string.Empty); _smtpusername = SettingService.GetSetting(settings, "SMTPUsername", string.Empty);
_smtppassword = SettingService.GetSetting(settings, "SMTPPassword", string.Empty); _smtppassword = SettingService.GetSetting(settings, "SMTPPassword", string.Empty);
_togglesmtppassword = Localizer["Show"];
_smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty); _smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty);
_retention = SettingService.GetSetting(settings, "NotificationRetention", "30");
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
@ -479,12 +493,13 @@
site = await SiteService.UpdateSiteAsync(site); site = await SiteService.UpdateSiteAsync(site);
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
SettingService.SetSetting(settings, "SMTPHost", _smtphost, true); settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
SettingService.SetSetting(settings, "SMTPPort", _smtpport, true); settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true); settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true);
SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true); settings = SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true);
SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true); settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true);
SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true); settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
settings = SettingService.SetSetting(settings, "NotificationRetention", _retention, true);
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId); await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
@ -605,8 +620,10 @@
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId); await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
await logger.LogInformation("Site SMTP Settings Saved"); await logger.LogInformation("Site SMTP Settings Saved");
await NotificationService.AddNotificationAsync(new Notification(PageState.Site.SiteId, PageState.User.DisplayName, PageState.User.Email, PageState.User.DisplayName, PageState.User.Email, PageState.Site.Name + " SMTP Configuration Test", "SMTP Server Is Configured Correctly.")); await NotificationService.AddNotificationAsync(new Notification(PageState.Site.SiteId, PageState.User, PageState.Site.Name + " SMTP Configuration Test", "SMTP Server Is Configured Correctly."));
AddModuleMessage(Localizer["Info.Smtp.SaveSettings"], MessageType.Info); AddModuleMessage(Localizer["Info.Smtp.SaveSettings"], MessageType.Info);
var interop = new Interop(JSRuntime);
await interop.ScrollTo(0, 0, "smooth");
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -616,7 +633,7 @@
} }
else else
{ {
AddModuleMessage(Localizer["Message.required.Smtp"], MessageType.Warning); AddModuleMessage(Localizer["Message.Required.Smtp"], MessageType.Warning);
} }
} }
@ -633,4 +650,18 @@
} }
if (string.IsNullOrEmpty(_defaultalias)) _defaultalias = _aliases.First().Name.Trim(); if (string.IsNullOrEmpty(_defaultalias)) _defaultalias = _aliases.First().Name.Trim();
} }
private void ToggleSMTPPassword()
{
if (_smtppasswordtype == "password")
{
_smtppasswordtype = "text";
_togglesmtppassword = Localizer["Hide"];
}
else
{
_smtppasswordtype = "password";
_togglesmtppassword = Localizer["Show"];
}
}
} }

View File

@ -27,9 +27,27 @@
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="serverpath" HelpText="Server Path" ResourceKey="ServerPath">Server Path: </Label> <Label Class="col-sm-3" For="machinename" HelpText="Machine Name" ResourceKey="MachineName">Machine Name: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="serverpath" class="form-control" @bind="@_serverpath" readonly /> <input id="machinename" class="form-control" @bind="@_machinename" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="ipaddress" HelpText="Server IP Address" ResourceKey="IPAddress">IP Address: </Label>
<div class="col-sm-9">
<input id="ipaddress" class="form-control" @bind="@_ipaddress" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="contentrootpath" HelpText="Root Path" ResourceKey="ContentRootPath">Root Path: </Label>
<div class="col-sm-9">
<input id="contentrootpath" class="form-control" @bind="@_contentrootpath" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="webrootpath" HelpText="Web Path" ResourceKey="WebRootPath">Web Path: </Label>
<div class="col-sm-9">
<input id="webrootpath" class="form-control" @bind="@_webrootpath" readonly />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -38,6 +56,18 @@
<input id="servertime" class="form-control" @bind="@_servertime" readonly /> <input id="servertime" class="form-control" @bind="@_servertime" readonly />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tickcount" HelpText="Amount Of Time The Service Has Been Available And Operational" ResourceKey="TickCount">Service Uptime: </Label>
<div class="col-sm-9">
<input id="tickcount" class="form-control" @bind="@_tickcount" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="workingset" HelpText="Memory Allocation Of Service (in MB)" ResourceKey="WorkingSet">Memory Allocation: </Label>
<div class="col-sm-9">
<input id="workingset" class="form-control" @bind="@_workingset" readonly />
</div>
</div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="installationid" HelpText="The Unique Identifier For Your Installation" ResourceKey="InstallationId">Installation ID: </Label> <Label Class="col-sm-3" For="installationid" HelpText="The Unique Identifier For Your Installation" ResourceKey="InstallationId">Installation ID: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
@ -69,6 +99,21 @@
<option value="Warning">@Localizer["Warning"]</option> <option value="Warning">@Localizer["Warning"]</option>
<option value="Error">@Localizer["Error"]</option> <option value="Error">@Localizer["Error"]</option>
<option value="Critical">@Localizer["Critical"]</option> <option value="Critical">@Localizer["Critical"]</option>
<option value="None">@Localizer["None"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="notificationlevel" HelpText="The Minimum Logging Level For Which Notifications Should Be Sent To Host Users." ResourceKey="NotificationLevel">Notification Level: </Label>
<div class="col-sm-9">
<select id="notificationlevel" class="form-select" @bind="@_notificationlevel">
<option value="Trace">@Localizer["Trace"]</option>
<option value="Debug">@Localizer["Debug"]</option>
<option value="Information">@Localizer["Information"]</option>
<option value="Warning">@Localizer["Warning"]</option>
<option value="Error">@Localizer["Error"]</option>
<option value="Critical">@Localizer["Critical"]</option>
<option value="None">@Localizer["None"]</option>
</select> </select>
</div> </div>
</div> </div>
@ -104,12 +149,18 @@
private string _version = string.Empty; private string _version = string.Empty;
private string _clrversion = string.Empty; private string _clrversion = string.Empty;
private string _osversion = string.Empty; private string _osversion = string.Empty;
private string _serverpath = string.Empty; private string _machinename = string.Empty;
private string _ipaddress = string.Empty;
private string _contentrootpath = string.Empty;
private string _webrootpath = string.Empty;
private string _servertime = string.Empty; private string _servertime = string.Empty;
private string _tickcount = string.Empty;
private string _workingset = string.Empty;
private string _installationid = string.Empty; private string _installationid = string.Empty;
private string _detailederrors = string.Empty; private string _detailederrors = string.Empty;
private string _logginglevel = string.Empty; private string _logginglevel = string.Empty;
private string _notificationlevel = string.Empty;
private string _swagger = string.Empty; private string _swagger = string.Empty;
private string _packageservice = string.Empty; private string _packageservice = string.Empty;
@ -117,31 +168,42 @@
{ {
_version = Constants.Version; _version = Constants.Version;
Dictionary<string, string> systeminfo = await SystemService.GetSystemInfoAsync(); Dictionary<string, object> systeminfo = await SystemService.GetSystemInfoAsync("environment");
if (systeminfo != null) if (systeminfo != null)
{ {
_clrversion = systeminfo["clrversion"]; _clrversion = systeminfo["CLRVersion"].ToString();
_osversion = systeminfo["osversion"]; _osversion = systeminfo["OSVersion"].ToString();
_serverpath = systeminfo["serverpath"]; _machinename = systeminfo["MachineName"].ToString();
_servertime = systeminfo["servertime"] + " UTC"; _ipaddress = systeminfo["IPAddress"].ToString();
_installationid = systeminfo["installationid"]; _contentrootpath = systeminfo["ContentRootPath"].ToString();
_webrootpath = systeminfo["WebRootPath"].ToString();
_servertime = systeminfo["ServerTime"].ToString() + " UTC";
_tickcount = TimeSpan.FromMilliseconds(Convert.ToInt64(systeminfo["TickCount"].ToString())).ToString();
_workingset = (Convert.ToInt64(systeminfo["WorkingSet"].ToString()) / 1000000).ToString() + " MB";
}
_detailederrors = systeminfo["detailederrors"]; systeminfo = await SystemService.GetSystemInfoAsync();
_logginglevel = systeminfo["logginglevel"]; if (systeminfo != null)
_swagger = systeminfo["swagger"]; {
_packageservice = systeminfo["packageservice"]; _installationid = systeminfo["InstallationId"].ToString();
} _detailederrors = systeminfo["DetailedErrors"].ToString();
} _logginglevel = systeminfo["Logging:LogLevel:Default"].ToString();
_notificationlevel = systeminfo["Logging:LogLevel:Notify"].ToString();
_swagger = systeminfo["UseSwagger"].ToString();
_packageservice = systeminfo["PackageService"].ToString();
}
}
private async Task SaveConfig() private async Task SaveConfig()
{ {
try try
{ {
var settings = new Dictionary<string, string>(); var settings = new Dictionary<string, object>();
settings.Add("detailederrors", _detailederrors); settings.Add("DetailedErrors", _detailederrors);
settings.Add("logginglevel", _logginglevel); settings.Add("Logging:LogLevel:Default", _logginglevel);
settings.Add("swagger", _swagger); settings.Add("Logging:LogLevel:Notify", _notificationlevel);
settings.Add("packageservice", _packageservice); settings.Add("UseSwagger", _swagger);
settings.Add("PackageService", _packageservice);
await SystemService.UpdateSystemInfoAsync(settings); await SystemService.UpdateSystemInfoAsync(settings);
AddModuleMessage(Localizer["Success.UpdateConfig.Restart"], MessageType.Success); AddModuleMessage(Localizer["Success.UpdateConfig.Restart"], MessageType.Success);
} }

View File

@ -49,7 +49,7 @@
var user = await UserService.GetUserAsync(username, PageState.Site.SiteId); var user = await UserService.GetUserAsync(username, PageState.Site.SiteId);
if (user != null) if (user != null)
{ {
var notification = new Notification(PageState.Site.SiteId, PageState.User, user, subject, body, null); var notification = new Notification(PageState.Site.SiteId, PageState.User, user, subject, body);
notification = await NotificationService.AddNotificationAsync(notification); notification = await NotificationService.AddNotificationAsync(notification);
await logger.LogInformation("Notification Created {NotificationId}", notification.NotificationId); await logger.LogInformation("Notification Created {NotificationId}", notification.NotificationId);
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());

View File

@ -41,6 +41,18 @@ else
<input id="confirm" type="password" class="form-control" @bind="@confirm" autocomplete="new-password" /> <input id="confirm" type="password" class="form-control" @bind="@confirm" autocomplete="new-password" />
</div> </div>
</div> </div>
@if (allowtwofactor)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="twofactor" HelpText="Indicates if you are using two factor authentication. Two factor authentication requires you to enter a verification code sent via email after you sign in." ResourceKey="TwoFactor"></Label>
<div class="col-sm-9">
<select id="twofactor" class="form-select" @bind="@twofactor" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="Your email address where you wish to receive notifications" ResourceKey="Email"></Label> <Label Class="col-sm-3" For="email" HelpText="Your email address where you wish to receive notifications" ResourceKey="Email"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
@ -201,104 +213,125 @@ else
} }
</TabPanel> </TabPanel>
</TabStrip> </TabStrip>
<br /><br />
@code { @code {
private string username = string.Empty; private string username = string.Empty;
private string password = string.Empty; private string password = string.Empty;
private string confirm = string.Empty; private string confirm = string.Empty;
private string email = string.Empty; private bool allowtwofactor = false;
private string displayname = string.Empty; private string twofactor = "False";
private FileManager filemanager; private string email = string.Empty;
private int folderid = -1; private string displayname = string.Empty;
private int photofileid = -1; private FileManager filemanager;
private File photo = null; private int folderid = -1;
private List<Profile> profiles; private int photofileid = -1;
private Dictionary<string, string> settings; private File photo = null;
private string category = string.Empty; private List<Profile> profiles;
private string filter = "to"; private Dictionary<string, string> settings;
private List<Notification> notifications; private string category = string.Empty;
private string filter = "to";
private List<Notification> notifications;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
try try
{ {
if (PageState.User != null) if (PageState.Site.Settings.ContainsKey("LoginOptions:TwoFactor") && !string.IsNullOrEmpty(PageState.Site.Settings["LoginOptions:TwoFactor"]))
{ {
username = PageState.User.Username; allowtwofactor = bool.Parse(PageState.Site.Settings["LoginOptions:TwoFactor"]);
email = PageState.User.Email; }
displayname = PageState.User.DisplayName;
// get user folder if (PageState.User != null)
var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath); {
if (folder != null) username = PageState.User.Username;
{ twofactor = PageState.User.TwoFactorRequired.ToString();
folderid = folder.FolderId; email = PageState.User.Email;
} displayname = PageState.User.DisplayName;
if (PageState.User.PhotoFileId != null) // get user folder
{ var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath);
photofileid = PageState.User.PhotoFileId.Value; if (folder != null)
photo = await FileService.GetFileAsync(photofileid); {
} folderid = folder.FolderId;
else }
{
photofileid = -1;
photo = null;
}
profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId); if (PageState.User.PhotoFileId != null)
settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId); {
photofileid = PageState.User.PhotoFileId.Value;
photo = await FileService.GetFileAsync(photofileid);
}
else
{
photofileid = -1;
photo = null;
}
await LoadNotificationsAsync(); profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
} settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
else
{
AddModuleMessage(Localizer["Message.User.NoLogIn"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading User Profile {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Profile.Load"], MessageType.Error);
}
}
private async Task LoadNotificationsAsync() await LoadNotificationsAsync();
{ }
notifications = await NotificationService.GetNotificationsAsync(PageState.Site.SiteId, filter, PageState.User.UserId); else
notifications = notifications.Where(item => item.DeletedBy != PageState.User.Username).ToList(); {
} AddModuleMessage(Localizer["Message.User.NoLogIn"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading User Profile {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Profile.Load"], MessageType.Error);
}
}
private string GetProfileValue(string SettingName, string DefaultValue) private async Task LoadNotificationsAsync()
=> SettingService.GetSetting(settings, SettingName, DefaultValue); {
notifications = await NotificationService.GetNotificationsAsync(PageState.Site.SiteId, filter, PageState.User.UserId);
notifications = notifications.Where(item => item.DeletedBy != PageState.User.Username).ToList();
}
private async Task Save() private string GetProfileValue(string SettingName, string DefaultValue)
{ => SettingService.GetSetting(settings, SettingName, DefaultValue);
try
{
if (username != string.Empty && email != string.Empty && ValidateProfiles())
{
if (password == confirm)
{
var user = PageState.User;
user.Username = username;
user.Password = password;
user.Email = email;
user.DisplayName = (displayname == string.Empty ? username : displayname);
user.PhotoFileId = filemanager.GetFileId();
if (user.PhotoFileId == -1)
{
user.PhotoFileId = null;
}
await UserService.UpdateUserAsync(user); private async Task Save()
await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId); {
await logger.LogInformation("User Profile Saved"); try
{
if (username != string.Empty && email != string.Empty && ValidateProfiles())
{
if (password == confirm)
{
var user = PageState.User;
user.Username = username;
user.Password = password;
user.TwoFactorRequired = bool.Parse(twofactor);
user.Email = email;
user.DisplayName = (displayname == string.Empty ? username : displayname);
user.PhotoFileId = filemanager.GetFileId();
if (user.PhotoFileId == -1)
{
user.PhotoFileId = null;
}
if (user.PhotoFileId != null)
{
photofileid = user.PhotoFileId.Value;
photo = await FileService.GetFileAsync(photofileid);
}
else
{
photofileid = -1;
photo = null;
}
NavigationManager.NavigateTo(NavigateUrl()); await UserService.UpdateUserAsync(user);
} await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId);
await logger.LogInformation("User Profile Saved");
AddModuleMessage(Localizer["Success.Profile.Update"], MessageType.Success);
StateHasChanged();
}
else else
{ {
AddModuleMessage(Localizer["Message.Password.Invalid"], MessageType.Warning); AddModuleMessage(Localizer["Message.Password.Invalid"], MessageType.Warning);

View File

@ -55,15 +55,281 @@ else
</TabPanel> </TabPanel>
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings"> <TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <Section Name="User" Heading="User Settings" ResourceKey="UserSettings">
<Label Class="col-sm-3" For="allowregistration" HelpText="Do you want to allow visitors to be able to register for a user account on the site" ResourceKey="AllowRegistration">Allow User Registration? </Label> <div class="row mb-1 align-items-center">
<div class="col-sm-9"> <Label Class="col-sm-3" For="allowregistration" HelpText="Do you want anonymous visitors to be able to register for an account on the site" ResourceKey="AllowRegistration">Allow User Registration?</Label>
<select id="allowregistration" class="form-select" @bind="@_allowregistration" required> <div class="col-sm-9">
<option value="True">@SharedLocalizer["Yes"]</option> <select id="allowregistration" class="form-select" @bind="@_allowregistration">
<option value="False">@SharedLocalizer["No"]</option> <option value="True">@SharedLocalizer["Yes"]</option>
</select> <option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div> </div>
</div> @if (_providertype != "")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowsitelogin" HelpText="Do you want to allow users to sign in using a username and password that is managed locally on this site? Note that you should only disable this option if you have already sucessfully configured an external login provider, or else you may lock yourself out of the site." ResourceKey="AllowSiteLogin">Allow Login?</Label>
<div class="col-sm-9">
<select id="allowsitelogin" class="form-select" @bind="@_allowsitelogin">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
else
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowsitelogin" HelpText="Do you want to allow users to sign in using a username and password that is managed locally on this site? Note that you should only disable this option if you have already sucessfully configured an external login provider, or else you may lock yourself out of the site." ResourceKey="AllowSiteLogin">Allow Login?</Label>
<div class="col-sm-9">
<input id="allowsitelogin" class="form-control" value="@SharedLocalizer["Yes"]" readonly />
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="twofactor" HelpText="Do you want to allow users to use two factor authentication? Note that the Notification Job in Scheduled Jobs needs to be enabled for this option to work properly." ResourceKey="TwoFactor">Allow Two Factor?</Label>
<div class="col-sm-9">
<select id="twofactor" class="form-select" @bind="@_twofactor">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
@if (!string.IsNullOrEmpty(PageState.Alias.Path))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cookietype" HelpText="Cookies are usually managed per domain. However you can also choose to have distinct cookies for each site (this option is only applicable to micro-sites)." ResourceKey="CookieType">Cookie Type:</Label>
<div class="col-sm-9">
<select id="cookietype" class="form-select" @bind="@_cookietype">
<option value="domain">@Localizer["Domain"]</option>
<option value="site">@Localizer["Site"]</option>
</select>
</div>
</div>
}
</Section>
<Section Name="Password" Heading="Password Settings" ResourceKey="PasswordSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="minimumlength" HelpText="The Minimum Length For A Password" ResourceKey="RequiredLength">Minimum Length:</Label>
<div class="col-sm-9">
<input id="minimumlength" class="form-control" @bind="@_minimumlength" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="uniquecharacters" HelpText="The Minimum Number Of Unique Characters Which A Password Must Contain" ResourceKey="UniqueCharacters">Unique Characters:</Label>
<div class="col-sm-9">
<input id="uniquecharacters" class="form-control" @bind="@_uniquecharacters" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requiredigit" HelpText="Indicate If Passwords Must Contain A Digit" ResourceKey="RequireDigit">Require Digit?</Label>
<div class="col-sm-9">
<select id="requiredigit" class="form-select" @bind="@_requiredigit" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requireupper" HelpText="Indicate If Passwords Must Contain An Upper Case Character" ResourceKey="RequireUpper">Require Uppercase?</Label>
<div class="col-sm-9">
<select id="requireupper" class="form-select" @bind="@_requireupper" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requirelower" HelpText="Indicate If Passwords Must Contain A Lower Case Character" ResourceKey="RequireLower">Require Lowercase?</Label>
<div class="col-sm-9">
<select id="requirelower" class="form-select" @bind="@_requirelower" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requirepunctuation" HelpText="Indicate if Passwords Must Contain A Non-alphanumeric Character (ie. Punctuation)" ResourceKey="RequirePunctuation">Require Punctuation?</Label>
<div class="col-sm-9">
<select id="requirepunctuation" class="form-select" @bind="@_requirepunctuation" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</Section>
<Section Name="Lockout" Heading="Lockout Settings" ResourceKey="LockoutSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="maximum" HelpText="The Maximum Number Of Sign In Attempts Before A User Is Locked Out" ResourceKey="MaximumFailures">Maximum Failures:</Label>
<div class="col-sm-9">
<input id="maximum" class="form-control" @bind="@_maximumfailures" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lockoutduration" HelpText="The Number Of Minutes A User Should Be Locked Out" ResourceKey="LockoutDuration">Lockout Duration:</Label>
<div class="col-sm-9">
<input id="lockoutduration" class="form-control" @bind="@_lockoutduration" required />
</div>
</div>
</Section>
<Section Name="ExternalLogin" Heading="External Login Settings" ResourceKey="ExternalLoginSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="providertype" HelpText="Select the external login provider type" ResourceKey="ProviderType">Provider Type:</Label>
<div class="col-sm-9">
<select id="providertype" class="form-select" value="@_providertype" @onchange="(e => ProviderTypeChanged(e))">
<option value="" selected>@Localizer["Not Specified"]</option>
<option value="@AuthenticationProviderTypes.OpenIDConnect">@Localizer["OpenID Connect"]</option>
<option value="@AuthenticationProviderTypes.OAuth2">@Localizer["OAuth 2.0"]</option>
</select>
</div>
</div>
@if (_providertype != "")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="providername" HelpText="The external login provider name which will be displayed on the login page" ResourceKey="ProviderName">Provider Name:</Label>
<div class="col-sm-9">
<input id="providername" class="form-control" @bind="@_providername" />
</div>
</div>
}
@if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="authority" HelpText="The Authority Url or Issuer Url associated with the OpenID Connect provider" ResourceKey="Authority">Authority:</Label>
<div class="col-sm-9">
<input id="authority" class="form-control" @bind="@_authority" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="metadataurl" HelpText="The discovery endpoint for obtaining metadata for this provider. Only specify if the OpenID Connect provider does not use the standard approach (ie. /.well-known/openid-configuration)" ResourceKey="MetadataUrl">Metadata Url:</Label>
<div class="col-sm-9">
<input id="metadataurl" class="form-control" @bind="@_metadataurl" />
</div>
</div>
}
@if (_providertype == AuthenticationProviderTypes.OAuth2)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="authorizationurl" HelpText="The endpoint for obtaining an Authorization Code" ResourceKey="AuthorizationUrl">Authorization Url:</Label>
<div class="col-sm-9">
<input id="authorizationurl" class="form-control" @bind="@_authorizationurl" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tokenurl" HelpText="The endpoint for obtaining an Auth Token" ResourceKey="TokenUrl">Token Url:</Label>
<div class="col-sm-9">
<input id="tokenurl" class="form-control" @bind="@_tokenurl" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="userinfourl" HelpText="The endpoint for obtaining user information. This should be an API or Page Url which contains the users email address." ResourceKey="UserInfoUrl">User Info Url:</Label>
<div class="col-sm-9">
<input id="userinfourl" class="form-control" @bind="@_userinfourl" />
</div>
</div>
}
@if (_providertype != "")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clientid" HelpText="The Client ID from the provider" ResourceKey="ClientID">Client ID:</Label>
<div class="col-sm-9">
<input id="clientid" class="form-control" @bind="@_clientid" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clientsecret" HelpText="The Client Secret from the provider" ResourceKey="ClientSecret">Client Secret:</Label>
<div class="col-sm-9">
<div class="input-group">
<input type="@_clientsecrettype" id="clientsecret" class="form-control" @bind="@_clientsecret" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleClientSecret">@_toggleclientsecret</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="scopes" HelpText="A list of Scopes to request from the provider (separated by commas). If none are specified, standard Scopes will be used by default." ResourceKey="Scopes">Scopes:</Label>
<div class="col-sm-9">
<input id="scopes" class="form-control" @bind="@_scopes" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pkce" HelpText="Indicate if the provider supports Proof Key for Code Exchange (PKCE)" ResourceKey="PKCE">Use PKCE?</Label>
<div class="col-sm-9">
<select id="pkce" class="form-select" @bind="@_pkce" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="redirecturl" HelpText="The Redirect Url (or Callback Url) which usually needs to be registered with the provider" ResourceKey="RedirectUrl">Redirect Url:</Label>
<div class="col-sm-9">
<input id="redirecturl" class="form-control" @bind="@_redirecturl" readonly />
</div>
</div>
@if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="emailclaimtype" HelpText="The type name for the email address claim provided by the provider" ResourceKey="EmailClaimType">Email Claim Type:</Label>
<div class="col-sm-9">
<input id="emailclaimtype" class="form-control" @bind="@_emailclaimtype" />
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="domainfilter" HelpText="Provide any email domain filter criteria (separated by commas). Domains to exclude should be prefixed with an exclamation point (!). For example 'microsoft.com,!hotmail.com' would include microsoft.com email addresses but not hotmail.com email addresses." ResourceKey="DomainFilter">Domain Filter:</Label>
<div class="col-sm-9">
<input id="domainfilter" class="form-control" @bind="@_domainfilter" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="createusers" HelpText="Do you want new users to be created automatically? If you disable this option, users must already be registered on the site in order to sign in with their external login." ResourceKey="CreateUsers">Create New Users?</Label>
<div class="col-sm-9">
<select id="createusers" class="form-select" @bind="@_createusers">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
</Section>
<Section Name="Token" Heading="Token Settings" ResourceKey="TokenSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="secret" HelpText="If you want to want to provide API access, please specify a secret which will be used to encrypt your tokens. The secret should be 16 characters or more to ensure optimal security. Please note that if you change this secret, all existing tokens will become invalid and will need to be regenerated." ResourceKey="Secret">Secret:</Label>
<div class="col-sm-9">
<div class="input-group">
<input type="@_secrettype" id="secret" class="form-control" @bind="@_secret" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleSecret">@_togglesecret</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="issuer" HelpText="Optionally provide the issuer of the token" ResourceKey="Issuer">Issuer:</Label>
<div class="col-sm-9">
<input id="issuer" class="form-control" @bind="@_issuer" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="audience" HelpText="Optionally provide the audience for the token" ResourceKey="Audience">Audience:</Label>
<div class="col-sm-9">
<input id="audience" class="form-control" @bind="@_audience" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lifetime" HelpText="The number of minutes for which a token should be valid" ResourceKey="Lifetime">Lifetime:</Label>
<div class="col-sm-9">
<input id="lifetime" class="form-control" @bind="@_lifetime" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="token" HelpText="Select the Create Token button to generate a long-lived access token (valid for 1 year). Be sure to store this token in a safe location as you will not be able to access it in the future." ResourceKey="Token">Access Token:</Label>
<div class="col-sm-9">
<div class="input-group">
<input id="token" class="form-control" @bind="@_token" />
<button type="button" class="btn btn-secondary" @onclick="@CreateToken">@Localizer["CreateToken"]</button>
</div>
</div>
</div>
</Section>
</div> </div>
<br /> <br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button> <button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
@ -72,79 +338,156 @@ else
} }
@code { @code {
private List<UserRole> allroles; private List<UserRole> allroles;
private List<UserRole> userroles; private List<UserRole> userroles;
private string _search; private string _search;
private string _allowregistration; private string _allowregistration;
private string _allowsitelogin;
private string _twofactor;
private string _cookietype;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; private string _minimumlength;
private string _uniquecharacters;
private string _requiredigit;
private string _requireupper;
private string _requirelower;
private string _requirepunctuation;
private string _maximumfailures;
private string _lockoutduration;
protected override async Task OnInitializedAsync() private string _providertype;
{ private string _providername;
allroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId); private string _authority;
await LoadSettingsAsync(); private string _metadataurl;
userroles = Search(_search); private string _authorizationurl;
private string _tokenurl;
private string _userinfourl;
private string _clientid;
private string _clientsecret;
private string _clientsecrettype = "password";
private string _toggleclientsecret = string.Empty;
private string _scopes;
private string _pkce;
private string _redirecturl;
private string _emailclaimtype;
private string _domainfilter;
private string _createusers;
private string _secret;
private string _secrettype = "password";
private string _togglesecret = string.Empty;
private string _issuer;
private string _audience;
private string _lifetime;
private string _token;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
protected override async Task OnInitializedAsync()
{
allroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId);
await LoadSettingsAsync();
userroles = Search(_search);
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_allowregistration = PageState.Site.AllowRegistration.ToString(); _allowregistration = PageState.Site.AllowRegistration.ToString();
} _allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true");
_twofactor = SettingService.GetSetting(settings, "LoginOptions:TwoFactor", "false");
_cookietype = SettingService.GetSetting(settings, "LoginOptions:CookieType", "domain");
private List<UserRole> Search(string search) _minimumlength = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredLength", "6");
{ _uniquecharacters = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", "1");
var results = allroles.Where(item => item.Role.Name == RoleNames.Registered || (item.Role.Name == RoleNames.Host && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))); _requiredigit = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireDigit", "true");
_requireupper = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireUppercase", "true");
_requirelower = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireLowercase", "true");
_requirepunctuation = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireNonAlphanumeric", "true");
if (!string.IsNullOrEmpty(_search)) _maximumfailures = SettingService.GetSetting(settings, "IdentityOptions:Lockout:MaxFailedAccessAttempts", "5");
{ _lockoutduration = TimeSpan.Parse(SettingService.GetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", "00:05:00")).TotalMinutes.ToString();
results = results.Where(item =>
(
item.User.Username.Contains(search, StringComparison.OrdinalIgnoreCase) ||
item.User.Email.Contains(search, StringComparison.OrdinalIgnoreCase) ||
item.User.DisplayName.Contains(search, StringComparison.OrdinalIgnoreCase)
)
);
}
return results.ToList();
}
private async Task OnSearch() _providertype = SettingService.GetSetting(settings, "ExternalLogin:ProviderType", "");
{ _providername = SettingService.GetSetting(settings, "ExternalLogin:ProviderName", "");
userroles = Search(_search); _authority = SettingService.GetSetting(settings, "ExternalLogin:Authority", "");
await UpdateSettingsAsync(); _metadataurl = SettingService.GetSetting(settings, "ExternalLogin:MetadataUrl", "");
} _authorizationurl = SettingService.GetSetting(settings, "ExternalLogin:AuthorizationUrl", "");
_tokenurl = SettingService.GetSetting(settings, "ExternalLogin:TokenUrl", "");
_userinfourl = SettingService.GetSetting(settings, "ExternalLogin:UserInfoUrl", "");
_clientid = SettingService.GetSetting(settings, "ExternalLogin:ClientId", "");
_clientsecret = SettingService.GetSetting(settings, "ExternalLogin:ClientSecret", "");
_toggleclientsecret = Localizer["Show"];
_scopes = SettingService.GetSetting(settings, "ExternalLogin:Scopes", "");
_pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false");
_redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype;
_emailclaimtype = SettingService.GetSetting(settings, "ExternalLogin:EmailClaimType", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress");
_domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", "");
_createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true");
private async Task DeleteUser(UserRole UserRole) _secret = SettingService.GetSetting(settings, "JwtOptions:Secret", "");
{ _togglesecret = Localizer["Show"];
try _issuer = SettingService.GetSetting(settings, "JwtOptions:Issuer", PageState.Uri.Scheme + "://" + PageState.Alias.Name);
{ _audience = SettingService.GetSetting(settings, "JwtOptions:Audience", "");
var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId); _lifetime = SettingService.GetSetting(settings, "JwtOptions:Lifetime", "20");
if (user != null) }
{
await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId);
await logger.LogInformation("User Deleted {User}", UserRole.User);
allroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId);
userroles = Search(_search);
StateHasChanged();
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting User {User} {Error}", UserRole.User, ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
}
}
private string settingSearch = "AU-search"; private List<UserRole> Search(string search)
{
var results = allroles.Where(item => item.Role.Name == RoleNames.Registered || (item.Role.Name == RoleNames.Host && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)));
private async Task LoadSettingsAsync() if (!string.IsNullOrEmpty(_search))
{ {
Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId); results = results.Where(item =>
_search = SettingService.GetSetting(settings, settingSearch, ""); (
} item.User.Username.Contains(search, StringComparison.OrdinalIgnoreCase) ||
item.User.Email.Contains(search, StringComparison.OrdinalIgnoreCase) ||
item.User.DisplayName.Contains(search, StringComparison.OrdinalIgnoreCase)
)
);
}
return results.ToList();
}
private async Task UpdateSettingsAsync() private async Task OnSearch()
{ {
Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId); userroles = Search(_search);
SettingService.SetSetting(settings, settingSearch, _search); await UpdateSettingsAsync();
await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId); }
}
private async Task DeleteUser(UserRole UserRole)
{
try
{
var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId);
if (user != null)
{
await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId);
await logger.LogInformation("User Deleted {User}", UserRole.User);
allroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId);
userroles = Search(_search);
StateHasChanged();
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting User {User} {Error}", UserRole.User, ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
}
}
private string settingSearch = "AU-search";
private async Task LoadSettingsAsync()
{
Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
_search = SettingService.GetSetting(settings, settingSearch, "");
}
private async Task UpdateSettingsAsync()
{
Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
SettingService.SetSetting(settings, settingSearch, _search);
await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId);
}
private async Task SaveSiteSettings() private async Task SaveSiteSettings()
{ {
@ -153,6 +496,46 @@ else
var site = PageState.Site; var site = PageState.Site;
site.AllowRegistration = bool.Parse(_allowregistration); site.AllowRegistration = bool.Parse(_allowregistration);
await SiteService.UpdateSiteAsync(site); await SiteService.UpdateSiteAsync(site);
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
settings = SettingService.SetSetting(settings, "LoginOptions:AllowSiteLogin", _allowsitelogin, false);
settings = SettingService.SetSetting(settings, "LoginOptions:TwoFactor", _twofactor, false);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieType", _cookietype, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredLength", _minimumlength, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", _uniquecharacters, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireDigit", _requiredigit, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireUppercase", _requireupper, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireLowercase", _requirelower, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireNonAlphanumeric", _requirepunctuation, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Lockout:MaxFailedAccessAttempts", _maximumfailures, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", TimeSpan.FromMinutes(Convert.ToInt64(_lockoutduration)).ToString(), true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ProviderType", _providertype, false);
settings = SettingService.SetSetting(settings, "ExternalLogin:ProviderName", _providername, false);
settings = SettingService.SetSetting(settings, "ExternalLogin:Authority", _authority, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:MetadataUrl", _metadataurl, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:AuthorizationUrl", _authorizationurl, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:TokenUrl", _tokenurl, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:UserInfoUrl", _userinfourl, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ClientId", _clientid, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ClientSecret", _clientsecret, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:Scopes", _scopes, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:PKCE", _pkce, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true);
if (!string.IsNullOrEmpty(_secret) && _secret.Length < 16) _secret = (_secret + "????????????????").Substring(0, 16);
settings = SettingService.SetSetting(settings, "JwtOptions:Secret", _secret, true);
settings = SettingService.SetSetting(settings, "JwtOptions:Issuer", _issuer, true);
settings = SettingService.SetSetting(settings, "JwtOptions:Audience", _audience, true);
settings = SettingService.SetSetting(settings, "JwtOptions:Lifetime", _lifetime, true);
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
await SettingService.ClearSiteSettingsCacheAsync();
AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success); AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);
} }
catch (Exception ex) catch (Exception ex)
@ -162,4 +545,51 @@ else
} }
} }
private void ProviderTypeChanged(ChangeEventArgs e)
{
_providertype = (string)e.Value;
if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{
_scopes = "openid,profile,email";
}
else
{
_scopes = "";
}
_redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype;
StateHasChanged();
}
private async Task CreateToken()
{
_token = await UserService.GetTokenAsync();
}
private void ToggleClientSecret()
{
if (_clientsecrettype == "password")
{
_clientsecrettype = "text";
_toggleclientsecret = Localizer["Hide"];
}
else
{
_clientsecrettype = "password";
_toggleclientsecret = Localizer["Show"];
}
}
private void ToggleSecret()
{
if (_secrettype == "password")
{
_secrettype = "text";
_togglesecret = Localizer["Hide"];
}
else
{
_secrettype = "password";
_togglesecret = Localizer["Show"];
}
}
} }

View File

@ -52,7 +52,7 @@
<input type="file" id="@_fileinputid" name="file" accept="@_filter" /> <input type="file" id="@_fileinputid" name="file" accept="@_filter" />
} }
</div> </div>
<div class="col mt-2 text-center"> <div class="col mt-2 text-end">
<button type="button" class="btn btn-success" @onclick="UploadFile">@SharedLocalizer["Upload"]</button> <button type="button" class="btn btn-success" @onclick="UploadFile">@SharedLocalizer["Upload"]</button>
@if (ShowFiles && GetFileId() != -1) @if (ShowFiles && GetFileId() != -1)
{ {

View File

@ -252,7 +252,7 @@
for (int i = 0; i < _permissions.Count; i++) for (int i = 0; i < _permissions.Count; i++)
{ {
permission = _permissions[i]; permission = _permissions[i];
List<string> ids = permission.Permissions.Split(';').ToList(); List<string> ids = permission.Permissions.Split(';', StringSplitOptions.RemoveEmptyEntries).ToList();
ids.Remove("!" + RoleNames.Everyone); // remove deny all users ids.Remove("!" + RoleNames.Everyone); // remove deny all users
ids.Remove("!" + RoleNames.Registered); // remove deny registered users ids.Remove("!" + RoleNames.Registered); // remove deny registered users
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))

View File

@ -53,9 +53,9 @@ namespace Oqtane.Modules
if (Resources != null && Resources.Exists(item => item.ResourceType == ResourceType.Script)) if (Resources != null && Resources.Exists(item => item.ResourceType == ResourceType.Script))
{ {
var scripts = new List<object>(); var scripts = new List<object>();
foreach (Resource resource in Resources.Where(item => item.ResourceType == ResourceType.Script && item.Declaration != ResourceDeclaration.Global)) foreach (Resource resource in Resources.Where(item => item.ResourceType == ResourceType.Script))
{ {
scripts.Add(new { href = resource.Url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "" }); scripts.Add(new { href = resource.Url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", es6module = resource.ES6Module });
} }
if (scripts.Any()) if (scripts.Any())
{ {

View File

@ -5,7 +5,7 @@
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<RazorLangVersion>3.0</RazorLangVersion> <RazorLangVersion>3.0</RazorLangVersion>
<Configurations>Debug;Release</Configurations> <Configurations>Debug;Release</Configurations>
<Version>3.0.3</Version> <Version>3.1.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -13,7 +13,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.0.3</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace> <RootNamespace>Oqtane</RootNamespace>
@ -22,11 +22,12 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.0" PrivateAssets="all" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="6.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="6.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="6.0.3" />
<PackageReference Include="System.Net.Http.Json" Version="6.0.0" /> <PackageReference Include="System.Net.Http.Json" Version="6.0.0" />
</ItemGroup> </ItemGroup>

View File

@ -28,8 +28,9 @@ namespace Oqtane.Client
var builder = WebAssemblyHostBuilder.CreateDefault(args); var builder = WebAssemblyHostBuilder.CreateDefault(args);
var httpClient = new HttpClient {BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)}; var httpClient = new HttpClient {BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)};
builder.Services.AddSingleton(httpClient); builder.Services.AddSingleton(httpClient);
builder.Services.AddHttpClient(); // IHttpClientFactory for calling remote services via RemoteServiceBase
builder.Services.AddOptions(); builder.Services.AddOptions();
// Register localization services // Register localization services

View File

@ -130,12 +130,15 @@
<value>Install Now</value> <value>Install Now</value>
</data> </data>
<data name="Error.DbConfig.Load" xml:space="preserve"> <data name="Error.DbConfig.Load" xml:space="preserve">
<value>Error loading Database Configuration Control</value> <value>Error Loading Database Configuration Control</value>
</data> </data>
<data name="Message.Require.DbInfo" xml:space="preserve"> <data name="Message.Require.DbInfo" xml:space="preserve">
<value>Please Enter All Required Fields. Ensure Passwords Match And Are Greater Than 5 Characters In Length. Ensure Email Address Provided Is Valid.</value> <value>Please Enter All Required Fields. Ensure Passwords Match And Email Address Provided Is Valid.</value>
</data> </data>
<data name="Register" xml:space="preserve"> <data name="Message.Password.Invalid" xml:space="preserve">
<value>The Password Provided Does Not Meet The Password Policy. Please Verify The Minimum Password Length And Complexity Requirements.</value>
</data>
<data name="Register" xml:space="preserve">
<value>Please Register Me For Major Product Updates And Security Bulletins</value> <value>Please Register Me For Major Product Updates And Security Bulletins</value>
</data> </data>
<data name="Confirm.HelpText" xml:space="preserve"> <data name="Confirm.HelpText" xml:space="preserve">

View File

@ -117,9 +117,6 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="RememberMe" xml:space="preserve">
<value>Remember Me?</value>
</data>
<data name="ForgotPassword" xml:space="preserve"> <data name="ForgotPassword" xml:space="preserve">
<value>Forgot Password</value> <value>Forgot Password</value>
</data> </data>
@ -130,10 +127,10 @@
<value>User Account Could Not Be Verified. Please Contact Your Administrator For Further Instructions.</value> <value>User Account Could Not Be Verified. Please Contact Your Administrator For Further Instructions.</value>
</data> </data>
<data name="Error.Login.Fail" xml:space="preserve"> <data name="Error.Login.Fail" xml:space="preserve">
<value>Login Failed. Please Remember That Passwords Are Case Sensitive And User Accounts Require Verification When They Are Initially Created So You May Wish To Check Your Email.</value> <value>Login Failed. Please Remember That Passwords Are Case Sensitive. If You Have Attempted To Sign In Multiple Times Unsuccessfully, Your Account Will Be Locked Out For A Period Of Time. Note That User Accounts Require Verification When They Are Initially Created So You May Wish To Check Your Email If You Are A New User.</value>
</data> </data>
<data name="Message.Required.UserInfo" xml:space="preserve"> <data name="Message.Required.UserInfo" xml:space="preserve">
<value>Please Provide Your Username And Password</value> <value>Please Provide All Required Fields</value>
</data> </data>
<data name="Info.SignedIn" xml:space="preserve"> <data name="Info.SignedIn" xml:space="preserve">
<value>You Are Already Signed In</value> <value>You Are Already Signed In</value>
@ -147,4 +144,61 @@
<data name="Message.UserDoesNotExist" xml:space="preserve"> <data name="Message.UserDoesNotExist" xml:space="preserve">
<value>User Does Not Exist</value> <value>User Does Not Exist</value>
</data> </data>
<data name="Code.HelpText" xml:space="preserve">
<value>Please Enter The Secure Verification Code Which Was Sent To You By Email.</value>
</data>
<data name="Code.Placeholder" xml:space="preserve">
<value>Verification Code</value>
</data>
<data name="Code.Text" xml:space="preserve">
<value>Verification Code:</value>
</data>
<data name="Error.TwoFactor.Fail" xml:space="preserve">
<value>Verification Failed. Please Ensure You Entered The Code Exactly In The Form Provided In Your Email. If You Wish To Request A New Verification Code Please Select The Cancel Option And Sign In Again. </value>
</data>
<data name="Message.TwoFactor" xml:space="preserve">
<value>A Secure Verification Code Has Been Sent To Your Email Address. Please Enter The Code That You Received. If You Do Not Receive The Code Or You Have Lost Access To Your Email, Please Contact Your Administrator.</value>
</data>
<data name="Password.HelpText" xml:space="preserve">
<value>Please Enter The Password Related To Your Account. Remember That Passwords Are Case Sensitive. If You Attempt Unsuccessfully To Log In To Your Account Multiple Times, You Will Be Locked Out For A Period Of Time.</value>
</data>
<data name="Password.Placeholder" xml:space="preserve">
<value>Password</value>
</data>
<data name="Password.Text" xml:space="preserve">
<value>Password:</value>
</data>
<data name="Remember.HelpText" xml:space="preserve">
<value>Specify If You Would Like To Be Signed Back In Automatically The Next Time You Visit This Site</value>
</data>
<data name="Remember.Text" xml:space="preserve">
<value>Remember Me?</value>
</data>
<data name="Username.HelpText" xml:space="preserve">
<value>Please Enter The Username Related To Your Account</value>
</data>
<data name="Username.Placeholder" xml:space="preserve">
<value>Username</value>
</data>
<data name="Username.Text" xml:space="preserve">
<value>Username:</value>
</data>
<data name="HidePassword" xml:space="preserve">
<value>Hide</value>
</data>
<data name="ShowPassword" xml:space="preserve">
<value>Show</value>
</data>
<data name="Use" xml:space="preserve">
<value>Use</value>
</data>
<data name="Error.LoadLogin" xml:space="preserve">
<value>Error Loading Login</value>
</data>
<data name="Error.Login" xml:space="preserve">
<value>Error Performing Login</value>
</data>
<data name="Error.ResetPassword" xml:space="preserve">
<value>Error Resetting Password</value>
</data>
</root> </root>

View File

@ -318,10 +318,16 @@
<data name="DefaultAlias.HelpText" xml:space="preserve"> <data name="DefaultAlias.HelpText" xml:space="preserve">
<value>The default alias for the site. Requests for non-default aliases will be redirected to the default alias.</value> <value>The default alias for the site. Requests for non-default aliases will be redirected to the default alias.</value>
</data> </data>
<data name="DefaultAlias.Text" xml:space="preserve"> <data name="DefaultAlias.Text" xml:space="preserve">
<value>Default Alias: </value> <value>Default Alias: </value>
</data> </data>
<data name="Aliases.Heading" xml:space="preserve"> <data name="Aliases.Heading" xml:space="preserve">
<value>Aliases</value> <value>Aliases</value>
</data> </data>
<data name="Hide" xml:space="preserve">
<value>Hide</value>
</data>
<data name="Show" xml:space="preserve">
<value>Show</value>
</data>
</root> </root>

View File

@ -129,8 +129,8 @@
<data name="OSVersion.HelpText" xml:space="preserve"> <data name="OSVersion.HelpText" xml:space="preserve">
<value>Operating System Version</value> <value>Operating System Version</value>
</data> </data>
<data name="ServerPath.HelpText" xml:space="preserve"> <data name="ContentRootPath.HelpText" xml:space="preserve">
<value>Server Path</value> <value>Server Root Path</value>
</data> </data>
<data name="ServerTime.HelpText" xml:space="preserve"> <data name="ServerTime.HelpText" xml:space="preserve">
<value>Server Date/Time (in UTC)</value> <value>Server Date/Time (in UTC)</value>
@ -144,8 +144,8 @@
<data name="OSVersion.Text" xml:space="preserve"> <data name="OSVersion.Text" xml:space="preserve">
<value>OS Version: </value> <value>OS Version: </value>
</data> </data>
<data name="ServerPath.Text" xml:space="preserve"> <data name="ContentRootPath.Text" xml:space="preserve">
<value>Server Path: </value> <value>Root Path: </value>
</data> </data>
<data name="ServerTime.Text" xml:space="preserve"> <data name="ServerTime.Text" xml:space="preserve">
<value>Server Date/Time: </value> <value>Server Date/Time: </value>
@ -231,4 +231,43 @@
<data name="RestartApplication.Text" xml:space="preserve"> <data name="RestartApplication.Text" xml:space="preserve">
<value>Restart Application</value> <value>Restart Application</value>
</data> </data>
<data name="None" xml:space="preserve">
<value>None</value>
</data>
<data name="NotificationLevel.HelpText" xml:space="preserve">
<value>The Minimum Logging Level For Which Notifications Should Be Sent To Host Users.</value>
</data>
<data name="NotificationLevel.Text" xml:space="preserve">
<value>Notification Level:</value>
</data>
<data name="IPAddress.HelpText" xml:space="preserve">
<value>Server IP Address</value>
</data>
<data name="IPAddress.Text" xml:space="preserve">
<value>IP Address:</value>
</data>
<data name="MachineName.HelpText" xml:space="preserve">
<value>Server Machine Name</value>
</data>
<data name="MachineName.Text" xml:space="preserve">
<value>Machine Name:</value>
</data>
<data name="TickCount.HelpText" xml:space="preserve">
<value>Amount Of Time The Service Has Been Available And Operational</value>
</data>
<data name="TickCount.Text" xml:space="preserve">
<value>Service Uptime:</value>
</data>
<data name="WebRootPath.HelpText" xml:space="preserve">
<value>Server Web Root Path</value>
</data>
<data name="WebRootPath.Text" xml:space="preserve">
<value>Web Path:</value>
</data>
<data name="WorkingSet.HelpText" xml:space="preserve">
<value>Memory Allocation Of Service (in MB)</value>
</data>
<data name="WorkingSet.Text" xml:space="preserve">
<value>Memory Allocation:</value>
</data>
</root> </root>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@ -169,10 +169,10 @@
<value>Identity</value> <value>Identity</value>
</data> </data>
<data name="Confirm.HelpText" xml:space="preserve"> <data name="Confirm.HelpText" xml:space="preserve">
<value>If you are changing your password you must enter it again to confirm it matches</value> <value>If you are changing your password you must enter it again to confirm it matches the value entered above</value>
</data> </data>
<data name="Confirm.Text" xml:space="preserve"> <data name="Confirm.Text" xml:space="preserve">
<value>Confirm Password:</value> <value>Confirmation:</value>
</data> </data>
<data name="DisplayName.HelpText" xml:space="preserve"> <data name="DisplayName.HelpText" xml:space="preserve">
<value>Your full name</value> <value>Your full name</value>
@ -204,4 +204,10 @@
<data name="Username.Text" xml:space="preserve"> <data name="Username.Text" xml:space="preserve">
<value>Username:</value> <value>Username:</value>
</data> </data>
<data name="TwoFactor.HelpText" xml:space="preserve">
<value>Indicates if you are using two factor authentication. Two factor authentication requires you to enter a verification code sent via email after you sign in.</value>
</data>
<data name="TwoFactor.Text" xml:space="preserve">
<value>Two Factor?</value>
</data>
</root> </root>

View File

@ -127,10 +127,10 @@
<value>Delete User</value> <value>Delete User</value>
</data> </data>
<data name="AllowRegistration.HelpText" xml:space="preserve"> <data name="AllowRegistration.HelpText" xml:space="preserve">
<value>Do you want the users to be able to register for an account on the site</value> <value>Do you want anonymous visitors to be able to register for an account on the site</value>
</data> </data>
<data name="AllowRegistration.Text" xml:space="preserve"> <data name="AllowRegistration.Text" xml:space="preserve">
<value>Allow User Registration? </value> <value>Allow Registration? </value>
</data> </data>
<data name="Error.SaveSiteSettings" xml:space="preserve"> <data name="Error.SaveSiteSettings" xml:space="preserve">
<value>Error Saving Settings</value> <value>Error Saving Settings</value>
@ -153,4 +153,217 @@
<data name="Roles.Text" xml:space="preserve"> <data name="Roles.Text" xml:space="preserve">
<value>Roles</value> <value>Roles</value>
</data> </data>
<data name="LockoutDuration.HelpText" xml:space="preserve">
<value>The number of minutes a user should be locked out</value>
</data>
<data name="LockoutDuration.Text" xml:space="preserve">
<value>Lockout Duration:</value>
</data>
<data name="MaximumFailures.HelpText" xml:space="preserve">
<value>The maximum number of sign in attempts before a user is locked out</value>
</data>
<data name="MaximumFailures.Text" xml:space="preserve">
<value>Maximum Failures:</value>
</data>
<data name="RequireDigit.HelpText" xml:space="preserve">
<value>Indicate if passwords must contain a digit</value>
</data>
<data name="RequireDigit.Text" xml:space="preserve">
<value>Require Digit?</value>
</data>
<data name="RequiredLength.HelpText" xml:space="preserve">
<value>The minimum length for a password</value>
</data>
<data name="RequiredLength.Text" xml:space="preserve">
<value>Minimum Length:</value>
</data>
<data name="RequireLower.HelpText" xml:space="preserve">
<value>Indicate if passwords must contain a lower case character</value>
</data>
<data name="RequireLower.Text" xml:space="preserve">
<value>Require Lowercase?</value>
</data>
<data name="RequirePunctuation.HelpText" xml:space="preserve">
<value>Indicate if passwords must contain a non-alphanumeric character (ie. punctuation)</value>
</data>
<data name="RequirePunctuation.Text" xml:space="preserve">
<value>Require Punctuation?</value>
</data>
<data name="RequireUpper.HelpText" xml:space="preserve">
<value>Indicate if passwords must contain an upper case character</value>
</data>
<data name="RequireUpper.Text" xml:space="preserve">
<value>Require Uppercase?</value>
</data>
<data name="Success.UpdateConfig.Restart" xml:space="preserve">
<value>Configuration Updated. Please Select Restart Application For These Changes To Be Activated.</value>
</data>
<data name="UniqueCharacters.HelpText" xml:space="preserve">
<value>The minimum number of unique characters which a password must contain</value>
</data>
<data name="UniqueCharacters.Text" xml:space="preserve">
<value>Unique Characters:</value>
</data>
<data name="AllowSiteLogin.HelpText" xml:space="preserve">
<value>Do you want to allow users to sign in using a username and password that is managed locally on this site? Note that you should only disable this option if you have already sucessfully configured an external login provider, or else you may lock yourself out of the site.</value>
</data>
<data name="AllowSiteLogin.Text" xml:space="preserve">
<value>Allow Login?</value>
</data>
<data name="Authority.HelpText" xml:space="preserve">
<value>The Authority Url or Issuer Url associated with the OpenID Connect provider</value>
</data>
<data name="Authority.Text" xml:space="preserve">
<value>Authority:</value>
</data>
<data name="AuthorizationUrl.HelpText" xml:space="preserve">
<value>The endpoint for obtaining an Authorization Code</value>
</data>
<data name="AuthorizationUrl.Text" xml:space="preserve">
<value>Authorization Url:</value>
</data>
<data name="ClientID.HelpText" xml:space="preserve">
<value>The Client ID from the provider</value>
</data>
<data name="ClientID.Text" xml:space="preserve">
<value>Client ID:</value>
</data>
<data name="ClientSecret.HelpText" xml:space="preserve">
<value>The Client Secret from the provider</value>
</data>
<data name="ClientSecret.Text" xml:space="preserve">
<value>Client Secret:</value>
</data>
<data name="CreateUsers.HelpText" xml:space="preserve">
<value>Do you want new users to be created automatically? If you disable this option, users must already be registered on the site in order to sign in with their external login.</value>
</data>
<data name="CreateUsers.Text" xml:space="preserve">
<value>Create New Users?</value>
</data>
<data name="DomainFilter.HelpText" xml:space="preserve">
<value>Provide any email domain filter criteria (separated by commas). Domains to exclude should be prefixed with an exclamation point (!). For example 'microsoft.com,!hotmail.com' would include microsoft.com email addresses but not hotmail.com email addresses.</value>
</data>
<data name="DomainFilter.Text" xml:space="preserve">
<value>Domain Filter:</value>
</data>
<data name="EmailClaimType.HelpText" xml:space="preserve">
<value>The type name for the email address claim provided by the provider</value>
</data>
<data name="EmailClaimType.Text" xml:space="preserve">
<value>Email Claim Type:</value>
</data>
<data name="ExternalLoginSettings.Heading" xml:space="preserve">
<value>External Login Settings</value>
</data>
<data name="LockoutSettings.Heading" xml:space="preserve">
<value>Lockout Settings</value>
</data>
<data name="MetadataUrl.HelpText" xml:space="preserve">
<value>The discovery endpoint for obtaining metadata for this provider. Only specify if the OpenID Connect provider does not use the standard approach (ie. /.well-known/openid-configuration)</value>
</data>
<data name="MetadataUrl.Text" xml:space="preserve">
<value>Metadata Url:</value>
</data>
<data name="PasswordSettings.Heading" xml:space="preserve">
<value>Password Settings</value>
</data>
<data name="PKCE.HelpText" xml:space="preserve">
<value>Indicate if the provider supports Proof Key for Code Exchange (PKCE)</value>
</data>
<data name="PKCE.Text" xml:space="preserve">
<value>Use PKCE?</value>
</data>
<data name="ProviderName.HelpText" xml:space="preserve">
<value>The external login provider name which will be displayed on the login page</value>
</data>
<data name="ProviderName.Text" xml:space="preserve">
<value>Provider Name:</value>
</data>
<data name="ProviderType.HelpText" xml:space="preserve">
<value>Select the external login provider type</value>
</data>
<data name="ProviderType.Text" xml:space="preserve">
<value>Provider Type:</value>
</data>
<data name="RedirectUrl.HelpText" xml:space="preserve">
<value>The Redirect Url (or Callback Url) which usually needs to be registered with the provider</value>
</data>
<data name="RedirectUrl.Text" xml:space="preserve">
<value>Redirect Url:</value>
</data>
<data name="Scopes.HelpText" xml:space="preserve">
<value>A list of Scopes to request from the provider (separated by commas). If none are specified, standard Scopes will be used by default.</value>
</data>
<data name="Scopes.Text" xml:space="preserve">
<value>Scopes:</value>
</data>
<data name="TokenUrl.HelpText" xml:space="preserve">
<value>The endpoint for obtaining an Auth Token</value>
</data>
<data name="TokenUrl.Text" xml:space="preserve">
<value>Token Url:</value>
</data>
<data name="UserInfoUrl.HelpText" xml:space="preserve">
<value>The endpoint for obtaining user information. This should be an API or Page Url which contains the users email address.</value>
</data>
<data name="UserInfoUrl.Text" xml:space="preserve">
<value>User Info Url:</value>
</data>
<data name="Audience.HelpText" xml:space="preserve">
<value>Optionally provide the audience for the token</value>
</data>
<data name="Audience.Text" xml:space="preserve">
<value>Audience:</value>
</data>
<data name="UserSettings.Heading" xml:space="preserve">
<value>User Settings</value>
</data>
<data name="CookieType.HelpText" xml:space="preserve">
<value>Cookies are usually managed per domain. However you can also choose to have distinct cookies for each site (this option is only applicable to micro-sites).</value>
</data>
<data name="CookieType.Text" xml:space="preserve">
<value>Login Cookie Type:</value>
</data>
<data name="CreateToken" xml:space="preserve">
<value>Create Token</value>
</data>
<data name="Issuer.HelpText" xml:space="preserve">
<value>Optionally provide the issuer of the token</value>
</data>
<data name="Issuer.Text" xml:space="preserve">
<value>Issuer:</value>
</data>
<data name="Lifetime.HelpText" xml:space="preserve">
<value>The number of minutes for which a token should be valid</value>
</data>
<data name="Lifetime.Text" xml:space="preserve">
<value>Lifetime:</value>
</data>
<data name="Secret.HelpText" xml:space="preserve">
<value>If you want to want to provide API access, please specify a secret which will be used to encrypt your tokens. The secret should be 16 characters or more to ensure optimal security. Please note that if you change this secret, all existing tokens will become invalid and will need to be regenerated.</value>
</data>
<data name="Secret.Text" xml:space="preserve">
<value>Secret:</value>
</data>
<data name="Token.HelpText" xml:space="preserve">
<value>Select the Create Token button to generate a long-lived access token (valid for 1 year). Be sure to store this token in a safe location as you will not be able to access it in the future.</value>
</data>
<data name="Token.Text" xml:space="preserve">
<value>Token:</value>
</data>
<data name="TokenSettings.Heading" xml:space="preserve">
<value>Token Settings</value>
</data>
<data name="TwoFactor.HelpText" xml:space="preserve">
<value>Do you want to allow users to use two factor authentication? Note that the Notification Job in Scheduled Jobs needs to be enabled and your SMTP options need to be configured in Site Settings for this option to work properly.</value>
</data>
<data name="TwoFactor.Text" xml:space="preserve">
<value>Allow Two Factor?</value>
</data>
<data name="Hide" xml:space="preserve">
<value>Hide</value>
</data>
<data name="Show" xml:space="preserve">
<value>Show</value>
</data>
</root> </root>

View File

@ -21,7 +21,9 @@ namespace Oqtane.Services
_siteState = siteState; _siteState = siteState;
} }
private string ApiUrl => CreateApiUrl("Installation", null, ControllerRoutes.ApiRoute); // tenant agnostic private string ApiUrl => (_siteState.Alias == null)
? CreateApiUrl("Installation", null, ControllerRoutes.ApiRoute) // tenant agnostic needed for initial installation
: CreateApiUrl("Installation", _siteState.Alias);
public async Task<Installation> IsInstalled() public async Task<Installation> IsInstalled()
{ {
@ -48,14 +50,5 @@ namespace Oqtane.Services
{ {
await PostJsonAsync($"{ApiUrl}/register?email={WebUtility.UrlEncode(email)}", true); await PostJsonAsync($"{ApiUrl}/register?email={WebUtility.UrlEncode(email)}", true);
} }
public void SetAntiForgeryTokenHeader(string antiforgerytokenvalue)
{
if (!string.IsNullOrEmpty(antiforgerytokenvalue))
{
AddRequestHeader(Constants.AntiForgeryTokenHeaderName, antiforgerytokenvalue);
}
}
} }
} }

View File

@ -42,10 +42,5 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task RegisterAsync(string email); Task RegisterAsync(string email);
/// <summary>
/// Sets the antiforgerytoken header so that it is included on all HttpClient calls for the lifetime of the app
/// </summary>
/// <returns></returns>
void SetAntiForgeryTokenHeader(string antiforgerytokenvalue);
} }
} }

View File

@ -38,6 +38,12 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task UpdateSiteSettingsAsync(Dictionary<string, string> siteSettings, int siteId); Task UpdateSiteSettingsAsync(Dictionary<string, string> siteSettings, int siteId);
/// <summary>
/// Clears site option cache
/// </summary>
/// <returns></returns>
Task ClearSiteSettingsCacheAsync();
/// <summary> /// <summary>
/// Returns a key-value dictionary of all page settings for the given page /// Returns a key-value dictionary of all page settings for the given page
/// </summary> /// </summary>
@ -149,7 +155,6 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task<Dictionary<string, string>> GetSettingsAsync(string entityName, int entityId); Task<Dictionary<string, string>> GetSettingsAsync(string entityName, int entityId);
/// <summary> /// <summary>
/// Updates settings for a given entityName and Id /// Updates settings for a given entityName and Id
/// </summary> /// </summary>
@ -166,7 +171,6 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task<Setting> GetSettingAsync(string entityName, int settingId); Task<Setting> GetSettingAsync(string entityName, int settingId);
/// <summary> /// <summary>
/// Creates a new setting /// Creates a new setting
/// </summary> /// </summary>

View File

@ -9,16 +9,34 @@ namespace Oqtane.Services
public interface ISystemService public interface ISystemService
{ {
/// <summary> /// <summary>
/// returns a key-value directory with the current system information (os-version, clr-version, etc.) /// returns a key-value directory with the current system configuration information
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
Task<Dictionary<string, string>> GetSystemInfoAsync(); Task<Dictionary<string, object>> GetSystemInfoAsync();
/// <summary>
/// returns a key-value directory with the current system information - "environment" or "configuration"
/// </summary>
/// <returns></returns>
Task<Dictionary<string, object>> GetSystemInfoAsync(string type);
/// <summary>
/// returns a config value
/// </summary>
/// <returns></returns>
Task<object> GetSystemInfoAsync(string settingKey, object defaultValue);
/// <summary> /// <summary>
/// Updates system information /// Updates system information
/// </summary> /// </summary>
/// <param name="settings"></param> /// <param name="settings"></param>
/// <returns></returns> /// <returns></returns>
Task UpdateSystemInfoAsync(Dictionary<string, string> settings); Task UpdateSystemInfoAsync(Dictionary<string, object> settings);
/// <summary>
/// updates a config value
/// </summary>
/// <returns></returns>
Task UpdateSystemInfoAsync(string settingKey, object settingValue);
} }
} }

View File

@ -88,5 +88,26 @@ namespace Oqtane.Services
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<User> ResetPasswordAsync(User user, string token); Task<User> ResetPasswordAsync(User user, string token);
/// <summary>
/// Verify the two factor verification code <see cref="User"/>
/// </summary>
/// <param name="user"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<User> VerifyTwoFactorAsync(User user, string token);
/// <summary>
/// Validate a users password against the password policy
/// </summary>
/// <param name="password"></param>
/// <returns></returns>
Task<bool> ValidatePasswordAsync(string password);
/// <summary>
/// Get token for current user
/// </summary>
/// <returns></returns>
Task<string> GetTokenAsync();
} }
} }

View File

@ -0,0 +1,147 @@
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Net.Http.Headers;
using Oqtane.Shared;
namespace Oqtane.Services
{
public class RemoteServiceBase
{
private readonly SiteState _siteState;
private readonly IHttpClientFactory _httpClientFactory;
protected RemoteServiceBase(IHttpClientFactory httpClientFactory, SiteState siteState)
{
_siteState = siteState;
_httpClientFactory = httpClientFactory;
}
private HttpClient GetHttpClient()
{
var httpClient = _httpClientFactory.CreateClient("Remote");
if (!httpClient.DefaultRequestHeaders.Contains(HeaderNames.Authorization) && _siteState != null && !string.IsNullOrEmpty(_siteState.AuthorizationToken))
{
httpClient.DefaultRequestHeaders.Add(HeaderNames.Authorization, "Bearer " + _siteState.AuthorizationToken);
}
return httpClient;
}
protected async Task GetAsync(string uri)
{
var response = await GetHttpClient().GetAsync(uri);
CheckResponse(response);
}
protected async Task<string> GetStringAsync(string uri)
{
try
{
return await GetHttpClient().GetStringAsync(uri);
}
catch (Exception e)
{
Console.WriteLine(e);
}
return default;
}
protected async Task<byte[]> GetByteArrayAsync(string uri)
{
try
{
return await GetHttpClient().GetByteArrayAsync(uri);
}
catch (Exception e)
{
Console.WriteLine(e);
}
return default;
}
protected async Task<T> GetJsonAsync<T>(string uri)
{
var response = await GetHttpClient().GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None);
if (CheckResponse(response) && ValidateJsonContent(response.Content))
{
return await response.Content.ReadFromJsonAsync<T>();
}
return default;
}
protected async Task PutAsync(string uri)
{
var response = await GetHttpClient().PutAsync(uri, null);
CheckResponse(response);
}
protected async Task<T> PutJsonAsync<T>(string uri, T value)
{
return await PutJsonAsync<T, T>(uri, value);
}
protected async Task<TResult> PutJsonAsync<TValue, TResult>(string uri, TValue value)
{
var response = await GetHttpClient().PutAsJsonAsync(uri, value);
if (CheckResponse(response) && ValidateJsonContent(response.Content))
{
var result = await response.Content.ReadFromJsonAsync<TResult>();
return result;
}
return default;
}
protected async Task PostAsync(string uri)
{
var response = await GetHttpClient().PostAsync(uri, null);
CheckResponse(response);
}
protected async Task<T> PostJsonAsync<T>(string uri, T value)
{
return await PostJsonAsync<T, T>(uri, value);
}
protected async Task<TResult> PostJsonAsync<TValue, TResult>(string uri, TValue value)
{
var response = await GetHttpClient().PostAsJsonAsync(uri, value);
if (CheckResponse(response) && ValidateJsonContent(response.Content))
{
var result = await response.Content.ReadFromJsonAsync<TResult>();
return result;
}
return default;
}
protected async Task DeleteAsync(string uri)
{
var response = await GetHttpClient().DeleteAsync(uri);
CheckResponse(response);
}
private bool CheckResponse(HttpResponseMessage response)
{
if (response.IsSuccessStatusCode) return true;
if (response.StatusCode != HttpStatusCode.NoContent && response.StatusCode != HttpStatusCode.NotFound)
{
Console.WriteLine($"Request: {response.RequestMessage.RequestUri}");
Console.WriteLine($"Response status: {response.StatusCode} {response.ReasonPhrase}");
}
return false;
}
private static bool ValidateJsonContent(HttpContent content)
{
var mediaType = content?.Headers.ContentType?.MediaType;
return mediaType != null && mediaType.Equals("application/json", StringComparison.OrdinalIgnoreCase);
}
}
}

View File

@ -5,24 +5,31 @@ using System.Net.Http;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Oqtane.Documentation;
using Oqtane.Models; using Oqtane.Models;
using Oqtane.Shared; using Oqtane.Shared;
namespace Oqtane.Services namespace Oqtane.Services
{ {
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class ServiceBase public class ServiceBase
{ {
private readonly HttpClient _http; private readonly HttpClient _httpClient;
private readonly SiteState _siteState; private readonly SiteState _siteState;
protected ServiceBase(HttpClient client, SiteState siteState) protected ServiceBase(HttpClient httpClient, SiteState siteState)
{ {
_http = client; _httpClient = httpClient;
_siteState = siteState; _siteState = siteState;
} }
private HttpClient GetHttpClient()
{
if (!_httpClient.DefaultRequestHeaders.Contains(Constants.AntiForgeryTokenHeaderName) && _siteState != null && !string.IsNullOrEmpty(_siteState.AntiForgeryToken))
{
_httpClient.DefaultRequestHeaders.Add(Constants.AntiForgeryTokenHeaderName, _siteState.AntiForgeryToken);
}
return _httpClient;
}
// should be used with new constructor // should be used with new constructor
public string CreateApiUrl(string serviceName) public string CreateApiUrl(string serviceName)
{ {
@ -95,24 +102,9 @@ namespace Oqtane.Services
} }
} }
// note that HttpClient is registered as a Scoped(shared) service and therefore you should not use request headers whose value can vary over the lifetime of the service
protected void AddRequestHeader(string name, string value)
{
RemoveRequestHeader(name);
_http.DefaultRequestHeaders.Add(name, value);
}
protected void RemoveRequestHeader(string name)
{
if (_http.DefaultRequestHeaders.Contains(name))
{
_http.DefaultRequestHeaders.Remove(name);
}
}
protected async Task GetAsync(string uri) protected async Task GetAsync(string uri)
{ {
var response = await _http.GetAsync(uri); var response = await GetHttpClient().GetAsync(uri);
CheckResponse(response); CheckResponse(response);
} }
@ -120,7 +112,7 @@ namespace Oqtane.Services
{ {
try try
{ {
return await _http.GetStringAsync(uri); return await GetHttpClient().GetStringAsync(uri);
} }
catch (Exception e) catch (Exception e)
{ {
@ -134,7 +126,7 @@ namespace Oqtane.Services
{ {
try try
{ {
return await _http.GetByteArrayAsync(uri); return await GetHttpClient().GetByteArrayAsync(uri);
} }
catch (Exception e) catch (Exception e)
{ {
@ -146,7 +138,7 @@ namespace Oqtane.Services
protected async Task<T> GetJsonAsync<T>(string uri) protected async Task<T> GetJsonAsync<T>(string uri)
{ {
var response = await _http.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None); var response = await GetHttpClient().GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None);
if (CheckResponse(response) && ValidateJsonContent(response.Content)) if (CheckResponse(response) && ValidateJsonContent(response.Content))
{ {
return await response.Content.ReadFromJsonAsync<T>(); return await response.Content.ReadFromJsonAsync<T>();
@ -157,7 +149,7 @@ namespace Oqtane.Services
protected async Task PutAsync(string uri) protected async Task PutAsync(string uri)
{ {
var response = await _http.PutAsync(uri, null); var response = await GetHttpClient().PutAsync(uri, null);
CheckResponse(response); CheckResponse(response);
} }
@ -168,7 +160,7 @@ namespace Oqtane.Services
protected async Task<TResult> PutJsonAsync<TValue, TResult>(string uri, TValue value) protected async Task<TResult> PutJsonAsync<TValue, TResult>(string uri, TValue value)
{ {
var response = await _http.PutAsJsonAsync(uri, value); var response = await GetHttpClient().PutAsJsonAsync(uri, value);
if (CheckResponse(response) && ValidateJsonContent(response.Content)) if (CheckResponse(response) && ValidateJsonContent(response.Content))
{ {
var result = await response.Content.ReadFromJsonAsync<TResult>(); var result = await response.Content.ReadFromJsonAsync<TResult>();
@ -179,7 +171,7 @@ namespace Oqtane.Services
protected async Task PostAsync(string uri) protected async Task PostAsync(string uri)
{ {
var response = await _http.PostAsync(uri, null); var response = await GetHttpClient().PostAsync(uri, null);
CheckResponse(response); CheckResponse(response);
} }
@ -190,7 +182,7 @@ namespace Oqtane.Services
protected async Task<TResult> PostJsonAsync<TValue, TResult>(string uri, TValue value) protected async Task<TResult> PostJsonAsync<TValue, TResult>(string uri, TValue value)
{ {
var response = await _http.PostAsJsonAsync(uri, value); var response = await GetHttpClient().PostAsJsonAsync(uri, value);
if (CheckResponse(response) && ValidateJsonContent(response.Content)) if (CheckResponse(response) && ValidateJsonContent(response.Content))
{ {
var result = await response.Content.ReadFromJsonAsync<TResult>(); var result = await response.Content.ReadFromJsonAsync<TResult>();
@ -202,7 +194,7 @@ namespace Oqtane.Services
protected async Task DeleteAsync(string uri) protected async Task DeleteAsync(string uri)
{ {
var response = await _http.DeleteAsync(uri); var response = await GetHttpClient().DeleteAsync(uri);
CheckResponse(response); CheckResponse(response);
} }
@ -228,7 +220,7 @@ namespace Oqtane.Services
// This constructor is obsolete. Use ServiceBase(HttpClient client, SiteState siteState) : base(http, siteState) {} instead. // This constructor is obsolete. Use ServiceBase(HttpClient client, SiteState siteState) : base(http, siteState) {} instead.
protected ServiceBase(HttpClient client) protected ServiceBase(HttpClient client)
{ {
_http = client; _httpClient = client;
} }
[Obsolete("This method is obsolete. Use CreateApiUrl(string serviceName, Alias alias) in conjunction with ControllerRoutes.ApiRoute in Controllers instead.", false)] [Obsolete("This method is obsolete. Use CreateApiUrl(string serviceName, Alias alias) in conjunction with ControllerRoutes.ApiRoute in Controllers instead.", false)]
@ -240,7 +232,7 @@ namespace Oqtane.Services
[Obsolete("This property of ServiceBase is deprecated. Cross tenant service calls are not supported.", false)] [Obsolete("This property of ServiceBase is deprecated. Cross tenant service calls are not supported.", false)]
public Alias Alias { get; set; } public Alias Alias { get; set; }
[Obsolete("This method is obsolete. Use CreateApiUrl(string entityName, int entityId) instead.", false)] [Obsolete("This method is obsolete. Use CreateAuthorizationPolicyUrl(string url, string entityName, int entityId) where entityName = EntityNames.Module instead.", false)]
public string CreateAuthorizationPolicyUrl(string url, int entityId) public string CreateAuthorizationPolicyUrl(string url, int entityId)
{ {
return url + ((url.Contains("?")) ? "&" : "?") + "entityid=" + entityId.ToString(); return url + ((url.Contains("?")) ? "&" : "?") + "entityid=" + entityId.ToString();

View File

@ -42,6 +42,11 @@ namespace Oqtane.Services
await UpdateSettingsAsync(siteSettings, EntityNames.Site, siteId); await UpdateSettingsAsync(siteSettings, EntityNames.Site, siteId);
} }
public async Task ClearSiteSettingsCacheAsync()
{
await DeleteAsync($"{Apiurl}/clear");
}
public async Task<Dictionary<string, string>> GetPageSettingsAsync(int pageId) public async Task<Dictionary<string, string>> GetPageSettingsAsync(int pageId)
{ {
return await GetSettingsAsync(EntityNames.Page, pageId); return await GetSettingsAsync(EntityNames.Page, pageId);

View File

@ -17,7 +17,6 @@ namespace Oqtane.Services
public SiteService(HttpClient http, SiteState siteState) : base(http) public SiteService(HttpClient http, SiteState siteState) : base(http)
{ {
_siteState = siteState; _siteState = siteState;
} }

View File

@ -18,14 +18,28 @@ namespace Oqtane.Services
private string Apiurl => CreateApiUrl("System", _siteState.Alias); private string Apiurl => CreateApiUrl("System", _siteState.Alias);
public async Task<Dictionary<string, string>> GetSystemInfoAsync() public async Task<Dictionary<string, object>> GetSystemInfoAsync()
{ {
return await GetJsonAsync<Dictionary<string, string>>(Apiurl); return await GetSystemInfoAsync("configuration");
} }
public async Task UpdateSystemInfoAsync(Dictionary<string, string> settings) public async Task<Dictionary<string, object>> GetSystemInfoAsync(string type)
{
return await GetJsonAsync<Dictionary<string, object>>($"{Apiurl}?type={type}");
}
public async Task<object> GetSystemInfoAsync(string settingKey, object defaultValue)
{
return await GetJsonAsync<object>($"{Apiurl}/{settingKey}/{defaultValue}");
}
public async Task UpdateSystemInfoAsync(Dictionary<string, object> settings)
{ {
await PostJsonAsync(Apiurl, settings); await PostJsonAsync(Apiurl, settings);
} }
public async Task UpdateSystemInfoAsync(string settingKey, object settingValue)
{
await PutJsonAsync($"{Apiurl}/{settingKey}/{settingValue}", "");
}
} }
} }

View File

@ -3,6 +3,7 @@ using Oqtane.Models;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Oqtane.Documentation; using Oqtane.Documentation;
using System.Net;
namespace Oqtane.Services namespace Oqtane.Services
{ {
@ -68,5 +69,20 @@ namespace Oqtane.Services
{ {
return await PostJsonAsync<User>($"{Apiurl}/reset?token={token}", user); return await PostJsonAsync<User>($"{Apiurl}/reset?token={token}", user);
} }
public async Task<User> VerifyTwoFactorAsync(User user, string token)
{
return await PostJsonAsync<User>($"{Apiurl}/twofactor?token={token}", user);
}
public async Task<bool> ValidatePasswordAsync(string password)
{
return await GetJsonAsync<bool>($"{Apiurl}/validate/{WebUtility.UrlEncode(password)}");
}
public async Task<string> GetTokenAsync()
{
return await GetStringAsync($"{Apiurl}/token");
}
} }
} }

View File

@ -333,9 +333,8 @@
if (PageId != "-") if (PageId != "-")
{ {
_modules = PageState.Modules _modules = PageState.Modules
.Where(module => module.PageId == int.Parse(PageId) .Where(module => module.PageId == int.Parse(PageId) &&
&& !module.IsDeleted UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.Permissions))
&& UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.Permissions))
.ToList(); .ToList();
} }
ModuleId = "-"; ModuleId = "-";

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop; using Microsoft.JSInterop;
@ -33,25 +34,26 @@ namespace Oqtane.Themes.Controls
protected async Task LogoutUser() protected async Task LogoutUser()
{ {
await UserService.LogoutUserAsync(PageState.User); await UserService.LogoutUserAsync(PageState.User);
await LoggingService.Log(PageState.Alias, PageState.Page.PageId, PageState.ModuleId, PageState.User.UserId, GetType().AssemblyQualifiedName, "Logout", LogFunction.Security, LogLevel.Information, null, "User Logout For Username {Username}", PageState.User.Username); await LoggingService.Log(PageState.Alias, PageState.Page.PageId, null, PageState.User.UserId, GetType().AssemblyQualifiedName, "Logout", LogFunction.Security, LogLevel.Information, null, "User Logout For Username {Username}", PageState.User.Username);
PageState.User = null; PageState.User = null;
bool authorizedtoviewpage = UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, PageState.Page.Permissions);
if (PageState.Runtime == Oqtane.Shared.Runtime.Server) var url = PageState.Alias.Path + "/" + PageState.Page.Path;
if (!UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, PageState.Page.Permissions))
{ {
// server-side Blazor needs to post to the Logout page url = PageState.Alias.Path;
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, returnurl = !authorizedtoviewpage ? PageState.Alias.Path : PageState.Alias.Path + "/" + PageState.Page.Path }; }
string url = Utilities.TenantUrl(PageState.Alias, "/pages/logout/");
var interop = new Interop(jsRuntime); if (PageState.Runtime == Shared.Runtime.Server)
await interop.SubmitForm(url, fields); {
// server-side Blazor needs to redirect to the Logout page
NavigationManager.NavigateTo(Utilities.TenantUrl(PageState.Alias, "/pages/logout/") + "?returnurl=" + WebUtility.UrlEncode(url), true);
} }
else else
{ {
// client-side Blazor // client-side Blazor
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider)); var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider));
authstateprovider.NotifyAuthenticationChanged(); authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(NavigateUrl(!authorizedtoviewpage ? PageState.Alias.Path : PageState.Page.Path, true)); NavigationManager.NavigateTo(NavigateUrl(url, true));
} }
} }
} }

View File

@ -30,7 +30,7 @@ namespace Oqtane.Themes.Controls
private IEnumerable<Page> GetMenuPages() private IEnumerable<Page> GetMenuPages()
{ {
var securityLevel = int.MaxValue; var securityLevel = int.MaxValue;
foreach (Page p in PageState.Pages.Where(item => item.IsNavigation && !item.IsDeleted)) foreach (Page p in PageState.Pages.Where(item => item.IsNavigation))
{ {
if (p.Level <= securityLevel && UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions)) if (p.Level <= securityLevel && UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions))
{ {

View File

@ -32,9 +32,9 @@ namespace Oqtane.Themes
if (Resources != null && Resources.Exists(item => item.ResourceType == ResourceType.Script)) if (Resources != null && Resources.Exists(item => item.ResourceType == ResourceType.Script))
{ {
var scripts = new List<object>(); var scripts = new List<object>();
foreach (Resource resource in Resources.Where(item => item.ResourceType == ResourceType.Script && item.Declaration != ResourceDeclaration.Global)) foreach (Resource resource in Resources.Where(item => item.ResourceType == ResourceType.Script))
{ {
scripts.Add(new { href = resource.Url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "" }); scripts.Add(new { href = resource.Url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", es6module = resource.ES6Module });
} }
if (scripts.Any()) if (scripts.Any())
{ {

View File

@ -72,13 +72,13 @@ namespace Oqtane.UI
} }
} }
public Task IncludeLink(string id, string rel, string href, string type, string integrity, string crossorigin, string key) public Task IncludeLink(string id, string rel, string href, string type, string integrity, string crossorigin, string includebefore)
{ {
try try
{ {
_jsRuntime.InvokeVoidAsync( _jsRuntime.InvokeVoidAsync(
"Oqtane.Interop.includeLink", "Oqtane.Interop.includeLink",
id, rel, href, type, integrity, crossorigin, key); id, rel, href, type, integrity, crossorigin, includebefore);
return Task.CompletedTask; return Task.CompletedTask;
} }
catch catch
@ -102,13 +102,13 @@ namespace Oqtane.UI
} }
} }
public Task IncludeScript(string id, string src, string integrity, string crossorigin, string content, string location, string key) public Task IncludeScript(string id, string src, string integrity, string crossorigin, string content, string location)
{ {
try try
{ {
_jsRuntime.InvokeVoidAsync( _jsRuntime.InvokeVoidAsync(
"Oqtane.Interop.includeScript", "Oqtane.Interop.includeScript",
id, src, integrity, crossorigin, content, location, key); id, src, integrity, crossorigin, content, location);
return Task.CompletedTask; return Task.CompletedTask;
} }
catch catch

View File

@ -48,7 +48,7 @@ else
if (Name.ToLower() == PaneNames.Admin.ToLower()) if (Name.ToLower() == PaneNames.Admin.ToLower())
{ {
Module module = PageState.Modules.FirstOrDefault(item => item.ModuleId == PageState.ModuleId); Module module = PageState.Modules.FirstOrDefault(item => item.ModuleId == PageState.ModuleId);
if (module != null && !module.IsDeleted) if (module != null)
{ {
var moduleType = Type.GetType(module.ModuleType); var moduleType = Type.GetType(module.ModuleType);
if (moduleType != null) if (moduleType != null)
@ -97,7 +97,7 @@ else
if (PageState.ModuleId != -1) if (PageState.ModuleId != -1)
{ {
Module module = PageState.Modules.FirstOrDefault(item => item.ModuleId == PageState.ModuleId); Module module = PageState.Modules.FirstOrDefault(item => item.ModuleId == PageState.ModuleId);
if (module != null && module.Pane.ToLower() == Name.ToLower() && !module.IsDeleted) if (module != null && module.Pane.ToLower() == Name.ToLower())
{ {
// check if user is authorized to view module // check if user is authorized to view module
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.Permissions)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.Permissions))
@ -108,7 +108,7 @@ else
} }
else else
{ {
foreach (Module module in PageState.Modules.Where(item => item.PageId == PageState.Page.PageId && item.Pane.ToLower() == Name.ToLower() && !item.IsDeleted).OrderBy(x => x.Order).ToArray()) foreach (Module module in PageState.Modules.Where(item => item.PageId == PageState.Page.PageId && item.Pane.ToLower() == Name.ToLower()).OrderBy(x => x.Order).ToArray())
{ {
// check if user is authorized to view module // check if user is authorized to view module
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.Permissions)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.Permissions))

View File

@ -164,6 +164,7 @@
if (PageState == null || refresh == UI.Refresh.Site) if (PageState == null || refresh == UI.Refresh.Site)
{ {
pages = await PageService.GetPagesAsync(site.SiteId); pages = await PageService.GetPagesAsync(site.SiteId);
pages = pages.Where(item => !item.IsDeleted).ToList();
} }
else else
{ {
@ -206,6 +207,7 @@
if (PageState == null || refresh == UI.Refresh.Site) if (PageState == null || refresh == UI.Refresh.Site)
{ {
modules = await ModuleService.GetModulesAsync(site.SiteId); modules = await ModuleService.GetModulesAsync(site.SiteId);
modules = modules.Where(item => !item.IsDeleted).ToList();
} }
else else
{ {
@ -256,10 +258,15 @@
} }
else else
{ {
await LogService.Log(null, null, user.UserId, GetType().AssemblyQualifiedName, Utilities.GetTypeNameLastSegment(GetType().AssemblyQualifiedName, 1), LogFunction.Security, LogLevel.Error, null, "Page Does Not Exist Or User Is Not Authorized To View Page {Path}", route.PagePath); if (route.PagePath != "404")
if (route.PagePath != "")
{ {
// redirect to home page await LogService.Log(null, null, user.UserId, "SiteRouter", "SiteRouter", LogFunction.Other, LogLevel.Information, null, "Page Path /{Path} Does Not Exist Or User Is Not Authorized To View", route.PagePath);
// redirect to 404 page
NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "404", ""));
}
else
{
// redirect to home page as a fallback
NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "", "")); NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "", ""));
} }
} }
@ -268,7 +275,7 @@
} }
else else
{ {
// site does not exist // site does not exist
} }
} }
@ -322,7 +329,7 @@
{ {
if (page.IsPersonalizable && user != null) if (page.IsPersonalizable && user != null)
{ {
// load the personalized page // load the personalized page
page = await PageService.GetPageAsync(page.PageId, user.UserId); page = await PageService.GetPageAsync(page.PageId, user.UserId);
} }
@ -338,7 +345,7 @@
Type themetype = Type.GetType(page.ThemeType); Type themetype = Type.GetType(page.ThemeType);
if (themetype == null) if (themetype == null)
{ {
// fallback // fallback
page.ThemeType = Constants.DefaultTheme; page.ThemeType = Constants.DefaultTheme;
themetype = Type.GetType(Constants.DefaultTheme); themetype = Type.GetType(Constants.DefaultTheme);
} }
@ -351,14 +358,14 @@
{ {
panes = themeobject.Panes; panes = themeobject.Panes;
} }
page.Resources = ManagePageResources(page.Resources, themeobject.Resources); page.Resources = ManagePageResources(page.Resources, themeobject.Resources, ResourceLevel.Page);
} }
} }
page.Panes = panes.Replace(";", ",").Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); page.Panes = panes.Replace(";", ",").Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
} }
catch catch
{ {
// error loading theme or layout // error loading theme or layout
} }
return page; return page;
@ -369,7 +376,7 @@
var paneindex = new Dictionary<string, int>(); var paneindex = new Dictionary<string, int>();
foreach (Module module in modules) foreach (Module module in modules)
{ {
// initialize module control properties // initialize module control properties
module.SecurityAccessLevel = SecurityAccessLevel.Host; module.SecurityAccessLevel = SecurityAccessLevel.Host;
module.ControlTitle = ""; module.ControlTitle = "";
module.Actions = ""; module.Actions = "";
@ -422,17 +429,17 @@
// get additional metadata from IModuleControl interface // get additional metadata from IModuleControl interface
if (moduletype != null && module.ModuleType != "") if (moduletype != null && module.ModuleType != "")
{ {
// retrieve module component resources // retrieve module component resources
var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources); page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module);
if (action.ToLower() == "settings" && module.ModuleDefinition != null) if (action.ToLower() == "settings" && module.ModuleDefinition != null)
{ {
// settings components are embedded within a framework settings module // settings components are embedded within a framework settings module
moduletype = Type.GetType(module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, action), false, true); moduletype = Type.GetType(module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, action), false, true);
if (moduletype != null) if (moduletype != null)
{ {
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources); page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module);
} }
} }
@ -483,15 +490,16 @@
return (page, modules); return (page, modules);
} }
private List<Resource> ManagePageResources(List<Resource> pageresources, List<Resource> resources) private List<Resource> ManagePageResources(List<Resource> pageresources, List<Resource> resources, ResourceLevel level)
{ {
if (resources != null) if (resources != null)
{ {
foreach (var resource in resources) foreach (var resource in resources)
{ {
// ensure resource does not exist already // ensure resource does not exist already
if (pageresources.Find(item => item.Url == resource.Url) == null) if (pageresources.Find(item => item.Url == resource.Url) == null)
{ {
resource.Level = level;
pageresources.Add(resource); pageresources.Add(resource);
} }
} }

View File

@ -28,20 +28,22 @@
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
var interop = new Interop(JsRuntime); var interop = new Interop(JsRuntime);
// manage stylesheets for this page // manage stylesheets for this page
string batch = DateTime.UtcNow.ToString("yyyyMMddHHmmssfff"); string batch = DateTime.UtcNow.ToString("yyyyMMddHHmmssfff");
var links = new List<object>(); var links = new List<object>();
foreach (Resource resource in PageState.Page.Resources.Where(item => item.ResourceType == ResourceType.Stylesheet && item.Declaration != ResourceDeclaration.Global)) foreach (Resource resource in PageState.Page.Resources.Where(item => item.ResourceType == ResourceType.Stylesheet))
{ {
links.Add(new { id = "app-stylesheet-" + batch + "-" + (links.Count + 1).ToString("00"), rel = "stylesheet", href = resource.Url, type = "text/css", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", key = "" }); var prefix = "app-stylesheet-" + resource.Level.ToString().ToLower();
links.Add(new { id = prefix + "-" + batch + "-" + (links.Count + 1).ToString("00"), rel = "stylesheet", href = resource.Url, type = "text/css", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", insertbefore = prefix });
} }
if (links.Any()) if (links.Any())
{ {
await interop.IncludeLinks(links.ToArray()); await interop.IncludeLinks(links.ToArray());
} }
await interop.RemoveElementsById("app-stylesheet", "", "app-stylesheet-" + batch + "-00"); await interop.RemoveElementsById("app-stylesheet-page-", "", "app-stylesheet-page-" + batch + "-00");
await interop.RemoveElementsById("app-stylesheet-module-", "", "app-stylesheet-module-" + batch + "-00");
// set page title // set page title
if (!string.IsNullOrEmpty(PageState.Page.Title)) if (!string.IsNullOrEmpty(PageState.Page.Title))

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<Version>3.0.3</Version> <Version>3.1.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.0.3</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -29,7 +29,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MySql.EntityFrameworkCore" Version="6.0.0-preview3.1" /> <PackageReference Include="MySql.EntityFrameworkCore" Version="6.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata> <metadata>
<id>Oqtane.Database.MySQL</id> <id>Oqtane.Database.MySQL</id>
<version>3.0.3</version> <version>3.1.0</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane MySQL Provider</title> <title>Oqtane MySQL Provider</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.0.3</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.0</releaseNotes>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
</metadata> </metadata>

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<Version>3.0.3</Version> <Version>3.1.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.0.3</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -29,9 +29,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="EFCore.NamingConventions" Version="6.0.0-rc.1" /> <PackageReference Include="EFCore.NamingConventions" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.0" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata> <metadata>
<id>Oqtane.Database.PostgreSQL</id> <id>Oqtane.Database.PostgreSQL</id>
<version>3.0.3</version> <version>3.1.0</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane PostgreSQL Provider</title> <title>Oqtane PostgreSQL Provider</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.0.3</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.0</releaseNotes>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
</metadata> </metadata>

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<Version>3.0.3</Version> <Version>3.1.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.0.3</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -29,7 +29,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata> <metadata>
<id>Oqtane.Database.SqlServer</id> <id>Oqtane.Database.SqlServer</id>
<version>3.0.3</version> <version>3.1.0</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane SQL Server Provider</title> <title>Oqtane SQL Server Provider</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.0.3</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.0</releaseNotes>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
</metadata> </metadata>

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<Version>3.0.3</Version> <Version>3.1.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.0.3</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -29,7 +29,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata> <metadata>
<id>Oqtane.Database.Sqlite</id> <id>Oqtane.Database.Sqlite</id>
<version>3.0.3</version> <version>3.1.0</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane SQLite Provider</title> <title>Oqtane SQLite Provider</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.0.3</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.0</releaseNotes>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
</metadata> </metadata>

View File

@ -35,6 +35,11 @@ namespace Oqtane.Database.Sqlite
// not implemented as SQLite does not support dropping columns // not implemented as SQLite does not support dropping columns
} }
public override void AlterStringColumn(MigrationBuilder builder, string name, string table, int length, bool nullable, bool unicode)
{
// not implemented as SQLite does not support altering columns
}
public override string ConcatenateSql(params string[] values) public override string ConcatenateSql(params string[] values)
{ {
var returnValue = String.Empty; var returnValue = String.Empty;

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata> <metadata>
<id>Oqtane.Client</id> <id>Oqtane.Client</id>
<version>3.0.3</version> <version>3.1.0</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.0.3</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.0</releaseNotes>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
</metadata> </metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata> <metadata>
<id>Oqtane.Framework</id> <id>Oqtane.Framework</id>
<version>3.0.3</version> <version>3.1.0</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@ -11,8 +11,8 @@
<copyright>.NET Foundation</copyright> <copyright>.NET Foundation</copyright>
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v3.0.3/Oqtane.Framework.3.0.3.Upgrade.zip</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v3.1.0/Oqtane.Framework.3.1.0.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.0.3</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.0</releaseNotes>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane framework</tags> <tags>oqtane framework</tags>
</metadata> </metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata> <metadata>
<id>Oqtane.Server</id> <id>Oqtane.Server</id>
<version>3.0.3</version> <version>3.1.0</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.0.3</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.0</releaseNotes>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
</metadata> </metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata> <metadata>
<id>Oqtane.Shared</id> <id>Oqtane.Shared</id>
<version>3.0.3</version> <version>3.1.0</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.0.3</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.0</releaseNotes>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
</metadata> </metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata> <metadata>
<id>Oqtane.Updater</id> <id>Oqtane.Updater</id>
<version>3.0.3</version> <version>3.1.0</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.0.3</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.0</releaseNotes>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
</metadata> </metadata>

View File

@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net6.0\publish\*" -DestinationPath "Oqtane.Framework.3.0.3.Install.zip" -Force Compress-Archive -Path "..\Oqtane.Server\bin\Release\net6.0\publish\*" -DestinationPath "Oqtane.Framework.3.1.0.Install.zip" -Force

View File

@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net6.0\publish\*" -DestinationPath "Oqtane.Framework.3.0.3.Upgrade.zip" -Force Compress-Archive -Path "..\Oqtane.Server\bin\Release\net6.0\publish\*" -DestinationPath "Oqtane.Framework.3.1.0.Upgrade.zip" -Force

View File

@ -8,6 +8,12 @@ using Oqtane.Enums;
using Oqtane.Infrastructure; using Oqtane.Infrastructure;
using Oqtane.Repository; using Oqtane.Repository;
using System.Net; using System.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.Options;
namespace Oqtane.Controllers namespace Oqtane.Controllers
{ {
@ -18,16 +24,26 @@ namespace Oqtane.Controllers
private readonly IPageModuleRepository _pageModules; private readonly IPageModuleRepository _pageModules;
private readonly IUserPermissions _userPermissions; private readonly IUserPermissions _userPermissions;
private readonly ISyncManager _syncManager; private readonly ISyncManager _syncManager;
private readonly IAliasAccessor _aliasAccessor;
private readonly IOptionsMonitorCache<CookieAuthenticationOptions> _cookieCache;
private readonly IOptionsMonitorCache<OpenIdConnectOptions> _oidcCache;
private readonly IOptionsMonitorCache<OAuthOptions> _oauthCache;
private readonly IOptionsMonitorCache<IdentityOptions> _identityCache;
private readonly ILogManager _logger; private readonly ILogManager _logger;
private readonly Alias _alias; private readonly Alias _alias;
private readonly string _visitorCookie; private readonly string _visitorCookie;
public SettingController(ISettingRepository settings, IPageModuleRepository pageModules, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger) public SettingController(ISettingRepository settings, IPageModuleRepository pageModules, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, IAliasAccessor aliasAccessor, IOptionsMonitorCache<CookieAuthenticationOptions> cookieCache, IOptionsMonitorCache<OpenIdConnectOptions> oidcCache, IOptionsMonitorCache<OAuthOptions> oauthCache, IOptionsMonitorCache<IdentityOptions> identityCache, ILogManager logger)
{ {
_settings = settings; _settings = settings;
_pageModules = pageModules; _pageModules = pageModules;
_userPermissions = userPermissions; _userPermissions = userPermissions;
_syncManager = syncManager; _syncManager = syncManager;
_aliasAccessor = aliasAccessor;
_cookieCache = cookieCache;
_oidcCache = oidcCache;
_oauthCache = oauthCache;
_identityCache = identityCache;
_logger = logger; _logger = logger;
_alias = tenantManager.GetAlias(); _alias = tenantManager.GetAlias();
_visitorCookie = "APP_VISITOR_" + _alias.SiteId.ToString(); _visitorCookie = "APP_VISITOR_" + _alias.SiteId.ToString();
@ -131,6 +147,30 @@ namespace Oqtane.Controllers
} }
} }
// DELETE api/<controller>/clear
[HttpDelete("clear")]
[Authorize(Roles = RoleNames.Admin)]
public void Clear()
{
// clear SiteOptionsCache for each option type
var cookieCache = new SiteOptionsCache<CookieAuthenticationOptions>(_aliasAccessor);
cookieCache.Clear();
var oidcCache = new SiteOptionsCache<OpenIdConnectOptions>(_aliasAccessor);
oidcCache.Clear();
var oauthCache = new SiteOptionsCache<OAuthOptions>(_aliasAccessor);
oauthCache.Clear();
var identityCache = new SiteOptionsCache<IdentityOptions>(_aliasAccessor);
identityCache.Clear();
// clear IOptionsMonitorCache for each option type
_cookieCache.Clear();
_oidcCache.Clear();
_oauthCache.Clear();
_identityCache.Clear();
_logger.Log(LogLevel.Information, this, LogFunction.Other, "Site Options Cache Cleared");
}
private bool IsAuthorized(string entityName, int entityId, string permissionName) private bool IsAuthorized(string entityName, int entityId, string permissionName)
{ {
bool authorized = false; bool authorized = false;

View File

@ -5,6 +5,7 @@ using Oqtane.Shared;
using System; using System;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Oqtane.Infrastructure; using Oqtane.Infrastructure;
using Microsoft.AspNetCore.Http.Features;
namespace Oqtane.Controllers namespace Oqtane.Controllers
{ {
@ -20,58 +21,68 @@ namespace Oqtane.Controllers
_configManager = configManager; _configManager = configManager;
} }
// GET: api/<controller> // GET: api/<controller>?type=x
[HttpGet] [HttpGet]
[Authorize(Roles = RoleNames.Host)] [Authorize(Roles = RoleNames.Host)]
public Dictionary<string, string> Get() public Dictionary<string, object> Get(string type)
{ {
Dictionary<string, string> systeminfo = new Dictionary<string, string>(); Dictionary<string, object> systeminfo = new Dictionary<string, object>();
systeminfo.Add("clrversion", Environment.Version.ToString()); switch (type.ToLower())
systeminfo.Add("osversion", Environment.OSVersion.ToString()); {
systeminfo.Add("machinename", Environment.MachineName); case "environment":
systeminfo.Add("serverpath", _environment.ContentRootPath); systeminfo.Add("CLRVersion", Environment.Version.ToString());
systeminfo.Add("servertime", DateTime.UtcNow.ToString()); systeminfo.Add("OSVersion", Environment.OSVersion.ToString());
systeminfo.Add("installationid", _configManager.GetInstallationId()); systeminfo.Add("MachineName", Environment.MachineName);
systeminfo.Add("WorkingSet", Environment.WorkingSet.ToString());
systeminfo.Add("runtime", _configManager.GetSetting("Runtime", "Server")); systeminfo.Add("TickCount", Environment.TickCount64.ToString());
systeminfo.Add("rendermode", _configManager.GetSetting("RenderMode", "ServerPrerendered")); systeminfo.Add("ContentRootPath", _environment.ContentRootPath);
systeminfo.Add("detailederrors", _configManager.GetSetting("DetailedErrors", "false")); systeminfo.Add("WebRootPath", _environment.WebRootPath);
systeminfo.Add("logginglevel", _configManager.GetSetting("Logging:LogLevel:Default", "Information")); systeminfo.Add("ServerTime", DateTime.UtcNow.ToString());
systeminfo.Add("swagger", _configManager.GetSetting("UseSwagger", "true")); var feature = HttpContext.Features.Get<IHttpConnectionFeature>();
systeminfo.Add("packageservice", _configManager.GetSetting("PackageService", "true")); systeminfo.Add("IPAddress", feature?.LocalIpAddress?.ToString());
break;
case "configuration":
systeminfo.Add("InstallationId", _configManager.GetInstallationId());
systeminfo.Add("Runtime", _configManager.GetSetting("Runtime", "Server"));
systeminfo.Add("RenderMode", _configManager.GetSetting("RenderMode", "ServerPrerendered"));
systeminfo.Add("DetailedErrors", _configManager.GetSetting("DetailedErrors", "false"));
systeminfo.Add("Logging:LogLevel:Default", _configManager.GetSetting("Logging:LogLevel:Default", "Information"));
systeminfo.Add("Logging:LogLevel:Notify", _configManager.GetSetting("Logging:LogLevel:Notify", "Error"));
systeminfo.Add("UseSwagger", _configManager.GetSetting("UseSwagger", "true"));
systeminfo.Add("PackageService", _configManager.GetSetting("PackageService", "true"));
break;
}
return systeminfo; return systeminfo;
} }
// GET: api/<controller>
[HttpGet("{key}/{value}")]
[Authorize(Roles = RoleNames.Host)]
public object Get(string key, object value)
{
return _configManager.GetSetting(key, value);
}
// POST: api/<controller>
[HttpPost] [HttpPost]
[Authorize(Roles = RoleNames.Host)] [Authorize(Roles = RoleNames.Host)]
public void Post([FromBody] Dictionary<string, string> settings) public void Post([FromBody] Dictionary<string, object> settings)
{ {
foreach(KeyValuePair<string, string> kvp in settings) foreach(KeyValuePair<string, object> kvp in settings)
{ {
switch (kvp.Key) _configManager.AddOrUpdateSetting(kvp.Key, kvp.Value, false);
{
case "runtime":
_configManager.AddOrUpdateSetting("Runtime", kvp.Value, false);
break;
case "rendermode":
_configManager.AddOrUpdateSetting("RenderMode", kvp.Value, false);
break;
case "detailederrors":
_configManager.AddOrUpdateSetting("DetailedErrors", kvp.Value, false);
break;
case "logginglevel":
_configManager.AddOrUpdateSetting("Logging:LogLevel:Default", kvp.Value, false);
break;
case "swagger":
_configManager.AddOrUpdateSetting("UseSwagger", kvp.Value, false);
break;
case "packageservice":
_configManager.AddOrUpdateSetting("PackageService", kvp.Value, false);
break;
}
} }
} }
// PUT: api/<controller>
[HttpPut("{key}/{value}")]
[Authorize(Roles = RoleNames.Host)]
public void Put(string key, object value)
{
_configManager.AddOrUpdateSetting(key, value, false);
}
} }
} }

View File

@ -14,6 +14,7 @@ using System.Net;
using Oqtane.Enums; using Oqtane.Enums;
using Oqtane.Infrastructure; using Oqtane.Infrastructure;
using Oqtane.Repository; using Oqtane.Repository;
using Oqtane.Security;
using Oqtane.Extensions; using Oqtane.Extensions;
namespace Oqtane.Controllers namespace Oqtane.Controllers
@ -26,26 +27,28 @@ namespace Oqtane.Controllers
private readonly IUserRoleRepository _userRoles; private readonly IUserRoleRepository _userRoles;
private readonly UserManager<IdentityUser> _identityUserManager; private readonly UserManager<IdentityUser> _identityUserManager;
private readonly SignInManager<IdentityUser> _identitySignInManager; private readonly SignInManager<IdentityUser> _identitySignInManager;
private readonly ITenantManager _tenantManager;
private readonly INotificationRepository _notifications; private readonly INotificationRepository _notifications;
private readonly IFolderRepository _folders; private readonly IFolderRepository _folders;
private readonly ISyncManager _syncManager; private readonly ISyncManager _syncManager;
private readonly ISiteRepository _sites; private readonly ISiteRepository _sites;
private readonly IJwtManager _jwtManager;
private readonly ILogManager _logger; private readonly ILogManager _logger;
private readonly Alias _alias;
public UserController(IUserRepository users, IRoleRepository roles, IUserRoleRepository userRoles, UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager, ITenantManager tenantManager, INotificationRepository notifications, IFolderRepository folders, ISyncManager syncManager, ISiteRepository sites, ILogManager logger) public UserController(IUserRepository users, IRoleRepository roles, IUserRoleRepository userRoles, UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager, ITenantManager tenantManager, INotificationRepository notifications, IFolderRepository folders, ISyncManager syncManager, ISiteRepository sites, IJwtManager jwtManager, ILogManager logger)
{ {
_users = users; _users = users;
_roles = roles; _roles = roles;
_userRoles = userRoles; _userRoles = userRoles;
_identityUserManager = identityUserManager; _identityUserManager = identityUserManager;
_identitySignInManager = identitySignInManager; _identitySignInManager = identitySignInManager;
_tenantManager = tenantManager;
_folders = folders; _folders = folders;
_notifications = notifications; _notifications = notifications;
_syncManager = syncManager; _syncManager = syncManager;
_sites = sites; _sites = sites;
_jwtManager = jwtManager;
_logger = logger; _logger = logger;
_alias = tenantManager.GetAlias();
} }
// GET api/<controller>/5?siteid=x // GET api/<controller>/5?siteid=x
@ -54,7 +57,7 @@ namespace Oqtane.Controllers
public User Get(int id, string siteid) public User Get(int id, string siteid)
{ {
int SiteId; int SiteId;
if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId) if (int.TryParse(siteid, out SiteId) && SiteId == _tenantManager.GetAlias().SiteId)
{ {
User user = _users.GetUser(id); User user = _users.GetUser(id);
if (user != null) if (user != null)
@ -77,7 +80,7 @@ namespace Oqtane.Controllers
public User Get(string name, string siteid) public User Get(string name, string siteid)
{ {
int SiteId; int SiteId;
if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId) if (int.TryParse(siteid, out SiteId) && SiteId == _tenantManager.GetAlias().SiteId)
{ {
User user = _users.GetUser(name); User user = _users.GetUser(name);
if (user != null) if (user != null)
@ -97,23 +100,30 @@ namespace Oqtane.Controllers
private User Filter(User user) private User Filter(User user)
{ {
if (user != null && !User.IsInRole(RoleNames.Admin) && User.Identity.Name?.ToLower() != user.Username.ToLower()) if (user != null)
{ {
user.DisplayName = "";
user.Email = "";
user.PhotoFileId = null;
user.LastLoginOn = DateTime.MinValue;
user.LastIPAddress = "";
user.Roles = "";
user.CreatedBy = "";
user.CreatedOn = DateTime.MinValue;
user.ModifiedBy = "";
user.ModifiedOn = DateTime.MinValue;
user.DeletedBy = "";
user.DeletedOn = DateTime.MinValue;
user.IsDeleted = false;
user.Password = ""; user.Password = "";
user.IsAuthenticated = false; user.IsAuthenticated = false;
user.TwoFactorCode = "";
user.TwoFactorExpiry = null;
if (!User.IsInRole(RoleNames.Admin) && User.Identity.Name?.ToLower() != user.Username.ToLower())
{
user.DisplayName = "";
user.Email = "";
user.PhotoFileId = null;
user.LastLoginOn = DateTime.MinValue;
user.LastIPAddress = "";
user.Roles = "";
user.CreatedBy = "";
user.CreatedOn = DateTime.MinValue;
user.ModifiedBy = "";
user.ModifiedOn = DateTime.MinValue;
user.DeletedBy = "";
user.DeletedOn = DateTime.MinValue;
user.IsDeleted = false;
user.TwoFactorRequired = false;
}
} }
return user; return user;
} }
@ -122,7 +132,7 @@ namespace Oqtane.Controllers
[HttpPost] [HttpPost]
public async Task<User> Post([FromBody] User user) public async Task<User> Post([FromBody] User user)
{ {
if (ModelState.IsValid && user.SiteId == _alias.SiteId) if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId)
{ {
var User = await CreateUser(user); var User = await CreateUser(user);
return User; return User;
@ -155,6 +165,7 @@ namespace Oqtane.Controllers
if (allowregistration) if (allowregistration)
{ {
bool succeeded;
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username); IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser == null) if (identityuser == null)
{ {
@ -163,74 +174,48 @@ namespace Oqtane.Controllers
identityuser.Email = user.Email; identityuser.Email = user.Email;
identityuser.EmailConfirmed = verified; identityuser.EmailConfirmed = verified;
var result = await _identityUserManager.CreateAsync(identityuser, user.Password); var result = await _identityUserManager.CreateAsync(identityuser, user.Password);
if (result.Succeeded) succeeded = result.Succeeded;
{
user.LastLoginOn = null;
user.LastIPAddress = "";
newUser = _users.AddUser(user);
if (!verified)
{
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = HttpContext.Request.Scheme + "://" + _alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string 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!";
var notification = new Notification(user.SiteId, null, newUser, "User Account Verification", body, null);
_notifications.AddNotification(notification);
}
// add folder for user
Folder folder = _folders.GetFolder(user.SiteId, Utilities.PathCombine("Users",Path.DirectorySeparatorChar.ToString()));
if (folder != null)
{
_folders.AddFolder(new Folder
{
SiteId = folder.SiteId,
ParentId = folder.FolderId,
Name = "My Folder",
Type = FolderTypes.Private,
Path = Utilities.PathCombine(folder.Path, newUser.UserId.ToString(), Path.DirectorySeparatorChar.ToString()),
Order = 1,
ImageSizes = "",
Capacity = Constants.UserFolderCapacity,
IsSystem = true,
Permissions = new List<Permission>
{
new Permission(PermissionNames.Browse, newUser.UserId, true),
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.Edit, newUser.UserId, true)
}.EncodePermissions()
}) ;
}
}
} }
else else
{ {
var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, false); var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, false);
if (result.Succeeded) succeeded = result.Succeeded;
{ verified = true;
newUser = _users.GetUser(user.Username); }
}
if (succeeded)
{
user.LastLoginOn = null;
user.LastIPAddress = "";
newUser = _users.AddUser(user);
} }
if (newUser != null) if (newUser != null)
{ {
// add auto assigned roles to user for site if (!verified)
List<Role> roles = _roles.GetRoles(user.SiteId).Where(item => item.IsAutoAssigned).ToList();
foreach (Role role in roles)
{ {
UserRole userrole = new UserRole(); string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
userrole.UserId = newUser.UserId; string url = HttpContext.Request.Scheme + "://" + _tenantManager.GetAlias().Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
userrole.RoleId = role.RoleId; string 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!";
userrole.EffectiveDate = null; var notification = new Notification(user.SiteId, newUser, "User Account Verification", body);
userrole.ExpiryDate = null; _notifications.AddNotification(notification);
_userRoles.AddUserRole(userrole); }
else
{
string url = HttpContext.Request.Scheme + "://" + _tenantManager.GetAlias().Name;
string body = "Dear " + user.DisplayName + ",\n\nA User Account Has Been Succesfully Created For You. Please Use The Following Link To Access The Site:\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, newUser, "User Account Notification", body);
_notifications.AddNotification(notification);
} }
}
if (newUser != null)
{
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);
} }
else
{
user.Password = ""; // remove sensitive information
_logger.Log(user.SiteId, LogLevel.Error, this, LogFunction.Create, "Unable To Add User {User}", user);
}
} }
else else
{ {
@ -245,19 +230,20 @@ namespace Oqtane.Controllers
[Authorize] [Authorize]
public async Task<User> Put(int id, [FromBody] User user) public async Task<User> Put(int id, [FromBody] User user)
{ {
if (ModelState.IsValid && user.SiteId == _alias.SiteId && _users.GetUser(user.UserId, false) != null && (User.IsInRole(RoleNames.Admin) || User.Identity.Name == user.Username)) if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && _users.GetUser(user.UserId, false) != null && (User.IsInRole(RoleNames.Admin) || User.Identity.Name == user.Username))
{ {
if (user.Password != "") IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null)
{ {
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username); identityuser.Email = user.Email;
if (identityuser != null) if (user.Password != "")
{ {
identityuser.PasswordHash = _identityUserManager.PasswordHasher.HashPassword(identityuser, user.Password); identityuser.PasswordHash = _identityUserManager.PasswordHasher.HashPassword(identityuser, user.Password);
await _identityUserManager.UpdateAsync(identityuser);
} }
await _identityUserManager.UpdateAsync(identityuser);
} }
user = _users.UpdateUser(user); user = _users.UpdateUser(user);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.User, user.UserId); _syncManager.AddSyncEvent(_tenantManager.GetAlias().TenantId, EntityNames.User, user.UserId);
user.Password = ""; // remove sensitive information user.Password = ""; // remove sensitive information
_logger.Log(LogLevel.Information, this, LogFunction.Update, "User Updated {User}", user); _logger.Log(LogLevel.Information, this, LogFunction.Update, "User Updated {User}", user);
} }
@ -278,7 +264,7 @@ namespace Oqtane.Controllers
{ {
int SiteId; int SiteId;
User user = _users.GetUser(id); User user = _users.GetUser(id);
if (user != null && int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId) if (user != null && int.TryParse(siteid, out SiteId) && SiteId == _tenantManager.GetAlias().SiteId)
{ {
// remove user roles for site // remove user roles for site
foreach (UserRole userrole in _userRoles.GetUserRoles(user.UserId, SiteId).ToList()) foreach (UserRole userrole in _userRoles.GetUserRoles(user.UserId, SiteId).ToList())
@ -332,40 +318,75 @@ namespace Oqtane.Controllers
[HttpPost("login")] [HttpPost("login")]
public async Task<User> Login([FromBody] User user, bool setCookie, bool isPersistent) public async Task<User> Login([FromBody] User user, bool setCookie, bool isPersistent)
{ {
User loginUser = new User { Username = user.Username, IsAuthenticated = false }; User loginUser = new User { SiteId = user.SiteId, Username = user.Username, IsAuthenticated = false };
if (ModelState.IsValid) if (ModelState.IsValid)
{ {
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username); IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null) if (identityuser != null)
{ {
var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, false); var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, true);
if (result.Succeeded) if (result.Succeeded)
{ {
loginUser = _users.GetUser(identityuser.UserName); user = _users.GetUser(user.Username);
if (loginUser != null) if (user.TwoFactorRequired)
{ {
if (identityuser.EmailConfirmed) var token = await _identityUserManager.GenerateTwoFactorTokenAsync(identityuser, "Email");
user.TwoFactorCode = token;
user.TwoFactorExpiry = DateTime.UtcNow.AddMinutes(10);
_users.UpdateUser(user);
string body = "Dear " + user.DisplayName + ",\n\nYou requested a secure verification code to log in to your account. Please enter the secure verification code on the site:\n\n" + token +
"\n\nPlease note that the code is only valid for 10 minutes so if you are unable to take action within that time period, you should initiate a new login on the site." +
"\n\nThank You!";
var notification = new Notification(loginUser.SiteId, user, "User Verification Code", body);
_notifications.AddNotification(notification);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Verification Notification Sent For {Username}", user.Username);
loginUser.TwoFactorRequired = true;
}
else
{
loginUser = _users.GetUser(identityuser.UserName);
if (loginUser != null)
{ {
loginUser.IsAuthenticated = true; if (identityuser.EmailConfirmed)
loginUser.LastLoginOn = DateTime.UtcNow;
loginUser.LastIPAddress = HttpContext.Connection.RemoteIpAddress.ToString();
_users.UpdateUser(loginUser);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful {Username}", user.Username);
if (setCookie)
{ {
await _identitySignInManager.SignInAsync(identityuser, isPersistent); loginUser.IsAuthenticated = true;
loginUser.LastLoginOn = DateTime.UtcNow;
loginUser.LastIPAddress = HttpContext.Connection.RemoteIpAddress.ToString();
_users.UpdateUser(loginUser);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful {Username}", user.Username);
if (setCookie)
{
await _identitySignInManager.SignInAsync(identityuser, isPersistent);
}
}
else
{
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Not Verified {Username}", user.Username);
} }
}
else
{
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Not Verified {Username}", user.Username);
} }
} }
} }
else else
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Security, "User Login Failed {Username}", user.Username); if (result.IsLockedOut)
{
user = _users.GetUser(user.Username);
string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser);
string url = HttpContext.Request.Scheme + "://" + _tenantManager.GetAlias().Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nYou attempted multiple times unsuccessfully to log in to your account and it is now locked out. Please wait a few minutes and then try again... or use the link below to reset your password:\n\n" + url +
"\n\nPlease note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site." +
"\n\nThank You!";
var notification = new Notification(loginUser.SiteId, user, "User Lockout", body);
_notifications.AddNotification(notification);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Lockout Notification Sent For {Username}", user.Username);
}
else
{
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Failed {Username}", user.Username);
}
} }
} }
} }
@ -422,13 +443,13 @@ namespace Oqtane.Controllers
{ {
user = _users.GetUser(user.Username); user = _users.GetUser(user.Username);
string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser); string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser);
string url = HttpContext.Request.Scheme + "://" + _alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); string url = HttpContext.Request.Scheme + "://" + _tenantManager.GetAlias().Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nYou recently requested to reset your password. Please use the link below to complete the process:\n\n" + url + string body = "Dear " + user.DisplayName + ",\n\nYou recently requested to reset your password. Please use the link below to complete the process:\n\n" + url +
"\n\nPlease note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site." + "\n\nPlease note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site." +
"\n\nIf you did not request to reset your password you can safely ignore this message." + "\n\nIf you did not request to reset your password you can safely ignore this message." +
"\n\nThank You!"; "\n\nThank You!";
var notification = new Notification(user.SiteId, null, user, "User Password Reset", body, null); var notification = new Notification(_tenantManager.GetAlias().SiteId, user, "User Password Reset", body);
_notifications.AddNotification(notification); _notifications.AddNotification(notification);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset Notification Sent For {Username}", user.Username); _logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset Notification Sent For {Username}", user.Username);
} }
@ -469,6 +490,52 @@ namespace Oqtane.Controllers
return user; return user;
} }
// POST api/<controller>/twofactor
[HttpPost("twofactor")]
public User TwoFactor([FromBody] User user, string token)
{
User loginUser = new User { SiteId = user.SiteId, Username = user.Username, IsAuthenticated = false };
if (ModelState.IsValid && !string.IsNullOrEmpty(token))
{
user = _users.GetUser(user.Username);
if (user != null)
{
if (user.TwoFactorRequired && user.TwoFactorCode == token && DateTime.UtcNow < user.TwoFactorExpiry)
{
loginUser.IsAuthenticated = true;
}
}
}
return loginUser;
}
// GET api/<controller>/validate/x
[HttpGet("validate/{password}")]
public async Task<bool> Validate(string password)
{
var validator = new PasswordValidator<IdentityUser>();
var result = await validator.ValidateAsync(_identityUserManager, null, password);
return result.Succeeded;
}
// GET api/<controller>/token
[HttpGet("token")]
[Authorize(Roles = RoleNames.Admin)]
public string Token()
{
var token = "";
var sitesettings = HttpContext.GetSiteSettings();
var secret = sitesettings.GetValue("JwtOptions:Secret", "");
if (!string.IsNullOrEmpty(secret))
{
var lifetime = 525600; // long-lived token set to 1 year
token = _jwtManager.GenerateToken(_tenantManager.GetAlias(), (ClaimsIdentity)User.Identity, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""), lifetime);
}
return token;
}
// GET api/<controller>/authenticate // GET api/<controller>/authenticate
[HttpGet("authenticate")] [HttpGet("authenticate")]
public User Authenticate() public User Authenticate()
@ -477,7 +544,10 @@ namespace Oqtane.Controllers
if (user.IsAuthenticated) if (user.IsAuthenticated)
{ {
user.Username = User.Identity.Name; user.Username = User.Identity.Name;
user.UserId = int.Parse(User.Claims.First(item => item.Type == ClaimTypes.PrimarySid).Value); if (User.HasClaim(item => item.Type == ClaimTypes.NameIdentifier))
{
user.UserId = int.Parse(User.Claims.First(item => item.Type == ClaimTypes.NameIdentifier).Value);
}
string roles = ""; string roles = "";
foreach (var claim in User.Claims.Where(item => item.Type == ClaimTypes.Role)) foreach (var claim in User.Claims.Where(item => item.Type == ClaimTypes.Role))
{ {

View File

@ -73,13 +73,6 @@ namespace Oqtane.Controllers
var role = _roles.GetRole(userRole.RoleId); var role = _roles.GetRole(userRole.RoleId);
if (ModelState.IsValid && role != null && SiteValid(role.SiteId) && RoleValid(role.Name)) if (ModelState.IsValid && role != null && SiteValid(role.SiteId) && RoleValid(role.Name))
{ {
if (role.Name == RoleNames.Host)
{
// host roles can only exist at global level - remove all site specific user roles
_userRoles.DeleteUserRoles(userRole.UserId);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "User Roles Deleted For UserId {UserId}", userRole.UserId);
}
userRole = _userRoles.AddUserRole(userRole); userRole = _userRoles.AddUserRole(userRole);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "User Role Added {UserRole}", userRole); _logger.Log(LogLevel.Information, this, LogFunction.Create, "User Role Added {UserRole}", userRole);

View File

@ -81,6 +81,11 @@ namespace Oqtane.Databases
builder.DropColumn(name, table); builder.DropColumn(name, table);
} }
public virtual void AlterStringColumn(MigrationBuilder builder, string name, string table, int length, bool nullable, bool unicode)
{
builder.AlterColumn<string>(RewriteName(name), RewriteName(table), maxLength: length, nullable: nullable, unicode: unicode);
}
public abstract DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString); public abstract DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString);
} }
} }

View File

@ -34,6 +34,8 @@ namespace Oqtane.Databases.Interfaces
public void DropColumn(MigrationBuilder builder, string name, string table); public void DropColumn(MigrationBuilder builder, string name, string table);
public void AlterStringColumn(MigrationBuilder builder, string name, string table, int length, bool nullable, bool unicode);
public DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString); public DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString);
} }
} }

View File

@ -41,5 +41,8 @@ namespace Oqtane.Extensions
public static IApplicationBuilder UseTenantResolution(this IApplicationBuilder builder) public static IApplicationBuilder UseTenantResolution(this IApplicationBuilder builder)
=> builder.UseMiddleware<TenantMiddleware>(); => builder.UseMiddleware<TenantMiddleware>();
public static IApplicationBuilder UseJwtAuthorization(this IApplicationBuilder builder)
=> builder.UseMiddleware<JwtMiddleware>();
} }
} }

View File

@ -0,0 +1,19 @@
using System.Collections.Generic;
namespace Oqtane.Extensions
{
public static class DictionaryExtensions
{
public static TValue GetValue<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key, TValue defaultValue, bool nullOrEmptyValueIsValid = false)
{
if (dictionary != null && key != null && dictionary.ContainsKey(key))
{
if (nullOrEmptyValueIsValid || (dictionary[key] != null && !string.IsNullOrEmpty(dictionary[key].ToString())))
{
return dictionary[key];
}
}
return defaultValue;
}
}
}

View File

@ -0,0 +1,28 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.Extensions
{
public static class HttpContextExtensions
{
public static Alias GetAlias(this HttpContext context)
{
if (context != null && context.Items.ContainsKey(Constants.HttpContextAliasKey))
{
return context.Items[Constants.HttpContextAliasKey] as Alias;
}
return null;
}
public static Dictionary<string, string> GetSiteSettings(this HttpContext context)
{
if (context != null && context.Items.ContainsKey(Constants.HttpContextSiteSettingsKey))
{
return context.Items[Constants.HttpContextSiteSettingsKey] as Dictionary<string, string>;
}
return null;
}
}
}

View File

@ -7,9 +7,12 @@ using System.Net.Http;
using System.Reflection; using System.Reflection;
using System.Runtime.Loader; using System.Runtime.Loader;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
@ -57,6 +60,11 @@ namespace Microsoft.Extensions.DependencyInjection
return services; return services;
} }
public static OqtaneSiteOptionsBuilder AddOqtaneSiteOptions(this IServiceCollection services)
{
return new OqtaneSiteOptionsBuilder(services);
}
internal static IServiceCollection AddOqtaneSingletonServices(this IServiceCollection services) internal static IServiceCollection AddOqtaneSingletonServices(this IServiceCollection services)
{ {
services.AddSingleton<IInstallationManager, InstallationManager>(); services.AddSingleton<IInstallationManager, InstallationManager>();
@ -67,13 +75,22 @@ namespace Microsoft.Extensions.DependencyInjection
return services; return services;
} }
internal static IServiceCollection AddOqtaneServerScopedServices(this IServiceCollection services)
{
services.AddScoped<Oqtane.Infrastructure.SiteState>();
return services;
}
internal static IServiceCollection AddOqtaneTransientServices(this IServiceCollection services) internal static IServiceCollection AddOqtaneTransientServices(this IServiceCollection services)
{ {
services.AddTransient<ITenantManager, TenantManager>(); services.AddTransient<ITenantManager, TenantManager>();
services.AddTransient<IModuleDefinitionRepository, ModuleDefinitionRepository>(); services.AddTransient<IAliasAccessor, AliasAccessor>();
services.AddTransient<IThemeRepository, ThemeRepository>();
services.AddTransient<IUserPermissions, UserPermissions>(); services.AddTransient<IUserPermissions, UserPermissions>();
services.AddTransient<ITenantResolver, TenantResolver>(); services.AddTransient<ITenantResolver, TenantResolver>();
services.AddTransient<IJwtManager, JwtManager>();
services.AddTransient<IModuleDefinitionRepository, ModuleDefinitionRepository>();
services.AddTransient<IThemeRepository, ThemeRepository>();
services.AddTransient<IAliasRepository, AliasRepository>(); services.AddTransient<IAliasRepository, AliasRepository>();
services.AddTransient<ITenantRepository, TenantRepository>(); services.AddTransient<ITenantRepository, TenantRepository>();
services.AddTransient<ISiteRepository, SiteRepository>(); services.AddTransient<ISiteRepository, SiteRepository>();
@ -100,6 +117,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddTransient<ILanguageRepository, LanguageRepository>(); services.AddTransient<ILanguageRepository, LanguageRepository>();
services.AddTransient<IVisitorRepository, VisitorRepository>(); services.AddTransient<IVisitorRepository, VisitorRepository>();
services.AddTransient<IUrlMappingRepository, UrlMappingRepository>(); services.AddTransient<IUrlMappingRepository, UrlMappingRepository>();
// obsolete - replaced by ITenantManager // obsolete - replaced by ITenantManager
services.AddTransient<ITenantResolver, TenantResolver>(); services.AddTransient<ITenantResolver, TenantResolver>();
@ -123,36 +141,60 @@ namespace Microsoft.Extensions.DependencyInjection
context.Response.StatusCode = (int)HttpStatusCode.Forbidden; context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return Task.CompletedTask; return Task.CompletedTask;
}; };
options.Events.OnRedirectToLogout = context =>
{
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return Task.CompletedTask;
};
options.Events.OnValidatePrincipal = PrincipalValidator.ValidateAsync; options.Events.OnValidatePrincipal = PrincipalValidator.ValidateAsync;
}); });
return services; return services;
} }
public static IServiceCollection ConfigureOqtaneIdentityOptions(this IServiceCollection services) public static IServiceCollection ConfigureOqtaneAuthenticationOptions(this IServiceCollection services, IConfigurationRoot Configuration)
{ {
services.Configure<IdentityOptions>(options => // settings defined in appsettings
{ services.Configure<OAuthOptions>(Configuration);
// Password settings services.Configure<OpenIdConnectOptions>(Configuration);
options.Password.RequireDigit = false;
options.Password.RequiredLength = 6;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
options.Lockout.AllowedForNewUsers = true;
// User settings
options.User.RequireUniqueEmail = false;
});
return services; return services;
} }
internal static IServiceCollection TryAddHttpClientWithAuthenticationCookie(this IServiceCollection services) public static IServiceCollection ConfigureOqtaneIdentityOptions(this IServiceCollection services, IConfigurationRoot Configuration)
{
// default settings
services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = true;
options.Password.RequiredLength = 6;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = true;
options.Password.RequiredUniqueChars = 1;
// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = false;
// SignIn settings
options.SignIn.RequireConfirmedEmail = true;
options.SignIn.RequireConfirmedPhoneNumber = false;
// User settings
options.User.RequireUniqueEmail = false; // changing to true will cause issues for legacy data
options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
});
// overrides defined in appsettings
services.Configure<IdentityOptions>(Configuration);
return services;
}
internal static IServiceCollection AddHttpClients(this IServiceCollection services)
{ {
if (!services.Any(x => x.ServiceType == typeof(HttpClient))) if (!services.Any(x => x.ServiceType == typeof(HttpClient)))
{ {
@ -174,6 +216,9 @@ namespace Microsoft.Extensions.DependencyInjection
}); });
} }
// IHttpClientFactory for calling remote services via RemoteServiceBase
services.AddHttpClient();
return services; return services;
} }
@ -303,7 +348,7 @@ namespace Microsoft.Extensions.DependencyInjection
try try
{ {
Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(File.ReadAllBytes(assemblyFile.FullName))); Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(System.IO.File.ReadAllBytes(assemblyFile.FullName)));
Debug.WriteLine($"Oqtane Info: Loaded Assembly {assemblyName}"); Debug.WriteLine($"Oqtane Info: Loaded Assembly {assemblyName}");
} }
catch (Exception ex) catch (Exception ex)
@ -322,9 +367,9 @@ namespace Microsoft.Extensions.DependencyInjection
private static Assembly ResolveDependencies(AssemblyLoadContext context, AssemblyName name) private static Assembly ResolveDependencies(AssemblyLoadContext context, AssemblyName name)
{ {
var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location) + Path.DirectorySeparatorChar + name.Name + ".dll"; var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location) + Path.DirectorySeparatorChar + name.Name + ".dll";
if (File.Exists(assemblyPath)) if (System.IO.File.Exists(assemblyPath))
{ {
return context.LoadFromStream(new MemoryStream(File.ReadAllBytes(assemblyPath))); return context.LoadFromStream(new MemoryStream(System.IO.File.ReadAllBytes(assemblyPath)));
} }
else else
{ {

View File

@ -0,0 +1,357 @@
using System;
using System.Linq;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Shared;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Oqtane.Repository;
using Oqtane.Security;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authentication.OAuth;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Authentication.Cookies;
namespace Oqtane.Extensions
{
public static class OqtaneSiteAuthenticationBuilderExtensions
{
public static OqtaneSiteOptionsBuilder WithSiteAuthentication(this OqtaneSiteOptionsBuilder builder)
{
// site cookie authentication options
builder.AddSiteOptions<CookieAuthenticationOptions>((options, alias, sitesettings) =>
{
if (sitesettings.GetValue("LoginOptions:CookieType", "domain") == "domain")
{
options.Cookie.Name = ".AspNetCore.Identity.Application";
}
else
{
// use unique cookie name for site
options.Cookie.Name = ".AspNetCore.Identity.Application" + alias.SiteKey;
}
});
// site OpenId Connect options
builder.AddSiteOptions<OpenIdConnectOptions>((options, alias, sitesettings) =>
{
if (sitesettings.GetValue("ExternalLogin:ProviderType", "") == AuthenticationProviderTypes.OpenIDConnect)
{
// default options
options.SignInScheme = Constants.AuthenticationScheme; // identity cookie
options.RequireHttpsMetadata = true;
options.SaveTokens = false;
options.GetClaimsFromUserInfoEndpoint = true;
options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-" + AuthenticationProviderTypes.OpenIDConnect : "/" + alias.Path + "/signin-" + AuthenticationProviderTypes.OpenIDConnect;
options.ResponseType = OpenIdConnectResponseType.Code; // authorization code flow
options.ResponseMode = OpenIdConnectResponseMode.FormPost; // recommended as most secure
// cookie config is required to avoid Correlation Failed errors
options.NonceCookie.SameSite = SameSiteMode.Unspecified;
options.CorrelationCookie.SameSite = SameSiteMode.Unspecified;
// site options
options.Authority = sitesettings.GetValue("ExternalLogin:Authority", "");
options.MetadataAddress = sitesettings.GetValue("ExternalLogin:MetadataUrl", "");
options.ClientId = sitesettings.GetValue("ExternalLogin:ClientId", "");
options.ClientSecret = sitesettings.GetValue("ExternalLogin:ClientSecret", "");
options.UsePkce = bool.Parse(sitesettings.GetValue("ExternalLogin:PKCE", "false"));
options.Scope.Clear();
foreach (var scope in sitesettings.GetValue("ExternalLogin:Scopes", "openid,profile,email").Split(',', StringSplitOptions.RemoveEmptyEntries))
{
options.Scope.Add(scope);
}
// openid connect events
options.Events.OnTokenValidated = OnTokenValidated;
options.Events.OnAccessDenied = OnAccessDenied;
options.Events.OnRemoteFailure = OnRemoteFailure;
}
});
// site OAuth 2.0 options
builder.AddSiteOptions<OAuthOptions>((options, alias, sitesettings) =>
{
if (sitesettings.GetValue("ExternalLogin:ProviderType", "") == AuthenticationProviderTypes.OAuth2)
{
// default options
options.SignInScheme = Constants.AuthenticationScheme; // identity cookie
options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-" + AuthenticationProviderTypes.OAuth2 : "/" + alias.Path + "/signin-" + AuthenticationProviderTypes.OAuth2;
options.SaveTokens = false;
// site options
options.AuthorizationEndpoint = sitesettings.GetValue("ExternalLogin:AuthorizationUrl", "");
options.TokenEndpoint = sitesettings.GetValue("ExternalLogin:TokenUrl", "");
options.UserInformationEndpoint = sitesettings.GetValue("ExternalLogin:UserInfoUrl", "");
options.ClientId = sitesettings.GetValue("ExternalLogin:ClientId", "");
options.ClientSecret = sitesettings.GetValue("ExternalLogin:ClientSecret", "");
options.UsePkce = bool.Parse(sitesettings.GetValue("ExternalLogin:PKCE", "false"));
options.Scope.Clear();
foreach (var scope in sitesettings.GetValue("ExternalLogin:Scopes", "").Split(',', StringSplitOptions.RemoveEmptyEntries))
{
options.Scope.Add(scope);
}
// cookie config is required to avoid Correlation Failed errors
options.CorrelationCookie.SameSite = SameSiteMode.Unspecified;
// oauth2 events
options.Events.OnCreatingTicket = OnCreatingTicket;
options.Events.OnAccessDenied = OnAccessDenied;
options.Events.OnRemoteFailure = OnRemoteFailure;
}
});
return builder;
}
private static async Task OnCreatingTicket(OAuthCreatingTicketContext context)
{
// OAuth 2.0
var email = "";
if (context.Options.UserInformationEndpoint != "")
{
try
{
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var output = await response.Content.ReadAsStringAsync();
// get email address using Regex on the raw output (could be json or html)
var regex = new Regex(@"\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*", RegexOptions.IgnoreCase);
foreach (Match match in regex.Matches(output))
{
if (EmailValid(match.Value, context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", "")))
{
email = match.Value.ToLower();
break;
}
}
}
catch (Exception ex)
{
var _logger = context.HttpContext.RequestServices.GetRequiredService<ILogManager>();
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "An Error Occurred Accessing The User Info Endpoint - {Error}", ex.Message);
}
}
// login user
await LoginUser(email, context.HttpContext, context.Principal);
}
private static async Task OnTokenValidated(TokenValidatedContext context)
{
// OpenID Connect
var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", "");
var email = context.Principal.FindFirstValue(emailClaimType);
// login user
await LoginUser(email, context.HttpContext, context.Principal);
}
private static Task OnAccessDenied(AccessDeniedContext context)
{
var _logger = context.HttpContext.RequestServices.GetRequiredService<ILogManager>();
_logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External Login Access Denied - User May Have Cancelled Their External Login Attempt");
// redirect to login page
var alias = context.HttpContext.GetAlias();
context.Response.Redirect(alias.Path + "/login?returnurl=" + context.Properties.RedirectUri, true);
context.HandleResponse();
return Task.CompletedTask;
}
private static Task OnRemoteFailure(RemoteFailureContext context)
{
var _logger = context.HttpContext.RequestServices.GetRequiredService<ILogManager>();
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "External Login Remote Failure - {Error}", context.Failure.Message);
// redirect to login page
var alias = context.HttpContext.GetAlias();
context.Response.Redirect(alias.Path + "/login", true);
context.HandleResponse();
return Task.CompletedTask;
}
private static async Task LoginUser(string email, HttpContext httpContext, ClaimsPrincipal claimsPrincipal)
{
var _logger = httpContext.RequestServices.GetRequiredService<ILogManager>();
if (EmailValid(email, httpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", "")))
{
var _identityUserManager = httpContext.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
var _users = httpContext.RequestServices.GetRequiredService<IUserRepository>();
var _userRoles = httpContext.RequestServices.GetRequiredService<IUserRoleRepository>();
var providerType = httpContext.GetSiteSettings().GetValue("ExternalLogin:ProviderType", "");
var providerKey = claimsPrincipal.FindFirstValue(ClaimTypes.NameIdentifier);
if (providerKey == null)
{
providerKey = email; // OAuth2 does not pass claims
}
User user = null;
bool duplicates = false;
IdentityUser identityuser = null;
try
{
identityuser = await _identityUserManager.FindByEmailAsync(email);
}
catch
{
// FindByEmailAsync will throw an error if the email matches multiple user accounts
duplicates = true;
}
if (identityuser == null)
{
if (duplicates)
{
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Multiple Users Exist With Email Address {Email}. Login Denied.", email);
}
else
{
if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:CreateUsers", "true")))
{
identityuser = new IdentityUser();
identityuser.UserName = email;
identityuser.Email = email;
identityuser.EmailConfirmed = true;
var result = await _identityUserManager.CreateAsync(identityuser, DateTime.UtcNow.ToString("yyyy-MMM-dd-HH-mm-ss"));
if (result.Succeeded)
{
user = new User
{
SiteId = httpContext.GetAlias().SiteId,
Username = email,
DisplayName = email,
Email = email,
LastLoginOn = null,
LastIPAddress = ""
};
user = _users.AddUser(user);
if (user != null)
{
var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>();
string url = httpContext.Request.Scheme + "://" + httpContext.GetAlias().Name;
string body = "You Recently Used An External Account To Sign In To Our Site.\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, user, "User Account Notification", body);
_notifications.AddNotification(notification);
// add user login
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType, providerKey, ""));
_logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "User Added {User}", user);
}
else
{
_logger.Log(user.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add User {Email}", email);
}
}
else
{
_logger.Log(user.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add Identity User {Email} {Error}", email, result.Errors.ToString());
}
}
else
{
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Creation Of New Users Is Disabled For This Site. User With Email Address {Email} Will First Need To Be Registered On The Site.", email);
}
}
}
else
{
var logins = await _identityUserManager.GetLoginsAsync(identityuser);
var login = logins.FirstOrDefault(item => item.LoginProvider == providerType);
if (login != null)
{
if (login.ProviderKey == providerKey)
{
user = _users.GetUser(identityuser.UserName);
}
else
{
// provider keys do not match
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Key Does Not Match For User {Username}. Login Denied.", identityuser.UserName);
}
}
else
{
// add user login
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType, providerKey, ""));
user = _users.GetUser(identityuser.UserName);
_logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External User Login Added For {Email} Using Provider {Provider}", email, providerType);
}
}
// add claims to principal
if (user != null)
{
var principal = (ClaimsIdentity)claimsPrincipal.Identity;
UserSecurity.ResetClaimsIdentity(principal);
var identity = UserSecurity.CreateClaimsIdentity(httpContext.GetAlias(), user, _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList());
principal.AddClaims(identity.Claims);
// update user
user.LastLoginOn = DateTime.UtcNow;
user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString();
_users.UpdateUser(user);
_logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} Using Provider {Provider}", user.Username, providerType);
}
else // user not valid
{
await httpContext.SignOutAsync();
}
}
else // email invalid
{
if (!string.IsNullOrEmpty(email))
{
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The Email Address {Email} Is Invalid Or Does Not Match The Domain Filter Criteria. Login Denied.", email);
}
else
{
var emailclaimtype = claimsPrincipal.Claims.FirstOrDefault(item => item.Value.Contains("@") && item.Value.Contains("."));
if (emailclaimtype != null)
{
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Please Verify If \"{ClaimType}\" Is A Valid Email Claim Type For The Provider And Update Your External Login Settings Accordingly", emailclaimtype.Type);
}
else
{
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return An Email To Uniquely Identify The User.");
}
}
await httpContext.SignOutAsync();
}
}
private static bool EmailValid(string email, string domainfilter)
{
if (!string.IsNullOrEmpty(email) && email.Contains("@") && email.Contains("."))
{
var domains = domainfilter.ToLower().Split(',', StringSplitOptions.RemoveEmptyEntries);
foreach (var domain in domains)
{
if (domain.StartsWith("!"))
{
if (email.ToLower().Contains(domain.Substring(1))) return false;
}
else
{
if (!email.ToLower().Contains(domain)) return false;
}
}
return true;
}
return false;
}
}
}

View File

@ -0,0 +1,31 @@
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Models;
using Microsoft.AspNetCore.Identity;
using System;
namespace Oqtane.Extensions
{
public static class OqtaneSiteIdentityBuilderExtensions
{
public static OqtaneSiteOptionsBuilder WithSiteIdentity(this OqtaneSiteOptionsBuilder builder)
{
// site identity options
builder.AddSiteOptions<IdentityOptions>((options, alias, sitesettings) =>
{
// password options
options.Password.RequiredLength = int.Parse(sitesettings.GetValue("IdentityOptions:Password:RequiredLength", options.Password.RequiredLength.ToString()));
options.Password.RequiredUniqueChars = int.Parse(sitesettings.GetValue("IdentityOptions:Password:RequiredUniqueChars", options.Password.RequiredUniqueChars.ToString()));
options.Password.RequireDigit = bool.Parse(sitesettings.GetValue("IdentityOptions:Password:RequireDigit", options.Password.RequireDigit.ToString()));
options.Password.RequireUppercase = bool.Parse(sitesettings.GetValue("IdentityOptions:Password:RequireUppercase", options.Password.RequireUppercase.ToString()));
options.Password.RequireLowercase = bool.Parse(sitesettings.GetValue("IdentityOptions:Password:RequireLowercase", options.Password.RequireLowercase.ToString()));
options.Password.RequireNonAlphanumeric = bool.Parse(sitesettings.GetValue("IdentityOptions:Password:RequireNonAlphanumeric", options.Password.RequireNonAlphanumeric.ToString()));
// lockout options
options.Lockout.MaxFailedAccessAttempts = int.Parse(sitesettings.GetValue("IdentityOptions:Password:MaxFailedAccessAttempts", options.Lockout.MaxFailedAccessAttempts.ToString()));
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.Parse(sitesettings.GetValue("IdentityOptions:Password:DefaultLockoutTimeSpan", options.Lockout.DefaultLockoutTimeSpan.ToString()));
});
return builder;
}
}
}

View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Oqtane.Infrastructure;
using Oqtane.Models;
namespace Microsoft.Extensions.DependencyInjection
{
public partial class OqtaneSiteOptionsBuilder
{
public IServiceCollection Services { get; set; }
public OqtaneSiteOptionsBuilder(IServiceCollection services)
{
Services = services;
}
public OqtaneSiteOptionsBuilder AddSiteOptions<TOptions>(
Action<TOptions, Alias, Dictionary<string, string>> action) where TOptions : class, new()
{
Services.TryAddSingleton<IOptionsMonitorCache<TOptions>, SiteOptionsCache<TOptions>>();
Services.AddSingleton<ISiteOptions<TOptions>, SiteOptions<TOptions>> (sp => new SiteOptions<TOptions>(action));
Services.TryAddTransient<IOptionsFactory<TOptions>, SiteOptionsFactory<TOptions>>();
Services.TryAddScoped<IOptionsSnapshot<TOptions>>(sp => BuildOptionsManager<TOptions>(sp));
Services.TryAddSingleton<IOptions<TOptions>>(sp => BuildOptionsManager<TOptions>(sp));
return this;
}
private static SiteOptionsManager<TOptions> BuildOptionsManager<TOptions>(IServiceProvider sp)
where TOptions : class, new()
{
var cache = ActivatorUtilities.CreateInstance(sp, typeof(SiteOptionsCache<TOptions>));
return (SiteOptionsManager<TOptions>)ActivatorUtilities.CreateInstance(sp, typeof(SiteOptionsManager<TOptions>), new[] { cache });
}
}
}

View File

@ -1,7 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.StaticFiles;
using Oqtane.Models;
namespace Oqtane.Extensions namespace Oqtane.Extensions
{ {

View File

@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Http;
using Oqtane.Extensions;
using Oqtane.Models;
namespace Oqtane.Infrastructure
{
public class AliasAccessor : IAliasAccessor
{
private readonly IHttpContextAccessor _httpContextAccessor;
public AliasAccessor(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public Alias Alias => _httpContextAccessor.HttpContext.GetAlias();
}
}

View File

@ -100,7 +100,7 @@ namespace Oqtane.Infrastructure
switch (action) switch (action)
{ {
case "set": case "set":
jsonObj[currentSection] = value; jsonObj[currentSection] = JToken.FromObject(value);
break; break;
case "remove": case "remove":
if (jsonObj.Property(currentSection) != null) if (jsonObj.Property(currentSection) != null)

View File

@ -190,6 +190,10 @@ namespace Oqtane.Infrastructure
if (result.Success) if (result.Success)
{ {
result = CreateSite(install); result = CreateSite(install);
if (result.Success)
{
result = MigrateSites();
}
} }
} }
} }
@ -620,35 +624,12 @@ namespace Oqtane.Infrastructure
LastIPAddress = "", LastIPAddress = "",
LastLoginOn = null LastLoginOn = null
}; };
user = users.AddUser(user); user = users.AddUser(user);
// add host role
var hostRoleId = roles.GetRoles(user.SiteId, true).FirstOrDefault(item => item.Name == RoleNames.Host)?.RoleId ?? 0; var hostRoleId = roles.GetRoles(user.SiteId, true).FirstOrDefault(item => item.Name == RoleNames.Host)?.RoleId ?? 0;
var userRole = new UserRole { UserId = user.UserId, RoleId = hostRoleId, EffectiveDate = null, ExpiryDate = null }; var userRole = new UserRole { UserId = user.UserId, RoleId = hostRoleId, EffectiveDate = null, ExpiryDate = null };
userRoles.AddUserRole(userRole); userRoles.AddUserRole(userRole);
// add user folder
var folder = folders.GetFolder(user.SiteId, Utilities.PathCombine("Users", Path.DirectorySeparatorChar.ToString()));
if (folder != null)
{
folders.AddFolder(new Folder
{
SiteId = folder.SiteId,
ParentId = folder.FolderId,
Name = "My Folder",
Type = FolderTypes.Private,
Path = Utilities.PathCombine(folder.Path, user.UserId.ToString(), Path.DirectorySeparatorChar.ToString()),
Order = 1,
ImageSizes = "",
Capacity = Constants.UserFolderCapacity,
IsSystem = true,
Permissions = new List<Permission>
{
new Permission(PermissionNames.Browse, user.UserId, true),
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.Edit, user.UserId, true),
}.EncodePermissions(),
});
}
} }
} }
} }
@ -688,6 +669,77 @@ namespace Oqtane.Infrastructure
return result; return result;
} }
private Installation MigrateSites()
{
var result = new Installation { Success = false, Message = string.Empty };
// get site upgrades
Dictionary<string, Type> siteupgrades = new Dictionary<string, Type>();
var assemblies = AppDomain.CurrentDomain.GetOqtaneAssemblies();
foreach (Assembly assembly in assemblies)
{
foreach (var type in assembly.GetTypes(typeof(ISiteMigration)))
{
if (Attribute.IsDefined(type, typeof(SiteMigrationAttribute)))
{
var attribute = (SiteMigrationAttribute)Attribute.GetCustomAttribute(type, typeof(SiteMigrationAttribute));
siteupgrades.Add(attribute.AliasName + " " + attribute.Version, type);
}
}
}
// execute site upgrades
if (siteupgrades.Count > 0)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var aliases = scope.ServiceProvider.GetRequiredService<IAliasRepository>();
var tenantManager = scope.ServiceProvider.GetRequiredService<ITenantManager>();
var sites = scope.ServiceProvider.GetRequiredService<ISiteRepository>();
var logger = scope.ServiceProvider.GetRequiredService<ILogManager>();
foreach (var alias in aliases.GetAliases().ToList().Where(item => item.IsDefault))
{
foreach (var upgrade in siteupgrades)
{
var aliasname = upgrade.Key.Split(' ').First();
// in the future this equality condition could use RegEx to allow for more flexible matching
if (string.Equals(alias.Name, aliasname, StringComparison.OrdinalIgnoreCase))
{
tenantManager.SetTenant(alias.TenantId);
var site = sites.GetSites().FirstOrDefault(item => item.SiteId == alias.SiteId);
if (site != null)
{
var version = upgrade.Key.Split(' ').Last();
if (string.IsNullOrEmpty(site.Version) || Version.Parse(version) > Version.Parse(site.Version))
{
try
{
var obj = Activator.CreateInstance(upgrade.Value) as ISiteMigration;
if (obj != null)
{
obj.Up(site, alias);
site.Version = version;
sites.UpdateSite(site);
logger.Log(alias.SiteId, Shared.LogLevel.Information, "Site Migration", LogFunction.Other, "Site Migrated Successfully To Version {version} For {Alias}", version, alias.Name);
}
}
catch (Exception ex)
{
logger.Log(alias.SiteId, Shared.LogLevel.Error, "Site Migration", LogFunction.Other, "An Error Occurred Executing Site Migration {Type} For {Alias} And Version {Version} {Error}", upgrade.Value, alias.Name, version, ex.Message);
}
}
}
}
}
}
}
}
result.Success = true;
return result;
}
private string DenormalizeConnectionString(string connectionString) private string DenormalizeConnectionString(string connectionString)
{ {
var dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString(); var dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString();

View File

@ -0,0 +1,9 @@
using Oqtane.Models;
namespace Oqtane.Infrastructure
{
public interface IAliasAccessor
{
Alias Alias { get; }
}
}

View File

@ -1,5 +1,3 @@
using Microsoft.EntityFrameworkCore;
using Oqtane.Enums;
using Oqtane.Models; using Oqtane.Models;
namespace Oqtane.Infrastructure namespace Oqtane.Infrastructure

View File

@ -0,0 +1,10 @@
using Oqtane.Models;
namespace Oqtane.Infrastructure
{
public interface ISiteMigration
{
void Up(Site site, Alias alias);
void Down(Site site, Alias alias); // for future use (if necessary)
}
}

View File

@ -0,0 +1,27 @@
using System;
namespace Oqtane.Infrastructure
{
[AttributeUsage(AttributeTargets.Class)]
public class SiteMigrationAttribute : Attribute
{
private string aliasname;
private string version;
public SiteMigrationAttribute(string AliasName, string Version)
{
aliasname = AliasName;
version = Version;
}
public virtual string AliasName
{
get { return aliasname; }
}
public virtual string Version
{
get { return version; }
}
}
}

View File

@ -266,17 +266,27 @@ namespace Oqtane.Infrastructure
jobs.UpdateJob(job); jobs.UpdateJob(job);
} }
} }
}
catch
{
// error updating the job
}
if (_executingTask == null) // stop called without start
{ if (_executingTask == null)
return; {
} return;
}
try
{
// force cancellation of the executing method
_cancellationTokenSource.Cancel(); _cancellationTokenSource.Cancel();
} }
finally finally
{ {
await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken)); // wait until the task completes or the stop token triggers
await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken)).ConfigureAwait(false);
} }
} }

View File

@ -31,6 +31,7 @@ namespace Oqtane.Infrastructure
var settingRepository = provider.GetRequiredService<ISettingRepository>(); var settingRepository = provider.GetRequiredService<ISettingRepository>();
var logRepository = provider.GetRequiredService<ILogRepository>(); var logRepository = provider.GetRequiredService<ILogRepository>();
var visitorRepository = provider.GetRequiredService<IVisitorRepository>(); var visitorRepository = provider.GetRequiredService<IVisitorRepository>();
var notificationRepository = provider.GetRequiredService<INotificationRepository>();
// iterate through sites for current tenant // iterate through sites for current tenant
List<Site> sites = siteRepository.GetSites().ToList(); List<Site> sites = siteRepository.GetSites().ToList();
@ -51,7 +52,7 @@ namespace Oqtane.Infrastructure
} }
try try
{ {
count = logRepository.DeleteLogs(retention); count = logRepository.DeleteLogs(site.SiteId, retention);
log += count.ToString() + " Events Purged<br />"; log += count.ToString() + " Events Purged<br />";
} }
catch (Exception ex) catch (Exception ex)
@ -69,7 +70,7 @@ namespace Oqtane.Infrastructure
} }
try try
{ {
count = visitorRepository.DeleteVisitors(retention); count = visitorRepository.DeleteVisitors(site.SiteId, retention);
log += count.ToString() + " Visitors Purged<br />"; log += count.ToString() + " Visitors Purged<br />";
} }
catch (Exception ex) catch (Exception ex)
@ -77,6 +78,22 @@ namespace Oqtane.Infrastructure
log += $"Error Purging Visitors - {ex.Message}<br />"; log += $"Error Purging Visitors - {ex.Message}<br />";
} }
} }
// purge notifications
retention = 30; // 30 days
if (settings.ContainsKey("NotificationRetention") && !string.IsNullOrEmpty(settings["NotificationRetention"]))
{
retention = int.Parse(settings["NotificationRetention"]);
}
try
{
count = notificationRepository.DeleteNotifications(site.SiteId, retention);
log += count.ToString() + " Notifications Purged<br />";
}
catch (Exception ex)
{
log += $"Error Purging Notifications - {ex.Message}<br />";
}
} }
return log; return log;

View File

@ -18,29 +18,33 @@ namespace Oqtane.Infrastructure
private readonly IConfigManager _config; private readonly IConfigManager _config;
private readonly IUserPermissions _userPermissions; private readonly IUserPermissions _userPermissions;
private readonly IHttpContextAccessor _accessor; private readonly IHttpContextAccessor _accessor;
private readonly IUserRoleRepository _userRoles;
private readonly INotificationRepository _notifications;
public LogManager(ILogRepository logs, ITenantManager tenantManager, IConfigManager config, IUserPermissions userPermissions, IHttpContextAccessor accessor) public LogManager(ILogRepository logs, ITenantManager tenantManager, IConfigManager config, IUserPermissions userPermissions, IHttpContextAccessor accessor, IUserRoleRepository userRoles, INotificationRepository notifications)
{ {
_logs = logs; _logs = logs;
_tenantManager = tenantManager; _tenantManager = tenantManager;
_config = config; _config = config;
_userPermissions = userPermissions; _userPermissions = userPermissions;
_accessor = accessor; _accessor = accessor;
_userRoles = userRoles;
_notifications = notifications;
} }
public void Log(LogLevel level, object @class, LogFunction function, string message, params object[] args) public void Log(LogLevel level, object @class, LogFunction function, string message, params object[] args)
{ {
Log(-1, level, @class.GetType().AssemblyQualifiedName, function, null, message, args); Log(-1, level, @class, function, null, message, args);
} }
public void Log(LogLevel level, object @class, LogFunction function, Exception exception, string message, params object[] args) public void Log(LogLevel level, object @class, LogFunction function, Exception exception, string message, params object[] args)
{ {
Log(-1, level, @class.GetType().AssemblyQualifiedName, function, exception, message, args); Log(-1, level, @class, function, exception, message, args);
} }
public void Log(int siteId, LogLevel level, object @class, LogFunction function, string message, params object[] args) public void Log(int siteId, LogLevel level, object @class, LogFunction function, string message, params object[] args)
{ {
Log(siteId, level, @class.GetType().AssemblyQualifiedName, function, null, message, args); Log(siteId, level, @class, function, null, message, args);
} }
public void Log(int siteId, LogLevel level, object @class, LogFunction function, Exception exception, string message, params object[] args) public void Log(int siteId, LogLevel level, object @class, LogFunction function, Exception exception, string message, params object[] args)
@ -76,8 +80,8 @@ namespace Oqtane.Infrastructure
} }
} }
Type type = Type.GetType(@class.ToString()); Type type = @class.GetType();
if (type != null) if (type != null && type != typeof(string))
{ {
log.Category = type.AssemblyQualifiedName; log.Category = type.AssemblyQualifiedName;
log.Feature = Utilities.GetTypeNameLastSegment(log.Category, 0); log.Feature = Utilities.GetTypeNameLastSegment(log.Category, 0);
@ -124,11 +128,11 @@ namespace Oqtane.Infrastructure
try try
{ {
_logs.AddLog(log); _logs.AddLog(log);
SendNotification(log);
} }
catch (Exception ex) catch
{ {
// an error occurred writing to the database // an error occurred writing to the database
var x = ex.Message;
} }
} }
} }
@ -188,5 +192,28 @@ namespace Oqtane.Infrastructure
} }
return log; return log;
} }
private void SendNotification(Log log)
{
LogLevel notifylevel = LogLevel.Error;
var section = _config.GetSection("Logging:LogLevel:Notify");
if (section.Exists())
{
notifylevel = Enum.Parse<LogLevel>(section.Value);
}
if (Enum.Parse<LogLevel>(log.Level) >= notifylevel)
{
foreach (var userrole in _userRoles.GetUserRoles(log.SiteId.Value))
{
if (userrole.Role.Name == RoleNames.Host)
{
var url = _accessor.HttpContext.Request.Scheme + "://" + _tenantManager.GetAlias().Name + "/admin/log";
var notification = new Notification(log.SiteId.Value, userrole.User, "Site " + log.Level + " Notification", "Please visit " + url + " for more information");
_notifications.AddNotification(notification);
}
}
}
}
} }
} }

View File

@ -0,0 +1,68 @@
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Oqtane.Extensions;
using Oqtane.Models;
using Oqtane.Repository;
using Oqtane.Security;
using Oqtane.Shared;
namespace Oqtane.Infrastructure
{
internal class JwtMiddleware
{
private readonly RequestDelegate _next;
public JwtMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
if (context.Request.Headers.ContainsKey("Authorization"))
{
var alias = context.GetAlias();
if (alias != null)
{
var sitesettings = context.GetSiteSettings();
var secret = sitesettings.GetValue("JwtOptions:Secret", "");
if (!string.IsNullOrEmpty(secret))
{
var logger = context.RequestServices.GetService(typeof(ILogManager)) as ILogManager;
var jwtManager = context.RequestServices.GetService(typeof(IJwtManager)) as IJwtManager;
var token = context.Request.Headers["Authorization"].First().Split(" ").Last();
var identity = jwtManager.ValidateToken(token, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""));
if (identity != null && identity.Claims.Any())
{
// create user identity using jwt claims (note the difference in claimtype names)
var user = new User
{
UserId = int.Parse(identity.Claims.FirstOrDefault(item => item.Type == "nameid")?.Value),
Username = identity.Claims.FirstOrDefault(item => item.Type == "name")?.Value
};
// jwt already contains the roles - we are reloading to ensure most accurate permissions
var _userRoles = context.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository;
identity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, alias.SiteId).ToList());
// populate principal
var principal = (ClaimsIdentity)context.User.Identity;
UserSecurity.ResetClaimsIdentity(principal);
principal.AddClaims(identity.Claims);
logger.Log(alias.SiteId, LogLevel.Information, "TokenValidation", Enums.LogFunction.Security, "Token Validated For User {Username}", user.Username);
}
else
{
logger.Log(alias.SiteId, LogLevel.Error, "TokenValidation", Enums.LogFunction.Security, "Token Validation Error");
}
}
}
}
// continue processing
if (_next != null) await _next(context);
}
}
}

View File

@ -1,40 +1,61 @@
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Oqtane.Repository;
using Oqtane.Shared;
namespace Oqtane.Infrastructure namespace Oqtane.Infrastructure
{ {
internal class TenantMiddleware internal class TenantMiddleware
{ {
private readonly RequestDelegate next; private readonly RequestDelegate _next;
public TenantMiddleware(RequestDelegate next) public TenantMiddleware(RequestDelegate next)
{ {
this.next = next; _next = next;
} }
public async Task Invoke(HttpContext context) public async Task Invoke(HttpContext context)
{ {
// check if framework is installed // check if framework is installed
var config = context.RequestServices.GetService(typeof(IConfigManager)) as IConfigManager; var config = context.RequestServices.GetService(typeof(IConfigManager)) as IConfigManager;
if (config.IsInstalled()) string path = context.Request.Path.ToString();
if (config.IsInstalled() && !path.StartsWith("/_blazor"))
{ {
// get alias // get alias (note that this also sets SiteState.Alias)
var tenantManager = context.RequestServices.GetService(typeof(ITenantManager)) as ITenantManager; var tenantManager = context.RequestServices.GetService(typeof(ITenantManager)) as ITenantManager;
var alias = tenantManager.GetAlias(); var alias = tenantManager.GetAlias();
// rewrite path by removing alias path prefix from api and pages requests if (alias != null)
if (alias != null && !string.IsNullOrEmpty(alias.Path))
{ {
string path = context.Request.Path.ToString(); // save alias in HttpContext
if (path.StartsWith("/" + alias.Path) && (path.Contains("/api/") || path.Contains("/pages/"))) context.Items.Add(Constants.HttpContextAliasKey, alias);
// save site settings in HttpContext
var cache = context.RequestServices.GetService(typeof(IMemoryCache)) as IMemoryCache;
var sitesettings = cache.GetOrCreate(Constants.HttpContextSiteSettingsKey + alias.SiteKey, entry =>
{ {
context.Request.Path = path.Replace("/" + alias.Path, ""); var settingRepository = context.RequestServices.GetService(typeof(ISettingRepository)) as ISettingRepository;
return settingRepository.GetSettings(EntityNames.Site, alias.SiteId)
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
});
context.Items.Add(Constants.HttpContextSiteSettingsKey, sitesettings);
// rewrite path by removing alias path prefix from api and pages requests (for consistent routing)
if (!string.IsNullOrEmpty(alias.Path))
{
if (path.StartsWith("/" + alias.Path) && (path.Contains("/api/") || path.Contains("/pages/")))
{
context.Request.Path = path.Replace("/" + alias.Path, "");
}
} }
} }
} }
// continue processing // continue processing
if (next != null) await next(context); if (_next != null) await _next(context);
} }
} }
} }

View File

@ -0,0 +1,11 @@
using System.Collections.Generic;
using Oqtane.Models;
namespace Oqtane.Infrastructure
{
public interface ISiteOptions<TOptions>
where TOptions : class, new()
{
void Configure(TOptions options, Alias alias, Dictionary<string, string> sitesettings);
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using Oqtane.Models;
namespace Oqtane.Infrastructure
{
public class SiteOptions<TOptions> : ISiteOptions<TOptions>
where TOptions : class, new()
{
private readonly Action<TOptions, Alias, Dictionary<string, string>> configureOptions;
public SiteOptions(Action<TOptions, Alias, Dictionary<string, string>> configureOptions)
{
this.configureOptions = configureOptions;
}
public void Configure(TOptions options, Alias alias, Dictionary<string, string> sitesettings)
{
configureOptions(options, alias, sitesettings);
}
}
}

View File

@ -0,0 +1,54 @@
using System;
using System.Collections.Concurrent;
using Microsoft.Extensions.Options;
namespace Oqtane.Infrastructure
{
public class SiteOptionsCache<TOptions> : IOptionsMonitorCache<TOptions>
where TOptions : class, new()
{
private readonly IAliasAccessor _aliasAccessor;
private readonly ConcurrentDictionary<string, IOptionsMonitorCache<TOptions>> map = new ConcurrentDictionary<string, IOptionsMonitorCache<TOptions>>();
public SiteOptionsCache(IAliasAccessor aliasAccessor)
{
_aliasAccessor = aliasAccessor;
}
public void Clear()
{
var cache = map.GetOrAdd(GetKey(), new OptionsCache<TOptions>());
cache.Clear();
}
public TOptions GetOrAdd(string name, Func<TOptions> createOptions)
{
name = name ?? Options.DefaultName;
var cache = map.GetOrAdd(GetKey(), new OptionsCache<TOptions>());
return cache.GetOrAdd(name, createOptions);
}
public bool TryAdd(string name, TOptions options)
{
name = name ?? Options.DefaultName;
var cache = map.GetOrAdd(GetKey(), new OptionsCache<TOptions>());
return cache.TryAdd(name, options);
}
public bool TryRemove(string name)
{
name = name ?? Options.DefaultName;
var cache = map.GetOrAdd(GetKey(), new OptionsCache<TOptions>());
return cache.TryRemove(name);
}
private string GetKey()
{
return _aliasAccessor?.Alias?.SiteKey ?? "";
}
}
}

View File

@ -0,0 +1,58 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Oqtane.Extensions;
namespace Oqtane.Infrastructure
{
public class SiteOptionsFactory<TOptions> : IOptionsFactory<TOptions>
where TOptions : class, new()
{
private readonly IConfigureOptions<TOptions>[] _configureOptions;
private readonly IPostConfigureOptions<TOptions>[] _postConfigureOptions;
private readonly ISiteOptions<TOptions>[] _siteOptions;
private readonly IHttpContextAccessor _accessor;
public SiteOptionsFactory(IEnumerable<IConfigureOptions<TOptions>> configureOptions, IEnumerable<IPostConfigureOptions<TOptions>> postConfigureOptions, IEnumerable<ISiteOptions<TOptions>> siteOptions, IHttpContextAccessor accessor)
{
_configureOptions = configureOptions as IConfigureOptions<TOptions>[] ?? new List<IConfigureOptions<TOptions>>(configureOptions).ToArray();
_postConfigureOptions = postConfigureOptions as IPostConfigureOptions<TOptions>[] ?? new List<IPostConfigureOptions<TOptions>>(postConfigureOptions).ToArray();
_siteOptions = siteOptions as ISiteOptions<TOptions>[] ?? new List<ISiteOptions<TOptions>>(siteOptions).ToArray();
_accessor = accessor;
}
public TOptions Create(string name)
{
// default options
var options = new TOptions();
foreach (var setup in _configureOptions)
{
if (setup is IConfigureNamedOptions<TOptions> namedSetup)
{
namedSetup.Configure(name, options);
}
else if (name == Options.DefaultName)
{
setup.Configure(options);
}
}
// override with site specific options
if (_accessor.HttpContext?.GetAlias() != null && _accessor.HttpContext?.GetSiteSettings() != null)
{
foreach (var siteOption in _siteOptions)
{
siteOption.Configure(options, _accessor.HttpContext.GetAlias(), _accessor.HttpContext.GetSiteSettings());
}
}
// post configuration
foreach (var post in _postConfigureOptions)
{
post.PostConfigure(name, options);
}
return options;
}
}
}

View File

@ -0,0 +1,35 @@
using Microsoft.Extensions.Options;
namespace Oqtane.Infrastructure
{
public class SiteOptionsManager<TOptions> : IOptions<TOptions>, IOptionsSnapshot<TOptions> where TOptions : class, new()
{
private readonly IOptionsFactory<TOptions> _factory;
private readonly IOptionsMonitorCache<TOptions> _cache; // private cache
public SiteOptionsManager(IOptionsFactory<TOptions> factory, IOptionsMonitorCache<TOptions> cache)
{
_factory = factory;
_cache = cache;
}
public TOptions Value
{
get
{
return Get(Options.DefaultName);
}
}
public virtual TOptions Get(string name)
{
name = name ?? Options.DefaultName;
return _cache.GetOrAdd(name, () => _factory.Create(name));
}
public void Reset()
{
_cache.Clear();
}
}
}

View File

@ -0,0 +1,18 @@
using Oqtane.Models;
namespace Oqtane.Infrastructure
{
[SiteMigration("localhost:44357", "01.00.00")]
public class ExampleSiteMigration : ISiteMigration
{
void ISiteMigration.Up(Site site, Alias alias)
{
// execute some version-specific upgrade logic for the site here such as adding pages, modules, content, etc...
}
void ISiteMigration.Down(Site site, Alias alias)
{
// not implemented
}
}
}

View File

@ -0,0 +1,9 @@
using Oqtane.Models;
namespace Oqtane.Infrastructure
{
public class SiteState
{
public Alias Alias { get; set; }
}
}

View File

@ -3,7 +3,6 @@ using System.Linq;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Oqtane.Models; using Oqtane.Models;
using Oqtane.Repository; using Oqtane.Repository;
using Oqtane.Shared;
namespace Oqtane.Infrastructure namespace Oqtane.Infrastructure
{ {
@ -26,7 +25,7 @@ namespace Oqtane.Infrastructure
{ {
Alias alias = null; Alias alias = null;
if (_siteState != null && _siteState.Alias != null) if (_siteState?.Alias != null && _siteState.Alias.AliasId != -1)
{ {
alias = _siteState.Alias; alias = _siteState.Alias;
} }
@ -63,7 +62,7 @@ namespace Oqtane.Infrastructure
public Tenant GetTenant() public Tenant GetTenant()
{ {
var alias = GetAlias(); var alias = _siteState?.Alias;
if (alias != null) if (alias != null)
{ {
// return tenant details // return tenant details

View File

@ -0,0 +1,41 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using Oqtane.Databases.Interfaces;
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global
namespace Oqtane.Migrations.EntityBuilders
{
public class AspNetUserLoginsEntityBuilder : BaseEntityBuilder<AspNetUserLoginsEntityBuilder>
{
private const string _entityTableName = "AspNetUserLogins";
private readonly PrimaryKey<AspNetUserLoginsEntityBuilder> _primaryKey = new("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
private readonly ForeignKey<AspNetUserLoginsEntityBuilder> _foreignKey = new("FK_AspNetUserLogins_AspNetUsers_UserId", x => x.UserId, "AspNetUsers", "Id", ReferentialAction.Cascade);
public AspNetUserLoginsEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database)
{
EntityTableName = _entityTableName;
PrimaryKey = _primaryKey;
ForeignKeys.Add(_foreignKey);
}
protected override AspNetUserLoginsEntityBuilder BuildTable(ColumnsBuilder table)
{
LoginProvider = AddStringColumn(table, "LoginProvider", 450);
ProviderKey = AddStringColumn(table, "ProviderKey", 450);
ProviderDisplayName = AddMaxStringColumn(table, "ProviderDisplayName", true);
UserId = AddStringColumn(table, "UserId", 450);
return this;
}
public OperationBuilder<AddColumnOperation> LoginProvider { get; set; }
public OperationBuilder<AddColumnOperation> ProviderKey { get; set; }
public OperationBuilder<AddColumnOperation> ProviderDisplayName { get; set; }
public OperationBuilder<AddColumnOperation> UserId { get; set; }
}
}

View File

@ -52,64 +52,119 @@ namespace Oqtane.Migrations.EntityBuilders
_migrationBuilder.AddColumn<bool>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable); _migrationBuilder.AddColumn<bool>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable);
} }
public void AddBooleanColumn(string name, bool nullable, bool defaultValue)
{
_migrationBuilder.AddColumn<bool>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue);
}
protected OperationBuilder<AddColumnOperation> AddBooleanColumn(ColumnsBuilder table, string name, bool nullable = false) protected OperationBuilder<AddColumnOperation> AddBooleanColumn(ColumnsBuilder table, string name, bool nullable = false)
{ {
return table.Column<bool>(name: RewriteName(name), nullable: nullable); return table.Column<bool>(name: RewriteName(name), nullable: nullable);
} }
protected OperationBuilder<AddColumnOperation> AddBooleanColumn(ColumnsBuilder table, string name, bool nullable, bool defaultValue)
{
return table.Column<bool>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue);
}
public void AddDateTimeColumn(string name, bool nullable = false) public void AddDateTimeColumn(string name, bool nullable = false)
{ {
_migrationBuilder.AddColumn<DateTime>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable); _migrationBuilder.AddColumn<DateTime>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable);
} }
public void AddDateTimeColumn(string name, bool nullable, DateTime defaultValue)
{
_migrationBuilder.AddColumn<DateTime>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue);
}
protected OperationBuilder<AddColumnOperation> AddDateTimeColumn(ColumnsBuilder table, string name, bool nullable = false) protected OperationBuilder<AddColumnOperation> AddDateTimeColumn(ColumnsBuilder table, string name, bool nullable = false)
{ {
return table.Column<DateTime>(name: RewriteName(name), nullable: nullable); return table.Column<DateTime>(name: RewriteName(name), nullable: nullable);
} }
protected OperationBuilder<AddColumnOperation> AddDateTimeColumn(ColumnsBuilder table, string name, bool nullable, DateTime defaultValue)
{
return table.Column<DateTime>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue);
}
public void AddDateTimeOffsetColumn(string name, bool nullable = false) public void AddDateTimeOffsetColumn(string name, bool nullable = false)
{ {
_migrationBuilder.AddColumn<DateTimeOffset>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable); _migrationBuilder.AddColumn<DateTimeOffset>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable);
} }
public void AddDateTimeOffsetColumn(string name, bool nullable, DateTimeOffset defaultValue)
{
_migrationBuilder.AddColumn<DateTimeOffset>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue);
}
protected OperationBuilder<AddColumnOperation> AddDateTimeOffsetColumn(ColumnsBuilder table, string name, bool nullable = false) protected OperationBuilder<AddColumnOperation> AddDateTimeOffsetColumn(ColumnsBuilder table, string name, bool nullable = false)
{ {
return table.Column<DateTimeOffset>(name: RewriteName(name), nullable: nullable); return table.Column<DateTimeOffset>(name: RewriteName(name), nullable: nullable);
} }
protected OperationBuilder<AddColumnOperation> AddDateTimeOffsetColumn(ColumnsBuilder table, string name, bool nullable, DateTimeOffset defaultValue)
{
return table.Column<DateTimeOffset>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue);
}
public void AddIntegerColumn(string name, bool nullable = false) public void AddIntegerColumn(string name, bool nullable = false)
{ {
_migrationBuilder.AddColumn<int>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable); _migrationBuilder.AddColumn<int>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable);
} }
public void AddIntegerColumn(string name, bool nullable, int defaultValue)
{
_migrationBuilder.AddColumn<int>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue);
}
protected OperationBuilder<AddColumnOperation> AddIntegerColumn(ColumnsBuilder table, string name, bool nullable = false) protected OperationBuilder<AddColumnOperation> AddIntegerColumn(ColumnsBuilder table, string name, bool nullable = false)
{ {
return table.Column<int>(name: RewriteName(name), nullable: nullable); return table.Column<int>(name: RewriteName(name), nullable: nullable);
} }
protected OperationBuilder<AddColumnOperation> AddIntegerColumn(ColumnsBuilder table, string name, bool nullable, int defaultValue)
{
return table.Column<int>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue);
}
public void AddMaxStringColumn(string name, bool nullable = false, bool unicode = true) public void AddMaxStringColumn(string name, bool nullable = false, bool unicode = true)
{ {
_migrationBuilder.AddColumn<string>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, unicode: unicode); _migrationBuilder.AddColumn<string>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, unicode: unicode);
} }
public void AddMaxStringColumn(string name, bool nullable, bool unicode, string defaultValue)
{
_migrationBuilder.AddColumn<string>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, unicode: unicode, defaultValue: defaultValue);
}
protected OperationBuilder<AddColumnOperation> AddMaxStringColumn(ColumnsBuilder table, string name, bool nullable = false, bool unicode = true) protected OperationBuilder<AddColumnOperation> AddMaxStringColumn(ColumnsBuilder table, string name, bool nullable = false, bool unicode = true)
{ {
return table.Column<string>(name: RewriteName(name), nullable: nullable, unicode: unicode); return table.Column<string>(name: RewriteName(name), nullable: nullable, unicode: unicode);
} }
protected OperationBuilder<AddColumnOperation> AddMaxStringColumn(ColumnsBuilder table, string name, bool nullable, bool unicode, string defaultValue)
{
return table.Column<string>(name: RewriteName(name), nullable: nullable, unicode: unicode, defaultValue: defaultValue);
}
public void AddStringColumn(string name, int length, bool nullable = false, bool unicode = true) public void AddStringColumn(string name, int length, bool nullable = false, bool unicode = true)
{ {
_migrationBuilder.AddColumn<string>(RewriteName(name), RewriteName(EntityTableName), maxLength: length, nullable: nullable, unicode: unicode); _migrationBuilder.AddColumn<string>(RewriteName(name), RewriteName(EntityTableName), maxLength: length, nullable: nullable, unicode: unicode);
} }
public void AddStringColumn(string name, int length, bool nullable, bool unicode, string defaultValue)
{
_migrationBuilder.AddColumn<string>(RewriteName(name), RewriteName(EntityTableName), maxLength: length, nullable: nullable, unicode: unicode, defaultValue: defaultValue);
}
protected OperationBuilder<AddColumnOperation> AddStringColumn(ColumnsBuilder table, string name, int length, bool nullable = false, bool unicode = true) protected OperationBuilder<AddColumnOperation> AddStringColumn(ColumnsBuilder table, string name, int length, bool nullable = false, bool unicode = true)
{ {
return table.Column<string>(name: RewriteName(name), maxLength: length, nullable: nullable, unicode: unicode); return table.Column<string>(name: RewriteName(name), maxLength: length, nullable: nullable, unicode: unicode);
} }
public void AlterStringColumn(string name, int length, bool nullable = false, bool unicode = true) protected OperationBuilder<AddColumnOperation> AddStringColumn(ColumnsBuilder table, string name, int length, bool nullable, bool unicode, string defaultValue)
{ {
_migrationBuilder.AlterColumn<string>(RewriteName(name), RewriteName(EntityTableName), maxLength: length, nullable: nullable, unicode: unicode); return table.Column<string>(name: RewriteName(name), maxLength: length, nullable: nullable, unicode: unicode, defaultValue: defaultValue);
} }
public void AddDecimalColumn(string name, int precision, int scale, bool nullable = false) public void AddDecimalColumn(string name, int precision, int scale, bool nullable = false)
@ -117,11 +172,26 @@ namespace Oqtane.Migrations.EntityBuilders
_migrationBuilder.AddColumn<decimal>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, precision: precision, scale: scale); _migrationBuilder.AddColumn<decimal>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, precision: precision, scale: scale);
} }
public void AddDecimalColumn(string name, int precision, int scale, bool nullable, decimal defaultValue)
{
_migrationBuilder.AddColumn<decimal>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, precision: precision, scale: scale, defaultValue: defaultValue);
}
protected OperationBuilder<AddColumnOperation> AddDecimalColumn(ColumnsBuilder table, string name, int precision, int scale, bool nullable = false) protected OperationBuilder<AddColumnOperation> AddDecimalColumn(ColumnsBuilder table, string name, int precision, int scale, bool nullable = false)
{ {
return table.Column<decimal>(name: RewriteName(name), nullable: nullable, precision: precision, scale: scale); return table.Column<decimal>(name: RewriteName(name), nullable: nullable, precision: precision, scale: scale);
} }
protected OperationBuilder<AddColumnOperation> AddDecimalColumn(ColumnsBuilder table, string name, int precision, int scale, bool nullable, decimal defaultValue)
{
return table.Column<decimal>(name: RewriteName(name), nullable: nullable, precision: precision, scale: scale, defaultValue: defaultValue);
}
public void AlterStringColumn(string name, int length, bool nullable = false, bool unicode = true)
{
ActiveDatabase.AlterStringColumn(_migrationBuilder, RewriteName(name), RewriteName(EntityTableName), length, nullable, unicode);
}
public void DropColumn(string name) public void DropColumn(string name)
{ {
ActiveDatabase.DropColumn(_migrationBuilder, RewriteName(name), RewriteName(EntityTableName)); ActiveDatabase.DropColumn(_migrationBuilder, RewriteName(name), RewriteName(EntityTableName));

View File

@ -16,33 +16,22 @@ namespace Oqtane.Migrations.Tenant
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
if (ActiveDatabase.Name != "Sqlite") var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase);
{ // Drop the index is needed because the Path is already associated with IX_Folder
var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase); folderEntityBuilder.DropIndex("IX_Folder");
folderEntityBuilder.AlterStringColumn("Name", 256);
folderEntityBuilder.AlterStringColumn("Name", 256); folderEntityBuilder.AlterStringColumn("Path", 512);
folderEntityBuilder.AddIndex("IX_Folder", new[] { "SiteId", "Path" }, true);
// Drop the index is needed because the Path is already associated with IX_Folder
folderEntityBuilder.DropForeignKey("FK_Folder_Site");
folderEntityBuilder.DropIndex("IX_Folder");
folderEntityBuilder.AlterStringColumn("Path", 512);
folderEntityBuilder.AddIndex("IX_Folder", new[] { "SiteId", "Path" }, true);
folderEntityBuilder.AddForeignKey("FK_Folder_Site");
}
} }
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
if (ActiveDatabase.Name != "Sqlite") var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase);
{ // Drop the index is needed because the Path is already associated with IX_Folder
var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase); folderEntityBuilder.DropIndex("IX_Folder");
folderEntityBuilder.AlterStringColumn("Path", 50);
folderEntityBuilder.AlterStringColumn("Name", 50); folderEntityBuilder.AlterStringColumn("Name", 50);
folderEntityBuilder.AddIndex("IX_Folder", new[] { "SiteId", "Path" }, true);
folderEntityBuilder.DropIndex("IX_Folder");
folderEntityBuilder.AlterStringColumn("Path", 50);
folderEntityBuilder.AddIndex("IX_Folder", new[] { "SiteId", "Path" }, true);
}
} }
} }
} }

View File

@ -16,29 +16,20 @@ namespace Oqtane.Migrations.Tenant
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
if (ActiveDatabase.Name != "Sqlite") var fileEntityBuilder = new FileEntityBuilder(migrationBuilder, ActiveDatabase);
{ // Drop the index is needed because the Name is already associated with IX_File
var fileEntityBuilder = new FileEntityBuilder(migrationBuilder, ActiveDatabase); fileEntityBuilder.DropIndex("IX_File");
fileEntityBuilder.AlterStringColumn("Name", 256);
// Drop the index is needed because the Name is already associated with IX_File fileEntityBuilder.AddIndex("IX_File", new[] { "FolderId", "Name" }, true);
fileEntityBuilder.DropForeignKey("FK_File_Folder");
fileEntityBuilder.DropIndex("IX_File");
fileEntityBuilder.AlterStringColumn("Name", 256);
fileEntityBuilder.AddIndex("IX_File", new[] { "FolderId", "Name" }, true);
fileEntityBuilder.AddForeignKey("FK_File_Folder");
}
} }
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
if (ActiveDatabase.Name != "Sqlite") var fileEntityBuilder = new FileEntityBuilder(migrationBuilder, ActiveDatabase);
{ // Drop the index is needed because the Name is already associated with IX_File
var fileEntityBuilder = new FileEntityBuilder(migrationBuilder, ActiveDatabase); fileEntityBuilder.DropIndex("IX_File");
fileEntityBuilder.AlterStringColumn("Name", 50);
fileEntityBuilder.DropIndex("IX_File"); fileEntityBuilder.AddIndex("IX_File", new[] { "FolderId", "Name" }, true);
fileEntityBuilder.AlterStringColumn("Name", 50);
fileEntityBuilder.AddIndex("IX_File", new[] { "FolderId", "Name" }, true);
}
} }
} }
} }

View File

@ -18,13 +18,13 @@ namespace Oqtane.Migrations.Tenant
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
var settingEntityBuilder = new SettingEntityBuilder(migrationBuilder, ActiveDatabase); var settingEntityBuilder = new SettingEntityBuilder(migrationBuilder, ActiveDatabase);
settingEntityBuilder.UpdateColumn("IsPublic", "1", "bool", "SettingName NOT LIKE 'SMTP%'"); settingEntityBuilder.UpdateColumn("IsPublic", "1", "bool", $"{RewriteName("SettingName")} NOT LIKE 'SMTP%'");
} }
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
var settingEntityBuilder = new SettingEntityBuilder(migrationBuilder, ActiveDatabase); var settingEntityBuilder = new SettingEntityBuilder(migrationBuilder, ActiveDatabase);
settingEntityBuilder.UpdateColumn("IsPublic", "0", "bool", "SettingName NOT LIKE 'SMTP%'"); settingEntityBuilder.UpdateColumn("IsPublic", "0", "bool", $"{RewriteName("SettingName")} NOT LIKE 'SMTP%'");
} }
} }
} }

View File

@ -20,7 +20,7 @@ namespace Oqtane.Migrations.Tenant
var settingEntityBuilder = new SettingEntityBuilder(migrationBuilder, ActiveDatabase); var settingEntityBuilder = new SettingEntityBuilder(migrationBuilder, ActiveDatabase);
settingEntityBuilder.AddBooleanColumn("IsPrivate", true); settingEntityBuilder.AddBooleanColumn("IsPrivate", true);
settingEntityBuilder.UpdateColumn("IsPrivate", "0", "bool", ""); settingEntityBuilder.UpdateColumn("IsPrivate", "0", "bool", "");
settingEntityBuilder.UpdateColumn("IsPrivate", "1", "bool", "EntityName = 'Site' AND SettingName LIKE 'SMTP%'"); settingEntityBuilder.UpdateColumn("IsPrivate", "1", "bool", $"{RewriteName("EntityName")} = 'Site' AND { RewriteName("SettingName")} LIKE 'SMTP%'");
settingEntityBuilder.DropColumn("IsPublic"); settingEntityBuilder.DropColumn("IsPublic");
} }

View File

@ -0,0 +1,43 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Oqtane.Databases.Interfaces;
using Oqtane.Migrations.EntityBuilders;
using Oqtane.Repository;
namespace Oqtane.Migrations.Tenant
{
[DbContext(typeof(TenantDBContext))]
[Migration("Tenant.03.01.00.01")]
public class ExpandVisitorAndUrlMappingUrls : MultiDatabaseMigration
{
public ExpandVisitorAndUrlMappingUrls(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var visitorEntityBuilder = new VisitorEntityBuilder(migrationBuilder, ActiveDatabase);
visitorEntityBuilder.AlterStringColumn("Url", 2048);
var urlMappingEntityBuilder = new UrlMappingEntityBuilder(migrationBuilder, ActiveDatabase);
// Drop the index is needed because the Url is already associated with IX_UrlMapping
urlMappingEntityBuilder.DropIndex("IX_UrlMapping");
urlMappingEntityBuilder.AlterStringColumn("Url", 2048);
urlMappingEntityBuilder.AlterStringColumn("MappedUrl", 2048);
urlMappingEntityBuilder.AddIndex("IX_UrlMapping", new[] { "SiteId", "Url" }, true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
var visitorEntityBuilder = new VisitorEntityBuilder(migrationBuilder, ActiveDatabase);
visitorEntityBuilder.AlterStringColumn("Url", 500);
var urlMappingEntityBuilder = new UrlMappingEntityBuilder(migrationBuilder, ActiveDatabase);
// Drop the index is needed because the Url is already associated with IX_UrlMapping
urlMappingEntityBuilder.DropIndex("IX_UrlMapping");
urlMappingEntityBuilder.AlterStringColumn("Url", 500);
urlMappingEntityBuilder.AlterStringColumn("MappedUrl", 500);
urlMappingEntityBuilder.AddIndex("IX_UrlMapping", new[] { "SiteId", "Url" }, true);
}
}
}

Some files were not shown because too many files have changed in this diff Show More