Compare commits

...

80 Commits

Author SHA1 Message Date
a16bc5db28 Merge pull request #2200 from oqtane/master
Merge pull request #2199 from oqtane/dev
2022-05-14 09:49:24 -04:00
51657338f5 Merge pull request #2199 from oqtane/dev
3.1.2 release
2022-05-14 09:48:59 -04:00
0fe3ea25af Merge pull request #2198 from sbwalker/dev
remove columns from main user management view  and migrate them to edit view
2022-05-13 17:00:32 -04:00
806daaf7c9 remove columns from main user management view and migrate them to edit view 2022-05-13 17:00:10 -04:00
21ff4a83b5 Merge pull request #2197 from sbwalker/dev
resolve login issue related to 'LoginOptions:TwoFactor' and order list of files alphabetically
2022-05-13 12:03:57 -04:00
ecc9aa40d7 resolve login issue related to 'LoginOptions:TwoFactor' and order list of files alphabetically 2022-05-13 12:03:34 -04:00
c34ca2a59b Merge pull request #2196 from sbwalker/dev
prepare for 3.1.2
2022-05-12 20:55:26 -04:00
dde7094fe3 prepare for 3.1.2 2022-05-12 20:55:11 -04:00
105afdfefc Merge pull request #2195 from sbwalker/dev
fix #2192 - Adding a new site fails
2022-05-12 20:42:20 -04:00
4c254a8686 fix #2192 - Adding a new site fails 2022-05-12 20:42:05 -04:00
33ca203e57 Merge pull request #2194 from sbwalker/dev
updated resource file
2022-05-12 13:56:01 -04:00
2ff4133cd4 updated resource file 2022-05-12 13:55:47 -04:00
49ad85713e Merge pull request #2193 from sbwalker/dev
add support for external login parameters and improve diagnostic messages related to claims
2022-05-12 13:52:06 -04:00
1978bf151f add support for external login parameters and improve diagnostic messages related to claims 2022-05-12 13:51:46 -04:00
506378de82 Merge pull request #2191 from sbwalker/dev
fix #2185 - alias auto registration including trailing slash
2022-05-10 08:03:55 -04:00
53ead7a03f fix #2185 - alias auto registration including trailing slash 2022-05-10 08:03:38 -04:00
ab979fd63c Merge pull request #2189 from sbwalker/dev
fix #2180 - Error in Module Creator if the template is not set
2022-05-09 11:35:17 -04:00
1e84a2238b fix #2180 - Error in Module Creator if the template is not set 2022-05-09 11:35:01 -04:00
5618adf86c Merge pull request #2187 from sbwalker/dev
fix #2182 - modifications to address MySQL compatibility issues
2022-05-08 22:14:54 -04:00
345b0bc95f fix #2182 - modifications to address MySQL compatibility issues 2022-05-08 22:14:26 -04:00
b1d6c35e99 Merge pull request #2183 from leigh-pointer/UserEmail
Args not in sink
2022-05-06 11:43:59 -04:00
d767f1a101 Args not in sink
The Display name and email address  not is the correct order!
2022-05-06 12:43:40 +02:00
c15f2b9a12 Merge pull request #2178 from leigh-pointer/UserEmail
Added the User Email field to the List
2022-05-05 17:14:08 -04:00
a21a53662b Update for real-estate
Removed Name
Removed Seconds from DateTime fields
Added Name to the Email link
2022-05-05 16:35:43 +02:00
ebb5340019 Merge pull request #2177 from leigh-pointer/NotIficationDateFormat
Updated the CreatedOn date format
2022-05-05 10:13:24 -04:00
6108bd214e Merge pull request #2179 from sbwalker/dev
fix #2176 - update LastIPAddress correctly during login
2022-05-05 09:57:26 -04:00
eed27e101a fix #2176 - update LastIPAddress correctly during login 2022-05-05 09:57:09 -04:00
2767680bed Added the User Email field to the List
Added the formatted email address of the user to the list view.
2022-05-05 13:25:35 +02:00
4080e30b6f Updated the CreatedOn date format
Updated the format to a more readable format of dd-MMM-yyyy
2022-05-05 13:07:09 +02:00
e89257be62 Merge pull request #2174 from sbwalker/dev
fix #2172 - File Upload issue caused by JS Interop not passing AntiForgery token in POST method
2022-05-04 17:15:10 -04:00
d3c40a7e8b fix #2172 - File Upload issue caused by JS Interop not passing AntiForgery token in POST methid 2022-05-04 17:14:45 -04:00
60657d5d25 Update README.md 2022-05-03 08:13:34 -04:00
71d5ae0e68 Merge pull request #2169 from oqtane/master
Merge pull request #2168 from oqtane/dev
2022-05-03 08:08:51 -04:00
46b8d202c7 Merge pull request #2168 from oqtane/dev
3.1.1 release
2022-05-03 08:08:34 -04:00
bc193a6470 Merge pull request #2167 from sbwalker/dev
remove custom module assets not part of framework
2022-05-03 07:54:30 -04:00
577528fa0a remove custom module assets not part of framework 2022-05-03 07:54:14 -04:00
35e78ea633 Merge pull request #2166 from sbwalker/dev
3.1.1 database providers, default module creator version to local install version
2022-05-02 17:08:56 -04:00
d5d4f85003 3.1.1 database providers, default module creator version to local install version 2022-05-02 17:08:29 -04:00
c6a49a6f5d Merge pull request #2164 from sbwalker/dev
enhance UserRole service with filtering and moved workload to server for better performance, improve error message details during installation
2022-04-29 21:39:36 -04:00
a3ff9373a2 enhance UserRole service with filtering and moved workload to server for better performance, improve error message details during installation 2022-04-29 21:39:11 -04:00
e6d2e74b17 Merge pull request #2162 from sbwalker/dev
refactor module upgrade logic, implement for themes and translations
2022-04-27 19:29:49 -04:00
e8464206e7 refactor module upgrade logic, implement for themes and translations 2022-04-27 19:29:29 -04:00
b6c4934123 Merge pull request #2161 from 2sic-forks/dev
Fix #2160 - upgrade module option
2022-04-27 15:03:46 -04:00
8ee83f738b fix# https://github.com/oqtane/oqtane.framework/issues/2160 2022-04-27 17:44:54 +02:00
c359300375 Merge pull request #2158 from sbwalker/dev
fix path on app-stylesheets
2022-04-26 16:34:08 -04:00
eb3361fa07 fix path on app-stylesheets 2022-04-26 16:33:50 -04:00
1b53da6749 Merge pull request #2155 from sbwalker/dev
external login improvements
2022-04-25 20:05:03 -04:00
c701895e29 external login improvements 2022-04-25 20:04:43 -04:00
e81222821e Merge pull request #2153 from sbwalker/dev
prepare for 3.1.1 release
2022-04-24 20:20:02 -04:00
cbca8c9e93 prepare for 3.1.1 release 2022-04-24 20:19:44 -04:00
92c4edacf2 Merge pull request #2152 from sbwalker/dev
completed antiforgery implementation, improved external login claim mapping, principal construction, and user experience
2022-04-22 17:54:49 -04:00
e4c648ee92 completed antiforgery implementation, improved external login claim mapping, principal construction, and user experience 2022-04-22 17:54:20 -04:00
69f6586aa9 Merge pull request #2149 from sbwalker/dev
Fix #2144 - install issue, Fix #2146 - move file issue, require verification of external login account linkage
2022-04-20 16:01:21 -04:00
391713b84d Fix #2144 - install issue, Fix #2146 - move file issue, require verification of external login account linkage 2022-04-20 16:00:58 -04:00
8c6a25e4b4 Merge pull request #2140 from sbwalker/dev
remove web.release.config as it causes installation issues in pure .net core environments (see #1957)
2022-04-15 09:23:14 -04:00
250701aff0 remove web.release.config as it causes installation issues in pure .net core environments (see #1957) 2022-04-15 09:22:51 -04:00
2dfd1bd5a2 Merge pull request #2139 from sbwalker/dev
removed method-level [ValidateAntiForgeryToken] attribute as it is now handled by global AutoValidateAntiforgeryTokenFilter, adjusted gitignore to improve filtering of Module and Theme folders in wwwroot and exclude all files in Oqtane.Server/Data
2022-04-15 08:02:04 -04:00
1c7380d4cf removed method-level [ValidateAntiForgeryToken] attribute as it is now handled by global AutoValidateAntiforgeryTokenFilter, adjusted gitignore to improve filtering of Module and Theme folders in wwwroot and exclude all files in Oqtane.Server/Data 2022-04-15 08:01:32 -04:00
68d9ac88b3 Merge pull request #2138 from sbwalker/dev
create separate API methods for tokens (short-lived) and personal access tokens (long-lived), include global antiforgery filter to mitigate XSRF when using cookie auth (ignored when using Jwt)
2022-04-14 19:42:24 -04:00
f6b3874668 create separate API methods for tokens (short-lived) and personal access tokens (long-lived), include global antiforgery filter to mitigate XSRF when using cookie auth (ignored when using Jwt) 2022-04-14 19:41:43 -04:00
c616878a64 Merge pull request #2134 from leigh-pointer/TogglePassword
User Areas to use the Toggle Password method
2022-04-14 08:58:59 -04:00
ad485a68ce Merge pull request #2136 from leigh-pointer/UsersCreatedOn
Added CreatedOn class "align-middle" to RowClass
2022-04-14 08:58:49 -04:00
2ebba3b8e7 Added CreatedOn class "align-middle" to RowClass 2022-04-14 14:52:34 +02:00
4117e6e1c5 Merge pull request #2135 from sbwalker/dev
Fix #2128 - site settings validation issue when logged in as Administrator (not Host)
2022-04-14 08:31:29 -04:00
423ee04879 Fix #2128 - site settings validation issue when logged in as Administrator (not Host) 2022-04-14 08:30:55 -04:00
1625e3ba6c User Areas to use the Toggle Password method
Updated the Components where the Password is required to allow toggle show / hide
2022-04-14 13:47:27 +02:00
5a71ab3c20 Merge pull request #2130 from leigh-pointer/LastLoggedIn
Update to User Management
2022-04-13 19:31:10 -04:00
b5833bf556 Merge pull request #2129 from leigh-pointer/NotificationDelAll
Allow the deletion of all Notifications
2022-04-13 19:28:37 -04:00
6c31765965 Merge pull request #2132 from sbwalker/dev
fix #2125 - cannot login using WebAssembly, remove granular 404 logging as it is already managed by url mapping, make IModule ReleaseVersions optional when using EF Core migrations
2022-04-13 19:27:35 -04:00
6dc1d42d90 fix #2125 - cannot login using WebAssembly, remove granular 404 logging as it is already managed by url mapping, make IModule ReleaseVersions optional when using EF Core migrations 2022-04-13 19:27:12 -04:00
e273a954e6 Update to User Management
Updated user management to display more of the User ; Last Login Last IP and Is Authenticated.
2022-04-13 15:24:28 +02:00
a602a942c4 Allow the deletion of all Notifications
Added button to delete all the notifications for the selected filter.
2022-04-13 14:33:30 +02:00
dd32b33621 Merge pull request #2124 from sbwalker/dev
minor improvements to security features, use ActivatorUtilities.CreateInstance with SiteMigration to enable simpler DI
2022-04-12 07:47:10 -04:00
355d0405f4 minor improvements to security features, use ActivatorUtilities.CreateInstance with SiteMigration to enable simpler DI 2022-04-12 07:46:43 -04:00
c1e1595d58 Update README.md 2022-04-11 08:30:24 -04:00
ef476ac9b3 Update README.md 2022-04-11 07:56:10 -04:00
a7aaa7b18b Merge pull request #2116 from sbwalker/dev
Fix #2111 - Adding user to Host role removes all other users roles
2022-04-05 17:11:29 -04:00
3abfbab5d1 Fix #2111 - Adding user to Host role removes all other users roles 2022-04-05 17:11:13 -04:00
b158474e21 Merge pull request #2113 from oqtane/master
Merge pull request #2112 from oqtane/dev
2022-04-05 08:51:40 -04:00
cfbcc41543 Merge pull request #2112 from oqtane/dev
3.1.0 release
2022-04-05 08:51:10 -04:00
130 changed files with 1965 additions and 1397 deletions

11
.gitignore vendored
View File

@ -12,9 +12,7 @@ msbuild.binlog
*.idea *.idea
Oqtane.Server/appsettings.json Oqtane.Server/appsettings.json
Oqtane.Server/Data/*.mdf Oqtane.Server/Data
Oqtane.Server/Data/*.ldf
Oqtane.Server/Data/*.db
/Oqtane.Server/Properties/PublishProfiles/FolderProfile.pubxml /Oqtane.Server/Properties/PublishProfiles/FolderProfile.pubxml
Oqtane.Server/Content Oqtane.Server/Content
@ -22,3 +20,10 @@ Oqtane.Server/Packages
Oqtane.Server/wwwroot/Content Oqtane.Server/wwwroot/Content
Oqtane.Server/wwwroot/Packages/*.log Oqtane.Server/wwwroot/Packages/*.log
Oqtane.Server/wwwroot/Modules
!Oqtane.Server/wwwroot/Modules/Oqtane.Modules.*
!Oqtane.Server/wwwroot/Modules/Templates
Oqtane.Server/wwwroot/Themes
!Oqtane.Server/wwwroot/Themes/Oqtane.Themes.*
!Oqtane.Server/wwwroot/Themes/Templates

View File

@ -28,7 +28,7 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pwd" HelpText="Enter the password to use for the database" ResourceKey="Pwd">Password:</Label> <Label Class="col-sm-3" For="pwd" HelpText="Enter the password to use for the database" ResourceKey="Pwd">Password:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="pwd" type="password" class="form-control" @bind="@_pwd" /> <input id="pwd" type="password" class="form-control" @bind="@_pwd" autocomplete="new-password" />
</div> </div>
</div> </div>

View File

@ -40,7 +40,7 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pwd" HelpText="Enter the password to use for the database" ResourceKey="Pwd">Password:</Label> <Label Class="col-sm-3" For="pwd" HelpText="Enter the password to use for the database" ResourceKey="Pwd">Password:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="pwd" type="password" class="form-control" @bind="@_pwd" /> <input id="pwd" type="password" class="form-control" @bind="@_pwd" autocomplete="new-password" />
</div> </div>
</div> </div>
} }

View File

@ -35,7 +35,7 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pwd" HelpText="Enter the password to use for the database" ResourceKey="Pwd">Password:</Label> <Label Class="col-sm-3" For="pwd" HelpText="Enter the password to use for the database" ResourceKey="Pwd">Password:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="pwd" type="password" class="form-control" @bind="@_pwd" /> <input id="pwd" type="password" class="form-control" @bind="@_pwd" autocomplete="new-password" />
</div> </div>
</div> </div>
} }

View File

@ -62,13 +62,19 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Provide a password for the primary user account" ResourceKey="Password">Password:</Label> <Label Class="col-sm-3" For="password" HelpText="Provide a password for the primary user account" ResourceKey="Password">Password:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="password" type="password" class="form-control" @bind="@_hostPassword" /> <div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_hostPassword" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</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="confirm" HelpText="Please confirm the password entered above by entering it again" ResourceKey="Confirm">Confirm:</Label> <Label Class="col-sm-3" For="confirm" HelpText="Please confirm the password entered above by entering it again" ResourceKey="Confirm">Confirm:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="confirm" type="password" class="form-control" @bind="@_confirmPassword" /> <div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirmPassword" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -104,6 +110,8 @@
private string _hostUsername = string.Empty; private string _hostUsername = string.Empty;
private string _hostPassword = string.Empty; private string _hostPassword = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string _confirmPassword = string.Empty; private string _confirmPassword = string.Empty;
private string _hostEmail = string.Empty; private string _hostEmail = string.Empty;
private bool _register = true; private bool _register = true;
@ -112,6 +120,7 @@
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
_togglepassword = SharedLocalizer["ShowPassword"];
_databases = await DatabaseService.GetDatabasesAsync(); _databases = await DatabaseService.GetDatabasesAsync();
if (_databases.Exists(item => item.IsDefault)) if (_databases.Exists(item => item.IsDefault))
{ {
@ -218,4 +227,18 @@
_message = Localizer["Message.Require.DbInfo"]; _message = Localizer["Message.Require.DbInfo"];
} }
} }
private void TogglePassword()
{
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = SharedLocalizer["ShowPassword"];
}
}
} }

View File

@ -84,7 +84,7 @@ else
var package = _packages.Where(item => item.PackageId == (Constants.PackageId + ".Client." + code)).FirstOrDefault(); var package = _packages.Where(item => item.PackageId == (Constants.PackageId + ".Client." + code)).FirstOrDefault();
if (package != null) if (package != null)
{ {
upgradeavailable = (Version.Parse(package.Version).CompareTo(Version.Parse(Constants.Version)) > 0); upgradeavailable = (Version.Parse(package.Version).CompareTo(Version.Parse(Constants.Version)) == 0);
} }
} }

View File

@ -95,7 +95,7 @@
{ {
try try
{ {
_togglepassword = Localizer["ShowPassword"]; _togglepassword = SharedLocalizer["ShowPassword"];
if (PageState.Site.Settings.ContainsKey("LoginOptions:AllowSiteLogin") && !string.IsNullOrEmpty(PageState.Site.Settings["LoginOptions:AllowSiteLogin"])) if (PageState.Site.Settings.ContainsKey("LoginOptions:AllowSiteLogin") && !string.IsNullOrEmpty(PageState.Site.Settings["LoginOptions:AllowSiteLogin"]))
{ {
@ -117,22 +117,47 @@
_username = PageState.QueryString["name"]; _username = PageState.QueryString["name"];
} }
if (PageState.QueryString.ContainsKey("token")) if (PageState.QueryString.ContainsKey("token") && !string.IsNullOrEmpty(_username))
{ {
var user = new User(); var user = new User();
user.SiteId = PageState.Site.SiteId; user.SiteId = PageState.Site.SiteId;
user.Username = _username; user.Username = _username;
user = await UserService.VerifyEmailAsync(user, PageState.QueryString["token"]);
if (user != null) if (PageState.QueryString.ContainsKey("key"))
{ {
await logger.LogInformation(LogFunction.Security, "Email Verified For For Username {Username}", _username); user = await UserService.LinkUserAsync(user, PageState.QueryString["token"], PageState.Site.Settings["ExternalLogin:ProviderType"], PageState.QueryString["key"], PageState.Site.Settings["ExternalLogin:ProviderName"]);
AddModuleMessage(Localizer["Success.Account.Verified"], MessageType.Info); if (user != null)
{
await logger.LogInformation(LogFunction.Security, "External Login Linkage Successful For Username {Username}", _username);
AddModuleMessage(Localizer["Success.Account.Linked"], MessageType.Info);
}
else
{
await logger.LogError(LogFunction.Security, "External Login Linkage Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Message.Account.NotLinked"], MessageType.Warning);
}
_username = "";
} }
else else
{ {
await logger.LogError(LogFunction.Security, "Email Verification Failed For Username {Username}", _username); user = await UserService.VerifyEmailAsync(user, PageState.QueryString["token"]);
AddModuleMessage(Localizer["Message.Account.NotVerfied"], MessageType.Warning); if (user != null)
{
await logger.LogInformation(LogFunction.Security, "Email Verified For For Username {Username}", _username);
AddModuleMessage(Localizer["Success.Account.Verified"], MessageType.Info);
}
else
{
await logger.LogError(LogFunction.Security, "Email Verification Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Message.Account.NotVerified"], MessageType.Warning);
}
}
}
else
{
if (PageState.QueryString.ContainsKey("status"))
{
AddModuleMessage(Localizer["ExternalLoginStatus." + PageState.QueryString["status"]], MessageType.Info);
} }
} }
} }
@ -159,11 +184,11 @@
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
if (await interop.FormValid(login)) if (await interop.FormValid(login))
{ {
var user = new User { SiteId = PageState.Site.SiteId, Username = _username, Password = _password}; var user = new User { SiteId = PageState.Site.SiteId, Username = _username, Password = _password, LastIPAddress = SiteState.RemoteIPAddress};
if (!twofactor) if (!twofactor)
{ {
user = await UserService.LoginUserAsync(user, false, false); user = await UserService.LoginUserAsync(user);
} }
else else
{ {
@ -174,23 +199,14 @@
{ {
await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username); await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username);
if (PageState.Runtime == Oqtane.Shared.Runtime.Server) // post back 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 };
// server-side Blazor needs to post to the Login page so that the cookies are set correctly string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/");
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = _returnUrl }; await interop.SubmitForm(url, fields);
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 else
{ {
if (user.TwoFactorRequired) if ((PageState.Site.Settings.ContainsKey("LoginOptions:TwoFactor") && PageState.Site.Settings["LoginOptions:TwoFactor"] == "required") || user.TwoFactorRequired)
{ {
twofactor = true; twofactor = true;
validated = false; validated = false;
@ -282,12 +298,12 @@
if (_passwordtype == "password") if (_passwordtype == "password")
{ {
_passwordtype = "text"; _passwordtype = "text";
_togglepassword = Localizer["HidePassword"]; _togglepassword = SharedLocalizer["HidePassword"];
} }
else else
{ {
_passwordtype = "password"; _passwordtype = "password";
_togglepassword = Localizer["ShowPassword"]; _togglepassword = SharedLocalizer["ShowPassword"];
} }
} }

View File

@ -76,64 +76,71 @@ else
} }
@code { @code {
private ElementReference form; private ElementReference form;
private bool validated = false; private bool validated = false;
private string _moduledefinitionname = string.Empty; private string _moduledefinitionname = string.Empty;
private string _owner = string.Empty; private string _owner = string.Empty;
private string _module = string.Empty; private string _module = string.Empty;
private string _description = string.Empty; private string _description = string.Empty;
private List<Template> _templates; private List<Template> _templates;
private string _template = "-"; private string _template = "-";
private string[] _versions; private string[] _versions;
private string _reference = Constants.Version; private string _reference = Constants.Version;
private string _minversion = "2.0.0"; private string _minversion = "2.0.0";
private string _location = string.Empty; private string _location = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override void OnInitialized() protected override void OnInitialized()
{ {
_moduledefinitionname = SettingService.GetSetting(ModuleState.Settings, "ModuleDefinitionName", ""); _moduledefinitionname = SettingService.GetSetting(ModuleState.Settings, "ModuleDefinitionName", "");
if (string.IsNullOrEmpty(_moduledefinitionname)) if (string.IsNullOrEmpty(_moduledefinitionname))
{ {
AddModuleMessage(Localizer["Info.Module.Creator"], MessageType.Info); AddModuleMessage(Localizer["Info.Module.Creator"], MessageType.Info);
} }
else else
{ {
AddModuleMessage(Localizer["Info.Module.Activate"], MessageType.Info); AddModuleMessage(Localizer["Info.Module.Activate"], MessageType.Info);
} }
} }
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
try try
{ {
_templates = await ModuleDefinitionService.GetModuleDefinitionTemplatesAsync(); _templates = await ModuleDefinitionService.GetModuleDefinitionTemplatesAsync();
_versions = Constants.ReleaseVersions.Split(',').Where(item => Version.Parse(item).CompareTo(Version.Parse("2.0.0")) >= 0).ToArray(); _versions = Constants.ReleaseVersions.Split(',').Where(item => Version.Parse(item).CompareTo(Version.Parse("2.0.0")) >= 0).ToArray();
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Loading Module Creator"); await logger.LogError(ex, "Error Loading Module Creator");
} }
} }
private async Task CreateModule() private async Task CreateModule()
{ {
validated = true; validated = true;
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
if (await interop.FormValid(form)) if (await interop.FormValid(form))
{ {
try try
{ {
var moduleDefinition = new ModuleDefinition { Owner = _owner, Name = _module, Description = _description, Template = _template, Version = _reference }; if (IsValid(_owner) && IsValid(_module) && _owner != _module && _template != "-")
moduleDefinition = await ModuleDefinitionService.CreateModuleDefinitionAsync(moduleDefinition); {
var moduleDefinition = new ModuleDefinition { Owner = _owner, Name = _module, Description = _description, Template = _template, Version = _reference };
moduleDefinition = await ModuleDefinitionService.CreateModuleDefinitionAsync(moduleDefinition);
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId); var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
SettingService.SetSetting(settings, "ModuleDefinitionName", moduleDefinition.ModuleDefinitionName); SettingService.SetSetting(settings, "ModuleDefinitionName", moduleDefinition.ModuleDefinitionName);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId); await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
GetLocation(); GetLocation();
AddModuleMessage(string.Format(Localizer["Success.Module.Create"], NavigateUrl("admin/system")), MessageType.Success); AddModuleMessage(string.Format(Localizer["Success.Module.Create"], NavigateUrl("admin/system")), MessageType.Success);
}
else
{
AddModuleMessage(Localizer["Message.Require.ValidName"], MessageType.Warning);
}
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -81,7 +81,7 @@
private List<Template> _templates; private List<Template> _templates;
private string _template = "-"; private string _template = "-";
private string[] _versions; private string[] _versions;
private string _reference = Constants.Version; private string _reference = "local";
private string _minversion = "2.0.0"; private string _minversion = "2.0.0";
private string _location = string.Empty; private string _location = string.Empty;

View File

@ -50,9 +50,12 @@ else
@((MarkupString)PurchaseLink(context.PackageName)) @((MarkupString)PurchaseLink(context.PackageName))
</td> </td>
<td> <td>
@if (UpgradeAvailable(context.PackageName, context.Version)) @{
var version = UpgradeAvailable(context.PackageName, context.Version);
}
@if (version != context.Version)
{ {
<button type="button" class="btn btn-success" @onclick=@(async () => await DownloadModule(context.PackageName, context.Version))>@SharedLocalizer["Upgrade"]</button> <button type="button" class="btn btn-success" @onclick=@(async () => await DownloadModule(context.PackageName, version))>@SharedLocalizer["Upgrade"]</button>
} }
</td> </td>
</Row> </Row>
@ -60,62 +63,60 @@ else
} }
@code { @code {
private List<ModuleDefinition> _moduleDefinitions; private List<ModuleDefinition> _moduleDefinitions;
private List<Package> _packages; private List<Package> _packages;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
try try
{ {
_moduleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId); _moduleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId);
_packages = await PackageService.GetPackagesAsync("module"); _packages = await PackageService.GetPackagesAsync("module");
}
catch (Exception ex)
{
if (_moduleDefinitions == null)
{
await logger.LogError(ex, "Error Loading Modules {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Module.Load"], MessageType.Error);
}
}
}
private string PurchaseLink(string packagename)
{
string link = "";
if (!string.IsNullOrEmpty(packagename) && _packages != null)
{
var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault();
if (package != null)
{
if (package.ExpiryDate != null && package.ExpiryDate.Value.Date != DateTime.MaxValue.Date)
{
link += "<span>" + package.ExpiryDate.Value.Date.ToString("MMM dd, yyyy") + "</span>";
if (!string.IsNullOrEmpty(package.PaymentUrl))
{
link += "&nbsp;&nbsp;<a class=\"btn btn-primary\" style=\"text-decoration: none !important\" href=\"" + package.PaymentUrl + "\" target=\"_new\">" + SharedLocalizer["Extend"] + "</a>";
}
}
}
}
return link;
}
private string UpgradeAvailable(string packagename, string version)
{
if (!string.IsNullOrEmpty(packagename) && _packages != null)
{
var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault();
if (package != null && Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0)
{
return package.Version;
}
} }
catch (Exception ex) return version;
{
if (_moduleDefinitions == null)
{
await logger.LogError(ex, "Error Loading Modules {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Module.Load"], MessageType.Error);
}
}
}
private string PurchaseLink(string packagename)
{
string link = "";
if (!string.IsNullOrEmpty(packagename) && _packages != null)
{
var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault();
if (package != null)
{
if (package.ExpiryDate != null && package.ExpiryDate.Value.Date != DateTime.MaxValue.Date)
{
link += "<span>" + package.ExpiryDate.Value.Date.ToString("MMM dd, yyyy") + "</span>";
if (!string.IsNullOrEmpty(package.PaymentUrl))
{
link += "&nbsp;&nbsp;<a class=\"btn btn-primary\" style=\"text-decoration: none !important\" href=\"" + package.PaymentUrl + "\" target=\"_new\">" + SharedLocalizer["Extend"] + "</a>";
}
}
}
}
return link;
}
private bool UpgradeAvailable(string packagename, string version)
{
var upgradeavailable = false;
if (!string.IsNullOrEmpty(packagename) && _packages != null)
{
var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault();
if (package != null)
{
upgradeavailable = (Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0);
}
}
return upgradeavailable;
} }
private async Task DownloadModule(string packagename, string version) private async Task DownloadModule(string packagename, string version)

View File

@ -27,19 +27,25 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Please choose a sufficiently secure password and enter it here" ResourceKey="Password"></Label> <Label Class="col-sm-3" For="password" HelpText="Please choose a sufficiently secure password and enter it here" ResourceKey="Password"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="password" type="password" class="form-control" @bind="@_password" autocomplete="new-password" required /> <div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</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="confirm" HelpText="Enter your password again to confirm it matches the value entered above" ResourceKey="Confirm"></Label> <Label Class="col-sm-3" For="confirm" HelpText="Enter your password again to confirm it matches the value entered above" ResourceKey="Confirm"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="confirm" type="password" class="form-control" @bind="@_confirm" autocomplete="new-password" required /> <div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</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="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">
<input id="email" class="form-control" @bind="@_email" maxlength="256" required /> <input id="email" class="form-control" @bind="@_email" maxlength="256" required />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -62,15 +68,22 @@ else
} }
@code { @code {
private string _username = string.Empty; private string _username = string.Empty;
private ElementReference form; private ElementReference form;
private bool validated = false; private bool validated = false;
private string _password = string.Empty; private string _password = string.Empty;
private string _confirm = string.Empty; private string _passwordtype = "password";
private string _email = string.Empty; private string _togglepassword = string.Empty;
private string _displayname = string.Empty; private string _confirm = string.Empty;
private string _email = string.Empty;
private string _displayname = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
protected override void OnParametersSet()
{
_togglepassword = SharedLocalizer["ShowPassword"];
}
private async Task Register() private async Task Register()
{ {
@ -134,4 +147,18 @@ else
{ {
NavigationManager.NavigateTo(NavigateUrl(string.Empty)); NavigationManager.NavigateTo(NavigateUrl(string.Empty));
} }
private void TogglePassword()
{
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = SharedLocalizer["ShowPassword"];
}
}
} }

View File

@ -16,13 +16,19 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="The new password. It must satisfy complexity rules for the site." ResourceKey="Password">Password: </Label> <Label Class="col-sm-3" For="password" HelpText="The new password. It must satisfy complexity rules for the site." ResourceKey="Password">Password: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="password" type="password" class="form-control" @bind="@_password" required /> <div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</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="confirm" HelpText="Enter the password again. It must exactly match the password entered above." ResourceKey="Confirm">Confirm: </Label> <Label Class="col-sm-3" For="confirm" HelpText="Enter the password again. It must exactly match the password entered above." ResourceKey="Confirm">Confirm: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="confirm" type="password" class="form-control" @bind="@_confirm" required /> <div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -36,12 +42,16 @@
private bool validated = false; private bool validated = false;
private string _username = string.Empty; private string _username = string.Empty;
private string _password = string.Empty; private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string _confirm = string.Empty; private string _confirm = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
_togglepassword = SharedLocalizer["ShowPassword"];
if (PageState.QueryString.ContainsKey("name") && PageState.QueryString.ContainsKey("token")) if (PageState.QueryString.ContainsKey("name") && PageState.QueryString.ContainsKey("token"))
{ {
_username = PageState.QueryString["name"]; _username = PageState.QueryString["name"];
@ -110,4 +120,18 @@
{ {
NavigationManager.NavigateTo(NavigateUrl(string.Empty)); NavigationManager.NavigateTo(NavigateUrl(string.Empty));
} }
private void TogglePassword()
{
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = SharedLocalizer["ShowPassword"];
}
}
} }

View File

@ -95,11 +95,7 @@ else
roleid = Int32.Parse(PageState.QueryString["id"]); roleid = Int32.Parse(PageState.QueryString["id"]);
Role role = await RoleService.GetRoleAsync(roleid); Role role = await RoleService.GetRoleAsync(roleid);
name = role.Name; name = role.Name;
users = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId); users = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Registered);
users = users
.Where(u => u.Role.Name == RoleNames.Registered)
.OrderBy(u => u.User.DisplayName)
.ToList();
await GetUserRoles(); await GetUserRoles();
} }
catch (Exception ex) catch (Exception ex)
@ -113,8 +109,7 @@ else
{ {
try try
{ {
userroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId); userroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, name);
userroles = userroles.Where(item => item.RoleId == roleid).ToList();
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -341,7 +341,7 @@
_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"]; _togglesmtppassword = SharedLocalizer["ShowPassword"];
_smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty); _smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty);
_retention = SettingService.GetSetting(settings, "NotificationRetention", "30"); _retention = SettingService.GetSetting(settings, "NotificationRetention", "30");
@ -407,7 +407,7 @@
{ {
try try
{ {
if (_name != string.Empty && _urls != string.Empty && _themetype != "-" && _containertype != "-") if (_name != string.Empty && _themetype != "-" && _containertype != "-")
{ {
var unique = true; var unique = true;
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
@ -656,12 +656,12 @@
if (_smtppasswordtype == "password") if (_smtppasswordtype == "password")
{ {
_smtppasswordtype = "text"; _smtppasswordtype = "text";
_togglesmtppassword = Localizer["Hide"]; _togglesmtppassword = SharedLocalizer["HidePassword"];
} }
else else
{ {
_smtppasswordtype = "password"; _smtppasswordtype = "password";
_togglesmtppassword = Localizer["Show"]; _togglesmtppassword = SharedLocalizer["ShowPassword"];
} }
} }
} }

View File

@ -156,7 +156,7 @@ else
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="hostPassword" HelpText="Enter the password of an existing host user" ResourceKey="HostPassword">Host Password:</Label> <Label Class="col-sm-3" For="hostPassword" HelpText="Enter the password of an existing host user" ResourceKey="HostPassword">Host Password:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="hostPassword" type="password" class="form-control" @bind="@_hostpassword" required /> <input id="hostPassword" type="password" class="form-control" @bind="@_hostpassword" autocomplete="new-password" required />
</div> </div>
</div> </div>
} }
@ -169,145 +169,146 @@ else
} }
@code { @code {
private List<Database> _databases; private List<Database> _databases;
private ElementReference form; private ElementReference form;
private bool validated = false; private bool validated = false;
private string _databaseName = "LocalDB"; private string _databaseName = "LocalDB";
private Type _databaseConfigType; private Type _databaseConfigType;
private object _databaseConfig; private object _databaseConfig;
private RenderFragment DatabaseConfigComponent { get; set; } private RenderFragment DatabaseConfigComponent { get; set; }
private List<Theme> _themeList; private List<Theme> _themeList;
private List<ThemeControl> _themes = new List<ThemeControl>(); private List<ThemeControl> _themes = new List<ThemeControl>();
private List<ThemeControl> _containers = new List<ThemeControl>(); private List<ThemeControl> _containers = new List<ThemeControl>();
private List<SiteTemplate> _siteTemplates; private List<SiteTemplate> _siteTemplates;
private List<Tenant> _tenants; private List<Tenant> _tenants;
private string _tenantid = "-"; private string _tenantid = "-";
private string _tenantName = string.Empty; private string _tenantName = string.Empty;
private string _hostusername = string.Empty; private string _hostusername = string.Empty;
private string _hostpassword = string.Empty; private string _hostpassword = string.Empty;
private string _name = string.Empty; private string _name = string.Empty;
private string _urls = string.Empty; private string _urls = string.Empty;
private string _themetype = "-"; private string _themetype = "-";
private string _containertype = "-"; private string _containertype = "-";
private string _admincontainertype = ""; private string _admincontainertype = "";
private string _sitetemplatetype = "-"; private string _sitetemplatetype = "-";
private string _runtime = "Server"; private string _runtime = "Server";
private string _prerender = "Prerendered"; private string _prerender = "Prerendered";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
_tenants = await TenantService.GetTenantsAsync(); _tenants = await TenantService.GetTenantsAsync();
_urls = PageState.Alias.Name; _urls = PageState.Alias.Name;
_themeList = await ThemeService.GetThemesAsync(); _themeList = await ThemeService.GetThemesAsync();
_themes = ThemeService.GetThemeControls(_themeList); _themes = ThemeService.GetThemeControls(_themeList);
_siteTemplates = await SiteTemplateService.GetSiteTemplatesAsync(); _siteTemplates = await SiteTemplateService.GetSiteTemplatesAsync();
_databases = await DatabaseService.GetDatabasesAsync(); _databases = await DatabaseService.GetDatabasesAsync();
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
{ {
AddModuleMessage(Localizer["Error.Database.LoadConfig"], MessageType.Error); AddModuleMessage(Localizer["Error.Database.LoadConfig"], MessageType.Error);
} }
} }
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();
}; };
} }
} }
private void TenantChanged(ChangeEventArgs e) private void TenantChanged(ChangeEventArgs e)
{ {
_tenantid = (string)e.Value; _tenantid = (string)e.Value;
if (string.IsNullOrEmpty(_tenantName)) if (string.IsNullOrEmpty(_tenantName))
{ {
_tenantName = _name; _tenantName = _name;
} }
StateHasChanged(); StateHasChanged();
} }
private async void ThemeChanged(ChangeEventArgs e) private async void ThemeChanged(ChangeEventArgs e)
{ {
try try
{ {
_themetype = (string)e.Value; _themetype = (string)e.Value;
if (_themetype != "-") if (_themetype != "-")
{ {
_containers = ThemeService.GetContainerControls(_themeList, _themetype); _containers = ThemeService.GetContainerControls(_themeList, _themetype);
} }
else else
{ {
_containers = new List<ThemeControl>(); _containers = new List<ThemeControl>();
} }
_containertype = "-"; _containertype = "-";
_admincontainertype = ""; _admincontainertype = "";
StateHasChanged(); StateHasChanged();
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Loading Containers For Theme {ThemeType} {Error}", _themetype, ex.Message); await logger.LogError(ex, "Error Loading Containers For Theme {ThemeType} {Error}", _themetype, ex.Message);
AddModuleMessage(Localizer["Error.Theme.LoadContainers"], MessageType.Error); AddModuleMessage(Localizer["Error.Theme.LoadContainers"], MessageType.Error);
} }
} }
private async Task SaveSite() private async Task SaveSite()
{ {
validated = true; validated = true;
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
if (await interop.FormValid(form)) if (await interop.FormValid(form))
{ {
if (_tenantid != "-" && _name != string.Empty && _urls != string.Empty && _themetype != "-" && _containertype != "-" && _sitetemplatetype != "-") if (_tenantid != "-" && _name != string.Empty && _urls != string.Empty && _themetype != "-" && _containertype != "-" && _sitetemplatetype != "-")
{ {
_urls = Regex.Replace(_urls, @"\r\n?|\n", ","); _urls = Regex.Replace(_urls, @"\r\n?|\n", ",");
var duplicates = new List<string>(); var duplicates = new List<string>();
var aliases = await AliasService.GetAliasesAsync(); var aliases = await AliasService.GetAliasesAsync();
foreach (string name in _urls.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) foreach (string name in _urls.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{ {
if (aliases.Exists(item => item.Name == name)) if (aliases.Exists(item => item.Name == name))
{ {
duplicates.Add(name); duplicates.Add(name);
} }
} }
if (duplicates.Count == 0) if (duplicates.Count == 0)
{ {
InstallConfig config = new InstallConfig(); InstallConfig config = new InstallConfig();
if (_tenantid == "+") if (_tenantid == "+")
{ {
if (!string.IsNullOrEmpty(_tenantName) && _tenants.FirstOrDefault(item => item.Name == _tenantName) == null) if (!string.IsNullOrEmpty(_tenantName) && _tenants.FirstOrDefault(item => item.Name == _tenantName) == null)
{ {
// validate host credentials // validate host credentials
var user = new User(); var user = new User();
user.SiteId = PageState.Site.SiteId; user.SiteId = PageState.Site.SiteId;
user.Username = _hostusername; user.Username = _hostusername;
user.Password = _hostpassword; user.Password = _hostpassword;
user = await UserService.LoginUserAsync(user, false, false); user.LastIPAddress = PageState.RemoteIPAddress;
user = await UserService.LoginUserAsync(user);
if (user.IsAuthenticated) if (user.IsAuthenticated)
{ {
var connectionString = String.Empty; var connectionString = String.Empty;

View File

@ -72,7 +72,7 @@
private List<Template> _templates; private List<Template> _templates;
private string _template = "-"; private string _template = "-";
private string[] _versions; private string[] _versions;
private string _reference = Constants.Version; private string _reference = "local";
private string _minversion = "2.0.0"; private string _minversion = "2.0.0";
private string _location = string.Empty; private string _location = string.Empty;

View File

@ -40,9 +40,12 @@ else
@((MarkupString)PurchaseLink(context.PackageName)) @((MarkupString)PurchaseLink(context.PackageName))
</td> </td>
<td> <td>
@if (UpgradeAvailable(context.PackageName, context.Version)) @{
var version = UpgradeAvailable(context.PackageName, context.Version);
}
@if (version != context.Version)
{ {
<button type="button" class="btn btn-success" @onclick=@(async () => await DownloadTheme(context.PackageName, context.Version))>@SharedLocalizer["Upgrade"]</button> <button type="button" class="btn btn-success" @onclick=@(async () => await DownloadTheme(context.PackageName, version))>@SharedLocalizer["Upgrade"]</button>
} }
</td> </td>
<td></td> <td></td>
@ -94,18 +97,17 @@ else
return link; return link;
} }
private bool UpgradeAvailable(string packagename, string version) private string UpgradeAvailable(string packagename, string version)
{ {
var upgradeavailable = false; if (!string.IsNullOrEmpty(packagename) && _packages != null)
if (!string.IsNullOrEmpty(packagename) && _packages != null) {
{ var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault();
var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault(); if (package != null && Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0)
if (package != null) {
{ return package.Version;
upgradeavailable = (Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0); }
}
} }
return upgradeavailable; return version;
} }
private async Task DownloadTheme(string packagename, string version) private async Task DownloadTheme(string packagename, string version)

View File

@ -31,14 +31,20 @@ else
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="If you wish to change your password you can enter it here. Please choose a sufficiently secure password." ResourceKey="Password"></Label> <Label Class="col-sm-3" For="password" HelpText="If you wish to change your password you can enter it here. Please choose a sufficiently secure password." ResourceKey="Password"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="password" type="password" class="form-control" @bind="@password" autocomplete="new-password" /> <div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</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="confirm" HelpText="If you are changing your password you must enter it again to confirm it matches" ResourceKey="Confirm"></Label> <Label Class="col-sm-3" For="confirm" HelpText="If you are changing your password you must enter it again to confirm it matches" ResourceKey="Confirm"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="confirm" type="password" class="form-control" @bind="@confirm" autocomplete="new-password" /> <div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@confirm" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</div> </div>
</div> </div>
@if (allowtwofactor) @if (allowtwofactor)
@ -154,7 +160,7 @@ else
<td><ActionDialog Header="Delete Notification" Message="Are You Sure You Wish To Delete This Notification?" Action="Delete" Security="SecurityAccessLevel.View" Class="btn btn-danger" OnClick="@(async () => await Delete(context))" EditMode="false" ResourceKey="DeleteNotification" /></td> <td><ActionDialog Header="Delete Notification" Message="Are You Sure You Wish To Delete This Notification?" Action="Delete" Security="SecurityAccessLevel.View" Class="btn btn-danger" OnClick="@(async () => await Delete(context))" EditMode="false" ResourceKey="DeleteNotification" /></td>
<td>@context.FromDisplayName</td> <td>@context.FromDisplayName</td>
<td>@context.Subject</td> <td>@context.Subject</td>
<td>@context.CreatedOn</td> <td>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", @context.CreatedOn)</td>
</Row> </Row>
<Detail> <Detail>
<td colspan="2"></td> <td colspan="2"></td>
@ -187,7 +193,7 @@ else
<td><ActionDialog Header="Delete Notification" Message="Are You Sure You Wish To Delete This Notification?" Action="Delete" Security="SecurityAccessLevel.View" Class="btn btn-danger" OnClick="@(async () => await Delete(context))" EditMode="false" ResourceKey="DeleteNotification" /></td> <td><ActionDialog Header="Delete Notification" Message="Are You Sure You Wish To Delete This Notification?" Action="Delete" Security="SecurityAccessLevel.View" Class="btn btn-danger" OnClick="@(async () => await Delete(context))" EditMode="false" ResourceKey="DeleteNotification" /></td>
<td>@context.ToDisplayName</td> <td>@context.ToDisplayName</td>
<td>@context.Subject</td> <td>@context.Subject</td>
<td>@context.CreatedOn</td> <td>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", @context.CreatedOn)</td>
</Row> </Row>
<Detail> <Detail>
<td colspan="2"></td> <td colspan="2"></td>
@ -205,6 +211,8 @@ else
</Detail> </Detail>
</Pager> </Pager>
} }
<br />
<ActionDialog Header="Clear Notifications" Message="Are You Sure You Wish To Permanently Delete All Notifications ?" Action="Delete All Notifications" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllNotifications())" ResourceKey="DeleteAllNotifications" />
<br /><hr /> <br /><hr />
<select class="form-select" @onchange="(e => FilterChanged(e))"> <select class="form-select" @onchange="(e => FilterChanged(e))">
<option value="to">@Localizer["Inbox"]</option> <option value="to">@Localizer["Inbox"]</option>
@ -216,122 +224,126 @@ else
<br /><br /> <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 _passwordtype = "password";
private bool allowtwofactor = false; private string _togglepassword = string.Empty;
private string twofactor = "False"; 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.Site.Settings.ContainsKey("LoginOptions:TwoFactor") && !string.IsNullOrEmpty(PageState.Site.Settings["LoginOptions:TwoFactor"])) _togglepassword = SharedLocalizer["ShowPassword"];
{
allowtwofactor = bool.Parse(PageState.Site.Settings["LoginOptions:TwoFactor"]);
}
if (PageState.User != null) if (PageState.Site.Settings.ContainsKey("LoginOptions:TwoFactor") && !string.IsNullOrEmpty(PageState.Site.Settings["LoginOptions:TwoFactor"]))
{ {
username = PageState.User.Username; allowtwofactor = (PageState.Site.Settings["LoginOptions:TwoFactor"] == "true");
twofactor = PageState.User.TwoFactorRequired.ToString(); }
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.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;
}
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;
}
AddModuleMessage(Localizer["Success.Profile.Update"], MessageType.Success); await UserService.UpdateUserAsync(user);
StateHasChanged(); 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);
@ -413,4 +425,47 @@ else
StateHasChanged(); StateHasChanged();
} }
private async Task DeleteAllNotifications()
{
try
{
foreach(var Notification in notifications)
{
if (!Notification.IsDeleted)
{
Notification.IsDeleted = true;
await NotificationService.UpdateNotificationAsync(Notification);
}
else
{
await NotificationService.DeleteNotificationAsync(Notification.NotificationId);
}
await logger.LogInformation("Notification Deleted {Notification}", Notification);
}
await logger.LogInformation("Notifications Permanently Deleted");
await LoadNotificationsAsync();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Notifications {Error}", ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
}
}
private void TogglePassword()
{
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = SharedLocalizer["ShowPassword"];
}
}
} }

View File

@ -20,14 +20,20 @@
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="The user's password. Please choose a password which is sufficiently secure." ResourceKey="Password"></Label> <Label Class="col-sm-3" For="password" HelpText="The user's password. Please choose a password which is sufficiently secure." ResourceKey="Password"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="password" type="password" class="form-control" @bind="@password" /> <div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</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="confirm" HelpText="Please enter the password again to confirm it matches with the value above" ResourceKey="Confirm"></Label> <Label Class="col-sm-3" For="confirm" HelpText="Please enter the password again to confirm it matches with the value above" ResourceKey="Confirm"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="confirm" type="password" class="form-control" @bind="@confirm" /> <div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@confirm" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -88,7 +94,9 @@
@code { @code {
private string username = string.Empty; private string username = string.Empty;
private string password = string.Empty; private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string confirm = string.Empty; private string confirm = string.Empty;
private string email = string.Empty; private string email = string.Empty;
private string displayname = string.Empty; private string displayname = string.Empty;
@ -102,6 +110,7 @@
{ {
try try
{ {
_togglepassword = SharedLocalizer["ShowPassword"];
profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId); profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
settings = new Dictionary<string, string>(); settings = new Dictionary<string, string>();
} }
@ -119,9 +128,9 @@
{ {
try try
{ {
if (username != string.Empty && password != string.Empty && confirm != string.Empty && email != string.Empty && ValidateProfiles()) if (username != string.Empty && _password != string.Empty && confirm != string.Empty && email != string.Empty && ValidateProfiles())
{ {
if (password == confirm) if (_password == confirm)
{ {
var user = await UserService.GetUserAsync(username, PageState.Site.SiteId); var user = await UserService.GetUserAsync(username, PageState.Site.SiteId);
if (user == null) if (user == null)
@ -129,7 +138,7 @@
user = new User(); user = new User();
user.SiteId = PageState.Site.SiteId; user.SiteId = PageState.Site.SiteId;
user.Username = username; user.Username = username;
user.Password = password; user.Password = _password;
user.Email = email; user.Email = email;
user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname; user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname;
user.PhotoFileId = null; user.PhotoFileId = null;
@ -193,4 +202,17 @@
settings = SettingService.SetSetting(settings, SettingName, value); settings = SettingService.SetSetting(settings, SettingName, value);
} }
private void TogglePassword()
{
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = SharedLocalizer["ShowPassword"];
}
}
} }

View File

@ -29,14 +29,20 @@ else
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="The user's password. Please choose a password which is sufficiently secure." ResourceKey="Password"></Label> <Label Class="col-sm-3" For="password" HelpText="The user's password. Please choose a password which is sufficiently secure." ResourceKey="Password"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="password" type="password" class="form-control" @bind="@password" /> <div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</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="confirm" HelpText="Please enter the password again to confirm it matches with the value above" ResourceKey="Confirm"></Label> <Label Class="col-sm-3" For="confirm" HelpText="Please enter the password again to confirm it matches with the value above" ResourceKey="Confirm"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="confirm" type="password" class="form-control" @bind="@confirm" /> <div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@confirm" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -66,8 +72,19 @@ else
</select> </select>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lastlogin" HelpText="The date and time when the user last signed in" ResourceKey="LastLogin"></Label>
<div class="col-sm-9">
<input id="lastlogin" class="form-control" @bind="@lastlogin" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lastipaddress" HelpText="The IP Address of the user recorded during their last login" ResourceKey="LastIPAddress"></Label>
<div class="col-sm-9">
<input id="lastipaddress" class="form-control" @bind="@lastipaddress" readonly />
</div>
</div>
</div> </div>
} }
</TabPanel> </TabPanel>
<TabPanel Name="Profile" ResourceKey="Profile"> <TabPanel Name="Profile" ResourceKey="Profile">
@ -131,61 +148,70 @@ else
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon" DeletedBy="@deletedby" DeletedOn="@deletedon"></AuditInfo> <AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon" DeletedBy="@deletedby" DeletedOn="@deletedon"></AuditInfo>
@code { @code {
private int userid; private int userid;
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 _passwordtype = "password";
private string email = string.Empty; private string _togglepassword = string.Empty;
private string displayname = string.Empty; private string confirm = string.Empty;
private FileManager filemanager; private string email = string.Empty;
private int photofileid = -1; private string displayname = string.Empty;
private File photo = null; private FileManager filemanager;
private List<Profile> profiles; private int photofileid = -1;
private Dictionary<string, string> settings; private File photo = null;
private string category = string.Empty; private string isdeleted;
private string createdby; private string lastlogin;
private DateTime createdon; private string lastipaddress;
private string modifiedby;
private DateTime modifiedon;
private string deletedby;
private DateTime? deletedon;
private string isdeleted;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; private List<Profile> profiles;
private Dictionary<string, string> settings;
private string category = string.Empty;
protected override async Task OnParametersSetAsync() private string createdby;
{ private DateTime createdon;
try private string modifiedby;
{ private DateTime modifiedon;
// OnParametersSetAsync is called when the edit modal is closed - in which case there is no id parameter private string deletedby;
if (PageState.QueryString.ContainsKey("id")) private DateTime? deletedon;
{
profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId); public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
userid = Int32.Parse(PageState.QueryString["id"]);
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId); protected override async Task OnParametersSetAsync()
if (user != null) {
{ try
username = user.Username; {
email = user.Email; if (PageState.QueryString.ContainsKey("id"))
displayname = user.DisplayName; {
if (user.PhotoFileId != null) _togglepassword = SharedLocalizer["ShowPassword"];
{ profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
photofileid = user.PhotoFileId.Value; userid = Int32.Parse(PageState.QueryString["id"]);
photo = await FileService.GetFileAsync(photofileid); var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId);
} if (user != null)
else {
{ username = user.Username;
photofileid = -1; email = user.Email;
photo = null; displayname = user.DisplayName;
} if (user.PhotoFileId != null)
settings = await SettingService.GetUserSettingsAsync(user.UserId); {
photofileid = user.PhotoFileId.Value;
photo = await FileService.GetFileAsync(photofileid);
}
else
{
photofileid = -1;
photo = null;
}
isdeleted = user.IsDeleted.ToString();
lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", user.LastLoginOn);
lastipaddress = user.LastIPAddress;
settings = await SettingService.GetUserSettingsAsync(user.UserId);
createdby = user.CreatedBy; createdby = user.CreatedBy;
createdon = user.CreatedOn; createdon = user.CreatedOn;
modifiedby = user.ModifiedBy; modifiedby = user.ModifiedBy;
modifiedon = user.ModifiedOn; modifiedon = user.ModifiedOn;
deletedby = user.DeletedBy; deletedby = user.DeletedBy;
deletedon = user.DeletedOn; deletedon = user.DeletedOn;
isdeleted = user.IsDeleted.ToString();
} }
} }
} }
@ -205,12 +231,12 @@ else
{ {
if (username != string.Empty && email != string.Empty && ValidateProfiles()) if (username != string.Empty && email != string.Empty && ValidateProfiles())
{ {
if (password == confirm) if (_password == confirm)
{ {
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId); var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId);
user.SiteId = PageState.Site.SiteId; user.SiteId = PageState.Site.SiteId;
user.Username = username; user.Username = username;
user.Password = password; user.Password = _password;
user.Email = email; user.Email = email;
user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname; user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname;
user.PhotoFileId = null; user.PhotoFileId = null;
@ -268,4 +294,17 @@ else
settings = SettingService.SetSetting(settings, SettingName, value); settings = SettingService.SetSetting(settings, SettingName, value);
} }
private void TogglePassword()
{
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = SharedLocalizer["ShowPassword"];
}
}
} }

View File

@ -6,8 +6,9 @@
@inject ISiteService SiteService @inject ISiteService SiteService
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@inject SiteState SiteState
@if (userroles == null) @if (users == null)
{ {
<p> <p>
<em>@SharedLocalizer["Loading"]</em> <em>@SharedLocalizer["Loading"]</em>
@ -30,13 +31,14 @@ else
</div> </div>
</div> </div>
</div> </div>
<Pager Items="@userroles"> <Pager Items="@users" RowClass="align-middle">
<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 style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th> <th>@SharedLocalizer["Username"]</th>
<th>@SharedLocalizer["Username"]</th> <th>@SharedLocalizer["Name"]</th>
<th>@Localizer["LastLoginOn"]</th>
</Header> </Header>
<Row> <Row>
<td> <td>
@ -48,8 +50,9 @@ else
<td> <td>
<ActionLink Action="Roles" Parameters="@($"id=" + context.UserId.ToString())" ResourceKey="Roles" /> <ActionLink Action="Roles" Parameters="@($"id=" + context.UserId.ToString())" ResourceKey="Roles" />
</td> </td>
<td>@context.User.DisplayName</td>
<td>@context.User.Username</td> <td>@context.User.Username</td>
<td>@((MarkupString)string.Format("<a href=\"mailto:{0}\">{1}</a>", @context.User.Email, @context.User.DisplayName))</td>
<td>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", context.User.LastLoginOn)</td>
</Row> </Row>
</Pager> </Pager>
</TabPanel> </TabPanel>
@ -86,250 +89,260 @@ else
</div> </div>
</div> </div>
} }
<div class="row mb-1 align-items-center"> @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
<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"> <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> <Label Class="col-sm-3" For="twofactor" HelpText="Do you want users to use two factor authentication? Note that you should use the Disabled option until you have successfully verified that the Notification Job in Scheduled Jobs is enabled and your SMTP options in Site Settings are configured or else you will lock yourself out." ResourceKey="TwoFactor">Two Factor?</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="cookietype" class="form-select" @bind="@_cookietype"> <select id="twofactor" class="form-select" @bind="@_twofactor">
<option value="domain">@Localizer["Domain"]</option> <option value="false">@Localizer["Disabled"]</option>
<option value="site">@Localizer["Site"]</option> <option value="true">@Localizer["Optional"]</option>
<option value="required">@Localizer["Required"]</option>
</select> </select>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cookiename" HelpText="You can choose to use a custom authentication cookie name for each site. However please be aware that if you want to share an authentication cookie between sites on the same domain they need to use a consistent cookie name. Also be aware that changing the authentication cookie name will logout all current users." ResourceKey="CookieName">Cookie Name:</Label>
<div class="col-sm-9">
<input id="cookiename" class="form-control" @bind="@_cookiename" />
</div>
</div>
} }
</Section> </Section>
<Section Name="Password" Heading="Password Settings" ResourceKey="PasswordSettings"> @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
<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> <Section Name="Password" Heading="Password Settings" ResourceKey="PasswordSettings">
<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"> <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> <Label Class="col-sm-3" For="minimumlength" HelpText="The Minimum Length For A Password" ResourceKey="RequiredLength">Minimum Length:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="providername" class="form-control" @bind="@_providername" /> <input id="minimumlength" class="form-control" @bind="@_minimumlength" required />
</div> </div>
</div> </div>
}
@if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{
<div class="row mb-1 align-items-center"> <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> <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"> <div class="col-sm-9">
<input id="authority" class="form-control" @bind="@_authority" /> <input id="uniquecharacters" class="form-control" @bind="@_uniquecharacters" required />
</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="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> <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"> <div class="col-sm-9">
<input id="metadataurl" class="form-control" @bind="@_metadataurl" /> <select id="requiredigit" class="form-select" @bind="@_requiredigit" required>
</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="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option> <option value="false">@SharedLocalizer["No"]</option>
</select> </select>
</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="redirecturl" HelpText="The Redirect Url (or Callback Url) which usually needs to be registered with the provider" ResourceKey="RedirectUrl">Redirect Url:</Label> <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"> <div class="col-sm-9">
<input id="redirecturl" class="form-control" @bind="@_redirecturl" readonly /> <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> </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"> <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> <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"> <div class="col-sm-9">
<input id="domainfilter" class="form-control" @bind="@_domainfilter" /> <select id="requirelower" class="form-select" @bind="@_requirelower" required>
</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="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option> <option value="false">@SharedLocalizer["No"]</option>
</select> </select>
</div> </div>
</div> </div>
} <div class="row mb-1 align-items-center">
</Section> <Label Class="col-sm-3" For="requirepunctuation" HelpText="Indicate if Passwords Must Contain A Non-alphanumeric Character (ie. Punctuation)" ResourceKey="RequirePunctuation">Require Punctuation?</Label>
<Section Name="Token" Heading="Token Settings" ResourceKey="TokenSettings"> <div class="col-sm-9">
<div class="row mb-1 align-items-center"> <select id="requirepunctuation" class="form-select" @bind="@_requirepunctuation" required>
<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> <option value="true">@SharedLocalizer["Yes"]</option>
<div class="col-sm-9"> <option value="false">@SharedLocalizer["No"]</option>
<div class="input-group"> </select>
<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>
</div> </Section>
<div class="row mb-1 align-items-center"> <Section Name="Lockout" Heading="Lockout Settings" ResourceKey="LockoutSettings">
<Label Class="col-sm-3" For="issuer" HelpText="Optionally provide the issuer of the token" ResourceKey="Issuer">Issuer:</Label> <div class="row mb-1 align-items-center">
<div class="col-sm-9"> <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>
<input id="issuer" class="form-control" @bind="@_issuer" /> <div class="col-sm-9">
</div> <input id="maximum" class="form-control" @bind="@_maximumfailures" required />
</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> </div>
</div> <div class="row mb-1 align-items-center">
</Section> <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="parameters" HelpText="Optionally specify any additional parameters as name/value pairs to send to the provider (separated by commas if there are multiple)." ResourceKey="Parameters">Parameters:</Label>
<div class="col-sm-9">
<input id="parameters" class="form-control" @bind="@_parameters" />
</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>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="identifierclaimtype" HelpText="The name of the unique user identifier claim provided by the provider" ResourceKey="IdentifierClaimType">Identifier Claim:</Label>
<div class="col-sm-9">
<input id="identifierclaimtype" class="form-control" @bind="@_identifierclaimtype" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="emailclaimtype" HelpText="The name of the email address claim provided by the provider" ResourceKey="EmailClaimType">Email Claim:</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>
@ -338,14 +351,14 @@ else
} }
@code { @code {
private List<UserRole> allroles; private List<UserRole> allusers;
private List<UserRole> userroles; private List<UserRole> users;
private string _search; private string _search = "";
private string _allowregistration; private string _allowregistration;
private string _allowsitelogin; private string _allowsitelogin;
private string _twofactor; private string _twofactor;
private string _cookietype; private string _cookiename;
private string _minimumlength; private string _minimumlength;
private string _uniquecharacters; private string _uniquecharacters;
@ -368,8 +381,10 @@ else
private string _clientsecrettype = "password"; private string _clientsecrettype = "password";
private string _toggleclientsecret = string.Empty; private string _toggleclientsecret = string.Empty;
private string _scopes; private string _scopes;
private string _parameters;
private string _pkce; private string _pkce;
private string _redirecturl; private string _redirecturl;
private string _identifierclaimtype;
private string _emailclaimtype; private string _emailclaimtype;
private string _domainfilter; private string _domainfilter;
private string _createusers; private string _createusers;
@ -386,71 +401,84 @@ else
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
allroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId); await LoadUserSettingsAsync();
await LoadSettingsAsync(); await LoadUsersAsync(true);
userroles = Search(_search);
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); 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"); _allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true");
_twofactor = SettingService.GetSetting(settings, "LoginOptions:TwoFactor", "false");
_cookietype = SettingService.GetSetting(settings, "LoginOptions:CookieType", "domain");
_minimumlength = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredLength", "6"); if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
_uniquecharacters = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", "1"); {
_requiredigit = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireDigit", "true"); _twofactor = SettingService.GetSetting(settings, "LoginOptions:TwoFactor", "false");
_requireupper = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireUppercase", "true"); _cookiename = SettingService.GetSetting(settings, "LoginOptions:CookieName", ".AspNetCore.Identity.Application");
_requirelower = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireLowercase", "true");
_requirepunctuation = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireNonAlphanumeric", "true");
_maximumfailures = SettingService.GetSetting(settings, "IdentityOptions:Lockout:MaxFailedAccessAttempts", "5"); _minimumlength = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredLength", "6");
_lockoutduration = TimeSpan.Parse(SettingService.GetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", "00:05:00")).TotalMinutes.ToString(); _uniquecharacters = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", "1");
_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");
_providertype = SettingService.GetSetting(settings, "ExternalLogin:ProviderType", ""); _maximumfailures = SettingService.GetSetting(settings, "IdentityOptions:Lockout:MaxFailedAccessAttempts", "5");
_providername = SettingService.GetSetting(settings, "ExternalLogin:ProviderName", ""); _lockoutduration = TimeSpan.Parse(SettingService.GetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", "00:05:00")).TotalMinutes.ToString();
_authority = SettingService.GetSetting(settings, "ExternalLogin:Authority", "");
_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");
_secret = SettingService.GetSetting(settings, "JwtOptions:Secret", ""); _providertype = SettingService.GetSetting(settings, "ExternalLogin:ProviderType", "");
_togglesecret = Localizer["Show"]; _providername = SettingService.GetSetting(settings, "ExternalLogin:ProviderName", "");
_issuer = SettingService.GetSetting(settings, "JwtOptions:Issuer", PageState.Uri.Scheme + "://" + PageState.Alias.Name); _authority = SettingService.GetSetting(settings, "ExternalLogin:Authority", "");
_audience = SettingService.GetSetting(settings, "JwtOptions:Audience", ""); _metadataurl = SettingService.GetSetting(settings, "ExternalLogin:MetadataUrl", "");
_lifetime = SettingService.GetSetting(settings, "JwtOptions:Lifetime", "20"); _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 = SharedLocalizer["ShowPassword"];
_scopes = SettingService.GetSetting(settings, "ExternalLogin:Scopes", "");
_parameters = SettingService.GetSetting(settings, "ExternalLogin:Parameters", "");
_pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false");
_redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype;
_identifierclaimtype = SettingService.GetSetting(settings, "ExternalLogin:IdentifierClaimType", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier");
_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");
_secret = SettingService.GetSetting(settings, "JwtOptions:Secret", "");
_togglesecret = SharedLocalizer["ShowPassword"];
_issuer = SettingService.GetSetting(settings, "JwtOptions:Issuer", PageState.Uri.Scheme + "://" + PageState.Alias.Name);
_audience = SettingService.GetSetting(settings, "JwtOptions:Audience", "");
_lifetime = SettingService.GetSetting(settings, "JwtOptions:Lifetime", "20"); }
} }
private List<UserRole> Search(string search) private async Task LoadUsersAsync(bool load)
{ {
var results = allroles.Where(item => item.Role.Name == RoleNames.Registered || (item.Role.Name == RoleNames.Host && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))); if (load)
{
allusers = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Registered);
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
var hosts = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Host);
allusers.AddRange(hosts);
allusers = allusers.OrderBy(u => u.User.DisplayName).ToList();
}
}
users = allusers;
if (!string.IsNullOrEmpty(_search)) if (!string.IsNullOrEmpty(_search))
{ {
results = results.Where(item => users = users.Where(item =>
( (
item.User.Username.Contains(search, StringComparison.OrdinalIgnoreCase) || item.User.Username.Contains(_search, StringComparison.OrdinalIgnoreCase) ||
item.User.Email.Contains(search, StringComparison.OrdinalIgnoreCase) || item.User.Email.Contains(_search, StringComparison.OrdinalIgnoreCase) ||
item.User.DisplayName.Contains(search, StringComparison.OrdinalIgnoreCase) item.User.DisplayName.Contains(_search, StringComparison.OrdinalIgnoreCase)
) )
); ).ToList();
} }
return results.ToList();
} }
private async Task OnSearch() private async Task OnSearch()
{ {
userroles = Search(_search); await UpdateUserSettingsAsync();
await UpdateSettingsAsync(); await LoadUsersAsync(false);
} }
private async Task DeleteUser(UserRole UserRole) private async Task DeleteUser(UserRole UserRole)
@ -462,8 +490,7 @@ else
{ {
await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId); await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId);
await logger.LogInformation("User Deleted {User}", UserRole.User); await logger.LogInformation("User Deleted {User}", UserRole.User);
allroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId); await LoadUsersAsync(true);
userroles = Search(_search);
StateHasChanged(); StateHasChanged();
} }
} }
@ -476,13 +503,13 @@ else
private string settingSearch = "AU-search"; private string settingSearch = "AU-search";
private async Task LoadSettingsAsync() private async Task LoadUserSettingsAsync()
{ {
Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId); Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
_search = SettingService.GetSetting(settings, settingSearch, ""); _search = SettingService.GetSetting(settings, settingSearch, "");
} }
private async Task UpdateSettingsAsync() private async Task UpdateUserSettingsAsync()
{ {
Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId); Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
SettingService.SetSetting(settings, settingSearch, _search); SettingService.SetSetting(settings, settingSearch, _search);
@ -499,43 +526,54 @@ else
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
settings = SettingService.SetSetting(settings, "LoginOptions:AllowSiteLogin", _allowsitelogin, false); 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); if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", _uniquecharacters, true); {
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireDigit", _requiredigit, true); settings = SettingService.SetSetting(settings, "LoginOptions:TwoFactor", _twofactor, false);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireUppercase", _requireupper, true); settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, 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:Password:RequiredLength", _minimumlength, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", TimeSpan.FromMinutes(Convert.ToInt64(_lockoutduration)).ToString(), 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, "ExternalLogin:ProviderType", _providertype, false); settings = SettingService.SetSetting(settings, "IdentityOptions:Lockout:MaxFailedAccessAttempts", _maximumfailures, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ProviderName", _providername, false); settings = SettingService.SetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", TimeSpan.FromMinutes(Convert.ToInt64(_lockoutduration)).ToString(), true);
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, "ExternalLogin:ProviderType", _providertype, false);
settings = SettingService.SetSetting(settings, "JwtOptions:Secret", _secret, true); settings = SettingService.SetSetting(settings, "ExternalLogin:ProviderName", _providername, false);
settings = SettingService.SetSetting(settings, "JwtOptions:Issuer", _issuer, true); settings = SettingService.SetSetting(settings, "ExternalLogin:Authority", _authority, true);
settings = SettingService.SetSetting(settings, "JwtOptions:Audience", _audience, true); settings = SettingService.SetSetting(settings, "ExternalLogin:MetadataUrl", _metadataurl, true);
settings = SettingService.SetSetting(settings, "JwtOptions:Lifetime", _lifetime, 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:Parameters", _parameters, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:PKCE", _pkce, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:IdentifierClaimType", _identifierclaimtype, 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.UpdateSiteSettingsAsync(settings, site.SiteId);
await SettingService.ClearSiteSettingsCacheAsync(); await SettingService.ClearSiteSettingsCacheAsync();
if (!string.IsNullOrEmpty(_secret))
{
SiteState.AuthorizationToken = await UserService.GetTokenAsync();
}
AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success); AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);
} }
catch (Exception ex) catch (Exception ex)
@ -548,13 +586,20 @@ else
private void ProviderTypeChanged(ChangeEventArgs e) private void ProviderTypeChanged(ChangeEventArgs e)
{ {
_providertype = (string)e.Value; _providertype = (string)e.Value;
if (_providertype == AuthenticationProviderTypes.OpenIDConnect) if (string.IsNullOrEmpty(_providername))
{ {
_scopes = "openid,profile,email"; if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
} {
else _scopes = "openid,profile,email";
{ _identifierclaimtype = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";
_scopes = ""; _emailclaimtype = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress";
}
else
{
_scopes = "";
_identifierclaimtype = "sub";
_emailclaimtype = "email";
}
} }
_redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype; _redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype;
StateHasChanged(); StateHasChanged();
@ -562,7 +607,7 @@ else
private async Task CreateToken() private async Task CreateToken()
{ {
_token = await UserService.GetTokenAsync(); _token = await UserService.GetPersonalAccessTokenAsync();
} }
private void ToggleClientSecret() private void ToggleClientSecret()
@ -570,12 +615,12 @@ else
if (_clientsecrettype == "password") if (_clientsecrettype == "password")
{ {
_clientsecrettype = "text"; _clientsecrettype = "text";
_toggleclientsecret = Localizer["Hide"]; _toggleclientsecret = SharedLocalizer["HidePassword"];
} }
else else
{ {
_clientsecrettype = "password"; _clientsecrettype = "password";
_toggleclientsecret = Localizer["Show"]; _toggleclientsecret = SharedLocalizer["ShowPassword"];
} }
} }
@ -584,12 +629,12 @@ else
if (_secrettype == "password") if (_secrettype == "password")
{ {
_secrettype = "text"; _secrettype = "text";
_togglesecret = Localizer["Hide"]; _togglesecret = SharedLocalizer["HidePassword"];
} }
else else
{ {
_secrettype = "password"; _secrettype = "password";
_togglesecret = Localizer["Show"]; _togglesecret = SharedLocalizer["ShowPassword"];
} }
} }
} }

View File

@ -110,8 +110,7 @@ else
{ {
try try
{ {
userroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId); userroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, userid);
userroles = userroles.Where(item => item.UserId == userid).ToList();
} }
catch (Exception ex) catch (Exception ex)
{ {

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.1.0</Version> <Version>3.1.2</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.1.0</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.2</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>

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
@ -123,9 +123,15 @@
<data name="Success.Account.Verified" xml:space="preserve"> <data name="Success.Account.Verified" xml:space="preserve">
<value>User Account Verified Successfully. You Can Now Login With Your Username And Password Below.</value> <value>User Account Verified Successfully. You Can Now Login With Your Username And Password Below.</value>
</data> </data>
<data name="Message.Account.NotVerfied" xml:space="preserve"> <data name="Message.Account.NotVerified" xml:space="preserve">
<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="Success.Account.Linked" xml:space="preserve">
<value>User Account Linked Successfully. You Can Now Login With Your External Login Below.</value>
</data>
<data name="Message.Account.NotLinked" xml:space="preserve">
<value>External Login Could Not Be Linked. Please Contact Your Administrator For Further Instructions.</value>
</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. 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> <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>
@ -183,12 +189,6 @@
<data name="Username.Text" xml:space="preserve"> <data name="Username.Text" xml:space="preserve">
<value>Username:</value> <value>Username:</value>
</data> </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"> <data name="Use" xml:space="preserve">
<value>Use</value> <value>Use</value>
</data> </data>
@ -201,4 +201,28 @@
<data name="Error.ResetPassword" xml:space="preserve"> <data name="Error.ResetPassword" xml:space="preserve">
<value>Error Resetting Password</value> <value>Error Resetting Password</value>
</data> </data>
<data name="ExternalLoginStatus.DuplicateEmail" xml:space="preserve">
<value>Multiple User Accounts Already Exist With The Email Address Of Your External Login. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="ExternalLoginStatus.InvalidEmail" xml:space="preserve">
<value>The External Login Provider Did Not Provide A Valid Email Address For Your Account. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="ExternalLoginStatus.ProviderKeyMismatch" xml:space="preserve">
<value>An Error Occurred Verifying Your External Login. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="ExternalLoginStatus.UserDoesNotExist" xml:space="preserve">
<value>A User Account Matching The Email Address Of Your External Login Does Not Exist. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="ExternalLoginStatus.UserNotCreated" xml:space="preserve">
<value>A User Account Could Not Be Created For Your External Login. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="ExternalLoginStatus.VerificationRequired" xml:space="preserve">
<value>In Order To Link Your External Login With Your User Account You Must Verify Your Identity. Please Check Your Email For Further Instructions.</value>
</data>
<data name="ExternalLoginStatus.AccessDenied" xml:space="preserve">
<value>Your External Login Was Denied Access. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="ExternalLoginStatus.RemoteFailure" xml:space="preserve">
<value>Your External Login Failed. Please Contact Your Administrator For Further Instructions.</value>
</data>
</root> </root>

View File

@ -324,10 +324,4 @@
<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

@ -210,4 +210,13 @@
<data name="TwoFactor.Text" xml:space="preserve"> <data name="TwoFactor.Text" xml:space="preserve">
<value>Two Factor?</value> <value>Two Factor?</value>
</data> </data>
<data name="DeleteAllNotifications.Header" xml:space="preserve">
<value>Clear Notifications</value>
</data>
<data name="DeleteAllNotifications.Message" xml:space="preserve">
<value>Are You Sure You Wish To Permanently Delete All Notifications?</value>
</data>
<data name="DeleteAllNotifications.Text" xml:space="preserve">
<value>Delete ALL Notifications</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
@ -168,4 +168,7 @@
<data name="Username.Text" xml:space="preserve"> <data name="Username.Text" xml:space="preserve">
<value>Username:</value> <value>Username:</value>
</data> </data>
<data name="Password.Placeholder" xml:space="preserve">
<value>Password</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
@ -183,4 +183,19 @@
<data name="Profile.Heading" xml:space="preserve"> <data name="Profile.Heading" xml:space="preserve">
<value>Profile</value> <value>Profile</value>
</data> </data>
<data name="Password.Placeholder" xml:space="preserve">
<value>Password</value>
</data>
<data name="LastIPAddress.HelpText" xml:space="preserve">
<value>The IP Address of the user recorded during their last login</value>
</data>
<data name="LastIPAddress.Text" xml:space="preserve">
<value>Last IP Address: </value>
</data>
<data name="LastLogin.HelpText" xml:space="preserve">
<value>The date and time when the user last signed in</value>
</data>
<data name="LastLogin.Text" xml:space="preserve">
<value>Last Login:</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
@ -247,10 +247,10 @@
<value>Domain Filter:</value> <value>Domain Filter:</value>
</data> </data>
<data name="EmailClaimType.HelpText" xml:space="preserve"> <data name="EmailClaimType.HelpText" xml:space="preserve">
<value>The type name for the email address claim provided by the provider</value> <value>The name of the email address claim provided by the provider</value>
</data> </data>
<data name="EmailClaimType.Text" xml:space="preserve"> <data name="EmailClaimType.Text" xml:space="preserve">
<value>Email Claim Type:</value> <value>Email Claim:</value>
</data> </data>
<data name="ExternalLoginSettings.Heading" xml:space="preserve"> <data name="ExternalLoginSettings.Heading" xml:space="preserve">
<value>External Login Settings</value> <value>External Login Settings</value>
@ -318,11 +318,11 @@
<data name="UserSettings.Heading" xml:space="preserve"> <data name="UserSettings.Heading" xml:space="preserve">
<value>User Settings</value> <value>User Settings</value>
</data> </data>
<data name="CookieType.HelpText" xml:space="preserve"> <data name="CookieName.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> <value>You can choose to use a custom authentication cookie name for each site. However please be aware that if you want to share an authentication cookie between sites on the same domain they need to use a consistent cookie name. Also be aware that changing the authentication cookie name will logout all current users.</value>
</data> </data>
<data name="CookieType.Text" xml:space="preserve"> <data name="CookieName.Text" xml:space="preserve">
<value>Login Cookie Type:</value> <value>Cookie Name:</value>
</data> </data>
<data name="CreateToken" xml:space="preserve"> <data name="CreateToken" xml:space="preserve">
<value>Create Token</value> <value>Create Token</value>
@ -355,15 +355,33 @@
<value>Token Settings</value> <value>Token Settings</value>
</data> </data>
<data name="TwoFactor.HelpText" xml:space="preserve"> <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> <value>Do you want users to use two factor authentication? Note that you should use the Disabled option until you have successfully verified that the Notification Job in Scheduled Jobs is enabled and your SMTP options in Site Settings are configured or else you will lock yourself out.</value>
</data> </data>
<data name="TwoFactor.Text" xml:space="preserve"> <data name="TwoFactor.Text" xml:space="preserve">
<value>Allow Two Factor?</value> <value>Two Factor?</value>
</data> </data>
<data name="Hide" xml:space="preserve"> <data name="Disabled" xml:space="preserve">
<value>Hide</value> <value>Disabled</value>
</data> </data>
<data name="Show" xml:space="preserve"> <data name="Optional" xml:space="preserve">
<value>Show</value> <value>Optional</value>
</data>
<data name="Required" xml:space="preserve">
<value>Required</value>
</data>
<data name="LastLoginOn" xml:space="preserve">
<value>Last Login</value>
</data>
<data name="IdentifierClaimType.HelpText" xml:space="preserve">
<value>The name of the unique user identifier claim provided by the provider</value>
</data>
<data name="IdentifierClaimType.Text" xml:space="preserve">
<value>Identifier Claim:</value>
</data>
<data name="Parameters.HelpText" xml:space="preserve">
<value>Optionally specify any additional parameters as name/value pairs to send to the provider (separated by commas if there are multiple).</value>
</data>
<data name="Parameters.Text" xml:space="preserve">
<value>Parameters:</value>
</data> </data>
</root> </root>

View File

@ -274,7 +274,7 @@
<value>Full Name:</value> <value>Full Name:</value>
</data> </data>
<data name="LocalVersion" xml:space="preserve"> <data name="LocalVersion" xml:space="preserve">
<value>Local Version</value> <value>Installed Version</value>
</data> </data>
<data name="Search.Source" xml:space="preserve"> <data name="Search.Source" xml:space="preserve">
<value>source</value> <value>source</value>
@ -321,4 +321,10 @@
<data name="Settings" xml:space="preserve"> <data name="Settings" xml:space="preserve">
<value>Settings</value> <value>Settings</value>
</data> </data>
<data name="HidePassword" xml:space="preserve">
<value>Hide</value>
</data>
<data name="ShowPassword" xml:space="preserve">
<value>Show</value>
</data>
</root> </root>

View File

@ -12,18 +12,12 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class AliasService : ServiceBase, IAliasService public class AliasService : ServiceBase, IAliasService
{ {
private readonly SiteState _siteState;
/// <summary> /// <summary>
/// Constructor - should only be used by Dependency Injection /// Constructor - should only be used by Dependency Injection
/// </summary> /// </summary>
public AliasService(HttpClient http, SiteState siteState) : base(http) public AliasService(HttpClient http, SiteState siteState) : base(http, siteState) { }
{
_siteState = siteState;
}
private string ApiUrl => CreateApiUrl("Alias", _siteState.Alias); private string ApiUrl => CreateApiUrl("Alias");
/// <inheritdoc /> /// <inheritdoc />
public async Task<List<Alias>> GetAliasesAsync() public async Task<List<Alias>> GetAliasesAsync()

View File

@ -11,15 +11,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class DatabaseService : ServiceBase, IDatabaseService public class DatabaseService : ServiceBase, IDatabaseService
{ {
public DatabaseService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private readonly SiteState _siteState; private string Apiurl => CreateApiUrl("Database");
public DatabaseService(HttpClient http, SiteState siteState) : base(http)
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl("Database", _siteState.Alias);
public async Task<List<Database>> GetDatabasesAsync() public async Task<List<Database>> GetDatabasesAsync()
{ {

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
@ -17,13 +18,13 @@ namespace Oqtane.Services
private readonly SiteState _siteState; private readonly SiteState _siteState;
private readonly IJSRuntime _jsRuntime; private readonly IJSRuntime _jsRuntime;
public FileService(HttpClient http, SiteState siteState, IJSRuntime jsRuntime) : base(http) public FileService(HttpClient http, SiteState siteState, IJSRuntime jsRuntime) : base(http, siteState)
{ {
_siteState = siteState; _siteState = siteState;
_jsRuntime = jsRuntime; _jsRuntime = jsRuntime;
} }
private string Apiurl => CreateApiUrl("File", _siteState.Alias); private string Apiurl => CreateApiUrl("File");
public async Task<List<File>> GetFilesAsync(int folderId) public async Task<List<File>> GetFilesAsync(int folderId)
{ {
@ -32,7 +33,8 @@ namespace Oqtane.Services
public async Task<List<File>> GetFilesAsync(string folder) public async Task<List<File>> GetFilesAsync(string folder)
{ {
return await GetJsonAsync<List<File>>($"{Apiurl}?folder={folder}"); List<File> files = await GetJsonAsync<List<File>>($"{Apiurl}?folder={folder}");
return files.OrderBy(item => item.Name).ToList();
} }
public async Task<List<File>> GetFilesAsync(int siteId, string folderPath) public async Task<List<File>> GetFilesAsync(int siteId, string folderPath)
@ -44,7 +46,8 @@ namespace Oqtane.Services
var path = WebUtility.UrlEncode(folderPath); var path = WebUtility.UrlEncode(folderPath);
return await GetJsonAsync<List<File>>($"{Apiurl}/{siteId}/{path}"); List<File> files = await GetJsonAsync<List<File>>($"{Apiurl}/{siteId}/{path}");
return files.OrderBy(item => item.Name).ToList();
} }
public async Task<File> GetFileAsync(int fileId) public async Task<File> GetFileAsync(int fileId)
@ -82,7 +85,7 @@ namespace Oqtane.Services
string result = ""; string result = "";
var interop = new Interop(_jsRuntime); var interop = new Interop(_jsRuntime);
await interop.UploadFiles($"{Apiurl}/upload", folder, id); await interop.UploadFiles($"{Apiurl}/upload", folder, id, _siteState.AntiForgeryToken);
// uploading files is asynchronous so we need to wait for the upload to complete // uploading files is asynchronous so we need to wait for the upload to complete
bool success = false; bool success = false;

View File

@ -14,14 +14,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class FolderService : ServiceBase, IFolderService public class FolderService : ServiceBase, IFolderService
{ {
private readonly SiteState _siteState; public FolderService(HttpClient http, SiteState siteState) : base(http, siteState) { }
public FolderService(HttpClient http, SiteState siteState) : base(http) private string ApiUrl => CreateApiUrl("Folder");
{
_siteState = siteState;
}
private string ApiUrl => CreateApiUrl("Folder", _siteState.Alias);
public async Task<List<Folder>> GetFoldersAsync(int siteId) public async Task<List<Folder>> GetFoldersAsync(int siteId)
{ {

View File

@ -15,7 +15,7 @@ namespace Oqtane.Services
private readonly NavigationManager _navigationManager; private readonly NavigationManager _navigationManager;
private readonly SiteState _siteState; private readonly SiteState _siteState;
public InstallationService(HttpClient http, NavigationManager navigationManager, SiteState siteState) : base(http) public InstallationService(HttpClient http, SiteState siteState, NavigationManager navigationManager) : base(http, siteState)
{ {
_navigationManager = navigationManager; _navigationManager = navigationManager;
_siteState = siteState; _siteState = siteState;

View File

@ -16,6 +16,31 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task<List<UserRole>> GetUserRolesAsync(int siteId); Task<List<UserRole>> GetUserRolesAsync(int siteId);
/// <summary>
/// Get all <see cref="UserRole"/>s on a <see cref="Site"/>
/// </summary>
/// <param name="siteId">ID-reference to a <see cref="Site"/></param>
/// <param name="userId">ID-reference to a <see cref="User"/></param>
/// <returns></returns>
Task<List<UserRole>> GetUserRolesAsync(int siteId, int userId);
/// <summary>
/// Get all <see cref="UserRole"/>s on a <see cref="Site"/>
/// </summary>
/// <param name="siteId">ID-reference to a <see cref="Site"/></param>
/// <param name="roleName">Name reference a <see cref="Role"/></param>
/// <returns></returns>
Task<List<UserRole>> GetUserRolesAsync(int siteId, string roleName);
/// <summary>
/// Get all <see cref="UserRole"/>s on a <see cref="Site"/>
/// </summary>
/// <param name="siteId">ID-reference to a <see cref="Site"/></param>
/// <param name="userId">ID-reference to a <see cref="User"/></param>
/// <param name="roleName">Name reference a <see cref="Role"/></param>
/// <returns></returns>
Task<List<UserRole>> GetUserRolesAsync(int siteId, int userId, string roleName);
/// <summary> /// <summary>
/// Get one specific <see cref="UserRole"/> /// Get one specific <see cref="UserRole"/>
/// </summary> /// </summary>

View File

@ -54,10 +54,8 @@ namespace Oqtane.Services
/// Note that this will probably not be a real User, but a user object where the `Username` and `Password` have been filled. /// Note that this will probably not be a real User, but a user object where the `Username` and `Password` have been filled.
/// </summary> /// </summary>
/// <param name="user">A <see cref="User"/> object which should have at least the <see cref="User.Username"/> and <see cref="User.Password"/> set.</param> /// <param name="user">A <see cref="User"/> object which should have at least the <see cref="User.Username"/> and <see cref="User.Password"/> set.</param>
/// <param name="setCookie">Determines if the login should be stored in the cookie.</param>
/// <param name="isPersistent">Determines if the login should be persisted in the cookie for a long time.</param>
/// <returns></returns> /// <returns></returns>
Task<User> LoginUserAsync(User user, bool setCookie, bool isPersistent); Task<User> LoginUserAsync(User user);
/// <summary> /// <summary>
/// Logout a <see cref="User"/> /// Logout a <see cref="User"/>
@ -109,5 +107,24 @@ namespace Oqtane.Services
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
Task<string> GetTokenAsync(); Task<string> GetTokenAsync();
/// <summary>
/// Get personal access token for current user (administrators only)
/// </summary>
/// <returns></returns>
Task<string> GetPersonalAccessTokenAsync();
/// <summary>
/// Link an external login with a local user account
/// </summary>
/// <param name="user">The <see cref="User"/> we're verifying</param>
/// <param name="token">A Hash value in the URL which verifies this user got the e-mail (containing this token)</param>
/// <param name="type">External Login provider type</param>
/// <param name="key">External Login provider key</param>
/// <param name="name">External Login provider display name</param>
/// <returns></returns>
Task<User> LinkUserAsync(User user, string token, string type, string key, string name);
} }
} }

View File

@ -11,14 +11,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class JobLogService : ServiceBase, IJobLogService public class JobLogService : ServiceBase, IJobLogService
{ {
private readonly SiteState _siteState; public JobLogService(HttpClient http, SiteState siteState) : base(http, siteState) { }
public JobLogService(HttpClient http, SiteState siteState) : base(http) private string Apiurl => CreateApiUrl("JobLog");
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl("JobLog", _siteState.Alias);
public async Task<List<JobLog>> GetJobLogsAsync() public async Task<List<JobLog>> GetJobLogsAsync()
{ {

View File

@ -11,14 +11,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class JobService : ServiceBase, IJobService public class JobService : ServiceBase, IJobService
{ {
private readonly SiteState _siteState; public JobService(HttpClient http, SiteState siteState) : base(http, siteState) { }
public JobService(HttpClient http, SiteState siteState) : base(http) private string Apiurl => CreateApiUrl("Job");
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl("Job", _siteState.Alias);
public async Task<List<Job>> GetJobsAsync() public async Task<List<Job>> GetJobsAsync()
{ {

View File

@ -10,16 +10,10 @@ namespace Oqtane.Services
{ {
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class LanguageService : ServiceBase, ILanguageService public class LanguageService : ServiceBase, ILanguageService
{ {
public LanguageService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private readonly SiteState _siteState;
public LanguageService(HttpClient http, SiteState siteState) : base(http) private string Apiurl => CreateApiUrl("Language");
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl("Language", _siteState.Alias);
public async Task<List<Language>> GetLanguagesAsync(int siteId) public async Task<List<Language>> GetLanguagesAsync(int siteId)
{ {

View File

@ -10,14 +10,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class LocalizationService : ServiceBase, ILocalizationService public class LocalizationService : ServiceBase, ILocalizationService
{ {
private readonly SiteState _siteState; public LocalizationService(HttpClient http, SiteState siteState) : base(http, siteState) { }
public LocalizationService(HttpClient http, SiteState siteState) : base(http) private string Apiurl => CreateApiUrl("Localization");
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl("Localization", _siteState.Alias);
public async Task<IEnumerable<Culture>> GetCulturesAsync() => await GetJsonAsync<IEnumerable<Culture>>(Apiurl); public async Task<IEnumerable<Culture>> GetCulturesAsync() => await GetJsonAsync<IEnumerable<Culture>>(Apiurl);
} }

View File

@ -14,18 +14,16 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class LogService : ServiceBase, ILogService public class LogService : ServiceBase, ILogService
{ {
private readonly SiteState _siteState; private readonly SiteState _siteState;
private readonly NavigationManager _navigationManager; private readonly NavigationManager _navigationManager;
public LogService(HttpClient http, SiteState siteState, NavigationManager navigationManager) : base(http) public LogService(HttpClient http, SiteState siteState, NavigationManager navigationManager) : base(http, siteState)
{ {
_siteState = siteState; _siteState = siteState;
_navigationManager = navigationManager; _navigationManager = navigationManager;
} }
private string Apiurl => CreateApiUrl("Log", _siteState.Alias); private string Apiurl => CreateApiUrl("Log");
public async Task<List<Log>> GetLogsAsync(int siteId, string level, string function, int rows) public async Task<List<Log>> GetLogsAsync(int siteId, string level, string function, int rows)
{ {

View File

@ -14,16 +14,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class ModuleDefinitionService : ServiceBase, IModuleDefinitionService public class ModuleDefinitionService : ServiceBase, IModuleDefinitionService
{ {
private readonly HttpClient _http; public ModuleDefinitionService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private readonly SiteState _siteState;
public ModuleDefinitionService(HttpClient http, SiteState siteState) : base(http) private string Apiurl => CreateApiUrl("ModuleDefinition");
{
_http = http;
_siteState = siteState;
}
private string Apiurl => CreateApiUrl("ModuleDefinition", _siteState.Alias);
public async Task<List<ModuleDefinition>> GetModuleDefinitionsAsync(int siteId) public async Task<List<ModuleDefinition>> GetModuleDefinitionsAsync(int siteId)
{ {

View File

@ -11,15 +11,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class ModuleService : ServiceBase, IModuleService public class ModuleService : ServiceBase, IModuleService
{ {
public ModuleService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private readonly SiteState _siteState;
public ModuleService(HttpClient http, SiteState siteState) : base(http) private string Apiurl => CreateApiUrl("Module");
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl("Module", _siteState.Alias);
public async Task<List<Module>> GetModulesAsync(int siteId) public async Task<List<Module>> GetModulesAsync(int siteId)
{ {

View File

@ -11,14 +11,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class NotificationService : ServiceBase, INotificationService public class NotificationService : ServiceBase, INotificationService
{ {
private readonly SiteState _siteState; public NotificationService(HttpClient http, SiteState siteState) : base(http, siteState) { }
public NotificationService(HttpClient http, SiteState siteState) : base(http) private string Apiurl => CreateApiUrl("Notification");
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl("Notification", _siteState.Alias);
public async Task<List<Notification>> GetNotificationsAsync(int siteId, string direction, int userId) public async Task<List<Notification>> GetNotificationsAsync(int siteId, string direction, int userId)
{ {

View File

@ -12,13 +12,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class PackageService : ServiceBase, IPackageService public class PackageService : ServiceBase, IPackageService
{ {
private readonly SiteState _siteState; public PackageService(HttpClient http, SiteState siteState) : base(http, siteState) { }
public PackageService(HttpClient http, SiteState siteState) : base(http) private string Apiurl => CreateApiUrl("Package");
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl("Package", _siteState.Alias);
public async Task<List<Package>> GetPackagesAsync(string type) public async Task<List<Package>> GetPackagesAsync(string type)
{ {

View File

@ -9,15 +9,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class PageModuleService : ServiceBase, IPageModuleService public class PageModuleService : ServiceBase, IPageModuleService
{ {
public PageModuleService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private readonly SiteState _siteState;
public PageModuleService(HttpClient http, SiteState siteState) : base(http) private string Apiurl => CreateApiUrl("PageModule");
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl("PageModule", _siteState.Alias);
public async Task<PageModule> GetPageModuleAsync(int pageModuleId) public async Task<PageModule> GetPageModuleAsync(int pageModuleId)
{ {

View File

@ -13,16 +13,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class PageService : ServiceBase, IPageService public class PageService : ServiceBase, IPageService
{ {
public PageService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private readonly SiteState _siteState;
public PageService(HttpClient http, SiteState siteState) : base(http) private string Apiurl => CreateApiUrl("Page");
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl("Page", _siteState.Alias);
public async Task<List<Page>> GetPagesAsync(int siteId) public async Task<List<Page>> GetPagesAsync(int siteId)
{ {

View File

@ -11,15 +11,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class ProfileService : ServiceBase, IProfileService public class ProfileService : ServiceBase, IProfileService
{ {
public ProfileService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private readonly SiteState _siteState;
public ProfileService(HttpClient http, SiteState siteState) : base(http) private string Apiurl => CreateApiUrl("Profile");
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl("Profile", _siteState.Alias);
public async Task<List<Profile>> GetProfilesAsync(int siteId) public async Task<List<Profile>> GetProfilesAsync(int siteId)
{ {

View File

@ -21,11 +21,16 @@ namespace Oqtane.Services
} }
private HttpClient GetHttpClient() private HttpClient GetHttpClient()
{
return GetHttpClient(_siteState?.AuthorizationToken);
}
private HttpClient GetHttpClient(string AuthorizationToken)
{ {
var httpClient = _httpClientFactory.CreateClient("Remote"); var httpClient = _httpClientFactory.CreateClient("Remote");
if (!httpClient.DefaultRequestHeaders.Contains(HeaderNames.Authorization) && _siteState != null && !string.IsNullOrEmpty(_siteState.AuthorizationToken)) if (!httpClient.DefaultRequestHeaders.Contains(HeaderNames.Authorization) && !string.IsNullOrEmpty(AuthorizationToken))
{ {
httpClient.DefaultRequestHeaders.Add(HeaderNames.Authorization, "Bearer " + _siteState.AuthorizationToken); httpClient.DefaultRequestHeaders.Add(HeaderNames.Authorization, "Bearer " + AuthorizationToken);
} }
return httpClient; return httpClient;
} }

View File

@ -11,16 +11,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class RoleService : ServiceBase, IRoleService public class RoleService : ServiceBase, IRoleService
{ {
public RoleService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private readonly SiteState _siteState;
public RoleService(HttpClient http, SiteState siteState) : base(http) private string Apiurl => CreateApiUrl("Role");
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl("Role", _siteState.Alias);
public async Task<List<Role>> GetRolesAsync(int siteId) public async Task<List<Role>> GetRolesAsync(int siteId)
{ {

View File

@ -12,15 +12,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class SettingService : ServiceBase, ISettingService public class SettingService : ServiceBase, ISettingService
{ {
public SettingService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private readonly SiteState _siteState;
public SettingService(HttpClient http, SiteState siteState) : base(http) private string Apiurl => CreateApiUrl("Setting");
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl("Setting", _siteState.Alias);
public async Task<Dictionary<string, string>> GetTenantSettingsAsync() public async Task<Dictionary<string, string>> GetTenantSettingsAsync()
{ {

View File

@ -12,15 +12,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class SiteService : ServiceBase, ISiteService public class SiteService : ServiceBase, ISiteService
{ {
public SiteService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private readonly SiteState _siteState;
public SiteService(HttpClient http, SiteState siteState) : base(http) private string Apiurl => CreateApiUrl("Site");
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl("Site", _siteState.Alias);
public async Task<List<Site>> GetSitesAsync() public async Task<List<Site>> GetSitesAsync()
{ {

View File

@ -11,13 +11,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class SiteTemplateService : ServiceBase, ISiteTemplateService public class SiteTemplateService : ServiceBase, ISiteTemplateService
{ {
private readonly SiteState _siteState; public SiteTemplateService(HttpClient http, SiteState siteState) : base(http, siteState) { }
public SiteTemplateService(HttpClient http, SiteState siteState) : base(http) private string Apiurl => CreateApiUrl("SiteTemplate");
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl("SiteTemplate", _siteState.Alias);
public async Task<List<SiteTemplate>> GetSiteTemplatesAsync() public async Task<List<SiteTemplate>> GetSiteTemplatesAsync()
{ {

View File

@ -9,14 +9,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class SqlService : ServiceBase, ISqlService public class SqlService : ServiceBase, ISqlService
{ {
private readonly SiteState _siteState; public SqlService(HttpClient http, SiteState siteState) : base(http, siteState) { }
public SqlService(HttpClient http, SiteState siteState) : base(http) private string Apiurl => CreateApiUrl("Sql");
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl("Sql", _siteState.Alias);
public async Task<SqlQuery> ExecuteQueryAsync(SqlQuery sqlquery) public async Task<SqlQuery> ExecuteQueryAsync(SqlQuery sqlquery)
{ {

View File

@ -11,18 +11,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class SyncService : ServiceBase, ISyncService public class SyncService : ServiceBase, ISyncService
{ {
public SyncService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private readonly SiteState _siteState; private string ApiUrl => CreateApiUrl("Sync");
/// <summary>
/// Constructor - should only be used by Dependency Injection
/// </summary>
public SyncService(HttpClient http, SiteState siteState) : base(http)
{
_siteState = siteState;
}
private string ApiUrl => CreateApiUrl("Sync", _siteState.Alias);
/// <inheritdoc /> /// <inheritdoc />
public async Task<Sync> GetSyncAsync(DateTime lastSyncDate) public async Task<Sync> GetSyncAsync(DateTime lastSyncDate)

View File

@ -9,14 +9,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class SystemService : ServiceBase, ISystemService public class SystemService : ServiceBase, ISystemService
{ {
private readonly SiteState _siteState; public SystemService(HttpClient http, SiteState siteState) : base(http, siteState) { }
public SystemService(HttpClient http, SiteState siteState) : base(http) private string Apiurl => CreateApiUrl("System");
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl("System", _siteState.Alias);
public async Task<Dictionary<string, object>> GetSystemInfoAsync() public async Task<Dictionary<string, object>> GetSystemInfoAsync()
{ {

View File

@ -11,14 +11,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class TenantService : ServiceBase, ITenantService public class TenantService : ServiceBase, ITenantService
{ {
private readonly SiteState _siteState; public TenantService(HttpClient http, SiteState siteState) : base(http, siteState) { }
public TenantService(HttpClient http, SiteState siteState) : base(http) private string Apiurl => CreateApiUrl("Tenant");
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl("Tenant", _siteState.Alias);
public async Task<List<Tenant>> GetTenantsAsync() public async Task<List<Tenant>> GetTenantsAsync()
{ {

View File

@ -11,14 +11,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class ThemeService : ServiceBase, IThemeService public class ThemeService : ServiceBase, IThemeService
{ {
private readonly SiteState _siteState; public ThemeService(HttpClient http, SiteState siteState) : base(http, siteState) { }
public ThemeService(HttpClient http, SiteState siteState) : base(http) private string ApiUrl => CreateApiUrl("Theme");
{
_siteState = siteState;
}
private string ApiUrl => CreateApiUrl("Theme", _siteState.Alias);
public async Task<List<Theme>> GetThemesAsync() public async Task<List<Theme>> GetThemesAsync()
{ {

View File

@ -12,16 +12,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class UrlMappingService : ServiceBase, IUrlMappingService public class UrlMappingService : ServiceBase, IUrlMappingService
{ {
public UrlMappingService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private readonly SiteState _siteState;
public UrlMappingService(HttpClient http, SiteState siteState) : base(http) private string Apiurl => CreateApiUrl("UrlMapping");
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl("UrlMapping", _siteState.Alias);
public async Task<List<UrlMapping>> GetUrlMappingsAsync(int siteId, bool isMapped) public async Task<List<UrlMapping>> GetUrlMappingsAsync(int siteId, bool isMapped)
{ {

View File

@ -10,19 +10,37 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class UserRoleService : ServiceBase, IUserRoleService public class UserRoleService : ServiceBase, IUserRoleService
{ {
public UserRoleService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private readonly SiteState _siteState;
public UserRoleService(HttpClient http, SiteState siteState) : base(http) private string Apiurl => CreateApiUrl("UserRole");
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl("UserRole", _siteState.Alias);
public async Task<List<UserRole>> GetUserRolesAsync(int siteId) public async Task<List<UserRole>> GetUserRolesAsync(int siteId)
{ {
return await GetJsonAsync<List<UserRole>>($"{Apiurl}?siteid={siteId}"); return await GetUserRolesAsync(siteId, -1, "");
}
public async Task<List<UserRole>> GetUserRolesAsync(int siteId, int userId)
{
return await GetUserRolesAsync(siteId, userId, "");
}
public async Task<List<UserRole>> GetUserRolesAsync(int siteId, string roleName)
{
return await GetUserRolesAsync(siteId, -1, roleName);
}
public async Task<List<UserRole>> GetUserRolesAsync(int siteId, int userId, string roleName)
{
var url = $"{Apiurl}?siteid={siteId}";
if (userId != -1)
{
url += $"&userid={userId}";
}
if (roleName != "")
{
url += $"&rolename={roleName}";
}
return await GetJsonAsync<List<UserRole>>(url);
} }
public async Task<UserRole> GetUserRoleAsync(int userRoleId) public async Task<UserRole> GetUserRoleAsync(int userRoleId)

View File

@ -10,14 +10,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class UserService : ServiceBase, IUserService public class UserService : ServiceBase, IUserService
{ {
private readonly SiteState _siteState; public UserService(HttpClient http, SiteState siteState) : base(http, siteState) { }
public UserService(HttpClient http, SiteState siteState) : base(http) private string Apiurl => CreateApiUrl("User");
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl("User", _siteState.Alias);
public async Task<User> GetUserAsync(int userId, int siteId) public async Task<User> GetUserAsync(int userId, int siteId)
{ {
@ -44,9 +39,9 @@ namespace Oqtane.Services
await DeleteAsync($"{Apiurl}/{userId}?siteid={siteId}"); await DeleteAsync($"{Apiurl}/{userId}?siteid={siteId}");
} }
public async Task<User> LoginUserAsync(User user, bool setCookie, bool isPersistent) public async Task<User> LoginUserAsync(User user)
{ {
return await PostJsonAsync<User>($"{Apiurl}/login?setcookie={setCookie}&persistent={isPersistent}", user); return await PostJsonAsync<User>($"{Apiurl}/login", user);
} }
public async Task LogoutUserAsync(User user) public async Task LogoutUserAsync(User user)
@ -84,5 +79,16 @@ namespace Oqtane.Services
{ {
return await GetStringAsync($"{Apiurl}/token"); return await GetStringAsync($"{Apiurl}/token");
} }
public async Task<string> GetPersonalAccessTokenAsync()
{
return await GetStringAsync($"{Apiurl}/personalaccesstoken");
}
public async Task<User> LinkUserAsync(User user, string token, string type, string key, string name)
{
return await PostJsonAsync<User>($"{Apiurl}/link?token={token}&type={type}&key={key}&name={name}", user);
}
} }
} }

View File

@ -12,16 +12,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class VisitorService : ServiceBase, IVisitorService public class VisitorService : ServiceBase, IVisitorService
{ {
public VisitorService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private readonly SiteState _siteState;
public VisitorService(HttpClient http, SiteState siteState) : base(http) private string Apiurl => CreateApiUrl("Visitor");
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl("Visitor", _siteState.Alias);
public async Task<List<Visitor>> GetVisitorsAsync(int siteId, DateTime fromDate) public async Task<List<Visitor>> GetVisitorsAsync(int siteId, DateTime fromDate)
{ {

View File

@ -1,10 +1,8 @@
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;
using Oqtane.Enums; using Oqtane.Enums;
using Oqtane.Providers;
using Oqtane.Security; using Oqtane.Security;
using Oqtane.Services; using Oqtane.Services;
using Oqtane.Shared; using Oqtane.Shared;
@ -33,28 +31,19 @@ namespace Oqtane.Themes.Controls
protected async Task LogoutUser() protected async Task LogoutUser()
{ {
await UserService.LogoutUserAsync(PageState.User);
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); 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;
// check if anonymous user can access page
var url = PageState.Alias.Path + "/" + PageState.Page.Path; var url = PageState.Alias.Path + "/" + PageState.Page.Path;
if (!UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, PageState.Page.Permissions)) if (!UserSecurity.IsAuthorized(null, PermissionNames.View, PageState.Page.Permissions))
{ {
url = PageState.Alias.Path; url = PageState.Alias.Path;
} }
if (PageState.Runtime == Shared.Runtime.Server) // post to the Logout page to complete the logout process
{ var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, returnurl = url };
// server-side Blazor needs to redirect to the Logout page var interop = new Interop(jsRuntime);
NavigationManager.NavigateTo(Utilities.TenantUrl(PageState.Alias, "/pages/logout/") + "?returnurl=" + WebUtility.UrlEncode(url), true); await interop.SubmitForm(Utilities.TenantUrl(PageState.Alias, "/pages/logout/"), fields);
}
else
{
// client-side Blazor
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider));
authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(NavigateUrl(url, true));
}
} }
} }
} }

View File

@ -189,13 +189,13 @@ namespace Oqtane.UI
} }
} }
public Task UploadFiles(string posturl, string folder, string id) public Task UploadFiles(string posturl, string folder, string id, string antiforgerytoken)
{ {
try try
{ {
_jsRuntime.InvokeVoidAsync( _jsRuntime.InvokeVoidAsync(
"Oqtane.Interop.uploadFiles", "Oqtane.Interop.uploadFiles",
posturl, folder, id); posturl, folder, id, antiforgerytoken);
return Task.CompletedTask; return Task.CompletedTask;
} }
catch catch

View File

@ -80,23 +80,33 @@
var runtime = (Shared.Runtime)Enum.Parse(typeof(Shared.Runtime), Runtime); var runtime = (Shared.Runtime)Enum.Parse(typeof(Shared.Runtime), Runtime);
Route route = new Route(_absoluteUri, SiteState.Alias.Path); Route route = new Route(_absoluteUri, SiteState.Alias.Path);
var moduleid = (int.TryParse(route.ModuleId, out int mid)) ? mid : -1; int moduleid = (int.TryParse(route.ModuleId, out moduleid)) ? moduleid : -1;
var action = (!string.IsNullOrEmpty(route.Action)) ? route.Action : Constants.DefaultAction; var action = (!string.IsNullOrEmpty(route.Action)) ? route.Action : Constants.DefaultAction;
var querystring = ParseQueryString(route.Query); var querystring = ParseQueryString(route.Query);
// reload the client application if there is a forced reload or the user navigated to a site with a different alias // reload the client application from the server if there is a forced reload or the user navigated to a site with a different alias
if (querystring.ContainsKey("reload") || (!route.AbsolutePath.Substring(1).ToLower().StartsWith(SiteState.Alias.Path.ToLower()) && !string.IsNullOrEmpty(SiteState.Alias.Path))) if (querystring.ContainsKey("reload") || (!NavigationManager.ToBaseRelativePath(_absoluteUri).ToLower().StartsWith(SiteState.Alias.Path.ToLower()) && !string.IsNullOrEmpty(SiteState.Alias.Path)))
{ {
NavigationManager.NavigateTo(_absoluteUri.Replace("?reload", ""), true); if (querystring["reload"] == "post")
return;
}
else
{
// the refresh parameter is used to refresh the PageState
if (querystring.ContainsKey("refresh"))
{ {
refresh = UI.Refresh.Site; // post back so that the cookies are set correctly - required on any change to the principal
var interop = new Interop(JSRuntime);
var fields = new { returnurl = "/" + NavigationManager.ToBaseRelativePath(_absoluteUri) };
string url = Utilities.TenantUrl(SiteState.Alias, "/pages/external/");
await interop.SubmitForm(url, fields);
return;
} }
else
{
NavigationManager.NavigateTo(_absoluteUri.Replace("?reload", ""), true);
return;
}
}
// the refresh parameter is used to refresh the client-side PageState
if (querystring.ContainsKey("refresh"))
{
refresh = UI.Refresh.Site;
} }
if (PageState != null) if (PageState != null)
@ -260,7 +270,6 @@
{ {
if (route.PagePath != "404") if (route.PagePath != "404")
{ {
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 // redirect to 404 page
NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "404", "")); NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "404", ""));
} }

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<Version>3.1.0</Version> <Version>3.1.2</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.1.0</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.2</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>

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.1.0</version> <version>3.1.2</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.1.0</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.2</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.1.0</Version> <Version>3.1.2</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.1.0</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.2</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>

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.1.0</version> <version>3.1.2</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.1.0</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.2</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.1.0</Version> <Version>3.1.2</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.1.0</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.2</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>

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.1.0</version> <version>3.1.2</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.1.0</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.2</releaseNotes>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
</metadata> </metadata>

View File

@ -32,6 +32,21 @@ namespace Oqtane.Database.SqlServer
return table.Column<int>(name: name, nullable: false).Annotation("SqlServer:Identity", "1, 1"); return table.Column<int>(name: name, nullable: false).Annotation("SqlServer:Identity", "1, 1");
} }
public override void AlterStringColumn(MigrationBuilder builder, string name, string table, int length, bool nullable, bool unicode, string index)
{
var elements = index.Split(':', StringSplitOptions.RemoveEmptyEntries);
if (elements.Length != 0)
{
builder.DropIndex(elements[0], table);
}
builder.AlterColumn<string>(name, table, maxLength: length, nullable: nullable, unicode: unicode);
if (elements.Length != 0)
{
var columns = elements[1].Split(',');
builder.CreateIndex(elements[0], table, columns, null, bool.Parse(elements[2]), null);
}
}
public override int ExecuteNonQuery(string connectionString, string query) public override int ExecuteNonQuery(string connectionString, string query)
{ {
var conn = new SqlConnection(FormatConnectionString(connectionString)); var conn = new SqlConnection(FormatConnectionString(connectionString));

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<Version>3.1.0</Version> <Version>3.1.2</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.1.0</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.2</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>

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.1.0</version> <version>3.1.2</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.1.0</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.2</releaseNotes>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
</metadata> </metadata>

View File

@ -35,7 +35,7 @@ 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) public override void AlterStringColumn(MigrationBuilder builder, string name, string table, int length, bool nullable, bool unicode, string index)
{ {
// not implemented as SQLite does not support altering columns // not implemented as SQLite does not support altering columns
} }

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.1.0</version> <version>3.1.2</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.1.0</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.2</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.1.0</version> <version>3.1.2</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.1.0/Oqtane.Framework.3.1.0.Upgrade.zip</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v3.1.2/Oqtane.Framework.3.1.2.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.0</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.2</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.1.0</version> <version>3.1.2</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.1.0</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.2</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.1.0</version> <version>3.1.2</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.1.0</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.2</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.1.0</version> <version>3.1.2</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.1.0</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.2</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.1.0.Install.zip" -Force Compress-Archive -Path "..\Oqtane.Server\bin\Release\net6.0\publish\*" -DestinationPath "Oqtane.Framework.3.1.2.Install.zip" -Force

View File

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

View File

@ -137,6 +137,7 @@ namespace Oqtane.Controllers
{ {
if (File.Name != file.Name || File.FolderId != file.FolderId) if (File.Name != file.Name || File.FolderId != file.FolderId)
{ {
file.Folder = _folders.GetFolder(file.FolderId);
string folderpath = _folders.GetFolderPath(file.Folder); string folderpath = _folders.GetFolderPath(file.Folder);
if (!Directory.Exists(folderpath)) if (!Directory.Exists(folderpath))
{ {

View File

@ -52,6 +52,7 @@ namespace Oqtane.Controllers
} }
catch (Exception ex) catch (Exception ex)
{ {
results.Add(new Dictionary<string, string>() { { "Error", ex.Message } });
_logger.Log(LogLevel.Error, this, LogFunction.Other, "Sql Query {Query} Executed on Tenant {TenantId} Resulted In An Error {Error}", sqlquery.Query, sqlquery.TenantId, ex.Message); _logger.Log(LogLevel.Error, this, LogFunction.Other, "Sql Query {Query} Executed on Tenant {TenantId} Resulted In An Error {Error}", sqlquery.Query, sqlquery.TenantId, ex.Message);
} }
sqlquery.Results = results; sqlquery.Results = results;

View File

@ -109,7 +109,6 @@ namespace Oqtane.Controllers
if (!User.IsInRole(RoleNames.Admin) && User.Identity.Name?.ToLower() != user.Username.ToLower()) if (!User.IsInRole(RoleNames.Admin) && User.Identity.Name?.ToLower() != user.Username.ToLower())
{ {
user.DisplayName = "";
user.Email = ""; user.Email = "";
user.PhotoFileId = null; user.PhotoFileId = null;
user.LastLoginOn = DateTime.MinValue; user.LastLoginOn = DateTime.MinValue;
@ -203,7 +202,7 @@ namespace Oqtane.Controllers
else else
{ {
string url = HttpContext.Request.Scheme + "://" + _tenantManager.GetAlias().Name; 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!"; string body = "Dear " + user.DisplayName + ",\n\nA User Account Has Been Successfully 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); var notification = new Notification(user.SiteId, newUser, "User Account Notification", body);
_notifications.AddNotification(notification); _notifications.AddNotification(notification);
} }
@ -316,7 +315,7 @@ namespace Oqtane.Controllers
// POST api/<controller>/login // POST api/<controller>/login
[HttpPost("login")] [HttpPost("login")]
public async Task<User> Login([FromBody] User user, bool setCookie, bool isPersistent) public async Task<User> Login([FromBody] User user)
{ {
User loginUser = new User { SiteId = user.SiteId, Username = user.Username, IsAuthenticated = false }; User loginUser = new User { SiteId = user.SiteId, Username = user.Username, IsAuthenticated = false };
@ -328,6 +327,8 @@ namespace Oqtane.Controllers
var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, true); var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, true);
if (result.Succeeded) if (result.Succeeded)
{ {
var LastIPAddress = user.LastIPAddress ?? "";
user = _users.GetUser(user.Username); user = _users.GetUser(user.Username);
if (user.TwoFactorRequired) if (user.TwoFactorRequired)
{ {
@ -354,13 +355,9 @@ namespace Oqtane.Controllers
{ {
loginUser.IsAuthenticated = true; loginUser.IsAuthenticated = true;
loginUser.LastLoginOn = DateTime.UtcNow; loginUser.LastLoginOn = DateTime.UtcNow;
loginUser.LastIPAddress = HttpContext.Connection.RemoteIpAddress.ToString(); loginUser.LastIPAddress = LastIPAddress;
_users.UpdateUser(loginUser); _users.UpdateUser(loginUser);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful {Username}", user.Username); _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful {Username}", user.Username);
if (setCookie)
{
await _identitySignInManager.SignInAsync(identityuser, isPersistent);
}
} }
else else
{ {
@ -419,7 +416,7 @@ namespace Oqtane.Controllers
} }
else else
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Email Verification Failed For {Username} - Error {Error}", user.Username, result.Errors.ToString()); _logger.Log(LogLevel.Error, this, LogFunction.Security, "Email Verification Failed For {Username} - Error {Error}", user.Username, string.Join(" ", result.Errors.ToList().Select(e => e.Description)));
user = null; user = null;
} }
} }
@ -477,7 +474,7 @@ namespace Oqtane.Controllers
} }
else else
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Password Reset Failed For {Username} - Error {Error}", user.Username, result.Errors.ToString()); _logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset Failed For {Username} - Error {Error}", user.Username, string.Join(" ", result.Errors.ToList().Select(e => e.Description)));
user = null; user = null;
} }
} }
@ -511,6 +508,38 @@ namespace Oqtane.Controllers
return loginUser; return loginUser;
} }
// POST api/<controller>/link
[HttpPost("link")]
public async Task<User> Link([FromBody] User user, string token, string type, string key, string name)
{
if (ModelState.IsValid)
{
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null && !string.IsNullOrEmpty(token))
{
var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token);
if (result.Succeeded)
{
// make LoginProvider multi-tenant aware
type += ":" + user.SiteId.ToString();
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(type, key, name));
_logger.Log(LogLevel.Information, this, LogFunction.Security, "External Login Linkage Successful For {Username} And Provider {Provider}", user.Username, type);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "External Login Linkage Failed For {Username} - Error {Error}", user.Username, string.Join(" ", result.Errors.ToList().Select(e => e.Description)));
user = null;
}
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "External Login Linkage Failed For {Username} And Token {Token}", user.Username, token);
user = null;
}
return user;
}
// GET api/<controller>/validate/x // GET api/<controller>/validate/x
[HttpGet("validate/{password}")] [HttpGet("validate/{password}")]
public async Task<bool> Validate(string password) public async Task<bool> Validate(string password)
@ -522,8 +551,23 @@ namespace Oqtane.Controllers
// GET api/<controller>/token // GET api/<controller>/token
[HttpGet("token")] [HttpGet("token")]
[Authorize(Roles = RoleNames.Admin)] [Authorize(Roles = RoleNames.Registered)]
public string Token() public string Token()
{
var token = "";
var sitesettings = HttpContext.GetSiteSettings();
var secret = sitesettings.GetValue("JwtOptions:Secret", "");
if (!string.IsNullOrEmpty(secret))
{
token = _jwtManager.GenerateToken(_tenantManager.GetAlias(), (ClaimsIdentity)User.Identity, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""), int.Parse(sitesettings.GetValue("JwtOptions:Audience", "20")));
}
return token;
}
// GET api/<controller>/personalaccesstoken
[HttpGet("personalaccesstoken")]
[Authorize(Roles = RoleNames.Admin)]
public string PersonalAccessToken()
{ {
var token = ""; var token = "";
var sitesettings = HttpContext.GetSiteSettings(); var sitesettings = HttpContext.GetSiteSettings();

View File

@ -8,6 +8,8 @@ using Oqtane.Infrastructure;
using Oqtane.Repository; using Oqtane.Repository;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using Oqtane.Security;
using System;
namespace Oqtane.Controllers namespace Oqtane.Controllers
{ {
@ -16,32 +18,57 @@ namespace Oqtane.Controllers
{ {
private readonly IUserRoleRepository _userRoles; private readonly IUserRoleRepository _userRoles;
private readonly IRoleRepository _roles; private readonly IRoleRepository _roles;
private readonly IUserPermissions _userPermissions;
private readonly ISyncManager _syncManager; private readonly ISyncManager _syncManager;
private readonly ILogManager _logger; private readonly ILogManager _logger;
private readonly Alias _alias; private readonly Alias _alias;
public UserRoleController(IUserRoleRepository userRoles, IRoleRepository roles, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger) public UserRoleController(IUserRoleRepository userRoles, IRoleRepository roles, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger)
{ {
_userRoles = userRoles; _userRoles = userRoles;
_roles = roles; _roles = roles;
_userPermissions = userPermissions;
_syncManager = syncManager; _syncManager = syncManager;
_logger = logger; _logger = logger;
_alias = tenantManager.GetAlias(); _alias = tenantManager.GetAlias();
} }
// GET: api/<controller>?siteid=x // GET: api/<controller>?siteid=x&userid=y&rolename=z
[HttpGet] [HttpGet]
[Authorize(Roles = RoleNames.Admin)] [Authorize(Roles = RoleNames.Registered)]
public IEnumerable<UserRole> Get(string siteid) public IEnumerable<UserRole> Get(string siteid, string userid = null, string rolename = null)
{ {
int SiteId; int SiteId;
if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId) if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId)
{ {
return _userRoles.GetUserRoles(SiteId); int UserId = (int.TryParse(userid, out UserId)) ? UserId : -1;
if (User.IsInRole(RoleNames.Admin) || ((userid == null || _userPermissions.GetUser().UserId == UserId) && (rolename == null || (User.IsInRole(rolename) && rolename != RoleNames.Registered))))
{
var userroles = _userRoles.GetUserRoles(SiteId).ToList();
if (userid != null)
{
userroles = userroles.Where(item => item.UserId == UserId).ToList();
}
if (rolename != null)
{
userroles = userroles.Where(item => item.Role.Name == rolename).ToList();
}
for (int i = 0; i < userroles.Count(); i++)
{
userroles[i] = Filter(userroles[i]);
}
return userroles.OrderBy(u => u.User.DisplayName);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized UserRole Get Attempt For Site {SiteId} User {UserId} Role {RoleName}", siteid, userid, rolename);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
} }
else else
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized UserRole Get Attempt {SiteId}", siteid); _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized UserRole Get Attempt For Site {SiteId} User {UserId} Role {RoleName}", siteid, userid, rolename);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null; return null;
} }
@ -49,13 +76,22 @@ namespace Oqtane.Controllers
// GET api/<controller>/5 // GET api/<controller>/5
[HttpGet("{id}")] [HttpGet("{id}")]
[Authorize(Roles = RoleNames.Admin)] [Authorize(Roles = RoleNames.Registered)]
public UserRole Get(int id) public UserRole Get(int id)
{ {
var userrole = _userRoles.GetUserRole(id); var userrole = _userRoles.GetUserRole(id);
if (userrole != null && SiteValid(userrole.Role.SiteId)) if (userrole != null && SiteValid(userrole.Role.SiteId))
{ {
return userrole; if (User.IsInRole(RoleNames.Admin) || User.Identity.Name?.ToLower() != userrole.User.Username.ToLower() || User.IsInRole(userrole.Role.Name))
{
return Filter(userrole);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Role Get Attempt {UserRoleId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
} }
else else
{ {
@ -65,6 +101,35 @@ namespace Oqtane.Controllers
} }
} }
private UserRole Filter(UserRole userrole)
{
if (userrole != null)
{
userrole.User.Password = "";
userrole.User.IsAuthenticated = false;
userrole.User.TwoFactorCode = "";
userrole.User.TwoFactorExpiry = null;
if (!User.IsInRole(RoleNames.Admin) && User.Identity.Name?.ToLower() != userrole.User.Username.ToLower())
{
userrole.User.Email = "";
userrole.User.PhotoFileId = null;
userrole.User.LastLoginOn = DateTime.MinValue;
userrole.User.LastIPAddress = "";
userrole.User.Roles = "";
userrole.User.CreatedBy = "";
userrole.User.CreatedOn = DateTime.MinValue;
userrole.User.ModifiedBy = "";
userrole.User.ModifiedOn = DateTime.MinValue;
userrole.User.DeletedBy = "";
userrole.User.DeletedOn = DateTime.MinValue;
userrole.User.IsDeleted = false;
userrole.User.TwoFactorRequired = false;
}
}
return userrole;
}
// POST api/<controller> // POST api/<controller>
[HttpPost] [HttpPost]
[Authorize(Roles = RoleNames.Admin)] [Authorize(Roles = RoleNames.Admin)]

View File

@ -81,7 +81,7 @@ 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) public virtual void AlterStringColumn(MigrationBuilder builder, string name, string table, int length, bool nullable, bool unicode, string index)
{ {
builder.AlterColumn<string>(RewriteName(name), RewriteName(table), maxLength: length, nullable: nullable, unicode: unicode); builder.AlterColumn<string>(RewriteName(name), RewriteName(table), maxLength: length, nullable: nullable, unicode: unicode);
} }

View File

@ -34,7 +34,7 @@ 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 void AlterStringColumn(MigrationBuilder builder, string name, string table, int length, bool nullable, bool unicode, string index);
public DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString); public DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString);
} }

View File

@ -72,6 +72,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddSingleton<IDatabaseManager, DatabaseManager>(); services.AddSingleton<IDatabaseManager, DatabaseManager>();
services.AddSingleton<IConfigManager, ConfigManager>(); services.AddSingleton<IConfigManager, ConfigManager>();
services.AddSingleton<ILoggerProvider, FileLoggerProvider>(); services.AddSingleton<ILoggerProvider, FileLoggerProvider>();
services.AddSingleton<AutoValidateAntiforgeryTokenFilter>();
return services; return services;
} }

View File

@ -16,8 +16,9 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authentication.OAuth; using Microsoft.AspNetCore.Authentication.OAuth;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using System.Net;
using System.Text.Json.Nodes;
namespace Oqtane.Extensions namespace Oqtane.Extensions
{ {
@ -28,15 +29,7 @@ namespace Oqtane.Extensions
// site cookie authentication options // site cookie authentication options
builder.AddSiteOptions<CookieAuthenticationOptions>((options, alias, sitesettings) => builder.AddSiteOptions<CookieAuthenticationOptions>((options, alias, sitesettings) =>
{ {
if (sitesettings.GetValue("LoginOptions:CookieType", "domain") == "domain") options.Cookie.Name = sitesettings.GetValue("LoginOptions:CookieName", ".AspNetCore.Identity.Application");
{
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 // site OpenId Connect options
@ -73,6 +66,20 @@ namespace Oqtane.Extensions
options.Events.OnTokenValidated = OnTokenValidated; options.Events.OnTokenValidated = OnTokenValidated;
options.Events.OnAccessDenied = OnAccessDenied; options.Events.OnAccessDenied = OnAccessDenied;
options.Events.OnRemoteFailure = OnRemoteFailure; options.Events.OnRemoteFailure = OnRemoteFailure;
if (sitesettings.GetValue("ExternalLogin:Parameters", "") != "")
{
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = context =>
{
foreach(var parameter in sitesettings.GetValue("ExternalLogin:Parameters", "").Split(","))
{
context.ProtocolMessage.SetParameter(parameter.Split("=")[0], parameter.Split("=")[1]);
}
return Task.FromResult(0);
}
};
}
} }
}); });
@ -104,8 +111,25 @@ namespace Oqtane.Extensions
// oauth2 events // oauth2 events
options.Events.OnCreatingTicket = OnCreatingTicket; options.Events.OnCreatingTicket = OnCreatingTicket;
options.Events.OnTicketReceived = OnTicketReceived;
options.Events.OnAccessDenied = OnAccessDenied; options.Events.OnAccessDenied = OnAccessDenied;
options.Events.OnRemoteFailure = OnRemoteFailure; options.Events.OnRemoteFailure = OnRemoteFailure;
if (sitesettings.GetValue("ExternalLogin:Parameters", "") != "")
{
options.Events = new OAuthEvents
{
OnRedirectToAuthorizationEndpoint = context =>
{
var url = context.RedirectUri;
foreach (var parameter in sitesettings.GetValue("ExternalLogin:Parameters", "").Split(","))
{
url += (!url.Contains("?")) ? "?" + parameter : "&" + parameter;
}
context.Response.Redirect(url);
return Task.FromResult(0);
}
};
}
} }
}); });
@ -116,28 +140,49 @@ namespace Oqtane.Extensions
{ {
// OAuth 2.0 // OAuth 2.0
var email = ""; var email = "";
var id = "";
var claims = "";
if (context.Options.UserInformationEndpoint != "") if (context.Options.UserInformationEndpoint != "")
{ {
try try
{ {
// call user information endpoint
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint); var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version)); request.Headers.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted); var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
var output = await response.Content.ReadAsStringAsync(); claims = await response.Content.ReadAsStringAsync();
// get email address using Regex on the raw output (could be json or html) // parse json output
var regex = new Regex(@"\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*", RegexOptions.IgnoreCase); var idClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", "");
foreach (Match match in regex.Matches(output)) var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", "");
if (!claims.StartsWith("[") && !claims.EndsWith("]"))
{ {
if (EmailValid(match.Value, context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", ""))) claims = "[" + claims + "]"; // convert to json array
}
JsonNode items = JsonNode.Parse(claims)!;
foreach (var item in items.AsArray())
{
if (item[emailClaimType] != null)
{ {
email = match.Value.ToLower(); if (EmailValid(item[emailClaimType].ToString(), context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", "")))
break; {
email = item[emailClaimType].ToString().ToLower();
if (item[idClaimType] != null)
{
id = item[idClaimType].ToString();
}
break;
}
} }
} }
if (string.IsNullOrEmpty(id))
{
id = email;
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -146,18 +191,54 @@ namespace Oqtane.Extensions
} }
} }
// login user // validate user
await LoginUser(email, context.HttpContext, context.Principal); var identity = await ValidateUser(email, id, claims, context.HttpContext);
if (identity.Label == ExternalLoginStatus.Success)
{
identity.AddClaim(new Claim("access_token", context.AccessToken));
context.Principal = new ClaimsPrincipal(identity);
}
// pass properties to OnTicketReceived
context.Properties.SetParameter("status", identity.Label);
context.Properties.SetParameter("redirecturl", context.Properties.RedirectUri);
}
private static Task OnTicketReceived(TicketReceivedContext context)
{
// OAuth 2.0
var status = context.Properties.GetParameter<string>("status");
if (status != ExternalLoginStatus.Success)
{
// redirect to login page and pass status
context.Response.Redirect(Utilities.TenantUrl(context.HttpContext.GetAlias(), $"/login?status={status}&returnurl={context.Properties.GetParameter<string>("redirecturl")}"), true);
context.HandleResponse();
}
return Task.CompletedTask;
} }
private static async Task OnTokenValidated(TokenValidatedContext context) private static async Task OnTokenValidated(TokenValidatedContext context)
{ {
// OpenID Connect // OpenID Connect
var idClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", "");
var id = context.Principal.FindFirstValue(idClaimType);
var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", ""); var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", "");
var email = context.Principal.FindFirstValue(emailClaimType); var email = context.Principal.FindFirstValue(emailClaimType);
var claims = string.Join(", ", context.Principal.Claims.Select(item => item.Type).ToArray());
// login user // validate user
await LoginUser(email, context.HttpContext, context.Principal); var identity = await ValidateUser(email, id, claims, context.HttpContext);
if (identity.Label == ExternalLoginStatus.Success)
{
identity.AddClaim(new Claim("access_token", context.SecurityToken.RawData));
context.Principal = new ClaimsPrincipal(identity);
}
else
{
// redirect to login page and pass status
context.Response.Redirect(Utilities.TenantUrl(context.HttpContext.GetAlias(), $"/login?status={identity.Label}&returnurl={context.Properties.RedirectUri}"), true);
context.HandleResponse();
}
} }
private static Task OnAccessDenied(AccessDeniedContext context) private static Task OnAccessDenied(AccessDeniedContext context)
@ -165,8 +246,7 @@ namespace Oqtane.Extensions
var _logger = context.HttpContext.RequestServices.GetRequiredService<ILogManager>(); 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"); _logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External Login Access Denied - User May Have Cancelled Their External Login Attempt");
// redirect to login page // redirect to login page
var alias = context.HttpContext.GetAlias(); context.Response.Redirect(Utilities.TenantUrl(context.HttpContext.GetAlias(), $"/login?status={ExternalLoginStatus.AccessDenied}&returnurl={context.Properties.RedirectUri}"), true);
context.Response.Redirect(alias.Path + "/login?returnurl=" + context.Properties.RedirectUri, true);
context.HandleResponse(); context.HandleResponse();
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -176,161 +256,169 @@ namespace Oqtane.Extensions
var _logger = context.HttpContext.RequestServices.GetRequiredService<ILogManager>(); var _logger = context.HttpContext.RequestServices.GetRequiredService<ILogManager>();
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "External Login Remote Failure - {Error}", context.Failure.Message); _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "External Login Remote Failure - {Error}", context.Failure.Message);
// redirect to login page // redirect to login page
var alias = context.HttpContext.GetAlias(); context.Response.Redirect(Utilities.TenantUrl(context.HttpContext.GetAlias(), $"/login?status={ExternalLoginStatus.RemoteFailure}"), true);
context.Response.Redirect(alias.Path + "/login", true);
context.HandleResponse(); context.HandleResponse();
return Task.CompletedTask; return Task.CompletedTask;
} }
private static async Task LoginUser(string email, HttpContext httpContext, ClaimsPrincipal claimsPrincipal) private static async Task<ClaimsIdentity> ValidateUser(string email, string id, string claims, HttpContext httpContext)
{ {
var _logger = httpContext.RequestServices.GetRequiredService<ILogManager>(); var _logger = httpContext.RequestServices.GetRequiredService<ILogManager>();
ClaimsIdentity identity = new ClaimsIdentity(Constants.AuthenticationScheme);
// use identity.Label as a temporary location to store validation status information
if (EmailValid(email, httpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", ""))) var providerType = httpContext.GetSiteSettings().GetValue("ExternalLogin:ProviderType", "");
var providerName = httpContext.GetSiteSettings().GetValue("ExternalLogin:ProviderName", "");
var alias = httpContext.GetAlias();
var _users = httpContext.RequestServices.GetRequiredService<IUserRepository>();
User user = null;
if (!string.IsNullOrEmpty(id))
{ {
// verify if external user is already registered for this site
var _identityUserManager = httpContext.RequestServices.GetRequiredService<UserManager<IdentityUser>>(); var _identityUserManager = httpContext.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
var _users = httpContext.RequestServices.GetRequiredService<IUserRepository>(); var identityuser = await _identityUserManager.FindByLoginAsync(providerType + ":" + alias.SiteId.ToString(), id);
var _userRoles = httpContext.RequestServices.GetRequiredService<IUserRoleRepository>(); if (identityuser != null)
var providerType = httpContext.GetSiteSettings().GetValue("ExternalLogin:ProviderType", "");
var providerKey = claimsPrincipal.FindFirstValue(ClaimTypes.NameIdentifier);
if (providerKey == null)
{ {
providerKey = email; // OAuth2 does not pass claims user = _users.GetUser(identityuser.UserName);
}
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 else
{ {
var logins = await _identityUserManager.GetLoginsAsync(identityuser); if (EmailValid(email, httpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", "")))
var login = logins.FirstOrDefault(item => item.LoginProvider == providerType);
if (login != null)
{ {
if (login.ProviderKey == providerKey) bool duplicates = false;
try
{ {
user = _users.GetUser(identityuser.UserName); 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)
{
identity.Label = ExternalLoginStatus.DuplicateEmail;
_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 = alias.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 + "://" + alias.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 + ":" + alias.SiteId.ToString(), id, providerName));
_logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "User Added {User}", user);
}
else
{
identity.Label = ExternalLoginStatus.UserNotCreated;
_logger.Log(user.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add User {Email}", email);
}
}
else
{
identity.Label = ExternalLoginStatus.UserNotCreated;
_logger.Log(user.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add Identity User {Email} {Error}", email, result.Errors.ToString());
}
}
else
{
identity.Label = ExternalLoginStatus.UserDoesNotExist;
_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 else
{ {
// provider keys do not match var logins = await _identityUserManager.GetLoginsAsync(identityuser);
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Key Does Not Match For User {Username}. Login Denied.", identityuser.UserName); var login = logins.FirstOrDefault(item => item.LoginProvider == (providerType + ":" + alias.SiteId.ToString()));
if (login == null)
{
// new external login using existing user account - verification required
var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>();
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = httpContext.Request.Scheme + "://" + alias.Name;
url += $"/login?name={identityuser.UserName}&token={WebUtility.UrlEncode(token)}&key={WebUtility.UrlEncode(id)}";
string body = $"You Recently Signed In To Our Site With {providerName} Using The Email Address {email}. ";
body += "In Order To Complete The Linkage Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
var notification = new Notification(alias.SiteId, email, email, "External Login Linkage", body);
_notifications.AddNotification(notification);
identity.Label = ExternalLoginStatus.VerificationRequired;
_logger.Log(alias.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Verification For Provider {Provider} Sent To {Email}", providerName, email);
}
else
{
// provider keys do not match
identity.Label = ExternalLoginStatus.ProviderKeyMismatch;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Key Does Not Match For User {Username}. Login Denied.", identityuser.UserName);
}
} }
} }
else else // email invalid
{ {
// add user login identity.Label = ExternalLoginStatus.InvalidEmail;
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType, providerKey, "")); if (!string.IsNullOrEmpty(email))
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); _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
{
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return An Email Address To Uniquely Identify The User. The Email Claim Specified Was {EmailCLaimType} And Actual Claim Types Are {Claims}. Login Denied.", httpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", ""), claims);
}
} }
} }
// add claims to principal // manage user
if (user != null) if (user != null)
{ {
var principal = (ClaimsIdentity)claimsPrincipal.Identity; // create claims identity
UserSecurity.ResetClaimsIdentity(principal); var _userRoles = httpContext.RequestServices.GetRequiredService<IUserRoleRepository>();
var identity = UserSecurity.CreateClaimsIdentity(httpContext.GetAlias(), user, _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList()); identity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList());
principal.AddClaims(identity.Claims); identity.Label = ExternalLoginStatus.Success;
// update user // update user
user.LastLoginOn = DateTime.UtcNow; user.LastLoginOn = DateTime.UtcNow;
user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString(); user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString();
_users.UpdateUser(user); _users.UpdateUser(user);
_logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} Using Provider {Provider}", user.Username, providerType); _logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} Using Provider {Provider}", user.Username, providerName);
}
else // user not valid
{
await httpContext.SignOutAsync();
} }
} }
else // email invalid else // id invalid
{ {
if (!string.IsNullOrEmpty(email)) _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return An Identifier To Uniquely Identify The User. The Identifier Claim Specified Was {IdentifierCLaimType} And Actual Claim Types Are {Claims}. Login Denied.", httpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", ""), claims);
{
_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();
} }
return identity;
} }
private static bool EmailValid(string email, string domainfilter) private static bool EmailValid(string email, string domainfilter)

View File

@ -14,5 +14,14 @@ namespace Oqtane.Extensions
return list.Any(f => s.StartsWith(f)); return list.Any(f => s.StartsWith(f));
} }
public static string ReplaceMultiple(this string s, string[] oldValues, string newValue)
{
foreach(string value in oldValues)
{
s = s.Replace(value, newValue);
}
return s;
}
} }
} }

View File

@ -267,13 +267,13 @@ namespace Oqtane.Infrastructure
var databaseType = install.DatabaseType; var databaseType = install.DatabaseType;
//Get database Type // get database type
var type = Type.GetType(databaseType); var type = Type.GetType(databaseType);
//Create database object from Type // create database object from type
var database = Activator.CreateInstance(type) as IDatabase; var database = Activator.CreateInstance(type) as IDatabase;
//create data directory if does not exist // create data directory if does not exist
var dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString(); var dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString();
if (!Directory.Exists(dataDirectory)) Directory.CreateDirectory(dataDirectory ?? String.Empty); if (!Directory.Exists(dataDirectory)) Directory.CreateDirectory(dataDirectory ?? String.Empty);
@ -287,7 +287,7 @@ namespace Oqtane.Infrastructure
} }
catch (Exception ex) catch (Exception ex)
{ {
result.Message = ex.Message; result.Message = "An Error Occurred Creating The Database. This Is Usually Related To Your User Not Having Sufficient Rights To Perform This Operation. Please Note That You Can Also Create The Database Manually Prior To Initiating The Install Wizard. " + ex.Message;
_filelogger.LogError(Utilities.LogMessage(this, result.Message)); _filelogger.LogError(Utilities.LogMessage(this, result.Message));
} }
} }
@ -321,14 +321,14 @@ namespace Oqtane.Infrastructure
{ {
UpgradeSqlServer(sql, install.ConnectionString, install.DatabaseType, true); UpgradeSqlServer(sql, install.ConnectionString, install.DatabaseType, true);
} }
// Push latest model into database // push latest model into database
masterDbContext.Database.Migrate(); masterDbContext.Database.Migrate();
result.Success = true; result.Success = true;
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
result.Message = ex.Message; result.Message = "An Error Occurred Provisioning The Master Database. This Is Usually Related To The Master Database Not Being In A Supported State. " + ex.Message;
_filelogger.LogError(Utilities.LogMessage(this, result.Message)); _filelogger.LogError(Utilities.LogMessage(this, result.Message));
} }
} }
@ -429,14 +429,14 @@ namespace Oqtane.Infrastructure
UpgradeSqlServer(sql, tenant.DBConnectionString, tenant.DBType, false); UpgradeSqlServer(sql, tenant.DBConnectionString, tenant.DBType, false);
} }
// Push latest model into database // push latest model into database
tenantDbContext.Database.Migrate(); tenantDbContext.Database.Migrate();
result.Success = true; result.Success = true;
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
result.Message = ex.Message; result.Message = "An Error Occurred Migrating A Tenant Database. This Is Usually Related To A Tenant Database Not Being In A Supported State. " + ex.Message;
_filelogger.LogError(Utilities.LogMessage(this, result.Message)); _filelogger.LogError(Utilities.LogMessage(this, result.Message));
} }
@ -445,13 +445,21 @@ namespace Oqtane.Infrastructure
var index = Array.FindIndex(versions, item => item == version); var index = Array.FindIndex(versions, item => item == version);
if (index != (versions.Length - 1)) if (index != (versions.Length - 1))
{ {
for (var i = (index + 1); i < versions.Length; i++) try
{ {
upgrades.Upgrade(tenant, versions[i]); for (var i = (index + 1); i < versions.Length; i++)
{
upgrades.Upgrade(tenant, versions[i]);
}
tenant.Version = versions[versions.Length - 1];
db.Entry(tenant).State = EntityState.Modified;
db.SaveChanges();
}
catch (Exception ex)
{
result.Message = "An Error Occurred Executing Upgrade Logic. " + ex.Message;
_filelogger.LogError(Utilities.LogMessage(this, result.Message));
} }
tenant.Version = versions[versions.Length - 1];
db.Entry(tenant).State = EntityState.Modified;
db.SaveChanges();
} }
} }
} }
@ -653,7 +661,7 @@ namespace Oqtane.Infrastructure
} }
catch (Exception ex) catch (Exception ex)
{ {
result.Message = "An Error Occurred Creating Site - " + ex.Message; result.Message = "An Error Occurred Creating Site. " + ex.Message;
} }
} }
@ -715,7 +723,7 @@ namespace Oqtane.Infrastructure
{ {
try try
{ {
var obj = Activator.CreateInstance(upgrade.Value) as ISiteMigration; var obj = ActivatorUtilities.CreateInstance(scope.ServiceProvider, upgrade.Value) as ISiteMigration;
if (obj != null) if (obj != null)
{ {
obj.Up(site, alias); obj.Up(site, alias);

View File

@ -45,12 +45,11 @@ namespace Oqtane.Infrastructure
}; };
// jwt already contains the roles - we are reloading to ensure most accurate permissions // jwt already contains the roles - we are reloading to ensure most accurate permissions
var _userRoles = context.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository; var _userRoles = context.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository;
identity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, alias.SiteId).ToList());
// populate principal // set claims identity
var principal = (ClaimsIdentity)context.User.Identity; var claimsidentity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, alias.SiteId).ToList());
UserSecurity.ResetClaimsIdentity(principal); context.User = new ClaimsPrincipal(claimsidentity);
principal.AddClaims(identity.Claims);
logger.Log(alias.SiteId, LogLevel.Information, "TokenValidation", Enums.LogFunction.Security, "Token Validated For User {Username}", user.Username); logger.Log(alias.SiteId, LogLevel.Information, "TokenValidation", Enums.LogFunction.Security, "Token Validated For User {Username}", user.Username);
} }
else else

View File

@ -23,7 +23,7 @@ namespace Oqtane.Migrations.EntityBuilders
protected override AspNetUserLoginsEntityBuilder BuildTable(ColumnsBuilder table) protected override AspNetUserLoginsEntityBuilder BuildTable(ColumnsBuilder table)
{ {
LoginProvider = AddStringColumn(table, "LoginProvider", 450); LoginProvider = AddStringColumn(table, "LoginProvider", 128);
ProviderKey = AddStringColumn(table, "ProviderKey", 450); ProviderKey = AddStringColumn(table, "ProviderKey", 450);
ProviderDisplayName = AddMaxStringColumn(table, "ProviderDisplayName", true); ProviderDisplayName = AddMaxStringColumn(table, "ProviderDisplayName", true);
UserId = AddStringColumn(table, "UserId", 450); UserId = AddStringColumn(table, "UserId", 450);

View File

@ -187,9 +187,20 @@ namespace Oqtane.Migrations.EntityBuilders
return table.Column<decimal>(name: RewriteName(name), nullable: nullable, precision: precision, scale: scale, defaultValue: 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) public void AlterStringColumn(string name, int length, bool nullable = false, bool unicode = true, string index = "")
{ {
ActiveDatabase.AlterStringColumn(_migrationBuilder, RewriteName(name), RewriteName(EntityTableName), length, nullable, unicode); if (index != "")
{
// indexes are in the form IndexName:Column1,Column2:Unique
var elements = index.Split(':');
index = RewriteName(elements[0]) + ":";
foreach (var column in elements[1].Split(','))
{
index += RewriteName(column) + ",";
}
index = index.Substring(0, index.Length - 1) + ":" + elements[2];
}
ActiveDatabase.AlterStringColumn(_migrationBuilder, RewriteName(name), RewriteName(EntityTableName), length, nullable, unicode, index);
} }
public void DropColumn(string name) public void DropColumn(string name)

View File

@ -17,21 +17,15 @@ namespace Oqtane.Migrations.Tenant
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase); var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase);
// Drop the index is needed because the Path is already associated with IX_Folder
folderEntityBuilder.DropIndex("IX_Folder");
folderEntityBuilder.AlterStringColumn("Name", 256); folderEntityBuilder.AlterStringColumn("Name", 256);
folderEntityBuilder.AlterStringColumn("Path", 512); folderEntityBuilder.AlterStringColumn("Path", 512, false, true, "IX_Folder:SiteId,Path:true");
folderEntityBuilder.AddIndex("IX_Folder", new[] { "SiteId", "Path" }, true);
} }
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase); var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase);
// Drop the index is needed because the Path is already associated with IX_Folder
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.AlterStringColumn("Path", 50, false, true, "IX_Folder:SiteId,Path:true");
} }
} }
} }

View File

@ -17,19 +17,13 @@ namespace Oqtane.Migrations.Tenant
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
var fileEntityBuilder = new FileEntityBuilder(migrationBuilder, ActiveDatabase); var fileEntityBuilder = new FileEntityBuilder(migrationBuilder, ActiveDatabase);
// Drop the index is needed because the Name is already associated with IX_File fileEntityBuilder.AlterStringColumn("Name", 256, false, true, "IX_File:FolderId,Name:true");
fileEntityBuilder.DropIndex("IX_File");
fileEntityBuilder.AlterStringColumn("Name", 256);
fileEntityBuilder.AddIndex("IX_File", new[] { "FolderId", "Name" }, true);
} }
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
var fileEntityBuilder = new FileEntityBuilder(migrationBuilder, ActiveDatabase); var fileEntityBuilder = new FileEntityBuilder(migrationBuilder, ActiveDatabase);
// Drop the index is needed because the Name is already associated with IX_File fileEntityBuilder.AlterStringColumn("Name", 50, false, true, "IX_File:FolderId,Name:true");
fileEntityBuilder.DropIndex("IX_File");
fileEntityBuilder.AlterStringColumn("Name", 50);
fileEntityBuilder.AddIndex("IX_File", new[] { "FolderId", "Name" }, true);
} }
} }
} }

View File

@ -20,11 +20,9 @@ namespace Oqtane.Migrations.Tenant
visitorEntityBuilder.AlterStringColumn("Url", 2048); visitorEntityBuilder.AlterStringColumn("Url", 2048);
var urlMappingEntityBuilder = new UrlMappingEntityBuilder(migrationBuilder, ActiveDatabase); 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.AlterStringColumn("MappedUrl", 2048);
urlMappingEntityBuilder.AddIndex("IX_UrlMapping", new[] { "SiteId", "Url" }, true); // Url is an index column and MySQL only supports indexes of 3072 bytes (this index will be 750X4+4=3004 bytes)
urlMappingEntityBuilder.AlterStringColumn("Url", 750, false, true, "IX_UrlMapping:SiteId,Url:true");
} }
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
@ -33,11 +31,8 @@ namespace Oqtane.Migrations.Tenant
visitorEntityBuilder.AlterStringColumn("Url", 500); visitorEntityBuilder.AlterStringColumn("Url", 500);
var urlMappingEntityBuilder = new UrlMappingEntityBuilder(migrationBuilder, ActiveDatabase); 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.AlterStringColumn("MappedUrl", 500);
urlMappingEntityBuilder.AddIndex("IX_UrlMapping", new[] { "SiteId", "Url" }, true); urlMappingEntityBuilder.AlterStringColumn("Url", 500, false, true, "IX_UrlMapping:SiteId,Url:true");
} }
} }
} }

View File

@ -84,7 +84,6 @@ namespace Oqtane.Modules.HtmlText.Controllers
} }
// POST api/<controller> // POST api/<controller>
[ValidateAntiForgeryToken]
[HttpPost] [HttpPost]
[Authorize(Policy = PolicyNames.EditModule)] [Authorize(Policy = PolicyNames.EditModule)]
public Models.HtmlText Post([FromBody] Models.HtmlText htmlText) public Models.HtmlText Post([FromBody] Models.HtmlText htmlText)
@ -104,7 +103,6 @@ namespace Oqtane.Modules.HtmlText.Controllers
} }
// DELETE api/<controller>/5 // DELETE api/<controller>/5
[ValidateAntiForgeryToken]
[HttpDelete("{id}/{moduleid}")] [HttpDelete("{id}/{moduleid}")]
[Authorize(Policy = PolicyNames.EditModule)] [Authorize(Policy = PolicyNames.EditModule)]
public void Delete(int id, int moduleId) public void Delete(int id, int moduleId)

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