Compare commits

...

89 Commits

Author SHA1 Message Date
8950e315f8 Merge pull request #2258 from oqtane/master
Merge pull request #2257 from oqtane/dev
2022-06-27 16:12:27 -04:00
a703df40c0 Merge pull request #2257 from oqtane/dev
3.1.3 release
2022-06-27 16:12:05 -04:00
225fce8810 Merge pull request #2256 from sbwalker/dev
3.1.3 data provider packages
2022-06-27 15:43:42 -04:00
bdc0f0fcdd 3.1.3 data provider packages 2022-06-27 15:43:24 -04:00
5bbb8c4858 Merge pull request #2255 from sbwalker/dev
Add schema support to BaseEntityBuilder
2022-06-27 13:55:08 -04:00
35b9551bfb Add schema support to BaseEntityBuilder 2022-06-27 13:54:52 -04:00
c8c5a05b39 Merge pull request #2254 from sbwalker/dev
Fix #2249 Fix #2250 - issues with site deletion
2022-06-27 13:46:02 -04:00
2771f0301a Fix #2249 Fix #2250 - issues with site deletion 2022-06-27 13:45:42 -04:00
c4f04edc59 Merge pull request #2253 from sbwalker/dev
Fix #2252 - unable to insert images into rich text editor
2022-06-27 12:24:16 -04:00
5530422846 Fix #2252 - unable to insert images into rich text editor 2022-06-27 12:23:55 -04:00
ce77c81fb5 Merge pull request #2248 from sbwalker/dev
prepare for 3.1.3
2022-06-21 09:25:33 -04:00
bc488d4ac2 prepare for 3.1.3 2022-06-21 09:25:14 -04:00
fe72a10346 Merge pull request #2247 from sbwalker/dev
fix #2239 - email notification encoding to support all cultures
2022-06-20 19:42:50 -04:00
0da88398b4 fix #2239 - email notification encoding to support all cultures 2022-06-20 19:42:32 -04:00
c42de3da20 Merge pull request #2246 from sbwalker/dev
fix #2245 - default database type not set correctly when adding new site for any DB other than LocalDB, added Source: info to all extension installation scenarios now that the Registry supports both Nuget and GitHub locations
2022-06-20 17:45:10 -04:00
4bf9f36baa fix #2245 - default database type not set correctly when adding new site for any DB other than LocalDB, added Source: info to all extension installation scenarios now that the Registry supports both Nuget and GitHub locations 2022-06-20 17:44:49 -04:00
380cb192c7 Merge pull request #2244 from sbwalker/dev
allow multiple aliases to be defined as default
2022-06-18 09:18:47 -04:00
8882e19ec5 allow multiple aliases to be defined as default 2022-06-18 09:18:23 -04:00
7f52059b98 Merge pull request #2243 from sbwalker/dev
added extension method for creating a LocalizerFactory using a type name, refactored Pager and LocalizableComponent to use LocalizerFactory
2022-06-15 16:19:51 -04:00
1ce3cc4d7c added extension method for creating a LocalizerFactory using a type name, refactored Pager and LocalizableComponent to use LocalizerFactory 2022-06-15 16:19:22 -04:00
657c71e94d Merge pull request #2242 from leigh-pointer/Issue#2234
Fix for Recycle bin not showing Deleted Date (Issue #2234)
2022-06-14 10:26:50 -04:00
c8cfb3c7b7 Fix for Recycle bin not showing Deleted Date (Issue #2234)
Added the Deleted data to Module from the PageModule
2022-06-14 09:22:12 +02:00
6e7e90acf4 Merge pull request #2241 from sbwalker/dev
additional changes for #2228
2022-06-13 09:10:20 -04:00
6d3a556d34 additional changes for #2228 2022-06-13 09:10:01 -04:00
7d9188b659 Merge pull request #2238 from chlupac/LogExceptions
Exception is not saved to log
2022-06-09 15:13:10 -04:00
4f0a805c79 Exception is not saved to log 2022-06-09 10:24:13 +02:00
53f3320492 Merge pull request #2228 from chlupac/Log_notification_improvement
Log notification improvement
2022-06-08 15:48:17 -04:00
f9ce51b4a5 Merge pull request #2222 from hishamco/pager
Pager should inherits from LocalizableComponent
2022-06-08 15:47:25 -04:00
f8bf432c0d Merge pull request #2237 from sbwalker/dev
Improvements for #2229 - relax userrole restrictions
2022-06-08 15:47:12 -04:00
a822482172 Improvements for #2229 - relax userrole restrictions 2022-06-08 15:46:36 -04:00
b22f8a0b02 Remove generic type suffix properly 2022-06-08 00:12:10 +03:00
227331bf24 Merge branch 'dev' into pager 2022-06-07 23:37:08 +03:00
744688cbe1 Merge pull request #2235 from sbwalker/dev
Fix #2230 - add support for an Unauthenticated User global role
2022-06-07 15:26:05 -04:00
79c8126c4a Fix #2230 - add support for an Unauthenticated User global role 2022-06-07 15:25:44 -04:00
0b7c8e4ef7 Merge pull request #2232 from ijaz-saeed/dev
missing translation keys for module names
2022-06-07 15:23:08 -04:00
35e00f61d8 Update README.md 2022-06-07 09:38:42 -04:00
aba3d58df8 missing translation keys for module names 2022-06-06 10:12:00 +05:00
45984a8166 Merge pull request #2231 from sbwalker/dev
Improvements for #2221 - validate file extensions client-side before initiating upload, validate file extension server-side before writing part to disk, optimize part cleanup logic, add error handling to JavaScript XMLHttpRequest, ensure FileInput gets initialized after upload
2022-06-04 15:41:38 -04:00
ea5655ae42 Improvements for #2221 - validate file extensions client-side before initiating upload, valid file extension server-side before writing part to disk, optimize cleanup logic, add error handling to JavaScript XMLHttpRequest, ensure FileInput gets initialized after upload 2022-06-04 15:40:26 -04:00
f06cb0dfbb Log notification improvement 2022-05-31 11:33:42 +02:00
1abae55976 Remove magic string 2022-05-31 11:59:17 +03:00
a83ed40ec4 Avoid breaking changes 2022-05-31 10:40:42 +03:00
75ecae4672 Merge pull request #2223 from hishamco/toggle-password
Avoid toggle password & confirm password as same time when one button is clicked
2022-05-28 21:59:40 -04:00
16a6f942c5 Avoid toggle password & confirm password as same time when one button is clicked 2022-05-27 16:20:35 +03:00
aa98508e57 Remove the entry from SharedResources 2022-05-27 15:37:13 +03:00
583383aee1 Pager should inherits from LocalizableComponent 2022-05-27 15:33:48 +03:00
13f69f81d7 Merge pull request #2219 from sbwalker/dev
fix #2213 - disabling show on all pages
2022-05-26 01:19:35 -04:00
43c34fcd64 fix #2213 - disabling show on all pages 2022-05-26 01:19:14 -04:00
c272238539 Merge pull request #2217 from leigh-pointer/MissingRes2215
Fix for missing Resx entries #2215
2022-05-25 07:23:01 -04:00
6e0d2706a8 Fix for missing Resx entries #2215
Updated the Resx File with the missing entries.  

However the UserProfile\Add cant be Localized at present as the IStringLocalizer is not loaded when that property assignment is executed.
2022-05-25 11:13:38 +02:00
25a7289ce8 Merge pull request #2210 from leigh-pointer/PageOf
Fix for #2209 Localization Pager component (Page @_page of @_pages)
2022-05-24 21:47:12 -04:00
1ba7d045e4 Merge pull request #2211 from elgransan/dev
* Collapse menu after click on a page (mobile version)
2022-05-24 21:46:11 -04:00
a4d75befe7 * Collapse menu after click on a page (mobile version) 2022-05-22 20:05:01 -03:00
88377529bc Fix for #2209 Localization Pager component (Page @_page of @_pages)
Added fixe for issue.  Added the resource "PageOfPages" to SharedResources as trying to inject IStringLocalizer<Pager<TableItem>> and adding Pager.resx the resource failed to load.
2022-05-22 20:23:17 +02:00
91b9a0280f Merge pull request #2207 from sbwalker/dev
Add filtering by Category to Module Management - default to Common
2022-05-21 10:11:24 -04:00
25173ae85c Ddd filtering by Category to Module Management - default to Common 2022-05-21 10:10:57 -04:00
ad3350705e Update README.md 2022-05-19 11:44:38 -04:00
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
87 changed files with 1806 additions and 1350 deletions

View File

@ -1,3 +1,5 @@
using System;
namespace Microsoft.Extensions.Localization namespace Microsoft.Extensions.Localization
{ {
public static class OqtaneLocalizationExtensions public static class OqtaneLocalizationExtensions
@ -18,5 +20,42 @@ namespace Microsoft.Extensions.Localization
} }
return localizedValue; return localizedValue;
} }
/// <summary>
/// Creates an IStringLocalizer based on a type name. This extension method is useful in scenarios where the default IStringLocalizer is unable to locate the resources.
/// </summary>
/// <param name="localizerFactory"></param>
/// <param name="fullTypeName">the full type name ie. GetType().FullName</param>
/// <returns></returns>
public static IStringLocalizer Create(this IStringLocalizerFactory localizerFactory, string fullTypeName)
{
var typename = fullTypeName;
// handle generic types
var type = Type.GetType(fullTypeName);
if (type.IsGenericType)
{
typename = type.GetGenericTypeDefinition().FullName;
typename = typename.Substring(0, typename.IndexOf("`")); // remove generic type info
}
// format typename
if (typename.Contains(","))
{
typename = typename.Substring(0, typename.IndexOf(",")); // remove assembly info
}
// remove rootnamespace
var rootnamespace = "";
var attributes = type.Assembly.GetCustomAttributes(typeof(RootNamespaceAttribute), false);
if (attributes.Length > 0)
{
rootnamespace = ((RootNamespaceAttribute)attributes[0]).RootNamespace;
}
typename = typename.Replace(rootnamespace + ".", "");
// create IStringLocalizer using factory
return localizerFactory.Create(typename, type.Assembly.GetName().Name);
}
} }
} }

View File

@ -29,14 +29,7 @@
<select id="databasetype" class="form-select custom-select" value="@_databaseName" @onchange="(e => DatabaseChanged(e))"> <select id="databasetype" class="form-select custom-select" value="@_databaseName" @onchange="(e => DatabaseChanged(e))">
@foreach (var database in _databases) @foreach (var database in _databases)
{ {
if (database.IsDefault) <option value="@database.Name">@Localizer[@database.Name]</option>
{
<option value="@database.Name" selected>@Localizer[@database.Name]</option>
}
else
{
<option value="@database.Name">@Localizer[@database.Name]</option>
}
} }
</select> </select>
} }
@ -63,8 +56,8 @@
<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">
<div class="input-group"> <div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_hostPassword" autocomplete="new-password" /> <input id="password" type="@_passwordType" class="form-control" @bind="@_hostPassword" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglePassword</button>
</div> </div>
</div> </div>
</div> </div>
@ -72,8 +65,8 @@
<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">
<div class="input-group"> <div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirmPassword" autocomplete="new-password" /> <input id="confirm" type="@_confirmPasswordType" class="form-control" @bind="@_confirmPassword" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button> <button type="button" class="btn btn-secondary" @onclick="@ToggleConfirmPassword">@_toggleConfirmPassword</button>
</div> </div>
</div> </div>
</div> </div>
@ -110,8 +103,10 @@
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 _passwordType = "password";
private string _togglepassword = string.Empty; private string _confirmPasswordType = "password";
private string _togglePassword = string.Empty;
private string _toggleConfirmPassword = 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;
@ -120,7 +115,9 @@
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
_togglepassword = SharedLocalizer["ShowPassword"]; _togglePassword = SharedLocalizer["ShowPassword"];
_toggleConfirmPassword = SharedLocalizer["ShowPassword"];
_databases = await DatabaseService.GetDatabasesAsync(); _databases = await DatabaseService.GetDatabasesAsync();
if (_databases.Exists(item => item.IsDefault)) if (_databases.Exists(item => item.IsDefault))
{ {
@ -230,15 +227,29 @@
private void TogglePassword() private void TogglePassword()
{ {
if (_passwordtype == "password") if (_passwordType == "password")
{ {
_passwordtype = "text"; _passwordType = "text";
_togglepassword = SharedLocalizer["HidePassword"]; _togglePassword = SharedLocalizer["HidePassword"];
} }
else else
{ {
_passwordtype = "password"; _passwordType = "password";
_togglepassword = SharedLocalizer["ShowPassword"]; _togglePassword = SharedLocalizer["ShowPassword"];
}
}
private void ToggleConfirmPassword()
{
if (_confirmPasswordType == "password")
{
_confirmPasswordType = "text";
_toggleConfirmPassword = SharedLocalizer["HidePassword"];
}
else
{
_confirmPasswordType = "password";
_toggleConfirmPassword = SharedLocalizer["ShowPassword"];
} }
} }
} }

View File

@ -20,6 +20,7 @@ else
@if (_availableCultures.Count() == 0) @if (_availableCultures.Count() == 0)
{ {
<ModuleMessage Type="MessageType.Info" Message="@_message"></ModuleMessage> <ModuleMessage Type="MessageType.Info" Message="@_message"></ModuleMessage>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
} }
else else
{ {
@ -47,9 +48,9 @@ else
</div> </div>
</div> </div>
<button type="button" class="btn btn-success" @onclick="SaveLanguage">@SharedLocalizer["Save"]</button> <button type="button" class="btn btn-success" @onclick="SaveLanguage">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
</form> </form>
} }
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
</TabPanel> </TabPanel>
<TabPanel Name="Download" ResourceKey="Download" Security="SecurityAccessLevel.Host"> <TabPanel Name="Download" ResourceKey="Download" Security="SecurityAccessLevel.Host">
<div class="row justify-content-center mb-3"> <div class="row justify-content-center mb-3">
@ -78,6 +79,7 @@ else
<strong>@(String.Format("{0:n0}", context.Downloads))</strong> @SharedLocalizer["Search.Downloads"]&nbsp;&nbsp;|&nbsp;&nbsp; <strong>@(String.Format("{0:n0}", context.Downloads))</strong> @SharedLocalizer["Search.Downloads"]&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Released"]: <strong>@context.ReleaseDate.ToString("MMM dd, yyyy")</strong>&nbsp;&nbsp;|&nbsp;&nbsp; @SharedLocalizer["Search.Released"]: <strong>@context.ReleaseDate.ToString("MMM dd, yyyy")</strong>&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Version"]: <strong>@context.Version</strong> @SharedLocalizer["Search.Version"]: <strong>@context.Version</strong>
@((MarkupString)(!string.IsNullOrEmpty(context.PackageUrl) ? "&nbsp;&nbsp;|&nbsp;&nbsp;" + SharedLocalizer["Search.Source"] + ": <strong>" + new Uri(context.PackageUrl).Host + "</strong>" : ""))
@((MarkupString)(context.TrialPeriod > 0 ? "&nbsp;&nbsp;|&nbsp;&nbsp;<strong>" + context.TrialPeriod + " " + @SharedLocalizer["Trial"] + "</strong>" : "")) @((MarkupString)(context.TrialPeriod > 0 ? "&nbsp;&nbsp;|&nbsp;&nbsp;<strong>" + context.TrialPeriod + " " + @SharedLocalizer["Trial"] + "</strong>" : ""))
</td> </td>
<td style="width: 1px; vertical-align: middle;"> <td style="width: 1px; vertical-align: middle;">

View File

@ -184,7 +184,7 @@
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)
{ {
@ -206,7 +206,7 @@
} }
else else
{ {
if (PageState.Site.Settings["LoginOptions:TwoFactor"] == "required" || 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;

View File

@ -1,5 +1,6 @@
@namespace Oqtane.Modules.Admin.Logs @namespace Oqtane.Modules.Admin.Logs
@inherits ModuleBase @inherits ModuleBase
@inject NavigationManager NavigationManager
@inject ILogService LogService @inject ILogService LogService
@inject ISettingService SettingService @inject ISettingService SettingService
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@ -91,19 +92,23 @@ else
} }
@code { @code {
private string _level = "-"; private string _level = "-";
private string _function = "-"; private string _function = "-";
private string _rows = "10"; private string _rows = "10";
private int _page = 1; private int _page = 1;
private List<Log> _logs; private List<Log> _logs;
private string _retention = ""; private string _retention = "";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
try try
{ {
if (PageState.QueryString.ContainsKey("id") && int.TryParse(PageState.QueryString["id"], out int id))
{
NavigationManager.NavigateTo(EditUrl(PageState.Page.Path, ModuleState.ModuleId, "Detail", $"id={id}"));
}
if (PageState.QueryString.ContainsKey("level")) if (PageState.QueryString.ContainsKey("level"))
{ {
_level = PageState.QueryString["level"]; _level = PageState.QueryString["level"];

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

@ -35,6 +35,7 @@
<strong>@(String.Format("{0:n0}", context.Downloads))</strong> @SharedLocalizer["Search.Downloads"]&nbsp;&nbsp;|&nbsp;&nbsp; <strong>@(String.Format("{0:n0}", context.Downloads))</strong> @SharedLocalizer["Search.Downloads"]&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Released"]: <strong>@context.ReleaseDate.ToString("MMM dd, yyyy")</strong>&nbsp;&nbsp;|&nbsp;&nbsp; @SharedLocalizer["Search.Released"]: <strong>@context.ReleaseDate.ToString("MMM dd, yyyy")</strong>&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Version"]: <strong>@context.Version</strong> @SharedLocalizer["Search.Version"]: <strong>@context.Version</strong>
@((MarkupString)(!string.IsNullOrEmpty(context.PackageUrl) ? "&nbsp;&nbsp;|&nbsp;&nbsp;" + SharedLocalizer["Search.Source"] + ": <strong>" + new Uri(context.PackageUrl).Host + "</strong>" : ""))
@((MarkupString)(context.TrialPeriod > 0 ? "&nbsp;&nbsp;|&nbsp;&nbsp;<strong>" + context.TrialPeriod + " " + @SharedLocalizer["Trial"] + "</strong>" : "")) @((MarkupString)(context.TrialPeriod > 0 ? "&nbsp;&nbsp;|&nbsp;&nbsp;<strong>" + context.TrialPeriod + " " + @SharedLocalizer["Trial"] + "</strong>" : ""))
</td> </td>
<td style="width: 1px; vertical-align: middle;"> <td style="width: 1px; vertical-align: middle;">
@ -114,145 +115,145 @@
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> <NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@code { @code {
private List<Package> _packages; private List<Package> _packages;
private string _price = "free"; private string _price = "free";
private string _search = ""; private string _search = "";
private string _productname = ""; private string _productname = "";
private string _license = ""; private string _license = "";
private string _packageid = ""; private string _packageid = "";
private string _version = ""; private string _version = "";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
try try
{ {
await LoadModuleDefinitions(); await LoadModuleDefinitions();
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Loading Packages {Error}", ex.Message); await logger.LogError(ex, "Error Loading Packages {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Package.Load"], MessageType.Error); AddModuleMessage(Localizer["Error.Package.Load"], MessageType.Error);
} }
} }
private async Task LoadModuleDefinitions() private async Task LoadModuleDefinitions()
{ {
var moduledefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId); var moduledefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId);
_packages = await PackageService.GetPackagesAsync("module", _search, _price, ""); _packages = await PackageService.GetPackagesAsync("module", _search, _price, "");
if (_packages != null) if (_packages != null)
{ {
foreach (Package package in _packages.ToArray()) foreach (Package package in _packages.ToArray())
{ {
if (moduledefinitions.Exists(item => item.PackageName == package.PackageId)) if (moduledefinitions.Exists(item => item.PackageName == package.PackageId))
{ {
_packages.Remove(package); _packages.Remove(package);
} }
} }
} }
} }
private async void PriceChanged(ChangeEventArgs e) private async void PriceChanged(ChangeEventArgs e)
{ {
try try
{ {
_price = (string)e.Value; _price = (string)e.Value;
_search = ""; _search = "";
await LoadModuleDefinitions(); await LoadModuleDefinitions();
StateHasChanged(); StateHasChanged();
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error On PriceChanged"); await logger.LogError(ex, "Error On PriceChanged");
} }
} }
private async Task Search() private async Task Search()
{ {
try try
{ {
await LoadModuleDefinitions(); await LoadModuleDefinitions();
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error On Search"); await logger.LogError(ex, "Error On Search");
} }
} }
private async Task Reset() private async Task Reset()
{ {
try try
{ {
_search = ""; _search = "";
await LoadModuleDefinitions(); await LoadModuleDefinitions();
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error On Reset"); await logger.LogError(ex, "Error On Reset");
} }
} }
private void HideModal() private void HideModal()
{ {
_productname = ""; _productname = "";
_license = ""; _license = "";
StateHasChanged(); StateHasChanged();
} }
private async Task GetPackage(string packageid, string version) private async Task GetPackage(string packageid, string version)
{ {
try try
{ {
var package = await PackageService.GetPackageAsync(packageid, version); var package = await PackageService.GetPackageAsync(packageid, version);
if (package != null) if (package != null)
{ {
_productname = package.Name; _productname = package.Name;
if (!string.IsNullOrEmpty(package.License)) if (!string.IsNullOrEmpty(package.License))
{ {
_license = package.License.Replace("\n", "<br />"); _license = package.License.Replace("\n", "<br />");
} }
_packageid = package.PackageId; _packageid = package.PackageId;
_version = package.Version; _version = package.Version;
} }
StateHasChanged(); StateHasChanged();
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Getting Package {PackageId} {Version}", packageid, version); await logger.LogError(ex, "Error Getting Package {PackageId} {Version}", packageid, version);
AddModuleMessage(Localizer["Error.Module.Download"], MessageType.Error); AddModuleMessage(Localizer["Error.Module.Download"], MessageType.Error);
} }
} }
private async Task DownloadPackage() private async Task DownloadPackage()
{ {
try try
{ {
await PackageService.DownloadPackageAsync(_packageid, _version, Constants.PackagesFolder); await PackageService.DownloadPackageAsync(_packageid, _version, Constants.PackagesFolder);
await logger.LogInformation("Package {PackageId} {Version} Downloaded Successfully", _packageid, _version); await logger.LogInformation("Package {PackageId} {Version} Downloaded Successfully", _packageid, _version);
AddModuleMessage(Localizer["Success.Module.Download"], MessageType.Success); AddModuleMessage(Localizer["Success.Module.Download"], MessageType.Success);
_productname = ""; _productname = "";
_license = ""; _license = "";
StateHasChanged(); StateHasChanged();
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Downloading Package {PackageId} {Version}", _packageid, _version); await logger.LogError(ex, "Error Downloading Package {PackageId} {Version}", _packageid, _version);
AddModuleMessage(Localizer["Error.Module.Download"], MessageType.Error); AddModuleMessage(Localizer["Error.Module.Download"], MessageType.Error);
} }
} }
private async Task InstallModules() private async Task InstallModules()
{ {
try try
{ {
await ModuleDefinitionService.InstallModuleDefinitionsAsync(); await ModuleDefinitionService.InstallModuleDefinitionsAsync();
AddModuleMessage(string.Format(Localizer["Success.Module.Install"], NavigateUrl("admin/system")), MessageType.Success); AddModuleMessage(string.Format(Localizer["Success.Module.Install"], NavigateUrl("admin/system")), MessageType.Success);
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Installing Module"); await logger.LogError(ex, "Error Installing Module");
} }
} }
} }

View File

@ -12,11 +12,32 @@
} }
else else
{ {
<ActionLink Action="Add" Text="Install Module" ResourceKey="InstallModule" /> <div class="container">
@((MarkupString)"&nbsp;") <div class="row mb-3 align-items-center">
<ActionLink Action="Create" Text="Create Module" ResourceKey="CreateModule" Class="btn btn-secondary" /> <div class="col-sm-6">
<ActionLink Action="Add" Text="Install Module" ResourceKey="InstallModule" />
<Pager Items="@_moduleDefinitions"> @((MarkupString)"&nbsp;")
<ActionLink Action="Create" Text="Create Module" ResourceKey="CreateModule" Class="btn btn-secondary" />
</div>
<div class="col-sm-6">
<select class="form-select" @onchange="(e => CategoryChanged(e))">
@foreach (var category in _categories)
{
if (category == _category)
{
<option value="@category" selected>@category @Localizer["Modules"]</option>
}
else
{
<option value="@category">@category @Localizer["Modules"]</option>
}
}
</select>
</div>
</div>
</div>
<Pager Items="@_moduleDefinitions.Where(item => item.Categories.Contains(_category))">
<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>
@ -30,9 +51,9 @@ else
<td><ActionLink Action="Edit" Parameters="@($"id=" + context.ModuleDefinitionId.ToString())" ResourceKey="EditModule" /></td> <td><ActionLink Action="Edit" Parameters="@($"id=" + context.ModuleDefinitionId.ToString())" ResourceKey="EditModule" /></td>
<td> <td>
@if (context.AssemblyName != "Oqtane.Client") @if (context.AssemblyName != "Oqtane.Client")
{ {
<ActionDialog Header="Delete Module" Message="@string.Format(Localizer["Confirm.Module.Delete", context.Name])" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" /> <ActionDialog Header="Delete Module" Message="@string.Format(Localizer["Confirm.Module.Delete", context.Name])" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" />
} }
</td> </td>
<td>@context.Name</td> <td>@context.Name</td>
<td>@context.Version</td> <td>@context.Version</td>
@ -65,6 +86,8 @@ else
@code { @code {
private List<ModuleDefinition> _moduleDefinitions; private List<ModuleDefinition> _moduleDefinitions;
private List<Package> _packages; private List<Package> _packages;
private List<string> _categories = new List<string>();
private string _category = "Common";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
@ -74,6 +97,7 @@ else
{ {
_moduleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId); _moduleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId);
_packages = await PackageService.GetPackagesAsync("module"); _packages = await PackageService.GetPackagesAsync("module");
_categories = _moduleDefinitions.SelectMany(m => m.Categories.Split(',')).Distinct().ToList();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -115,38 +139,44 @@ else
{ {
return package.Version; return package.Version;
} }
} }
return version; return version;
} }
private async Task DownloadModule(string packagename, string version) private async Task DownloadModule(string packagename, string version)
{ {
try try
{ {
await PackageService.DownloadPackageAsync(packagename, version, Constants.PackagesFolder); await PackageService.DownloadPackageAsync(packagename, version, Constants.PackagesFolder);
await logger.LogInformation("Module Downloaded {ModuleDefinitionName} {Version}", packagename, version); await logger.LogInformation("Module Downloaded {ModuleDefinitionName} {Version}", packagename, version);
await ModuleDefinitionService.InstallModuleDefinitionsAsync(); await ModuleDefinitionService.InstallModuleDefinitionsAsync();
AddModuleMessage(string.Format(Localizer["Success.Module.Install"], NavigateUrl("admin/system")), MessageType.Success); AddModuleMessage(string.Format(Localizer["Success.Module.Install"], NavigateUrl("admin/system")), MessageType.Success);
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Downloading Module {ModuleDefinitionName} {Version} {Error}", packagename, version, ex.Message); await logger.LogError(ex, "Error Downloading Module {ModuleDefinitionName} {Version} {Error}", packagename, version, ex.Message);
AddModuleMessage(Localizer["Error.Module.Download"], MessageType.Error); AddModuleMessage(Localizer["Error.Module.Download"], MessageType.Error);
} }
} }
private async Task DeleteModule(ModuleDefinition moduleDefinition) private async Task DeleteModule(ModuleDefinition moduleDefinition)
{ {
try try
{ {
await ModuleDefinitionService.DeleteModuleDefinitionAsync(moduleDefinition.ModuleDefinitionId, moduleDefinition.SiteId); await ModuleDefinitionService.DeleteModuleDefinitionAsync(moduleDefinition.ModuleDefinitionId, moduleDefinition.SiteId);
AddModuleMessage(Localizer["Success.Module.Delete"], MessageType.Success); AddModuleMessage(Localizer["Success.Module.Delete"], MessageType.Success);
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true)); NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true));
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Deleting Module {ModuleDefinition} {Error}", moduleDefinition, ex.Message); await logger.LogError(ex, "Error Deleting Module {ModuleDefinition} {Error}", moduleDefinition, ex.Message);
AddModuleMessage(Localizer["Error.Module.Delete"], MessageType.Error); AddModuleMessage(Localizer["Error.Module.Delete"], MessageType.Error);
} }
} }
private void CategoryChanged(ChangeEventArgs e)
{
_category = (string)e.Value;
StateHasChanged();
}
} }

View File

@ -90,116 +90,117 @@
</form> </form>
@code { @code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Title => "Module Settings"; public override string Title => "Module Settings";
private ElementReference form; private ElementReference form;
private bool validated = false; private bool validated = false;
private List<Theme> _themes; private List<Theme> _themes;
private List<ThemeControl> _containers = new List<ThemeControl>(); private List<ThemeControl> _containers = new List<ThemeControl>();
private string _title; private string _title;
private string _containerType; private string _containerType;
private string _allPages = "false"; private string _allPages = "false";
private string _permissionNames = ""; private string _permissionNames = "";
private string _permissions = null; private string _permissions = null;
private string _pageId; private string _pageId;
private PermissionGrid _permissionGrid; private PermissionGrid _permissionGrid;
private Type _moduleSettingsType; private Type _moduleSettingsType;
private object _moduleSettings; private object _moduleSettings;
private string _moduleSettingsTitle = "Module Settings"; private string _moduleSettingsTitle = "Module Settings";
private RenderFragment ModuleSettingsComponent { get; set; } private RenderFragment ModuleSettingsComponent { get; set; }
private Type _containerSettingsType; private Type _containerSettingsType;
private object _containerSettings; private object _containerSettings;
private RenderFragment ContainerSettingsComponent { get; set; } private RenderFragment ContainerSettingsComponent { get; set; }
private string createdby; private string createdby;
private DateTime createdon; private DateTime createdon;
private string modifiedby; private string modifiedby;
private DateTime modifiedon; private DateTime modifiedon;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
_title = ModuleState.Title; _title = ModuleState.Title;
_themes = await ThemeService.GetThemesAsync(); _themes = await ThemeService.GetThemesAsync();
_containers = ThemeService.GetContainerControls(_themes, PageState.Page.ThemeType); _containers = ThemeService.GetContainerControls(_themes, PageState.Page.ThemeType);
_containerType = ModuleState.ContainerType; _containerType = ModuleState.ContainerType;
_allPages = ModuleState.AllPages.ToString(); _allPages = ModuleState.AllPages.ToString();
_permissions = ModuleState.Permissions; _permissions = ModuleState.Permissions;
_permissionNames = ModuleState.ModuleDefinition.PermissionNames; _permissionNames = ModuleState.ModuleDefinition.PermissionNames;
_pageId = ModuleState.PageId.ToString(); _pageId = ModuleState.PageId.ToString();
createdby = ModuleState.CreatedBy; createdby = ModuleState.CreatedBy;
createdon = ModuleState.CreatedOn; createdon = ModuleState.CreatedOn;
modifiedby = ModuleState.ModifiedBy; modifiedby = ModuleState.ModifiedBy;
modifiedon = ModuleState.ModifiedOn; modifiedon = ModuleState.ModifiedOn;
if (!string.IsNullOrEmpty(ModuleState.ModuleDefinition.SettingsType)) if (!string.IsNullOrEmpty(ModuleState.ModuleDefinition.SettingsType))
{ {
// module settings type explicitly declared in IModule interface // module settings type explicitly declared in IModule interface
_moduleSettingsType = Type.GetType(ModuleState.ModuleDefinition.SettingsType); _moduleSettingsType = Type.GetType(ModuleState.ModuleDefinition.SettingsType);
} }
else else
{ {
// legacy support - module settings type determined by convention ( ie. existence of a "Settings.razor" component in module ) // legacy support - module settings type determined by convention ( ie. existence of a "Settings.razor" component in module )
_moduleSettingsType = Type.GetType(ModuleState.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, PageState.Action), false, true); _moduleSettingsType = Type.GetType(ModuleState.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, PageState.Action), false, true);
} }
if (_moduleSettingsType != null) if (_moduleSettingsType != null)
{ {
var moduleobject = Activator.CreateInstance(_moduleSettingsType) as IModuleControl; var moduleobject = Activator.CreateInstance(_moduleSettingsType) as IModuleControl;
if (!string.IsNullOrEmpty(moduleobject.Title)) if (!string.IsNullOrEmpty(moduleobject.Title))
{ {
_moduleSettingsTitle = moduleobject.Title; _moduleSettingsTitle = moduleobject.Title;
} }
ModuleSettingsComponent = builder => ModuleSettingsComponent = builder =>
{ {
builder.OpenComponent(0, _moduleSettingsType); builder.OpenComponent(0, _moduleSettingsType);
builder.AddComponentReferenceCapture(1, inst => { _moduleSettings = Convert.ChangeType(inst, _moduleSettingsType); }); builder.AddComponentReferenceCapture(1, inst => { _moduleSettings = Convert.ChangeType(inst, _moduleSettingsType); });
builder.CloseComponent(); builder.CloseComponent();
}; };
} }
var theme = _themes.FirstOrDefault(item => item.Containers.Any(themecontrol => themecontrol.TypeName.Equals(_containerType))); var theme = _themes.FirstOrDefault(item => item.Containers.Any(themecontrol => themecontrol.TypeName.Equals(_containerType)));
if (theme != null && !string.IsNullOrEmpty(theme.ContainerSettingsType)) if (theme != null && !string.IsNullOrEmpty(theme.ContainerSettingsType))
{ {
_containerSettingsType = Type.GetType(theme.ContainerSettingsType); _containerSettingsType = Type.GetType(theme.ContainerSettingsType);
if (_containerSettingsType != null) if (_containerSettingsType != null)
{ {
ContainerSettingsComponent = builder => ContainerSettingsComponent = builder =>
{ {
builder.OpenComponent(0, _containerSettingsType); builder.OpenComponent(0, _containerSettingsType);
builder.AddComponentReferenceCapture(1, inst => { _containerSettings = Convert.ChangeType(inst, _containerSettingsType); }); builder.AddComponentReferenceCapture(1, inst => { _containerSettings = Convert.ChangeType(inst, _containerSettingsType); });
builder.CloseComponent(); builder.CloseComponent();
}; };
} }
} }
} }
private async Task SaveModule() private async Task SaveModule()
{ {
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 (!string.IsNullOrEmpty(_title)) if (!string.IsNullOrEmpty(_title))
{ {
var pagemodule = await PageModuleService.GetPageModuleAsync(ModuleState.PageModuleId); var pagemodule = await PageModuleService.GetPageModuleAsync(ModuleState.PageModuleId);
pagemodule.PageId = int.Parse(_pageId); pagemodule.PageId = int.Parse(_pageId);
pagemodule.Title = _title; pagemodule.Title = _title;
pagemodule.ContainerType = (_containerType != "-") ? _containerType : string.Empty; pagemodule.ContainerType = (_containerType != "-") ? _containerType : string.Empty;
if (!string.IsNullOrEmpty(pagemodule.ContainerType) && pagemodule.ContainerType == PageState.Page.DefaultContainerType) if (!string.IsNullOrEmpty(pagemodule.ContainerType) && pagemodule.ContainerType == PageState.Page.DefaultContainerType)
{ {
pagemodule.ContainerType = string.Empty; pagemodule.ContainerType = string.Empty;
} }
if (!string.IsNullOrEmpty(pagemodule.ContainerType) && pagemodule.ContainerType == PageState.Site.DefaultContainerType) if (!string.IsNullOrEmpty(pagemodule.ContainerType) && pagemodule.ContainerType == PageState.Site.DefaultContainerType)
{ {
pagemodule.ContainerType = string.Empty; pagemodule.ContainerType = string.Empty;
} }
await PageModuleService.UpdatePageModuleAsync(pagemodule); await PageModuleService.UpdatePageModuleAsync(pagemodule);
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane); await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane);
var module = ModuleState; var module = ModuleState;
module.AllPages = bool.Parse(_allPages); module.AllPages = bool.Parse(_allPages);
module.Permissions = _permissionGrid.GetPermissions(); module.PageModuleId = ModuleState.PageModuleId;
await ModuleService.UpdateModuleAsync(module); module.Permissions = _permissionGrid.GetPermissions();
await ModuleService.UpdateModuleAsync(module);
if (_moduleSettingsType != null) if (_moduleSettingsType != null)
{ {

View File

@ -59,7 +59,7 @@ else
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
_roles = await RoleService.GetRolesAsync(PageState.Site.SiteId, true); _roles = await RoleService.GetRolesAsync(PageState.Site.SiteId, true);
_roles = _roles.Where(item => item.Name != RoleNames.Everyone).ToList(); _roles.RemoveAll(item => item.Name == RoleNames.Everyone || item.Name == RoleNames.Unauthenticated);
} }
else else
{ {

View File

@ -170,24 +170,54 @@
@if (_aliases != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) @if (_aliases != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
<Section Name="Aliases" Heading="Aliases" ResourceKey="Aliases"> <Section Name="Aliases" Heading="Aliases" ResourceKey="Aliases">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="alias" HelpText="The aliases for the site. An alias can be a domain name (www.site.com) or a virtual folder (ie. www.site.com/folder). If a site has multiple aliases they should be separated by commas." ResourceKey="Aliases">Aliases: </Label> <Label Class="col-sm-3" For="aliases" HelpText="The list of aliases for this site" ResourceKey="Aliases">Aliases: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<textarea id="alias" class="form-control" @bind="@_urls" rows="3" required></textarea> <button type="button" class="btn btn-primary" @onclick="AddAlias">@SharedLocalizer["Add"]</button>
<Pager Items="@_aliases">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["AliasName"]</th>
<th>@Localizer["AliasDefault"]</th>
</Header>
<Row>
@if (context.AliasId != _aliasid)
{
<td>
@if (_aliasid == -1)
{
<button type="button" class="btn btn-primary" @onclick="@(() => EditAlias(context))">@SharedLocalizer["Edit"]</button>
}
</td>
<td>
@if (_aliasid == -1)
{
<ActionDialog Action="Delete" OnClick="@(async () => await DeleteAlias(context))" ResourceKey="DeleteModule" Class="btn btn-danger" Header="Delete Alias" Message="@string.Format(Localizer["Confirm.Alias.Delete", context.Name])" />
}
</td>
<td>@context.Name</td>
<td>@context.IsDefault</td>
}
else
{
<td><button type="button" class="btn btn-success" @onclick="@(async () => await SaveAlias())">@SharedLocalizer["Save"]</button></td>
<td><button type="button" class="btn btn-secondary" @onclick="@(async () => await CancelAlias())">@SharedLocalizer["Cancel"]</button></td>
<td>
<input id="aliasname" class="form-control" @bind="@_aliasname" />
</td>
<td>
<select id="defaultaias" class="form-select" @bind="@_defaultalias" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</td>
}
</Row>
</Pager>
</div>
</div> </div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="defaultalias" HelpText="The default alias for the site. Requests for non-default aliases will be redirected to the default alias." ResourceKey="DefaultAlias">Default Alias: </Label>
<div class="col-sm-9">
<select id="defaultalias" class="form-select" @bind="@_defaultalias" required>
@foreach (string name in _urls.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(sValue => sValue.Trim()).ToArray())
{
<option value="@name">@name</option>
}
</select>
</div>
</div>
</div> </div>
</Section> </Section>
<Section Name="Hosting" Heading="Hosting Model" ResourceKey="Hosting"> <Section Name="Hosting" Heading="Hosting Model" ResourceKey="Hosting">
@ -253,8 +283,9 @@
private List<ThemeControl> _containers = new List<ThemeControl>(); private List<ThemeControl> _containers = new List<ThemeControl>();
private string _name = string.Empty; private string _name = string.Empty;
private List<Alias> _aliases; private List<Alias> _aliases;
private string _defaultalias = string.Empty; private int _aliasid = -1;
private string _urls = string.Empty; private string _aliasname;
private string _defaultalias;
private string _runtime = ""; private string _runtime = "";
private string _prerender = ""; private string _prerender = "";
private int _logofileid = -1; private int _logofileid = -1;
@ -304,10 +335,7 @@
_prerender = site.RenderMode.Replace(_runtime, ""); _prerender = site.RenderMode.Replace(_runtime, "");
_isdeleted = site.IsDeleted.ToString(); _isdeleted = site.IsDeleted.ToString();
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) await GetAliases();
{
await GetAliases();
}
if (site.LogoFileId != null) if (site.LogoFileId != null)
{ {
@ -409,152 +437,92 @@
{ {
if (_name != string.Empty && _themetype != "-" && _containertype != "-") if (_name != string.Empty && _themetype != "-" && _containertype != "-")
{ {
var unique = true; var site = await SiteService.GetSiteAsync(PageState.Site.SiteId);
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (site != null)
{ {
_urls = Regex.Replace(_urls, @"\r\n?|\n", ","); // convert line breaks to commas bool refresh = false;
var aliases = await AliasService.GetAliasesAsync(); bool reload = false;
foreach (string name in _urls.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(sValue => sValue.Trim()).ToArray())
site.Name = _name;
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
var alias = aliases.Where(item => item.Name == name).FirstOrDefault(); if (site.Runtime != _runtime || site.RenderMode != _runtime + _prerender)
if (alias != null && unique)
{ {
unique = (alias.TenantId == PageState.Site.TenantId && alias.SiteId == PageState.Site.SiteId); site.Runtime = _runtime;
site.RenderMode = _runtime + _prerender;
reload = true; // needs to be reloaded on server
} }
} }
if (unique && string.IsNullOrEmpty(_defaultalias)) unique = false; site.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted));
}
if (unique) site.LogoFileId = null;
{ var logofileid = _logofilemanager.GetFileId();
var site = await SiteService.GetSiteAsync(PageState.Site.SiteId); if (logofileid != -1)
if (site != null)
{ {
bool refresh = false; site.LogoFileId = logofileid;
bool reload = false; }
int? faviconFieldId = _faviconfilemanager.GetFileId();
site.Name = _name; if (faviconFieldId == -1) faviconFieldId = null;
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (site.FaviconFileId != faviconFieldId)
{ {
if (site.Runtime != _runtime || site.RenderMode != _runtime + _prerender) site.FaviconFileId = faviconFieldId;
{ reload = true; // needs to be reloaded on server
site.Runtime = _runtime; }
site.RenderMode = _runtime + _prerender; if (site.DefaultThemeType != _themetype)
reload = true; // needs to be reloaded on server {
} site.DefaultThemeType = _themetype;
} refresh = true; // needs to be refreshed on client
site.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted)); }
if (site.DefaultContainerType != _containertype)
site.LogoFileId = null; {
var logofileid = _logofilemanager.GetFileId(); site.DefaultContainerType = _containertype;
if (logofileid != -1) refresh = true; // needs to be refreshed on client
{ }
site.LogoFileId = logofileid; site.AdminContainerType = _admincontainertype;
}
int? faviconFieldId = _faviconfilemanager.GetFileId(); if (site.PwaIsEnabled.ToString() != _pwaisenabled)
if (faviconFieldId == -1) faviconFieldId = null; {
if (site.FaviconFileId != faviconFieldId) site.PwaIsEnabled = Boolean.Parse(_pwaisenabled);
{ reload = true; // needs to be reloaded on server
site.FaviconFileId = faviconFieldId; }
reload = true; // needs to be reloaded on server int? pwaappiconfileid = _pwaappiconfilemanager.GetFileId();
} if (pwaappiconfileid == -1) pwaappiconfileid = null;
if (site.DefaultThemeType != _themetype) if (site.PwaAppIconFileId != pwaappiconfileid)
{ {
site.DefaultThemeType = _themetype; site.PwaAppIconFileId = pwaappiconfileid;
refresh = true; // needs to be refreshed on client reload = true; // needs to be reloaded on server
} }
if (site.DefaultContainerType != _containertype) int? pwasplashiconfileid = _pwasplashiconfilemanager.GetFileId();
{ if (pwasplashiconfileid == -1) pwasplashiconfileid = null;
site.DefaultContainerType = _containertype; if (site.PwaSplashIconFileId != pwasplashiconfileid)
refresh = true; // needs to be refreshed on client {
} site.PwaSplashIconFileId = pwasplashiconfileid;
site.AdminContainerType = _admincontainertype; reload = true; // needs to be reloaded on server
}
if (site.PwaIsEnabled.ToString() != _pwaisenabled)
{ site = await SiteService.UpdateSiteAsync(site);
site.PwaIsEnabled = Boolean.Parse(_pwaisenabled);
reload = true; // needs to be reloaded on server var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
} settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
int? pwaappiconfileid = _pwaappiconfilemanager.GetFileId(); settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
if (pwaappiconfileid == -1) pwaappiconfileid = null; settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true);
if (site.PwaAppIconFileId != pwaappiconfileid) settings = SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true);
{ settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true);
site.PwaAppIconFileId = pwaappiconfileid; settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
reload = true; // needs to be reloaded on server settings = SettingService.SetSetting(settings, "NotificationRetention", _retention, true);
} await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
int? pwasplashiconfileid = _pwasplashiconfilemanager.GetFileId();
if (pwasplashiconfileid == -1) pwasplashiconfileid = null; await logger.LogInformation("Site Settings Saved {Site}", site);
if (site.PwaSplashIconFileId != pwasplashiconfileid)
{ if (refresh || reload)
site.PwaSplashIconFileId = pwasplashiconfileid; {
reload = true; // needs to be reloaded on server NavigationManager.NavigateTo(NavigateUrl(true), reload); // refresh/reload
} }
else
site = await SiteService.UpdateSiteAsync(site); {
AddModuleMessage(Localizer["Success.Settings.SaveSite"], MessageType.Success);
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); await interop.ScrollTo(0, 0, "smooth");
settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true);
settings = SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true);
settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true);
settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
settings = SettingService.SetSetting(settings, "NotificationRetention", _retention, true);
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
var names = _urls.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(sValue => sValue.Trim()).ToArray();
foreach (Alias alias in _aliases)
{
if (!names.Contains(alias.Name.Trim()))
{
await AliasService.DeleteAliasAsync(alias.AliasId);
}
}
foreach (string name in names)
{
var alias = _aliases.Find(item => item.Name.Trim() == name);
if (alias == null)
{
alias = new Alias();
alias.Name = name;
alias.TenantId = site.TenantId;
alias.SiteId = site.SiteId;
alias.IsDefault = (name == _defaultalias);
await AliasService.AddAliasAsync(alias);
}
else
{
if (alias.Name != name || alias.IsDefault != (alias.Name.Trim() == _defaultalias))
{
alias.Name = name;
alias.IsDefault = (name == _defaultalias);
await AliasService.UpdateAliasAsync(alias);
}
}
}
await GetAliases();
}
await logger.LogInformation("Site Settings Saved {Site}", site);
if (refresh || reload)
{
NavigationManager.NavigateTo(NavigateUrl(true), reload); // refresh/reload
}
else
{
AddModuleMessage(Localizer["Success.Settings.SaveSite"], MessageType.Success);
await interop.ScrollTo(0, 0, "smooth");
}
} }
}
else // deuplicate alias or default alias not specified
{
AddModuleMessage(Localizer["Message.Aliases.Taken"], MessageType.Warning);
} }
} }
else else
@ -578,19 +546,19 @@
{ {
try try
{ {
var sites = await SiteService.GetSitesAsync(); var aliases = await AliasService.GetAliasesAsync();
if (sites.Count > 1) if (aliases.Any(item => item.SiteId != PageState.Site.SiteId || item.TenantId != PageState.Site.TenantId))
{ {
await SiteService.DeleteSiteAsync(PageState.Site.SiteId); await SiteService.DeleteSiteAsync(PageState.Site.SiteId);
await logger.LogInformation("Site Deleted {SiteId}", PageState.Site.SiteId); await logger.LogInformation("Site Deleted {SiteId}", PageState.Site.SiteId);
var aliases = await AliasService.GetAliasesAsync(); foreach (Alias alias in aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Site.TenantId))
foreach (Alias a in aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Site.TenantId))
{ {
await AliasService.DeleteAliasAsync(a.AliasId); await AliasService.DeleteAliasAsync(alias.AliasId);
} }
NavigationManager.NavigateTo(NavigateUrl("admin/sites")); aliases = await AliasService.GetAliasesAsync();
NavigationManager.NavigateTo(PageState.Uri.Scheme + "://" + aliases.First().Name, true);
} }
else else
{ {
@ -637,20 +605,6 @@
} }
} }
private async Task GetAliases()
{
_urls = string.Empty;
_defaultalias = string.Empty;
_aliases = await AliasService.GetAliasesAsync();
_aliases = _aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Site.TenantId).OrderBy(item => item.AliasId).ToList();
foreach (Alias alias in _aliases)
{
_urls += (_urls == string.Empty) ? alias.Name.Trim() : ", " + alias.Name.Trim();
if (alias.IsDefault && string.IsNullOrEmpty(_defaultalias)) _defaultalias = alias.Name.Trim();
}
if (string.IsNullOrEmpty(_defaultalias)) _defaultalias = _aliases.First().Name.Trim();
}
private void ToggleSMTPPassword() private void ToggleSMTPPassword()
{ {
if (_smtppasswordtype == "password") if (_smtppasswordtype == "password")
@ -664,4 +618,84 @@
_togglesmtppassword = SharedLocalizer["ShowPassword"]; _togglesmtppassword = SharedLocalizer["ShowPassword"];
} }
} }
private async Task GetAliases()
{
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
_aliases = await AliasService.GetAliasesAsync();
_aliases = _aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Site.TenantId).OrderBy(item => item.AliasId).ToList();
}
}
private void AddAlias()
{
_aliases.Add(new Alias { AliasId = 0, Name = "", IsDefault = false });
_aliasid = 0;
_aliasname = "";
_defaultalias = "False";
StateHasChanged();
}
private void EditAlias(Alias alias)
{
_aliasid = alias.AliasId;
_aliasname = alias.Name;
_defaultalias = alias.IsDefault.ToString();
StateHasChanged();
}
private async Task DeleteAlias(Alias alias)
{
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
await AliasService.DeleteAliasAsync(alias.AliasId);
await GetAliases();
StateHasChanged();
}
}
private async Task SaveAlias()
{
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
if (!string.IsNullOrEmpty(_aliasname))
{
var aliases = await AliasService.GetAliasesAsync();
var alias = aliases.Where(item => item.Name == _aliasname).FirstOrDefault();
bool unique = (alias == null || alias.AliasId == _aliasid);
if (unique)
{
if (_aliasid == 0)
{
alias = new Alias { SiteId = PageState.Site.SiteId, TenantId = PageState.Site.TenantId, Name = _aliasname, IsDefault = bool.Parse(_defaultalias) };
await AliasService.AddAliasAsync(alias);
}
else
{
alias = _aliases.Single(item => item.AliasId == _aliasid);
alias.Name = _aliasname;
alias.IsDefault = bool.Parse(_defaultalias);
await AliasService.UpdateAliasAsync(alias);
}
}
else // duplicate alias
{
AddModuleMessage(Localizer["Message.Aliases.Taken"], MessageType.Warning);
}
}
await GetAliases();
_aliasid = -1;
_aliasname = "";
StateHasChanged();
}
}
private async Task CancelAlias()
{
await GetAliases();
_aliasid = -1;
_aliasname = "";
StateHasChanged();
}
} }

View File

@ -131,14 +131,7 @@ else
<select id="databaseType" class="form-select" value="@_databaseName" @onchange="(e => DatabaseChanged(e))" required> <select id="databaseType" class="form-select" value="@_databaseName" @onchange="(e => DatabaseChanged(e))" required>
@foreach (var database in _databases) @foreach (var database in _databases)
{ {
if (database.IsDefault) <option value="@database.Name">@Localizer[@database.Name]</option>
{
<option value="@database.Name" selected>@Localizer[@database.Name]</option>
}
else
{
<option value="@database.Name">@Localizer[@database.Name]</option>
}
} }
</select> </select>
</div> </div>
@ -169,145 +162,154 @@ 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;
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<ThemeControl> _themes = new List<ThemeControl>();
private List<ThemeControl> _containers = new List<ThemeControl>();
private List<SiteTemplate> _siteTemplates;
private List<Tenant> _tenants;
private string _tenantid = "-";
private List<Theme> _themeList; private string _tenantName = string.Empty;
private List<ThemeControl> _themes = new List<ThemeControl>();
private List<ThemeControl> _containers = new List<ThemeControl>();
private List<SiteTemplate> _siteTemplates;
private List<Tenant> _tenants;
private string _tenantid = "-";
private string _tenantName = string.Empty; private string _hostusername = string.Empty;
private string _hostpassword = string.Empty;
private string _hostusername = string.Empty; private string _name = string.Empty;
private string _hostpassword = string.Empty; private string _urls = string.Empty;
private string _themetype = "-";
private string _containertype = "-";
private string _admincontainertype = "";
private string _sitetemplatetype = "-";
private string _runtime = "Server";
private string _prerender = "Prerendered";
private string _name = string.Empty; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
private string _urls = string.Empty;
private string _themetype = "-";
private string _containertype = "-";
private string _admincontainertype = "";
private string _sitetemplatetype = "-";
private string _runtime = "Server";
private string _prerender = "Prerendered";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; protected override async Task OnInitializedAsync()
{
_tenants = await TenantService.GetTenantsAsync();
_urls = PageState.Alias.Name;
_themeList = await ThemeService.GetThemesAsync();
_themes = ThemeService.GetThemeControls(_themeList);
_siteTemplates = await SiteTemplateService.GetSiteTemplatesAsync();
protected override async Task OnInitializedAsync() _databases = await DatabaseService.GetDatabasesAsync();
{ if (_databases.Exists(item => item.IsDefault))
_tenants = await TenantService.GetTenantsAsync(); {
_urls = PageState.Alias.Name; _databaseName = _databases.Find(item => item.IsDefault).Name;
_themeList = await ThemeService.GetThemesAsync(); }
_themes = ThemeService.GetThemeControls(_themeList); else
_siteTemplates = await SiteTemplateService.GetSiteTemplatesAsync(); {
_databases = await DatabaseService.GetDatabasesAsync(); _databaseName = "LocalDB";
LoadDatabaseConfigComponent(); }
} LoadDatabaseConfigComponent();
}
private void DatabaseChanged(ChangeEventArgs eventArgs) private void DatabaseChanged(ChangeEventArgs eventArgs)
{ {
try try
{ {
_databaseName = (string)eventArgs.Value; _databaseName = (string)eventArgs.Value;
LoadDatabaseConfigComponent(); LoadDatabaseConfigComponent();
} }
catch catch
{ {
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); 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

@ -142,6 +142,7 @@
<ActionDialog Header="Restart Application" Message="Are You Sure You Wish To Restart The Application?" Action="Restart Application" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await RestartApplication())" ResourceKey="RestartApplication" /> <ActionDialog Header="Restart Application" Message="Are You Sure You Wish To Restart The Application?" Action="Restart Application" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await RestartApplication())" ResourceKey="RestartApplication" />
</TabPanel> </TabPanel>
</TabStrip> </TabStrip>
<br /><br />
@code { @code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;

View File

@ -35,6 +35,7 @@
<strong>@(String.Format("{0:n0}", context.Downloads))</strong> @SharedLocalizer["Search.Downloads"]&nbsp;&nbsp;|&nbsp;&nbsp; <strong>@(String.Format("{0:n0}", context.Downloads))</strong> @SharedLocalizer["Search.Downloads"]&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Released"]: <strong>@context.ReleaseDate.ToString("MMM dd, yyyy")</strong>&nbsp;&nbsp;|&nbsp;&nbsp; @SharedLocalizer["Search.Released"]: <strong>@context.ReleaseDate.ToString("MMM dd, yyyy")</strong>&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Version"]: <strong>@context.Version</strong> @SharedLocalizer["Search.Version"]: <strong>@context.Version</strong>
@((MarkupString)(!string.IsNullOrEmpty(context.PackageUrl) ? "&nbsp;&nbsp;|&nbsp;&nbsp;" + SharedLocalizer["Search.Source"] + ": <strong>" + new Uri(context.PackageUrl).Host + "</strong>" : ""))
@((MarkupString)(context.TrialPeriod > 0 ? "&nbsp;&nbsp;|&nbsp;&nbsp;<strong>" + context.TrialPeriod + " " + @SharedLocalizer["Trial"] + "</strong>" : "")) @((MarkupString)(context.TrialPeriod > 0 ? "&nbsp;&nbsp;|&nbsp;&nbsp;<strong>" + context.TrialPeriod + " " + @SharedLocalizer["Trial"] + "</strong>" : ""))
</td> </td>
<td style="width: 1px; vertical-align: middle;"> <td style="width: 1px; vertical-align: middle;">

View File

@ -160,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>
@ -193,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>

View File

@ -72,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">
@ -137,63 +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 _passwordtype = "password"; private string _passwordtype = "password";
private string _togglepassword = string.Empty; 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;
private FileManager filemanager; private FileManager filemanager;
private int photofileid = -1; private int photofileid = -1;
private File photo = null; private File photo = null;
private List<Profile> profiles; private string isdeleted;
private Dictionary<string, string> settings; private string lastlogin;
private string category = string.Empty; private string lastipaddress;
private string createdby;
private DateTime createdon;
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;
if (PageState.QueryString.ContainsKey("id")) private string deletedby;
{ private DateTime? deletedon;
_togglepassword = SharedLocalizer["ShowPassword"];
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();
} }
} }
} }

View File

@ -33,14 +33,12 @@ else
</div> </div>
<Pager Items="@users" RowClass="align-middle"> <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> <th>@Localizer["LastLoginOn"]</th>
<th>@Localizer["LastIPAddress"]</th>
<th>@Localizer["CreatedOn"]</th>
</Header> </Header>
<Row> <Row>
<td> <td>
@ -52,12 +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>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}",context.User.LastLoginOn)</td> <td>@((MarkupString)string.Format("<a href=\"mailto:{0}\">{1}</a>", @context.User.Email, @context.User.DisplayName))</td>
<td>@context.User.LastIPAddress</td> <td>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", context.User.LastLoginOn)</td>
<td>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}",context.User.CreatedOn)</td>
</Row> </Row>
</Pager> </Pager>
</TabPanel> </TabPanel>
@ -259,6 +254,12 @@ else
<input id="scopes" class="form-control" @bind="@_scopes" /> <input id="scopes" class="form-control" @bind="@_scopes" />
</div> </div>
</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"> <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> <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"> <div class="col-sm-9">
@ -380,6 +381,7 @@ 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 _identifierclaimtype;
@ -432,6 +434,7 @@ else
_clientsecret = SettingService.GetSetting(settings, "ExternalLogin:ClientSecret", ""); _clientsecret = SettingService.GetSetting(settings, "ExternalLogin:ClientSecret", "");
_toggleclientsecret = SharedLocalizer["ShowPassword"]; _toggleclientsecret = SharedLocalizer["ShowPassword"];
_scopes = SettingService.GetSetting(settings, "ExternalLogin:Scopes", ""); _scopes = SettingService.GetSetting(settings, "ExternalLogin:Scopes", "");
_parameters = SettingService.GetSetting(settings, "ExternalLogin:Parameters", "");
_pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false"); _pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false");
_redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype; _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"); _identifierclaimtype = SettingService.GetSetting(settings, "ExternalLogin:IdentifierClaimType", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier");
@ -549,6 +552,7 @@ else
settings = SettingService.SetSetting(settings, "ExternalLogin:ClientId", _clientid, true); settings = SettingService.SetSetting(settings, "ExternalLogin:ClientId", _clientid, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ClientSecret", _clientsecret, true); settings = SettingService.SetSetting(settings, "ExternalLogin:ClientSecret", _clientsecret, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:Scopes", _scopes, 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:PKCE", _pkce, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:IdentifierClaimType", _identifierclaimtype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:IdentifierClaimType", _identifierclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true);

View File

@ -88,15 +88,17 @@ else
userid = Int32.Parse(PageState.QueryString["id"]); userid = Int32.Parse(PageState.QueryString["id"]);
User user = await UserService.GetUserAsync(userid, PageState.Site.SiteId); User user = await UserService.GetUserAsync(userid, PageState.Site.SiteId);
name = user.DisplayName; name = user.DisplayName;
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
roles = await RoleService.GetRolesAsync(PageState.Site.SiteId, true); roles = await RoleService.GetRolesAsync(PageState.Site.SiteId, true);
roles = roles.Where(item => item.Name != RoleNames.Everyone).ToList(); roles.RemoveAll(item => item.Name == RoleNames.Everyone || item.Name == RoleNames.Unauthenticated);
} }
else else
{ {
roles = await RoleService.GetRolesAsync(PageState.Site.SiteId); roles = await RoleService.GetRolesAsync(PageState.Site.SiteId);
} }
await GetUserRoles(); await GetUserRoles();
} }
catch (Exception ex) catch (Exception ex)

View File

@ -86,279 +86,296 @@
} }
@code { @code {
private string _id; private string _id;
private List<Folder> _folders; private List<Folder> _folders;
private List<File> _files = new List<File>(); private List<File> _files = new List<File>();
private string _fileinputid = string.Empty; private string _fileinputid = string.Empty;
private string _progressinfoid = string.Empty; private string _progressinfoid = string.Empty;
private string _progressbarid = string.Empty; private string _progressbarid = string.Empty;
private string _filter = "*"; private string _filter = "*";
private bool _haseditpermission = false; private bool _haseditpermission = false;
private string _image = string.Empty; private string _image = string.Empty;
private File _file = null; private File _file = null;
private string _guid; private string _guid;
private string _message = string.Empty; private string _message = string.Empty;
private MessageType _messagetype; private MessageType _messagetype;
[Parameter] [Parameter]
public string Id { get; set; } // optional - for setting the id of the FileManager component for accessibility public string Id { get; set; } // optional - for setting the id of the FileManager component for accessibility
[Parameter] [Parameter]
public int FolderId { get; set; } = -1; // optional - for setting a specific default folder by folderid public int FolderId { get; set; } = -1; // optional - for setting a specific default folder by folderid
[Parameter] [Parameter]
public string Folder { get; set; } = ""; // optional - for setting a specific default folder by folder path public string Folder { get; set; } = ""; // optional - for setting a specific default folder by folder path
[Parameter] [Parameter]
public int FileId { get; set; } = -1; // optional - for selecting a specific file by default public int FileId { get; set; } = -1; // optional - for selecting a specific file by default
[Parameter] [Parameter]
public string Filter { get; set; } // optional - comma delimited list of file types that can be selected or uploaded ie. "jpg,gif" public string Filter { get; set; } // optional - comma delimited list of file types that can be selected or uploaded ie. "jpg,gif"
[Parameter] [Parameter]
public bool ShowFiles { get; set; } = true; // optional - for indicating whether a list of files should be displayed - default is true public bool ShowFiles { get; set; } = true; // optional - for indicating whether a list of files should be displayed - default is true
[Parameter] [Parameter]
public bool ShowUpload { get; set; } = true; // optional - for indicating whether a Upload controls should be displayed - default is true public bool ShowUpload { get; set; } = true; // optional - for indicating whether a Upload controls should be displayed - default is true
[Parameter] [Parameter]
public bool ShowFolders { get; set; } = true; // optional - for indicating whether a list of folders should be displayed - default is true public bool ShowFolders { get; set; } = true; // optional - for indicating whether a list of folders should be displayed - default is true
[Parameter] [Parameter]
public bool ShowImage { get; set; } = true; // optional - for indicating whether an image thumbnail should be displayed - default is true public bool ShowImage { get; set; } = true; // optional - for indicating whether an image thumbnail should be displayed - default is true
[Parameter] [Parameter]
public bool ShowSuccess { get; set; } = false; // optional - for indicating whether a success message should be displayed upon successful upload - default is false public bool ShowSuccess { get; set; } = false; // optional - for indicating whether a success message should be displayed upon successful upload - default is false
[Parameter] [Parameter]
public bool UploadMultiple { get; set; } = false; // optional - enable multiple file uploads - default false public bool UploadMultiple { get; set; } = false; // optional - enable multiple file uploads - default false
[Parameter] [Parameter]
public EventCallback<int> OnUpload { get; set; } // optional - executes a method in the calling component when a file is uploaded public EventCallback<int> OnUpload { get; set; } // optional - executes a method in the calling component when a file is uploaded
[Parameter] [Parameter]
public EventCallback<int> OnSelect { get; set; } // optional - executes a method in the calling component when a file is selected public EventCallback<int> OnSelect { get; set; } // optional - executes a method in the calling component when a file is selected
[Parameter] [Parameter]
public EventCallback<int> OnDelete { get; set; } // optional - executes a method in the calling component when a file is deleted public EventCallback<int> OnDelete { get; set; } // optional - executes a method in the calling component when a file is deleted
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
if (!string.IsNullOrEmpty(Id)) if (!string.IsNullOrEmpty(Id))
{ {
_id = Id; _id = Id;
} }
// packages folder is a framework folder for uploading installable nuget packages // packages folder is a framework folder for uploading installable nuget packages
if (Folder == Constants.PackagesFolder) if (Folder == Constants.PackagesFolder)
{ {
ShowFiles = false; ShowFiles = false;
ShowFolders = false; ShowFolders = false;
Filter = "nupkg"; Filter = "nupkg";
ShowSuccess = true; ShowSuccess = true;
} }
if (!ShowFiles) if (!ShowFiles)
{ {
ShowImage = false; ShowImage = false;
} }
_folders = await FolderService.GetFoldersAsync(ModuleState.SiteId); _folders = await FolderService.GetFoldersAsync(ModuleState.SiteId);
if (!string.IsNullOrEmpty(Folder) && Folder != Constants.PackagesFolder) if (!string.IsNullOrEmpty(Folder) && Folder != Constants.PackagesFolder)
{ {
Folder folder = await FolderService.GetFolderAsync(ModuleState.SiteId, Folder); Folder folder = await FolderService.GetFolderAsync(ModuleState.SiteId, Folder);
if (folder != null) if (folder != null)
{ {
FolderId = folder.FolderId; FolderId = folder.FolderId;
} }
else else
{ {
FolderId = -1; FolderId = -1;
_message = "Folder Path " + Folder + "Does Not Exist"; _message = "Folder Path " + Folder + "Does Not Exist";
_messagetype = MessageType.Error; _messagetype = MessageType.Error;
} }
} }
if (FileId != -1) if (FileId != -1)
{ {
File file = await FileService.GetFileAsync(FileId); File file = await FileService.GetFileAsync(FileId);
if (file != null) if (file != null)
{ {
FolderId = file.FolderId; FolderId = file.FolderId;
await OnSelect.InvokeAsync(FileId); await OnSelect.InvokeAsync(FileId);
} }
else else
{ {
FileId = -1; // file does not exist FileId = -1; // file does not exist
_message = "FileId " + FileId.ToString() + "Does Not Exist"; _message = "FileId " + FileId.ToString() + "Does Not Exist";
_messagetype = MessageType.Error; _messagetype = MessageType.Error;
} }
} }
await SetImage(); await SetImage();
if (!string.IsNullOrEmpty(Filter)) if (!string.IsNullOrEmpty(Filter))
{ {
_filter = "." + Filter.Replace(",", ",."); _filter = "." + Filter.Replace(",", ",.");
} }
await GetFiles(); await GetFiles();
// create unique id for component // create unique id for component
_guid = Guid.NewGuid().ToString("N"); _guid = Guid.NewGuid().ToString("N");
_fileinputid = _guid + "FileInput"; _fileinputid = _guid + "FileInput";
_progressinfoid = _guid + "ProgressInfo"; _progressinfoid = _guid + "ProgressInfo";
_progressbarid = _guid + "ProgressBar"; _progressbarid = _guid + "ProgressBar";
} }
private async Task GetFiles() private async Task GetFiles()
{ {
_haseditpermission = false; _haseditpermission = false;
if (Folder == Constants.PackagesFolder) if (Folder == Constants.PackagesFolder)
{ {
_haseditpermission = UserSecurity.IsAuthorized(PageState.User, RoleNames.Host); _haseditpermission = UserSecurity.IsAuthorized(PageState.User, RoleNames.Host);
_files = new List<File>(); _files = new List<File>();
} }
else else
{ {
Folder folder = _folders.FirstOrDefault(item => item.FolderId == FolderId); Folder folder = _folders.FirstOrDefault(item => item.FolderId == FolderId);
if (folder != null) if (folder != null)
{ {
_haseditpermission = UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, folder.Permissions); _haseditpermission = UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, folder.Permissions);
_files = await FileService.GetFilesAsync(FolderId); _files = await FileService.GetFilesAsync(FolderId);
} }
else else
{ {
_haseditpermission = false; _haseditpermission = false;
_files = new List<File>(); _files = new List<File>();
} }
} }
if (_filter != "*") if (_filter != "*")
{ {
List<File> filtered = new List<File>(); List<File> filtered = new List<File>();
foreach (File file in _files) foreach (File file in _files)
{ {
if (_filter.ToUpper().IndexOf("." + file.Extension.ToUpper()) != -1) if (_filter.ToUpper().IndexOf("." + file.Extension.ToUpper()) != -1)
{ {
filtered.Add(file); filtered.Add(file);
} }
} }
_files = filtered; _files = filtered;
} }
} }
private async Task FolderChanged(ChangeEventArgs e) private async Task FolderChanged(ChangeEventArgs e)
{ {
_message = string.Empty; _message = string.Empty;
try try
{ {
FolderId = int.Parse((string)e.Value); FolderId = int.Parse((string)e.Value);
await GetFiles(); await GetFiles();
FileId = -1; FileId = -1;
_file = null; _file = null;
_image = string.Empty; _image = string.Empty;
StateHasChanged(); StateHasChanged();
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Loading Files {Error}", ex.Message); await logger.LogError(ex, "Error Loading Files {Error}", ex.Message);
_message = Localizer["Error.File.Load"]; _message = Localizer["Error.File.Load"];
_messagetype = MessageType.Error; _messagetype = MessageType.Error;
} }
} }
private async Task FileChanged(ChangeEventArgs e) private async Task FileChanged(ChangeEventArgs e)
{ {
_message = string.Empty; _message = string.Empty;
FileId = int.Parse((string)e.Value); FileId = int.Parse((string)e.Value);
if (FileId != -1) if (FileId != -1)
{ {
await OnSelect.InvokeAsync(FileId); await OnSelect.InvokeAsync(FileId);
} }
await SetImage(); await SetImage();
StateHasChanged(); StateHasChanged();
} }
private async Task SetImage() private async Task SetImage()
{ {
_image = string.Empty; _image = string.Empty;
_file = null; _file = null;
if (FileId != -1) if (FileId != -1)
{ {
_file = await FileService.GetFileAsync(FileId); _file = await FileService.GetFileAsync(FileId);
if (_file != null && ShowImage && _file.ImageHeight != 0 && _file.ImageWidth != 0) if (_file != null && ShowImage && _file.ImageHeight != 0 && _file.ImageWidth != 0)
{ {
var maxwidth = 200; var maxwidth = 200;
var maxheight = 200; var maxheight = 200;
var ratioX = (double)maxwidth / (double)_file.ImageWidth; var ratioX = (double)maxwidth / (double)_file.ImageWidth;
var ratioY = (double)maxheight / (double)_file.ImageHeight; var ratioY = (double)maxheight / (double)_file.ImageHeight;
var ratio = ratioX < ratioY ? ratioX : ratioY; var ratio = ratioX < ratioY ? ratioX : ratioY;
_image = "<img src=\"" + _file.Url + "\" alt=\"" + _file.Name + _image = "<img src=\"" + _file.Url + "\" alt=\"" + _file.Name +
"\" width=\"" + Convert.ToInt32(_file.ImageWidth * ratio).ToString() + "\" width=\"" + Convert.ToInt32(_file.ImageWidth * ratio).ToString() +
"\" height=\"" + Convert.ToInt32(_file.ImageHeight * ratio).ToString() + "\" />"; "\" height=\"" + Convert.ToInt32(_file.ImageHeight * ratio).ToString() + "\" />";
} }
} }
} }
private async Task UploadFile() private async Task UploadFile()
{ {
_message = string.Empty; _message = string.Empty;
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
var upload = await interop.GetFiles(_fileinputid); var upload = await interop.GetFiles(_fileinputid);
if (upload.Length > 0) if (upload.Length > 0)
{ {
try string restricted = "";
{ foreach (var file in upload)
string result; {
if (Folder == Constants.PackagesFolder) var extension = (file.LastIndexOf(".") != -1) ? file.Substring(file.LastIndexOf(".") + 1) : "";
{ if (!Constants.UploadableFiles.Split(',').Contains(extension.ToLower()))
result = await FileService.UploadFilesAsync(Folder, upload, _guid); {
} restricted += (restricted == "" ? "" : ",") + extension;
else }
{ }
result = await FileService.UploadFilesAsync(FolderId, upload, _guid); if (restricted == "")
} {
try
{
string result;
if (Folder == Constants.PackagesFolder)
{
result = await FileService.UploadFilesAsync(Folder, upload, _guid);
}
else
{
result = await FileService.UploadFilesAsync(FolderId, upload, _guid);
}
if (result == string.Empty) if (result == string.Empty)
{ {
await logger.LogInformation("File Upload Succeeded {Files}", upload); await logger.LogInformation("File Upload Succeeded {Files}", upload);
if (ShowSuccess) if (ShowSuccess)
{ {
_message = Localizer["Success.File.Upload"]; _message = Localizer["Success.File.Upload"];
_messagetype = MessageType.Success; _messagetype = MessageType.Success;
} }
// set FileId to first file in upload collection // set FileId to first file in upload collection
await GetFiles(); await GetFiles();
var file = _files.Where(item => item.Name == upload[0]).FirstOrDefault(); var file = _files.Where(item => item.Name == upload[0]).FirstOrDefault();
if (file != null) if (file != null)
{ {
FileId = file.FileId; FileId = file.FileId;
await SetImage(); await SetImage();
await OnUpload.InvokeAsync(FileId); await OnUpload.InvokeAsync(FileId);
} }
StateHasChanged(); StateHasChanged();
} }
else else
{ {
await logger.LogError("File Upload Failed For {Files}", result.Replace(",", ", ")); await logger.LogError("File Upload Failed For {Files}", result.Replace(",", ", "));
_message = Localizer["Error.File.Upload"]; _message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error; _messagetype = MessageType.Error;
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "File Upload Failed {Error}", ex.Message); await logger.LogError(ex, "File Upload Failed {Error}", ex.Message);
_message = Localizer["Error.File.Upload"]; _message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error; _messagetype = MessageType.Error;
} }
} }
else
{
_message = string.Format(Localizer["Message.File.Restricted"], restricted);
_messagetype = MessageType.Warning;
}
}
else else
{ {
_message = Localizer["Message.File.NotSelected"]; _message = Localizer["Message.File.NotSelected"];

View File

@ -1,13 +1,13 @@
using System; using System;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Oqtane.Shared;
namespace Oqtane.Modules.Controls namespace Oqtane.Modules.Controls
{ {
public class LocalizableComponent : ModuleControlBase public class LocalizableComponent : ModuleControlBase
{ {
[Inject] public IStringLocalizerFactory LocalizerFactory { get; set; }
private IStringLocalizer _localizer; private IStringLocalizer _localizer;
[Parameter] [Parameter]
@ -30,22 +30,15 @@ namespace Oqtane.Modules.Controls
var key = $"{ResourceKey}.{propertyName}"; var key = $"{ResourceKey}.{propertyName}";
var value = Localize(key); var value = Localize(key);
if (value == key) if (value == key || value == String.Empty)
{ {
// Returns default property value (English version) instead of ResourceKey.PropertyName // return default property value if key does not exist in resource file or value is empty
return propertyValue; return propertyValue;
} }
else else
{ {
if (value == String.Empty) // return localized value
{ return value;
// Returns default property value (English version)
return propertyValue;
}
else
{
return value;
}
} }
} }
@ -53,24 +46,15 @@ namespace Oqtane.Modules.Controls
{ {
IsLocalizable = false; IsLocalizable = false;
if (string.IsNullOrEmpty(ResourceType)) if (String.IsNullOrEmpty(ResourceType))
{ {
ResourceType = ModuleState?.ModuleType; ResourceType = ModuleState?.ModuleType;
} }
if (!String.IsNullOrEmpty(ResourceKey) && !string.IsNullOrEmpty(ResourceType)) if (!String.IsNullOrEmpty(ResourceKey) && !String.IsNullOrEmpty(ResourceType))
{ {
var moduleType = Type.GetType(ResourceType); _localizer = LocalizerFactory.Create(ResourceType);
if (moduleType != null) IsLocalizable = true;
{
using (var scope = ServiceActivator.GetScope())
{
var localizerFactory = scope.ServiceProvider.GetService<IStringLocalizerFactory>();
_localizer = localizerFactory.Create(moduleType);
IsLocalizable = true;
}
}
} }
} }
} }

View File

@ -1,5 +1,6 @@
@namespace Oqtane.Modules.Controls @namespace Oqtane.Modules.Controls
@inherits ModuleControlBase @inherits ModuleControlBase
@inject IStringLocalizerFactory LocalizerFactory
@typeparam TableItem @typeparam TableItem
@if (ItemList != null) @if (ItemList != null)
@ -48,7 +49,7 @@
<a class="page-link" @onclick=@(async () => UpdateList(_pages))><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => UpdateList(_pages))><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a>
</li> </li>
<li class="page-item disabled"> <li class="page-item disabled">
<a class="page-link" style="white-space: nowrap;">Page @_page of @_pages</a> <a class="page-link" style="white-space: nowrap;">@Localizer["PageOfPages", _page, _pages]</a>
</li> </li>
</ul> </ul>
} }
@ -156,67 +157,73 @@
<a class="page-link" @onclick=@(async () => UpdateList(_pages))><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => UpdateList(_pages))><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a>
</li> </li>
<li class="page-item disabled"> <li class="page-item disabled">
<a class="page-link" style="white-space: nowrap;">Page @_page of @_pages</a> <a class="page-link" style="white-space: nowrap;">@Localizer["PageOfPages", _page, _pages]</a>
</li> </li>
</ul> </ul>
} }
} }
@code { @code {
private int _pages = 0; private IStringLocalizer Localizer;
private int _page = 1; private int _pages = 0;
private int _maxItems = 10; private int _page = 1;
private int _displayPages = 5; private int _maxItems = 10;
private int _startPage = 0; private int _displayPages = 5;
private int _endPage = 0; private int _startPage = 0;
private int _columns = 0; private int _endPage = 0;
private int _columns = 0;
[Parameter]
public string Format { get; set; } // Table or Grid
[Parameter]
public string Toolbar { get; set; } // Top, Bottom or Both
[Parameter]
public RenderFragment Header { get; set; } = null; // only applicable to Table layouts
[Parameter]
public RenderFragment<TableItem> Row { get; set; } = null; // required
[Parameter]
public RenderFragment<TableItem> Detail { get; set; } = null; // only applicable to Table layouts
[Parameter]
public IEnumerable<TableItem> Items { get; set; } // the IEnumerable data source
[Parameter]
public string PageSize { get; set; } // number of items to display on a page
[Parameter]
public string Columns { get; set; } // only applicable to Grid layouts - default is zero indicating use responsive behavior
[Parameter]
public string CurrentPage { get; set; } // sets the initial page to display
[Parameter]
public string DisplayPages { get; set; } // maximum number of page numbers to display for user selection
[Parameter]
public string Class { get; set; } // class for the containing element - ie. <table> for Table or <div> for Grid
[Parameter]
public string RowClass { get; set; } // class for row element - ie. <tr> for Table or <div> for Grid
[Parameter] [Parameter]
public string ColumnClass { get; set; } // class for column element - only applicable to Grid format public string Format { get; set; } // Table or Grid
[Parameter] [Parameter]
public Action<int> OnPageChange { get; set; } // a method to be executed in the calling component when the page changes public string Toolbar { get; set; } // Top, Bottom or Both
private IEnumerable<TableItem> ItemList { get; set; } [Parameter]
public RenderFragment Header { get; set; } = null; // only applicable to Table layouts
protected override void OnParametersSet() [Parameter]
{ public RenderFragment<TableItem> Row { get; set; } = null; // required
[Parameter]
public RenderFragment<TableItem> Detail { get; set; } = null; // only applicable to Table layouts
[Parameter]
public IEnumerable<TableItem> Items { get; set; } // the IEnumerable data source
[Parameter]
public string PageSize { get; set; } // number of items to display on a page
[Parameter]
public string Columns { get; set; } // only applicable to Grid layouts - default is zero indicating use responsive behavior
[Parameter]
public string CurrentPage { get; set; } // sets the initial page to display
[Parameter]
public string DisplayPages { get; set; } // maximum number of page numbers to display for user selection
[Parameter]
public string Class { get; set; } // class for the containing element - ie. <table> for Table or <div> for Grid
[Parameter]
public string RowClass { get; set; } // class for row element - ie. <tr> for Table or <div> for Grid
[Parameter]
public string ColumnClass { get; set; } // class for column element - only applicable to Grid format
[Parameter]
public Action<int> OnPageChange { get; set; } // a method to be executed in the calling component when the page changes
private IEnumerable<TableItem> ItemList { get; set; }
protected override void OnInitialized()
{
Localizer = LocalizerFactory.Create(GetType().FullName);
}
protected override void OnParametersSet()
{
if (string.IsNullOrEmpty(Format)) if (string.IsNullOrEmpty(Format))
{ {
Format = "Table"; Format = "Table";

View File

@ -127,11 +127,10 @@
_permissionnames = PermissionNames; _permissionnames = PermissionNames;
} }
_roles = await RoleService.GetRolesAsync(ModuleState.SiteId); _roles = await RoleService.GetRolesAsync(ModuleState.SiteId, true);
_roles.Insert(0, new Role { Name = RoleNames.Everyone }); if (!UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
_roles.Add(new Role { Name = RoleNames.Host }); _roles.RemoveAll(item => item.Name == RoleNames.Host);
} }
_permissions = new List<PermissionString>(); _permissions = new List<PermissionString>();
@ -254,6 +253,7 @@
permission = _permissions[i]; permission = _permissions[i];
List<string> ids = permission.Permissions.Split(';', StringSplitOptions.RemoveEmptyEntries).ToList(); List<string> ids = permission.Permissions.Split(';', StringSplitOptions.RemoveEmptyEntries).ToList();
ids.Remove("!" + RoleNames.Everyone); // remove deny all users ids.Remove("!" + RoleNames.Everyone); // remove deny all users
ids.Remove("!" + RoleNames.Unauthenticated); // remove deny unauthenticated
ids.Remove("!" + RoleNames.Registered); // remove deny registered users ids.Remove("!" + RoleNames.Registered); // remove deny registered users
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {

View File

@ -150,10 +150,6 @@
// preserve a copy of the rich text content (Quill sanitizes content so we need to retrieve it from the editor) // preserve a copy of the rich text content (Quill sanitizes content so we need to retrieve it from the editor)
_originalrichhtml = await interop.GetHtml(_editorElement); _originalrichhtml = await interop.GetHtml(_editorElement);
} }
else
{
await interop.LoadEditorContent(_editorElement, _richhtml);
}
} }
public void CloseFileManager() public void CloseFileManager()

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.1</Version> <Version>3.1.3</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.1</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.3</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

@ -150,4 +150,7 @@
<data name="EditModule.Text" xml:space="preserve"> <data name="EditModule.Text" xml:space="preserve">
<value>Edit</value> <value>Edit</value>
</data> </data>
<data name="Modules" xml:space="preserve">
<value>Modules</value>
</data>
</root> </root>

View File

@ -142,7 +142,7 @@
<value>Site Settings Saved</value> <value>Site Settings Saved</value>
</data> </data>
<data name="Message.Aliases.Taken" xml:space="preserve"> <data name="Message.Aliases.Taken" xml:space="preserve">
<value>The Default Alias Has Not Been Specified Or An Alias Was Specified That Has Already Been Used For Another Site</value> <value>An Alias Was Specified That Has Already Been Used For Another Site</value>
</data> </data>
<data name="Message.Required.SiteName" xml:space="preserve"> <data name="Message.Required.SiteName" xml:space="preserve">
<value>You Must Provide A Site Name, Alias, And Default Theme/Container</value> <value>You Must Provide A Site Name, Alias, And Default Theme/Container</value>
@ -324,4 +324,13 @@
<data name="Aliases.Heading" xml:space="preserve"> <data name="Aliases.Heading" xml:space="preserve">
<value>Aliases</value> <value>Aliases</value>
</data> </data>
<data name="AliasName" xml:space="preserve">
<value>Name</value>
</data>
<data name="AliasDefault" xml:space="preserve">
<value>Default?</value>
</data>
<data name="Confirm.Alias.Delete" xml:space="preserve">
<value>Are You Sure You Wish To Delete {0}?</value>
</data>
</root> </root>

View File

@ -219,4 +219,10 @@
<data name="DeleteAllNotifications.Text" xml:space="preserve"> <data name="DeleteAllNotifications.Text" xml:space="preserve">
<value>Delete ALL Notifications</value> <value>Delete ALL Notifications</value>
</data> </data>
<data name="Notifications.Heading" xml:space="preserve">
<value>Notifications</value>
</data>
<data name="Profile.Heading" xml:space="preserve">
<value>Profile</value>
</data>
</root> </root>

View File

@ -186,4 +186,16 @@
<data name="Password.Placeholder" xml:space="preserve"> <data name="Password.Placeholder" xml:space="preserve">
<value>Password</value> <value>Password</value>
</data> </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

@ -369,12 +369,6 @@
<data name="Required" xml:space="preserve"> <data name="Required" xml:space="preserve">
<value>Required</value> <value>Required</value>
</data> </data>
<data name="CreatedOn" xml:space="preserve">
<value>Created On</value>
</data>
<data name="LastIPAddress" xml:space="preserve">
<value>Last IP Address</value>
</data>
<data name="LastLoginOn" xml:space="preserve"> <data name="LastLoginOn" xml:space="preserve">
<value>Last Login</value> <value>Last Login</value>
</data> </data>
@ -384,4 +378,10 @@
<data name="IdentifierClaimType.Text" xml:space="preserve"> <data name="IdentifierClaimType.Text" xml:space="preserve">
<value>Identifier Claim:</value> <value>Identifier Claim:</value>
</data> </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>
</root> </root>

View File

@ -141,4 +141,7 @@
<data name="Success.File.Upload" xml:space="preserve"> <data name="Success.File.Upload" xml:space="preserve">
<value>File Upload Succeeded</value> <value>File Upload Succeeded</value>
</data> </data>
<data name="Message.File.Restricted" xml:space="preserve">
<value>Files With Extension Of {0} Are Restricted From Upload. Please Contact Your Administrator For More Information.</value>
</data>
</root> </root>

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="PageOfPages" xml:space="preserve">
<value>Page {0} of {1}</value>
</data>
</root>

View File

@ -327,4 +327,13 @@
<data name="ShowPassword" xml:space="preserve"> <data name="ShowPassword" xml:space="preserve">
<value>Show</value> <value>Show</value>
</data> </data>
<data name="PageOfPages" xml:space="preserve">
<value>Page {0} of {1}</value>
</data>
<data name="Url Mappings" xml:space="preserve">
<value>Url Mappings</value>
</data>
<data name="Visitor Management" xml:space="preserve">
<value>Visitor Management</value>
</data>
</root> </root>

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;
@ -14,10 +15,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 FileService : ServiceBase, IFileService public class FileService : ServiceBase, IFileService
{ {
private readonly SiteState _siteState;
private readonly IJSRuntime _jsRuntime; private readonly IJSRuntime _jsRuntime;
public FileService(HttpClient http, SiteState siteState, IJSRuntime jsRuntime) : base(http, siteState) public FileService(HttpClient http, SiteState siteState, IJSRuntime jsRuntime) : base(http, siteState)
{ {
_siteState = siteState;
_jsRuntime = jsRuntime; _jsRuntime = jsRuntime;
} }
@ -30,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)
@ -42,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)
@ -80,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

@ -9,15 +9,19 @@
if (childPage.PageId == PageState.Page.PageId) if (childPage.PageId == PageState.Page.PageId)
{ {
<a class="nav-link active px-3" href="@GetUrl(childPage)" target="@GetTarget(childPage)"> <a class="nav-link active px-3" href="@GetUrl(childPage)" target="@GetTarget(childPage)">
<span class="w-100" data-bs-toggle="collapse" data-bs-target=".navbar-collapse.show">
<span class="@childPage.Icon" aria-hidden="true" /> <span class="@childPage.Icon" aria-hidden="true" />
@childPage.Name <span class="visually-hidden-focusable">(current)</span> @childPage.Name <span class="visually-hidden-focusable">(current)</span>
</span>
</a> </a>
} }
else else
{ {
<a class="nav-link px-3" href="@GetUrl(childPage)" target="@GetTarget(childPage)"> <a class="nav-link px-3" href="@GetUrl(childPage)" target="@GetTarget(childPage)">
<span class="@childPage.Icon" aria-hidden="true" /> <span class="w-100" data-bs-toggle="collapse" data-bs-target=".navbar-collapse.show">
@childPage.Name <span class="@childPage.Icon" aria-hidden="true" />
@childPage.Name
</span>
</a> </a>
} }
} }
@ -34,8 +38,10 @@ else
{ {
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" href="@GetUrl(childPage)" target="@GetTarget(childPage)"> <a class="nav-link active" href="@GetUrl(childPage)" target="@GetTarget(childPage)">
<span class="@childPage.Icon" aria-hidden="true" /> <span class="w-100" data-bs-toggle="collapse" data-bs-target=".navbar-collapse.show">
@childPage.Name <span class="visually-hidden-focusable">(current)</span> <span class="@childPage.Icon" aria-hidden="true" />
@childPage.Name <span class="visually-hidden-focusable">(current)</span>
</span>
</a> </a>
</li> </li>
} }
@ -43,8 +49,10 @@ else
{ {
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="@GetUrl(childPage)" target="@GetTarget(childPage)"> <a class="nav-link" href="@GetUrl(childPage)" target="@GetTarget(childPage)">
<span class="@childPage.Icon" aria-hidden="true" /> <span class="w-100" data-bs-toggle="collapse" data-bs-target=".navbar-collapse.show">
@childPage.Name <span class="@childPage.Icon" aria-hidden="true" />
@childPage.Name
</span>
</a> </a>
</li> </li>
} }

View File

@ -9,8 +9,10 @@
{ {
<li class="nav-item px-3" style="margin-left: @(childPage.Level * 15)px;"> <li class="nav-item px-3" style="margin-left: @(childPage.Level * 15)px;">
<a class="nav-link active" href="@GetUrl(childPage)" target="@GetTarget(childPage)"> <a class="nav-link active" href="@GetUrl(childPage)" target="@GetTarget(childPage)">
<span class="@childPage.Icon" aria-hidden="true" /> <span class="w-100" data-bs-toggle="collapse" data-bs-target=".navbar-collapse.show">
@childPage.Name <span class="visually-hidden-focusable">(current)</span> <span class="@childPage.Icon" aria-hidden="true" />
@childPage.Name <span class="visually-hidden-focusable">(current)</span>
</span>
</a> </a>
</li> </li>
} }
@ -18,8 +20,10 @@
{ {
<li class="nav-item px-3" style="margin-left: @(childPage.Level * 15)px;"> <li class="nav-item px-3" style="margin-left: @(childPage.Level * 15)px;">
<a class="nav-link" href="@GetUrl(childPage)" target="@GetTarget(childPage)"> <a class="nav-link" href="@GetUrl(childPage)" target="@GetTarget(childPage)">
<span class="@childPage.Icon" aria-hidden="true" /> <span class="w-100" data-bs-toggle="collapse" data-bs-target=".navbar-collapse.show">
@childPage.Name <span class="@childPage.Icon" aria-hidden="true" />
@childPage.Name
</span>
</a> </a>
</li> </li>
} }
@ -38,8 +42,10 @@ else
{ {
<li class="nav-item px-3" style="margin-left: @(childPage.Level * 15)px;"> <li class="nav-item px-3" style="margin-left: @(childPage.Level * 15)px;">
<a class="nav-link active" href="@GetUrl(childPage)" target="@GetTarget(childPage)"> <a class="nav-link active" href="@GetUrl(childPage)" target="@GetTarget(childPage)">
<span class="@childPage.Icon" aria-hidden="true" /> <span class="w-100" data-bs-toggle="collapse" data-bs-target=".navbar-collapse.show">
@childPage.Name <span class="visually-hidden-focusable">(current)</span> <span class="@childPage.Icon" aria-hidden="true" />
@childPage.Name <span class="visually-hidden-focusable">(current)</span>
</span>
</a> </a>
</li> </li>
} }
@ -47,8 +53,10 @@ else
{ {
<li class="nav-item px-3" style="margin-left: @(childPage.Level * 15)px;"> <li class="nav-item px-3" style="margin-left: @(childPage.Level * 15)px;">
<a class="nav-link" href="@GetUrl(childPage)" target="@GetTarget(childPage)"> <a class="nav-link" href="@GetUrl(childPage)" target="@GetTarget(childPage)">
<span class="@childPage.Icon" aria-hidden="true" /> <span class="w-100" data-bs-toggle="collapse" data-bs-target=".navbar-collapse.show">
@childPage.Name <span class="@childPage.Icon" aria-hidden="true" />
@childPage.Name
</span>
</a> </a>
</li> </li>
} }

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

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<Version>3.1.1</Version> <Version>3.1.3</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.1</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.3</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.1</version> <version>3.1.3</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.1</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.3</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.1</Version> <Version>3.1.3</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.1</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.3</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.1</version> <version>3.1.3</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.1</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.3</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.1</Version> <Version>3.1.3</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.1</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.3</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.1</version> <version>3.1.3</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.1</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.3</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.1</Version> <Version>3.1.3</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.1</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.3</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.1</version> <version>3.1.3</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.1</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.3</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.1</version> <version>3.1.3</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.1</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.3</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.1</version> <version>3.1.3</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.1/Oqtane.Framework.3.1.1.Upgrade.zip</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v3.1.3/Oqtane.Framework.3.1.2.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.1</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.3</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.1</version> <version>3.1.3</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.1</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.3</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.1</version> <version>3.1.3</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.1</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.3</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.1</version> <version>3.1.3</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.1</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.3</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.1.Install.zip" -Force Compress-Archive -Path "..\Oqtane.Server\bin\Release\net6.0\publish\*" -DestinationPath "Oqtane.Framework.3.1.3.Install.zip" -Force

View File

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

View File

@ -255,7 +255,7 @@ namespace Oqtane.Controllers
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Create, "File Could Not Be Downloaded From Url {Url} {Error}", url, ex.Message); _logger.Log(LogLevel.Error, this, LogFunction.Create, ex, "File Could Not Be Downloaded From Url {Url} {Error}", url, ex.Message);
} }
} }
else else
@ -276,9 +276,17 @@ namespace Oqtane.Controllers
return; return;
} }
if (!formfile.FileName.IsPathOrFileValid()) // ensure filename is valid
string token = ".part_";
if (!formfile.FileName.IsPathOrFileValid() || !formfile.FileName.Contains(token))
{
return;
}
// check for allowable file extensions (ignore token)
var extension = Path.GetExtension(formfile.FileName.Substring(0, formfile.FileName.IndexOf(token))).Replace(".", "");
if (!Constants.UploadableFiles.Split(',').Contains(extension.ToLower()))
{ {
HttpContext.Response.StatusCode = (int)HttpStatusCode.Conflict;
return; return;
} }
@ -331,9 +339,9 @@ namespace Oqtane.Controllers
{ {
string merged = ""; string merged = "";
// parse the filename which is in the format of filename.ext.part_x_y // parse the filename which is in the format of filename.ext.part_001_999
string token = ".part_"; string token = ".part_";
string parts = Path.GetExtension(filename)?.Replace(token, ""); // returns "x_y" string parts = Path.GetExtension(filename)?.Replace(token, ""); // returns "001_999"
int totalparts = int.Parse(parts?.Substring(parts.IndexOf("_") + 1)); int totalparts = int.Parse(parts?.Substring(parts.IndexOf("_") + 1));
filename = Path.GetFileNameWithoutExtension(filename); // base filename filename = Path.GetFileNameWithoutExtension(filename); // base filename
@ -370,23 +378,15 @@ namespace Oqtane.Controllers
System.IO.File.Delete(filepart); System.IO.File.Delete(filepart);
} }
// check for allowable file extensions // remove file if it already exists
if (!Constants.UploadableFiles.Split(',').Contains(Path.GetExtension(filename)?.ToLower().Replace(".", ""))) if (System.IO.File.Exists(Path.Combine(folder, filename)))
{ {
System.IO.File.Delete(Path.Combine(folder, filename + ".tmp")); System.IO.File.Delete(Path.Combine(folder, filename));
} }
else
{
// remove file if it already exists
if (System.IO.File.Exists(Path.Combine(folder, filename)))
{
System.IO.File.Delete(Path.Combine(folder, filename));
}
// rename file now that the entire process is completed // rename file now that the entire process is completed
System.IO.File.Move(Path.Combine(folder, filename + ".tmp"), Path.Combine(folder, filename)); System.IO.File.Move(Path.Combine(folder, filename + ".tmp"), Path.Combine(folder, filename));
_logger.Log(LogLevel.Information, this, LogFunction.Create, "File Uploaded {File}", Path.Combine(folder, filename)); _logger.Log(LogLevel.Information, this, LogFunction.Create, "File Uploaded {File}", Path.Combine(folder, filename));
}
merged = filename; merged = filename;
} }
@ -394,8 +394,7 @@ namespace Oqtane.Controllers
// clean up file parts which are more than 2 hours old ( which can happen if a prior file upload failed ) // clean up file parts which are more than 2 hours old ( which can happen if a prior file upload failed )
var cleanupFiles = Directory.EnumerateFiles(folder, "*" + token + "*") var cleanupFiles = Directory.EnumerateFiles(folder, "*" + token + "*")
.Where(f => Path.GetExtension(f).StartsWith(token)); .Where(f => Path.GetExtension(f).StartsWith(token) && !Path.GetFileName(f).StartsWith(filename));
foreach (var file in cleanupFiles) foreach (var file in cleanupFiles)
{ {
var createdDate = System.IO.File.GetCreationTime(file).ToUniversalTime(); var createdDate = System.IO.File.GetCreationTime(file).ToUniversalTime();
@ -601,9 +600,9 @@ namespace Oqtane.Controllers
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Error Creating Image For File {FilePath} {Width} {Height} {Mode} {Rotate} {Error}", filepath, width, height, mode, rotate, ex.Message); _logger.Log(LogLevel.Error, this, LogFunction.Security, ex, "Error Creating Image For File {FilePath} {Width} {Height} {Mode} {Rotate} {Error}", filepath, width, height, mode, rotate, ex.Message);
imagepath = ""; imagepath = "";
} }

View File

@ -63,6 +63,8 @@ namespace Oqtane.Controllers
module.CreatedOn = pagemodule.Module.CreatedOn; module.CreatedOn = pagemodule.Module.CreatedOn;
module.ModifiedBy = pagemodule.Module.ModifiedBy; module.ModifiedBy = pagemodule.Module.ModifiedBy;
module.ModifiedOn = pagemodule.Module.ModifiedOn; module.ModifiedOn = pagemodule.Module.ModifiedOn;
module.DeletedBy = pagemodule.DeletedBy;
module.DeletedOn = pagemodule.DeletedOn;
module.IsDeleted = pagemodule.IsDeleted; module.IsDeleted = pagemodule.IsDeleted;
module.PageModuleId = pagemodule.PageModuleId; module.PageModuleId = pagemodule.PageModuleId;
@ -139,24 +141,41 @@ namespace Oqtane.Controllers
[Authorize(Roles = RoleNames.Registered)] [Authorize(Roles = RoleNames.Registered)]
public Module Put(int id, [FromBody] Module module) public Module Put(int id, [FromBody] Module module)
{ {
if (ModelState.IsValid && module.SiteId == _alias.SiteId && _modules.GetModule(module.ModuleId, false) != null && _userPermissions.IsAuthorized(User, EntityNames.Module, module.ModuleId, PermissionNames.Edit)) var _module = _modules.GetModule(module.ModuleId, false);
if (ModelState.IsValid && module.SiteId == _alias.SiteId && _module != null && _userPermissions.IsAuthorized(User, EntityNames.Module, module.ModuleId, PermissionNames.Edit))
{ {
module = _modules.UpdateModule(module); module = _modules.UpdateModule(module);
if (module.AllPages)
{
var pageModule = _pageModules.GetPageModules(module.SiteId).FirstOrDefault(item => item.ModuleId == module.ModuleId);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Module Updated {Module}", module);
var pages = _pages.GetPages(module.SiteId).ToList(); if (_module.AllPages != module.AllPages)
foreach (Page page in pages) {
var pageModules = _pageModules.GetPageModules(module.SiteId).ToList();
if (module.AllPages)
{ {
if (page.PageId != pageModule.PageId && !page.Path.StartsWith("admin/")) var pageModule = _pageModules.GetPageModule(module.PageModuleId);
var pages = _pages.GetPages(module.SiteId).ToList();
foreach (Page page in pages)
{ {
_pageModules.AddPageModule(new PageModule { PageId = page.PageId, ModuleId = pageModule.ModuleId, Title = pageModule.Title, Pane = pageModule.Pane, Order = pageModule.Order, ContainerType = pageModule.ContainerType }); if (!pageModules.Exists(item => item.ModuleId == module.ModuleId && item.PageId == page.PageId) && !page.Path.StartsWith("admin/"))
{
_pageModules.AddPageModule(new PageModule { PageId = page.PageId, ModuleId = pageModule.ModuleId, Title = pageModule.Title, Pane = pageModule.Pane, Order = pageModule.Order, ContainerType = pageModule.ContainerType });
}
}
}
else
{
foreach (var pageModule in pageModules)
{
if (pageModule.ModuleId == module.ModuleId && pageModule.PageModuleId != module.PageModuleId)
{
_pageModules.DeletePageModule(pageModule.PageModuleId);
}
} }
} }
} }
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Site, _alias.SiteId); _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Site, _alias.SiteId);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Module Updated {Module}", module);
} }
else else
{ {

View File

@ -53,7 +53,7 @@ namespace Oqtane.Controllers
catch (Exception ex) catch (Exception ex)
{ {
results.Add(new Dictionary<string, string>() { { "Error", ex.Message } }); 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, ex, "Sql Query {Query} Executed on Tenant {TenantId} Resulted In An Error {Error}", sqlquery.Query, sqlquery.TenantId, ex.Message);
} }
sqlquery.Results = results; sqlquery.Results = results;
return sqlquery; return sqlquery;

View File

@ -327,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)
{ {
@ -353,7 +355,7 @@ 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);
} }

View File

@ -39,32 +39,24 @@ namespace Oqtane.Controllers
public IEnumerable<UserRole> Get(string siteid, string userid = null, string rolename = null) 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 && (userid != null || rolename != null))
{ {
int UserId = (int.TryParse(userid, out UserId)) ? UserId : -1; var userroles = _userRoles.GetUserRoles(SiteId).ToList();
if (User.IsInRole(RoleNames.Admin) || ((userid == null || _userPermissions.GetUser().UserId == UserId) && (rolename == null || (User.IsInRole(rolename) && rolename != RoleNames.Registered)))) if (userid != null)
{ {
var userroles = _userRoles.GetUserRoles(SiteId).ToList(); int UserId = int.TryParse(userid, out UserId) ? UserId : -1;
if (userid != null) userroles = userroles.Where(item => item.UserId == UserId).ToList();
{
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 if (rolename != null)
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized UserRole Get Attempt For Site {SiteId} User {UserId} Role {RoleName}", siteid, userid, rolename); userroles = userroles.Where(item => item.Role.Name == rolename).ToList();
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
} }
var user = _userPermissions.GetUser();
for (int i = 0; i < userroles.Count(); i++)
{
userroles[i] = Filter(userroles[i], user.UserId);
}
return userroles.OrderBy(u => u.User.DisplayName);
} }
else else
{ {
@ -82,16 +74,7 @@ namespace Oqtane.Controllers
var userrole = _userRoles.GetUserRole(id); var userrole = _userRoles.GetUserRole(id);
if (userrole != null && SiteValid(userrole.Role.SiteId)) if (userrole != null && SiteValid(userrole.Role.SiteId))
{ {
if (User.IsInRole(RoleNames.Admin) || User.Identity.Name?.ToLower() != userrole.User.Username.ToLower() || User.IsInRole(userrole.Role.Name)) return Filter(userrole, _userPermissions.GetUser().UserId);
{
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
{ {
@ -101,7 +84,7 @@ namespace Oqtane.Controllers
} }
} }
private UserRole Filter(UserRole userrole) private UserRole Filter(UserRole userrole, int userid)
{ {
if (userrole != null) if (userrole != null)
{ {
@ -110,7 +93,7 @@ namespace Oqtane.Controllers
userrole.User.TwoFactorCode = ""; userrole.User.TwoFactorCode = "";
userrole.User.TwoFactorExpiry = null; userrole.User.TwoFactorExpiry = null;
if (!User.IsInRole(RoleNames.Admin) && User.Identity.Name?.ToLower() != userrole.User.Username.ToLower()) if (!User.IsInRole(RoleNames.Admin) && userid != userrole.User.UserId)
{ {
userrole.User.Email = ""; userrole.User.Email = "";
userrole.User.PhotoFileId = null; userrole.User.PhotoFileId = null;

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

@ -66,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);
}
};
}
} }
}); });
@ -100,6 +114,22 @@ namespace Oqtane.Extensions
options.Events.OnTicketReceived = OnTicketReceived; 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);
}
};
}
} }
}); });
@ -111,6 +141,7 @@ namespace Oqtane.Extensions
// OAuth 2.0 // OAuth 2.0
var email = ""; var email = "";
var id = ""; var id = "";
var claims = "";
if (context.Options.UserInformationEndpoint != "") if (context.Options.UserInformationEndpoint != "")
{ {
@ -123,16 +154,16 @@ namespace Oqtane.Extensions
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();
// parse json output // parse json output
var idClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", ""); var idClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", "");
var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", ""); var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", "");
if (!output.StartsWith("[") && !output.EndsWith("]")) if (!claims.StartsWith("[") && !claims.EndsWith("]"))
{ {
output = "[" + output + "]"; // convert to json array claims = "[" + claims + "]"; // convert to json array
} }
JsonNode items = JsonNode.Parse(output)!; JsonNode items = JsonNode.Parse(claims)!;
foreach (var item in items.AsArray()) foreach (var item in items.AsArray())
{ {
if (item[emailClaimType] != null) if (item[emailClaimType] != null)
@ -156,12 +187,12 @@ namespace Oqtane.Extensions
catch (Exception ex) catch (Exception ex)
{ {
var _logger = context.HttpContext.RequestServices.GetRequiredService<ILogManager>(); var _logger = context.HttpContext.RequestServices.GetRequiredService<ILogManager>();
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "An Error Occurred Accessing The User Info Endpoint - {Error}", ex.Message); _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, ex, "An Error Occurred Accessing The User Info Endpoint - {Error}", ex.Message);
} }
} }
// validate user // validate user
var identity = await ValidateUser(email, id, context.HttpContext); var identity = await ValidateUser(email, id, claims, context.HttpContext);
if (identity.Label == ExternalLoginStatus.Success) if (identity.Label == ExternalLoginStatus.Success)
{ {
identity.AddClaim(new Claim("access_token", context.AccessToken)); identity.AddClaim(new Claim("access_token", context.AccessToken));
@ -193,9 +224,10 @@ namespace Oqtane.Extensions
var id = context.Principal.FindFirstValue(idClaimType); 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());
// validate user // validate user
var identity = await ValidateUser(email, id, context.HttpContext); var identity = await ValidateUser(email, id, claims, context.HttpContext);
if (identity.Label == ExternalLoginStatus.Success) if (identity.Label == ExternalLoginStatus.Success)
{ {
identity.AddClaim(new Claim("access_token", context.SecurityToken.RawData)); identity.AddClaim(new Claim("access_token", context.SecurityToken.RawData));
@ -229,7 +261,7 @@ namespace Oqtane.Extensions
return Task.CompletedTask; return Task.CompletedTask;
} }
private static async Task<ClaimsIdentity> ValidateUser(string email, string id, HttpContext httpContext) 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); ClaimsIdentity identity = new ClaimsIdentity(Constants.AuthenticationScheme);
@ -241,144 +273,151 @@ namespace Oqtane.Extensions
var _users = httpContext.RequestServices.GetRequiredService<IUserRepository>(); var _users = httpContext.RequestServices.GetRequiredService<IUserRepository>();
User user = null; User user = null;
// verify if external user is already registerd for this site if (!string.IsNullOrEmpty(id))
var _identityUserManager = httpContext.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
var identityuser = await _identityUserManager.FindByLoginAsync(providerType + ":" + alias.SiteId.ToString(), id);
if (identityuser != null)
{ {
user = _users.GetUser(identityuser.UserName); // verify if external user is already registered for this site
} var _identityUserManager = httpContext.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
else var identityuser = await _identityUserManager.FindByLoginAsync(providerType + ":" + alias.SiteId.ToString(), id);
{ if (identityuser != null)
if (EmailValid(email, httpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", "")))
{ {
bool duplicates = false; user = _users.GetUser(identityuser.UserName);
try }
else
{
if (EmailValid(email, httpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", "")))
{ {
identityuser = await _identityUserManager.FindByEmailAsync(email); bool duplicates = false;
} try
catch
{
// FindByEmailAsync will throw an error if the email matches multiple user accounts
duplicates = true;
}
if (identityuser == null)
{
if (duplicates)
{ {
identity.Label = ExternalLoginStatus.DuplicateEmail; identityuser = await _identityUserManager.FindByEmailAsync(email);
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Multiple Users Exist With Email Address {Email}. Login Denied.", email);
} }
else catch
{ {
if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:CreateUsers", "true"))) // FindByEmailAsync will throw an error if the email matches multiple user accounts
duplicates = true;
}
if (identityuser == null)
{
if (duplicates)
{ {
identityuser = new IdentityUser(); identity.Label = ExternalLoginStatus.DuplicateEmail;
identityuser.UserName = email; _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Multiple Users Exist With Email Address {Email}. Login Denied.", email);
identityuser.Email = email; }
identityuser.EmailConfirmed = true; else
var result = await _identityUserManager.CreateAsync(identityuser, DateTime.UtcNow.ToString("yyyy-MMM-dd-HH-mm-ss")); {
if (result.Succeeded) if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:CreateUsers", "true")))
{ {
user = new User 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)
{ {
SiteId = alias.SiteId, user = new User
Username = email, {
DisplayName = email, SiteId = alias.SiteId,
Email = email, Username = email,
LastLoginOn = null, DisplayName = email,
LastIPAddress = "" Email = email,
}; LastLoginOn = null,
user = _users.AddUser(user); LastIPAddress = ""
};
user = _users.AddUser(user);
if (user != null) if (user != null)
{ {
var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>(); var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>();
string url = httpContext.Request.Scheme + "://" + alias.Name; 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!"; 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); var notification = new Notification(user.SiteId, user, "User Account Notification", body);
_notifications.AddNotification(notification); _notifications.AddNotification(notification);
// add user login // add user login
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + alias.SiteId.ToString(), id, providerName)); 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); _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 else
{ {
identity.Label = ExternalLoginStatus.UserNotCreated; identity.Label = ExternalLoginStatus.UserNotCreated;
_logger.Log(user.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add User {Email}", email); _logger.Log(user.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add Identity User {Email} {Error}", email, result.Errors.ToString());
} }
} }
else else
{ {
identity.Label = ExternalLoginStatus.UserNotCreated; identity.Label = ExternalLoginStatus.UserDoesNotExist;
_logger.Log(user.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add Identity User {Email} {Error}", email, result.Errors.ToString()); _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
{
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
{
var logins = await _identityUserManager.GetLoginsAsync(identityuser);
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 else
{ {
// provider keys do not match var logins = await _identityUserManager.GetLoginsAsync(identityuser);
identity.Label = ExternalLoginStatus.ProviderKeyMismatch; var login = logins.FirstOrDefault(item => item.LoginProvider == (providerType + ":" + alias.SiteId.ToString()));
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Key Does Not Match For User {Username}. Login Denied.", identityuser.UserName); 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 // email invalid
{
identity.Label = ExternalLoginStatus.InvalidEmail;
if (!string.IsNullOrEmpty(email))
{
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The Email Address {Email} Is Invalid Or Does Not Match The Domain Filter Criteria. Login Denied.", email);
}
else
{
_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);
} }
} }
} }
else // email invalid
// manage user
if (user != null)
{ {
identity.Label = ExternalLoginStatus.InvalidEmail; // create claims identity
if (!string.IsNullOrEmpty(email)) var _userRoles = httpContext.RequestServices.GetRequiredService<IUserRoleRepository>();
{ identity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList());
_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); identity.Label = ExternalLoginStatus.Success;
}
else // update user
{ user.LastLoginOn = DateTime.UtcNow;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return An Email To Uniquely Identify The User."); user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString();
} _users.UpdateUser(user);
_logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} Using Provider {Provider}", user.Username, providerName);
} }
} }
else // id invalid
// manage user
if (user != null)
{ {
// create claims identity _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);
var _userRoles = httpContext.RequestServices.GetRequiredService<IUserRoleRepository>();
identity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList());
identity.Label = ExternalLoginStatus.Success;
// update user
user.LastLoginOn = DateTime.UtcNow;
user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString();
_users.UpdateUser(user);
_logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} Using Provider {Provider}", user.Username, providerName);
} }
return identity; return identity;
} }

View File

@ -120,6 +120,10 @@ namespace Oqtane.Infrastructure
mailMessage.Body += "Subject: " + notification.Subject + "\n\n"; mailMessage.Body += "Subject: " + notification.Subject + "\n\n";
mailMessage.Body += notification.Body; mailMessage.Body += notification.Body;
// set encoding
mailMessage.SubjectEncoding = System.Text.Encoding.UTF8;
mailMessage.BodyEncoding = System.Text.Encoding.UTF8;
// send mail // send mail
try try
{ {

View File

@ -203,12 +203,15 @@ namespace Oqtane.Infrastructure
} }
if (Enum.Parse<LogLevel>(log.Level) >= notifylevel) if (Enum.Parse<LogLevel>(log.Level) >= notifylevel)
{ {
var alias = _tenantManager.GetAlias();
foreach (var userrole in _userRoles.GetUserRoles(log.SiteId.Value)) foreach (var userrole in _userRoles.GetUserRoles(log.SiteId.Value))
{ {
if (userrole.Role.Name == RoleNames.Host) if (userrole.Role.Name == RoleNames.Host)
{ {
var url = _accessor.HttpContext.Request.Scheme + "://" + _tenantManager.GetAlias().Name + "/admin/log"; var subject = $"{alias.Name} Site {log.Level} Notification";
var notification = new Notification(log.SiteId.Value, userrole.User, "Site " + log.Level + " Notification", "Please visit " + url + " for more information"); var url = $"{_accessor.HttpContext.Request.Scheme}://{alias.Name}/admin/log?id={log.LogId}";
string body = $"Log Message: {log.Message}\n\nPlease visit {url} for more information";
var notification = new Notification(log.SiteId.Value, userrole.User, subject, body);
_notifications.AddNotification(notification); _notifications.AddNotification(notification);
} }
} }

View File

@ -50,6 +50,9 @@ namespace Oqtane.Infrastructure
case "3.0.1": case "3.0.1":
Upgrade_3_0_1(tenant, scope); Upgrade_3_0_1(tenant, scope);
break; break;
case "3.1.3":
Upgrade_3_1_3(tenant, scope);
break;
} }
} }
} }
@ -182,5 +185,15 @@ namespace Oqtane.Infrastructure
sites.CreatePages(site, pageTemplates); sites.CreatePages(site, pageTemplates);
} }
} }
private void Upgrade_3_1_3(Tenant tenant, IServiceScope scope)
{
var roles = scope.ServiceProvider.GetRequiredService<IRoleRepository>();
if (!roles.GetRoles(-1, true).ToList().Where(item => item.Name == RoleNames.Unauthenticated).Any())
{
roles.AddRole(new Role { SiteId = null, Name = RoleNames.Unauthenticated, Description = RoleNames.Unauthenticated, IsAutoAssigned = false, IsSystem = true });
}
}
} }
} }

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

@ -18,6 +18,7 @@ namespace Oqtane.Migrations.EntityBuilders
_migrationBuilder = migrationBuilder; _migrationBuilder = migrationBuilder;
ActiveDatabase = database; ActiveDatabase = database;
ForeignKeys = new List<ForeignKey<TEntityBuilder>>(); ForeignKeys = new List<ForeignKey<TEntityBuilder>>();
Schema = null;
} }
protected IDatabase ActiveDatabase { get; } protected IDatabase ActiveDatabase { get; }
@ -30,6 +31,8 @@ namespace Oqtane.Migrations.EntityBuilders
protected List<ForeignKey<TEntityBuilder>> ForeignKeys { get; } protected List<ForeignKey<TEntityBuilder>> ForeignKeys { get; }
protected string Schema { get; init; }
private string RewriteName(string name) private string RewriteName(string name)
{ {
return ActiveDatabase.RewriteName(name); return ActiveDatabase.RewriteName(name);
@ -187,9 +190,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)
@ -308,7 +322,7 @@ namespace Oqtane.Migrations.EntityBuilders
/// </summary> /// </summary>
public void Create() public void Create()
{ {
_migrationBuilder.CreateTable(RewriteName(EntityTableName), BuildTable, null, AddKeys); _migrationBuilder.CreateTable(RewriteName(EntityTableName), BuildTable, Schema, AddKeys);
} }
/// <summary> /// <summary>

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

@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<Configurations>Debug;Release</Configurations> <Configurations>Debug;Release</Configurations>
<Version>3.1.1</Version> <Version>3.1.3</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -11,7 +11,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.1</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.3</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

@ -96,7 +96,7 @@ namespace Oqtane.Repository
alias = new Alias(); alias = new Alias();
alias.TenantId = aliases.First().TenantId; alias.TenantId = aliases.First().TenantId;
alias.SiteId = aliases.First().SiteId; alias.SiteId = aliases.First().SiteId;
alias.Name = url; alias.Name = segments[0]; // root domain
alias.IsDefault = false; alias.IsDefault = false;
alias = AddAlias(alias); alias = AddAlias(alias);
} }

View File

@ -94,16 +94,18 @@ namespace Oqtane.Repository
List<Role> roles = _roleRepository.GetRoles(site.SiteId, true).ToList(); List<Role> roles = _roleRepository.GetRoles(site.SiteId, true).ToList();
if (!roles.Where(item => item.Name == RoleNames.Everyone).Any()) if (!roles.Where(item => item.Name == RoleNames.Everyone).Any())
{ {
_roleRepository.AddRole(new Role {SiteId = null, Name = RoleNames.Everyone, Description = "All Users", IsAutoAssigned = false, IsSystem = true}); _roleRepository.AddRole(new Role {SiteId = null, Name = RoleNames.Everyone, Description = RoleNames.Everyone, IsAutoAssigned = false, IsSystem = true});
}
if (!roles.Where(item => item.Name == RoleNames.Unauthenticated).Any())
{
_roleRepository.AddRole(new Role { SiteId = null, Name = RoleNames.Unauthenticated, Description = RoleNames.Unauthenticated, IsAutoAssigned = false, IsSystem = true });
} }
if (!roles.Where(item => item.Name == RoleNames.Host).Any()) if (!roles.Where(item => item.Name == RoleNames.Host).Any())
{ {
_roleRepository.AddRole(new Role {SiteId = null, Name = RoleNames.Host, Description = "Application Administrators", IsAutoAssigned = false, IsSystem = true}); _roleRepository.AddRole(new Role {SiteId = null, Name = RoleNames.Host, Description = RoleNames.Host, IsAutoAssigned = false, IsSystem = true});
} }
_roleRepository.AddRole(new Role {SiteId = site.SiteId, Name = RoleNames.Registered, Description = RoleNames.Registered, IsAutoAssigned = true, IsSystem = true});
_roleRepository.AddRole(new Role {SiteId = site.SiteId, Name = RoleNames.Registered, Description = "Registered Users", IsAutoAssigned = true, IsSystem = true}); _roleRepository.AddRole(new Role {SiteId = site.SiteId, Name = RoleNames.Admin, Description = RoleNames.Admin, IsAutoAssigned = false, IsSystem = true});
_roleRepository.AddRole(new Role {SiteId = site.SiteId, Name = RoleNames.Admin, Description = "Site Administrators", IsAutoAssigned = false, IsSystem = true});
_profileRepository.AddProfile(new Profile _profileRepository.AddProfile(new Profile
{SiteId = site.SiteId, Name = "FirstName", Title = "First Name", Description = "Your First Or Given Name", Category = "Name", ViewOrder = 1, MaxLength = 50, DefaultValue = "", IsRequired = false, IsPrivate = false, Options = ""}); {SiteId = site.SiteId, Name = "FirstName", Title = "First Name", Description = "Your First Or Given Name", Category = "Name", ViewOrder = 1, MaxLength = 50, DefaultValue = "", IsRequired = false, IsPrivate = false, Options = ""});

View File

@ -62,6 +62,7 @@ namespace Oqtane.Repository
public UrlMapping GetUrlMapping(int siteId, string url) public UrlMapping GetUrlMapping(int siteId, string url)
{ {
url = (url.Length > 750) ? url.Substring(0, 750) : url;
var urlMapping = _db.UrlMapping.Where(item => item.SiteId == siteId && item.Url == url).FirstOrDefault(); var urlMapping = _db.UrlMapping.Where(item => item.SiteId == siteId && item.Url == url).FirstOrDefault();
if (urlMapping == null) if (urlMapping == null)
{ {

View File

@ -2,19 +2,24 @@ using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.Logging;
using Oqtane.Shared;
namespace Oqtane.Security namespace Oqtane.Security
{ {
public class AutoValidateAntiforgeryTokenFilter : IAsyncAuthorizationFilter, IAntiforgeryPolicy public class AutoValidateAntiforgeryTokenFilter : IAsyncAuthorizationFilter, IAntiforgeryPolicy
{ {
private readonly IAntiforgery _antiforgery; private readonly IAntiforgery _antiforgery;
private readonly ILogger<AutoValidateAntiforgeryTokenFilter> _filelogger;
public AutoValidateAntiforgeryTokenFilter(IAntiforgery antiforgery) public AutoValidateAntiforgeryTokenFilter(IAntiforgery antiforgery, ILogger<AutoValidateAntiforgeryTokenFilter> filelogger)
{ {
_antiforgery = antiforgery; _antiforgery = antiforgery;
_filelogger = filelogger;
} }
public async Task OnAuthorizationAsync(AuthorizationFilterContext context) public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
@ -38,6 +43,7 @@ namespace Oqtane.Security
catch catch
{ {
context.Result = new AntiforgeryValidationFailedResult(); context.Result = new AntiforgeryValidationFailedResult();
_filelogger.LogError(Utilities.LogMessage(this, $"AutoValidateAntiforgeryTokenFilter Failure For {context.HttpContext.Request.GetEncodedUrl()}"));
} }
} }
} }

View File

@ -294,7 +294,7 @@ Oqtane.Interop = {
} }
return files; return files;
}, },
uploadFiles: function (posturl, folder, id) { uploadFiles: function (posturl, folder, id, antiforgerytoken) {
var fileinput = document.getElementById(id + 'FileInput'); var fileinput = document.getElementById(id + 'FileInput');
var files = fileinput.files; var files = fileinput.files;
var progressinfo = document.getElementById(id + 'ProgressInfo'); var progressinfo = document.getElementById(id + 'ProgressInfo');
@ -326,6 +326,7 @@ Oqtane.Interop = {
var FileName = file.name + ".part_" + PartCount.toString().padStart(3, '0') + "_" + TotalParts.toString().padStart(3, '0'); var FileName = file.name + ".part_" + PartCount.toString().padStart(3, '0') + "_" + TotalParts.toString().padStart(3, '0');
var data = new FormData(); var data = new FormData();
data.append('__RequestVerificationToken', antiforgerytoken);
data.append('folder', folder); data.append('folder', folder);
data.append('formfile', Chunk, FileName); data.append('formfile', Chunk, FileName);
var request = new XMLHttpRequest(); var request = new XMLHttpRequest();
@ -343,10 +344,17 @@ Oqtane.Interop = {
progressinfo.innerHTML = file.name + ' 100%'; progressinfo.innerHTML = file.name + ' 100%';
progressbar.value = 1; progressbar.value = 1;
}; };
request.upload.onerror = function () {
progressinfo.innerHTML = file.name + ' Error: ' + xhr.status;
progressbar.value = 0;
};
request.send(data); request.send(data);
} }
if (i === files.length - 1) {
fileinput.value = '';
}
} }
fileinput.value = '';
}, },
refreshBrowser: function (reload, wait) { refreshBrowser: function (reload, wait) {
setInterval(function () { setInterval(function () {

View File

@ -60,6 +60,7 @@ namespace Oqtane.Models
public Dictionary<string, string> Settings { get; set; } public Dictionary<string, string> Settings { get; set; }
#region PageModule properties #region PageModule properties
[NotMapped] [NotMapped]
public int PageModuleId { get; set; } public int PageModuleId { get; set; }
@ -68,6 +69,7 @@ namespace Oqtane.Models
/// </summary> /// </summary>
[NotMapped] [NotMapped]
public int PageId { get; set; } public int PageId { get; set; }
[NotMapped] [NotMapped]
public string Title { get; set; } public string Title { get; set; }
@ -76,8 +78,10 @@ namespace Oqtane.Models
/// </summary> /// </summary>
[NotMapped] [NotMapped]
public string Pane { get; set; } public string Pane { get; set; }
[NotMapped] [NotMapped]
public int Order { get; set; } public int Order { get; set; }
[NotMapped] [NotMapped]
public string ContainerType { get; set; } public string ContainerType { get; set; }

View File

@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<Configurations>Debug;Release</Configurations> <Configurations>Debug;Release</Configurations>
<Version>3.1.1</Version> <Version>3.1.3</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -11,7 +11,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.1</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.3</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

@ -104,11 +104,14 @@ namespace Oqtane.Security
private static bool IsAllowed(int userId, string roles, string permission) private static bool IsAllowed(int userId, string roles, string permission)
{ {
if (permission == RoleNames.Unauthenticated)
{
return userId == -1;
}
if ("[" + userId + "]" == permission) if ("[" + userId + "]" == permission)
{ {
return true; return true;
} }
if (roles != null) if (roles != null)
{ {
return roles.IndexOf(";" + permission + ";") != -1; return roles.IndexOf(";" + permission + ";") != -1;

View File

@ -4,8 +4,8 @@ namespace Oqtane.Shared
{ {
public class Constants public class Constants
{ {
public static readonly string Version = "3.1.1"; public static readonly string Version = "3.1.3";
public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1"; public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3";
public const string PackageId = "Oqtane.Framework"; public const string PackageId = "Oqtane.Framework";
public const string UpdaterPackageId = "Oqtane.Updater"; public const string UpdaterPackageId = "Oqtane.Updater";
public const string PackageRegistryUrl = "https://www.oqtane.net"; public const string PackageRegistryUrl = "https://www.oqtane.net";

View File

@ -1,8 +1,9 @@
namespace Oqtane.Shared { namespace Oqtane.Shared {
public class RoleNames { public class RoleNames {
public const string Everyone = "All Users"; public const string Everyone = "All Users";
public const string Host = "Host Users"; public const string Host = "Host Users";
public const string Admin = "Administrators"; public const string Admin = "Administrators";
public const string Registered = "Registered Users"; public const string Registered = "Registered Users";
public const string Unauthenticated = "Unauthenticated Users";
} }
} }

View File

@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<Configurations>Debug;Release</Configurations> <Configurations>Debug;Release</Configurations>
<Version>3.1.1</Version> <Version>3.1.3</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -11,7 +11,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.1</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.3</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

@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<Version>3.1.1</Version> <Version>3.1.3</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -11,7 +11,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.1</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.3</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

@ -57,8 +57,17 @@ There is a separate [Documentation repository](https://github.com/oqtane/oqtane.
# Roadmap # Roadmap
This project is open source, and therefore is a work in progress... This project is open source, and therefore is a work in progress...
Backlog (Not Yet Assigned) V.4.0.0 ( Q4 2022 )
- [ ] Allow language specification in Url (#1731) - [ ] MAUI / Blazor Hybrid support
V.3.1.3 ( June 2022 )
- [ ] Stabilization improvements
V.3.1.2 ( May 14, 2022 )
- [x] Stabilization improvements
V.3.1.1 ( May 3, 2022 )
- [x] Stabilization improvements
V.3.1.0 ( April 5, 2022 ) V.3.1.0 ( April 5, 2022 )
- [x] User account lockout support - [x] User account lockout support