Compare commits

..

128 Commits

Author SHA1 Message Date
f6cd04fdb8 Merge pull request #4662 from oqtane/master
Merge pull request #4661 from oqtane/dev
2024-09-23 16:26:39 -04:00
a5eede6c7a Merge pull request #4661 from oqtane/dev
5.2.3 release
2024-09-23 16:26:24 -04:00
2e83817c83 Merge pull request #4660 from sbwalker/dev
prepare for 5.2.3
2024-09-23 16:18:10 -04:00
82aea40ae4 prepare for 5.2.3 2024-09-23 16:17:55 -04:00
1b289eae24 Merge pull request #4659 from sbwalker/dev
use RoleName rather than RoleId for consistency
2024-09-23 16:08:02 -04:00
81420b2c88 use RoleName rather than RoleId for consistency 2024-09-23 16:07:49 -04:00
775731b745 Merge pull request #4657 from thabaum/patch-9
FIX #4655 - autocomplete="off" for SMTP Username/Password
2024-09-23 15:44:06 -04:00
489e7d4a67 Merge pull request #4658 from sbwalker/dev
fix RoleId missing from Permission Clone method
2024-09-23 15:38:51 -04:00
b6508764d8 fix RoleId missing from Permission Clone method 2024-09-23 15:38:35 -04:00
6dedd87305 autocomplete="off" for SMTP Username/Password 2024-09-23 12:19:49 -07:00
89fa29b310 Merge pull request #4653 from sbwalker/dev
improve scroll position script
2024-09-23 09:55:59 -04:00
be5df9c22a improve scroll position script 2024-09-23 09:55:44 -04:00
db17739716 Update README.md 2024-09-23 07:48:14 -04:00
1439362a5e Merge pull request #4651 from oqtane/master
5.2.2 Release
2024-09-23 07:39:21 -04:00
65c1b04772 Merge pull request #4650 from oqtane/dev
5.2.2 Release
2024-09-23 07:39:02 -04:00
83d30ebdc4 Merge pull request #4645 from sbwalker/dev
fix #4638 - add Logout Everywhere option to User Profile
2024-09-20 15:18:38 -04:00
b7928a5255 fix #4638 - add Logout Everywhere option to User Profile 2024-09-20 15:18:25 -04:00
a83ff9253d Merge pull request #4644 from sbwalker/dev
fix code formatting
2024-09-20 12:54:44 -04:00
8cdcdaf6d9 fix code formatting 2024-09-20 12:54:34 -04:00
d208edf153 Merge pull request #4643 from sbwalker/dev
fix #4641 - highlight default site theme in list of themes in page management
2024-09-20 12:28:06 -04:00
8ada4765b1 fix #4641 - highlight default site theme in list of themes in page management 2024-09-20 12:27:49 -04:00
9d3a808c24 Merge pull request #4642 from sbwalker/dev
fix #4826 - improve url handling
2024-09-20 12:09:16 -04:00
828eb80266 fix #4826 - improve url handling 2024-09-20 12:09:04 -04:00
0639f6c1d1 Merge pull request #4640 from sbwalker/dev
fix #4628 - preserve Url for all ModuleActions
2024-09-19 14:09:55 -04:00
49b971280f fix #4628 - preserve Url for all ModuleActions 2024-09-19 14:09:45 -04:00
3682a1010d Merge pull request #4637 from sbwalker/dev
synchronize interop.js with .NET MAUI
2024-09-19 12:14:36 -04:00
f8b58866dc synchronize interop.js with .NET MAUI 2024-09-19 12:14:24 -04:00
5d8d815d84 Merge pull request #4636 from zyhfish/task/4610-loadjs-issue
Fix #4610: wait for js loaded.
2024-09-19 12:10:55 -04:00
Ben
67743c7597 Fix #4610: wait for js loaded. 2024-09-19 23:34:59 +08:00
b33cc90447 Merge pull request #4635 from sbwalker/dev
use deep cloning to not muttate cache
2024-09-19 09:41:24 -04:00
78177f7890 use deep cloning to not muttate cache 2024-09-19 09:41:11 -04:00
3c417bfa99 Merge pull request #4633 from sbwalker/dev
fix trimming of site, page, and module settings
2024-09-18 18:32:04 -04:00
f2c8d80ff8 fix trimming of site, page, and module settings 2024-09-18 18:31:40 -04:00
8c1eb1e19f Merge pull request #4632 from sbwalker/dev
fix #4257 - load settings resources
2024-09-18 15:22:58 -04:00
eb7188e81b fix #4257 - load settings resources 2024-09-18 15:22:44 -04:00
f352fc5c67 Merge pull request #4631 from sbwalker/dev
on .NET MAUI provide message to user if Security Token has not been configured
2024-09-18 11:28:30 -04:00
ac313722f9 on .NET MAUI provide message to user if Security Token has not been configured 2024-09-18 11:28:11 -04:00
64c7f1962c Merge pull request #4630 from zyhfish/task/sync-maui-resources
sync changes to maui resources.
2024-09-18 10:46:34 -04:00
Ben
c83f994b21 sync changes to maui resources. 2024-09-18 22:44:14 +08:00
10f38a72f7 Merge pull request #4629 from sbwalker/dev
fix #4628 - preserve ReturnUrl when navigating to Settings component
2024-09-18 09:43:11 -04:00
da35434f58 fix #4628 - preserve ReturnUrl when navigating to Settings component 2024-09-18 09:42:56 -04:00
c79409e094 Merge pull request #4627 from sbwalker/dev
use FileLogger as fallback in LogManager when site cannot be determined
2024-09-18 07:38:20 -04:00
355ce00968 use FileLogger as fallback in LogManager when site cannot be determined 2024-09-18 07:37:52 -04:00
56c832f3ba Merge pull request #4626 from sbwalker/dev
fix logic to force authenticated user to provide email address so it works in static rendering
2024-09-17 17:29:30 -04:00
40abb2720e fix logic to force authenticated user to provide email address so it works in static rendering 2024-09-17 17:29:11 -04:00
9b6051afee Merge pull request #4625 from sbwalker/dev
update nuget.exe to latest and fix nuspec specification of readme
2024-09-17 15:19:57 -04:00
dcf6f26792 update nuget.exe to latest and fix nuspec specification of readme 2024-09-17 15:19:40 -04:00
85734c1146 Merge pull request #4624 from sbwalker/dev
remove unecessary using
2024-09-17 11:54:33 -04:00
c3fb8fcb6e remove unecessary using 2024-09-17 11:54:21 -04:00
013bbc1638 Merge pull request #4623 from sbwalker/dev
fix external login
2024-09-17 11:53:47 -04:00
b0669a3b60 fix external login 2024-09-17 11:53:34 -04:00
6f5da1ce7c Merge pull request #4622 from sbwalker/dev
allow external login to support SecurityStamp
2024-09-17 09:28:47 -04:00
3351732a2f allow external login to support SecurityStamp 2024-09-17 09:28:35 -04:00
d4d4034ecd Merge pull request #4621 from sbwalker/dev
remove unecessary usings
2024-09-17 09:07:57 -04:00
b45e2742c3 remove unecessary usings 2024-09-17 09:07:45 -04:00
42eba290e4 Merge pull request #4620 from sbwalker/dev
allow JwtMiddleware to support SecurityStamp
2024-09-17 09:07:14 -04:00
32d1e08b57 allow JwtMiddleware to support SecurityStamp 2024-09-17 09:06:49 -04:00
7b7d19da7c Merge pull request #4619 from sbwalker/dev
fix commented logic
2024-09-17 08:55:09 -04:00
f78e400918 fix commented logic 2024-09-17 08:54:52 -04:00
be4e9bf7e9 Merge pull request #4618 from sbwalker/dev
fix #4580 - add logout everywhere support using SecurityStamp
2024-09-17 08:45:41 -04:00
48f2079f88 fix #4580 - add logout everywhere support using SecurityStamp 2024-09-17 08:45:27 -04:00
36ad1ceef2 Merge pull request #4617 from sbwalker/dev
fix #4607 - site level scripts added twice in static rendering
2024-09-16 13:22:10 -04:00
1f2e2148d5 fix #4607 - scripts added twice 2024-09-16 13:21:43 -04:00
76f3d345f9 Merge pull request #4616 from sbwalker/dev
include SecurityStamp in User object
2024-09-16 13:03:32 -04:00
f2c854b53a include SecurityStamp in User object 2024-09-16 13:03:21 -04:00
e2d336d90b Merge pull request #4613 from sbwalker/dev
further modifications for #4604 - support for site name in logo component
2024-09-15 09:30:24 -04:00
c74065ff26 further modifications for #4604 - support for site name in logo component 2024-09-15 09:30:04 -04:00
b6d97dc5d5 Merge pull request #4612 from sbwalker/dev
fix #4606 - allow logo to show site name as fallback (credit @JanOlsmar)
2024-09-13 16:13:14 -04:00
1c1c26948a fix #4606 - allow logo to show site name as fallback (credit @JanOlsmar) 2024-09-13 16:13:01 -04:00
d954e3ffb7 Merge pull request #4608 from hishamco/toggle-edit-mode
Fix issue in toggle edit mode
2024-09-13 15:54:37 -04:00
d9759a95eb Merge pull request #4611 from sbwalker/dev
fix #4607 - Site HeadContent scripts being added twice
2024-09-13 15:19:31 -04:00
d196402dd0 fix #4607 - Site HeadContent scripts being added twice 2024-09-13 15:18:12 -04:00
0f2b5531de Merge pull request #4601 from hishamco/module-settings
Add null operator in ModuleState.ModuleDefinition
2024-09-13 14:13:40 -04:00
26e4398905 Address feedback 2024-09-13 21:12:18 +03:00
06995f22fe Merge pull request #4609 from sbwalker/dev
improve support for external login roles
2024-09-13 07:35:10 -04:00
caa2073d48 improve support for external login roles 2024-09-13 07:34:57 -04:00
d9466fe4bc Fix issue in toggle edit mode 2024-09-13 14:15:34 +03:00
72e623a3a7 Merge pull request #4606 from sbwalker/dev
fix #4598 - user experience improvements for file upload
2024-09-12 14:04:48 -04:00
69bc06685f fix #4598 - user experience improvements for file upload 2024-09-12 14:04:35 -04:00
4d4a7bfd0d Merge pull request #4605 from sbwalker/dev
fix #4600 - filter user settings in API layer
2024-09-11 17:21:24 -04:00
044cee30a5 fix #4600 - filter user settings in API layer 2024-09-11 17:21:12 -04:00
3c0c5aed60 Add null operator in ModuleState.ModuleDefinition 2024-09-11 02:09:02 +03:00
e194971727 Merge pull request #4597 from sbwalker/dev
fix #4503 - ensure all state is initialized before rendering
2024-09-09 09:17:40 -04:00
bbe85def23 fix #4503 - ensure all state is initialized before rendering 2024-09-09 09:17:27 -04:00
9c2d53f2ae Merge pull request #4596 from sbwalker/dev
fix #4575 - add support for DateOnly and TimeOnly columns in migrations
2024-09-08 11:43:50 -04:00
1cf36e2156 fix #4575 - add support for DateOnly and TimeOnly columns in migrations 2024-09-08 11:43:36 -04:00
ce96e309af Merge pull request #4594 from sbwalker/dev
reverting #4507 - permission grid
2024-09-06 11:57:38 -04:00
9ea7dc8b3c reverting #4507 2024-09-06 11:57:15 -04:00
282242efd2 Merge pull request #4593 from mdmontesinos/dev
Fix IconOnly not working
2024-09-06 10:47:45 -04:00
351ba22d64 Fix IconOnly not working 2024-09-06 16:35:44 +02:00
2916625747 Merge pull request #4588 from thabaum/patch-8
Update MySQL Project File Version - Prepare for 5.2.2
2024-09-05 10:32:54 -04:00
744dbeb7a3 Prepare for 5.2.2 2024-09-04 17:43:20 -07:00
fbb3c309ee Merge pull request #4583 from sbwalker/dev
prepare for 5.2.2
2024-09-02 12:09:24 -04:00
473c265bac prepare for 5.2.2 2024-09-02 12:09:06 -04:00
01b06c2c3c Merge pull request #4582 from sbwalker/dev
remove reference to HttpContext as it is not used
2024-09-02 11:08:38 -04:00
8b1f95c743 remove reference to HttpContext as it is not used 2024-09-02 11:08:25 -04:00
ff5dbec579 Merge pull request #4578 from sbwalker/dev
fix issue adding existing user to a new site
2024-08-29 17:53:27 -04:00
9620c5a98f fix issue adding existing user to a new site 2024-08-29 17:53:11 -04:00
7e849a2e95 Merge pull request #4577 from zyhfish/task/update-bootstrap-reference 2024-08-29 09:35:32 -04:00
Ben
229aed306e update bootstrap reference. 2024-08-29 19:41:27 +08:00
d718969cbd Merge pull request #4574 from zyhfish/task/display-upgrade-progress
display the upgrade progress.
2024-08-28 21:22:08 -04:00
8593f4c3a9 Merge pull request #4576 from sbwalker/dev
improve developer experience for Url helper methods
2024-08-28 21:21:57 -04:00
d4f71d5026 improve developer experience for Url helper methods 2024-08-28 21:21:41 -04:00
a683a5f206 Merge pull request #4573 from leigh-pointer/AddExisitng
Fix for Deleted Modules showing in AddExisting & CopyExisting dropdow…
2024-08-28 21:17:54 -04:00
Ben
7917cc3eb5 display the upgrade progress. 2024-08-28 21:48:40 +08:00
6cd7ca755e Fix for Deleted Modules showing in AddExisting & CopyExisting dropdown #4572 2024-08-28 10:57:42 +02:00
fb7dfdc800 Merge pull request #4571 from sbwalker/dev
provide better support for AllowTextInput on Search component
2024-08-27 12:16:50 -04:00
e096af320f provide better support for AllowTextInput on Search component 2024-08-27 12:16:36 -04:00
65bff8f511 Merge pull request #4570 from sbwalker/dev
allow progress indicator to be displayed in search results
2024-08-27 10:22:53 -04:00
ca19d8a842 allow progress indicator to be displayed in search results 2024-08-27 10:22:41 -04:00
cde08c64ce Update README.md 2024-08-26 17:00:07 -04:00
3d6d7b7cb9 Update README.md 2024-08-26 16:56:08 -04:00
a56ef6f398 Update README.md 2024-08-26 16:55:30 -04:00
5a38b6614d Update README.md 2024-08-26 16:54:45 -04:00
3d5c44e8aa Update README.md 2024-08-26 16:53:25 -04:00
46a68bd5e7 Update README.md 2024-08-26 16:51:53 -04:00
c77ded51a9 Merge pull request #4568 from sbwalker/dev
fix #4559 - prevednt Log fields from exceeding column length
2024-08-26 12:05:05 -04:00
59bd9fdc22 fix #4559 - prevednt Log fields from exceeding column length 2024-08-26 12:04:50 -04:00
b81941394c Merge pull request #4567 from sbwalker/dev
change parameter name to AllowTextInput for clarity
2024-08-26 10:47:29 -04:00
197d5ca1f2 change parameter name to AllowTextInput for clarity 2024-08-26 10:47:18 -04:00
97eb986fa6 Merge pull request #4566 from sbwalker/dev
fix Search theme control so that it checks if search is enabled for site, and include AllowInput parameter to control input textbox
2024-08-26 10:41:39 -04:00
aba3110e31 fix Search theme control so that it checks if search is enabled for site, and include AllowInput parameter to control input textbox 2024-08-26 10:41:22 -04:00
51c27be236 Merge pull request #4565 from sbwalker/dev
fix #4562 - module template issue caused by gitignore
2024-08-26 09:41:01 -04:00
592255284f fix #4562 - module template issue caused by gitignore 2024-08-26 09:38:10 -04:00
82fd41dd4c Merge pull request #4555 from sbwalker/dev
add readme.md to Oqtane nuget packages
2024-08-22 14:10:42 -04:00
4ae8df9652 add readme.md to Oqtane nuget packages 2024-08-22 14:09:59 -04:00
759b19e444 Update README.md 2024-08-22 12:23:21 -04:00
36f705e46f Update README.md 2024-08-22 12:23:09 -04:00
96 changed files with 1819 additions and 946 deletions

8
.gitignore vendored
View File

@ -22,10 +22,14 @@ Oqtane.Server/Packages
Oqtane.Server/wwwroot/Content
Oqtane.Server/wwwroot/Packages/*.log
Oqtane.Server/wwwroot/Modules
Oqtane.Server/wwwroot/Modules/*
!Oqtane.Server/wwwroot/Modules/Oqtane.Modules.*
!Oqtane.Server/wwwroot/Modules/Templates
Oqtane.Server/wwwroot/Modules/Templates/*
!Oqtane.Server/wwwroot/Modules/Templates/External
Oqtane.Server/wwwroot/Themes
Oqtane.Server/wwwroot/Themes/*
!Oqtane.Server/wwwroot/Themes/Oqtane.Themes.*
!Oqtane.Server/wwwroot/Themes/Templates
Oqtane.Server/wwwroot/Themes/Templates/*
Oqtane.Server/wwwroot/Themes/Templates/External

View File

@ -162,7 +162,7 @@
protected override async Task OnInitializedAsync()
{
// include CSS
var content = "<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css\" integrity=\"sha512-t4GWSVZO1eC8BM339Xd7Uphw5s17a86tIZIj8qRxhnKub6WoyhnrxeCIMeAqBPgdZGlCcG2PrZjMc+Wr78+5Xg==\" crossorigin=\"anonymous\" type=\"text/css\"/>";
var content = $"<link rel=\"stylesheet\" href=\"{Constants.BootstrapStylesheetUrl}\" integrity=\"{Constants.BootstrapStylesheetIntegrity}\" crossorigin=\"anonymous\" type=\"text/css\"/>";
SiteState.AppendHeadContent(content);
_togglePassword = SharedLocalizer["ShowPassword"];
@ -217,7 +217,7 @@
{
// include JavaScript
var interop = new Interop(JSRuntime);
await interop.IncludeScript("", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js", "sha512-VK2zcvntEufaimc+efOYi622VN5ZacdnufnmX7zIhCPmjhKnOi9ZDMtg1/ug5l183f19gG1/cBstPO4D8N/Img==", "anonymous", "", "head");
await interop.IncludeScript("", Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous", "", "head");
}
}

View File

@ -8,74 +8,77 @@
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<AuthorizeView Roles="@RoleNames.Registered">
<Authorizing>
<text>...</text>
</Authorizing>
<Authorized>
<ModuleMessage Message="@Localizer["Info.SignedIn"]" Type="MessageType.Info" />
</Authorized>
<NotAuthorized>
@if (!twofactor)
{
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="Oqtane-Modules-Admin-Login" @onkeypress="@(e => KeyPressed(e))">
@if (_allowexternallogin)
{
<button type="button" class="btn btn-primary" @onclick="ExternalLogin">@Localizer["Use"] @PageState.Site.Settings["ExternalLogin:ProviderName"]</button>
<br /><br />
}
@if (_allowsitelogin)
{
<div class="form-group">
<Label Class="control-label" For="username" HelpText="Please enter your Username" ResourceKey="Username">Username:</Label>
<input id="username" type="text" @ref="username" class="form-control" placeholder="@Localizer["Username.Placeholder"]" @bind="@_username" required />
</div>
<div class="form-group mt-2">
<Label Class="control-label" For="password" HelpText="Please enter your Password" ResourceKey="Password">Password:</Label>
<div class="input-group">
<input id="password" type="@_passwordtype" name="Password" class="form-control" placeholder="@Localizer["Password.Placeholder"]" @bind="@_password" required />
@if (PageState.User != null)
{
<ModuleMessage Message="@Localizer["Info.SignedIn"]" Type="MessageType.Info" />
}
else
{
@if (!twofactor)
{
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="Oqtane-Modules-Admin-Login" @onkeypress="@(e => KeyPressed(e))">
@if (_allowexternallogin)
{
<button type="button" class="btn btn-primary" @onclick="ExternalLogin">@Localizer["Use"] @PageState.Site.Settings["ExternalLogin:ProviderName"]</button>
<br />
<br />
}
@if (_allowsitelogin)
{
<div class="form-group">
<Label Class="control-label" For="username" HelpText="Please enter your Username" ResourceKey="Username">Username:</Label>
<input id="username" type="text" @ref="username" class="form-control" placeholder="@Localizer["Username.Placeholder"]" @bind="@_username" required />
</div>
<div class="form-group mt-2">
<Label Class="control-label" For="password" HelpText="Please enter your Password" ResourceKey="Password">Password:</Label>
<div class="input-group">
<input id="password" type="@_passwordtype" name="Password" class="form-control" placeholder="@Localizer["Password.Placeholder"]" @bind="@_password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
<div class="form-group mt-2">
@if (!_alwaysremember)
{
<div class="form-check">
<input id="remember" type="checkbox" class="form-check-input" @bind="@_remember" />
<Label Class="control-label" For="remember" HelpText="Specify if you would like to be signed back in automatically the next time you visit this site" ResourceKey="Remember">Remember Me?</Label>
</div>
}
</div>
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
<br /><br />
<button type="button" class="btn btn-secondary" @onclick="Forgot">@Localizer["ForgotPassword"]</button>
@if (PageState.Site.AllowRegistration)
</div>
</div>
<div class="form-group mt-2">
@if (!_alwaysremember)
{
<br /><br />
<NavLink href="@NavigateUrl("register")">@Localizer["Register"]</NavLink>
<div class="form-check">
<input id="remember" type="checkbox" class="form-check-input" @bind="@_remember" />
<Label Class="control-label" For="remember" HelpText="Specify if you would like to be signed back in automatically the next time you visit this site" ResourceKey="Remember">Remember Me?</Label>
</div>
}
</div>
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
<br />
<br />
<button type="button" class="btn btn-secondary" @onclick="Forgot">@Localizer["ForgotPassword"]</button>
@if (PageState.Site.AllowRegistration)
{
<br />
<br />
<NavLink href="@NavigateUrl("register")">@Localizer["Register"]</NavLink>
}
</div>
</form>
}
else
{
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container Oqtane-Modules-Admin-Login">
<div class="form-group">
<Label Class="control-label" For="code" HelpText="Please enter the secure verification code which was sent to you by email" ResourceKey="Code">Verification Code:</Label>
<input id="code" class="form-control" @bind="@_code" placeholder="@Localizer["Code.Placeholder"]" maxlength="6" required />
</div>
<br />
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
<button type="button" class="btn btn-secondary" @onclick="Reset">@SharedLocalizer["Cancel"]</button>
</div>
</form>
}
</NotAuthorized>
</AuthorizeView>
}
</div>
</form>
}
else
{
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container Oqtane-Modules-Admin-Login">
<div class="form-group">
<Label Class="control-label" For="code" HelpText="Please enter the secure verification code which was sent to you by email" ResourceKey="Code">Verification Code:</Label>
<input id="code" class="form-control" @bind="@_code" placeholder="@Localizer["Code.Placeholder"]" maxlength="6" required />
</div>
<br />
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
<button type="button" class="btn btn-secondary" @onclick="Reset">@SharedLocalizer["Cancel"]</button>
</div>
</form>
}
}
@code {
private bool _allowsitelogin = true;
@ -204,9 +207,9 @@
user = await UserService.VerifyTwoFactorAsync(user, _code);
}
if (user.IsAuthenticated)
if (user != null && user.IsAuthenticated)
{
await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username);
await logger.LogInformation(LogFunction.Security, "Login Successful For {Username} From IP Address {IPAddress}", _username, SiteState.RemoteIPAddress);
// return url is not specified if user navigated directly to login page
var returnurl = (!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : PageState.Alias.Path;
@ -228,7 +231,7 @@
}
else
{
if (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "required" || user.TwoFactorRequired)
if (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "required" || (user != null && user.TwoFactorRequired))
{
twofactor = true;
validated = false;

View File

@ -15,7 +15,7 @@
</div>
<button type="button" class="btn btn-success" @onclick="ExportModule">@Localizer["Export"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink>
@code {
private string _content = string.Empty;

View File

@ -17,7 +17,7 @@
</div>
<button type="button" class="btn btn-success" @onclick="ImportModule">@Localizer["Import"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink>
</form>
@code {

View File

@ -9,129 +9,133 @@
@inject IStringLocalizer<Settings> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<TabStrip>
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings">
@if (_containers != null)
{
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="module" HelpText="The name of the module" ResourceKey="Module">Module: </Label>
<div class="col-sm-9">
<input id="module" type="text" class="form-control" @bind="@_module" disabled />
@if (_initialized)
{
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<TabStrip>
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings">
@if (_containers != null)
{
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="module" HelpText="The name of the module" ResourceKey="Module">Module: </Label>
<div class="col-sm-9">
<input id="module" type="text" class="form-control" @bind="@_module" disabled />
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="title" HelpText="Enter the title of the module" ResourceKey="Title">Title: </Label>
<div class="col-sm-9">
<input id="title" type="text" class="form-control" @bind="@_title" required />
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="title" HelpText="Enter the title of the module" ResourceKey="Title">Title: </Label>
<div class="col-sm-9">
<input id="title" type="text" class="form-control" @bind="@_title" required />
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pane" HelpText="The pane where the module will be displayed" ResourceKey="Pane">Pane: </Label>
<div class="col-sm-9">
<select class="form-select" @bind="@_pane">
@foreach (string pane in PageState.Page.Panes)
{
<option value="@pane">@pane Pane</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="container" HelpText="Select the module's container" ResourceKey="Container">Container: </Label>
<div class="col-sm-9">
<select id="container" class="form-select" @bind="@_containerType" required>
@foreach (var container in _containers)
{
<option value="@container.TypeName">@container.Name</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="effectiveDate" HelpText="The date that this module is active" ResourceKey="EffectiveDate">Effective Date: </Label>
<div class="col-sm-9">
<input type="date" id="effectiveDate" class="form-control" @bind="@_effectivedate" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="expiryDate" HelpText="The date that this module expires" ResourceKey="ExpiryDate">Expiry Date: </Label>
<div class="col-sm-9">
<input type="date" id="expiryDate" class="form-control" @bind="@_expirydate" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allpages" HelpText="Indicate if this module should be displayed on all pages" ResourceKey="DisplayOnAllPages">Display On All Pages? </Label>
<div class="col-sm-9">
<select id="allpages" class="form-select" @bind="@_allPages" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="page" HelpText="The page that the module is located on" ResourceKey="Page">Page: </Label>
<div class="col-sm-9">
<select id="page" class="form-select" @bind="@_pageId" required>
@if (PageState.Page.UserId != null)
{
<option value="@PageState.Page.PageId">@(PageState.Page.Name)</option>
}
else
{
if (_pages != null)
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pane" HelpText="The pane where the module will be displayed" ResourceKey="Pane">Pane: </Label>
<div class="col-sm-9">
<select class="form-select" @bind="@_pane">
@foreach (string pane in PageState.Page.Panes)
{
foreach (Page p in _pages)
<option value="@pane">@pane Pane</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="container" HelpText="Select the module's container" ResourceKey="Container">Container: </Label>
<div class="col-sm-9">
<select id="container" class="form-select" @bind="@_containerType" required>
@foreach (var container in _containers)
{
<option value="@container.TypeName">@container.Name</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="effectiveDate" HelpText="The date that this module is active" ResourceKey="EffectiveDate">Effective Date: </Label>
<div class="col-sm-9">
<input type="date" id="effectiveDate" class="form-control" @bind="@_effectivedate" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="expiryDate" HelpText="The date that this module expires" ResourceKey="ExpiryDate">Expiry Date: </Label>
<div class="col-sm-9">
<input type="date" id="expiryDate" class="form-control" @bind="@_expirydate" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allpages" HelpText="Indicate if this module should be displayed on all pages" ResourceKey="DisplayOnAllPages">Display On All Pages? </Label>
<div class="col-sm-9">
<select id="allpages" class="form-select" @bind="@_allPages" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="page" HelpText="The page that the module is located on" ResourceKey="Page">Page: </Label>
<div class="col-sm-9">
<select id="page" class="form-select" @bind="@_pageId" required>
@if (PageState.Page.UserId != null)
{
<option value="@PageState.Page.PageId">@(PageState.Page.Name)</option>
}
else
{
if (_pages != null)
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, p.PermissionList))
foreach (Page p in _pages)
{
<option value="@p.PageId">@(new string('-', p.Level * 2))@(p.Name)</option>
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, p.PermissionList))
{
<option value="@p.PageId">@(new string('-', p.Level * 2))@(p.Name)</option>
}
}
}
}
}
</select>
</select>
</div>
</div>
</div>
</div>
}
</TabPanel>
<TabPanel Name="Permissions" Heading="Permissions" ResourceKey="Permissions">
@if (_permissions != null)
{
<div class="container">
<div class="row mb-1 align-items-center">
<PermissionGrid EntityName="@EntityNames.Module" PermissionNames="@_permissionNames" PermissionList="@_permissions" @ref="_permissionGrid" />
}
</TabPanel>
<TabPanel Name="Permissions" Heading="Permissions" ResourceKey="Permissions">
@if (_permissions != null)
{
<div class="container">
<div class="row mb-1 align-items-center">
<PermissionGrid EntityName="@EntityNames.Module" PermissionNames="@_permissionNames" PermissionList="@_permissions" @ref="_permissionGrid" />
</div>
</div>
</div>
}
</TabPanel>
@if (_moduleSettingsType != null)
{
<TabPanel Name="ModuleSettings" Heading="@_moduleSettingsTitle" ResourceKey="ModuleSettings">
@ModuleSettingsComponent
</TabPanel>
}
</TabPanel>
@if (_moduleSettingsType != null)
{
<TabPanel Name="ModuleSettings" Heading="@_moduleSettingsTitle" ResourceKey="ModuleSettings">
@ModuleSettingsComponent
</TabPanel>
}
@if (_containerSettingsType != null)
{
<TabPanel Name="ContainerSettings" Heading="Container Settings" ResourceKey="ContainerSettings">
@ContainerSettingsComponent
</TabPanel>
}
</TabStrip>
<br />
<button type="button" class="btn btn-success" @onclick="SaveModule">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon"></AuditInfo>
</form>
@if (_containerSettingsType != null)
{
<TabPanel Name="ContainerSettings" Heading="Container Settings" ResourceKey="ContainerSettings">
@ContainerSettingsComponent
</TabPanel>
}
</TabStrip>
<br />
<button type="button" class="btn btn-success" @onclick="SaveModule">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon"></AuditInfo>
</form>
}
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
private bool _initialized = false;
private ElementReference form;
private bool validated = false;
private List<ThemeControl> _containers = new List<ThemeControl>();
@ -163,7 +167,6 @@
{
SetModuleTitle(Localizer["ModuleSettings.Title"]);
_module = ModuleState.ModuleDefinition.Name;
_title = ModuleState.Title;
_moduleSettingsTitle = Localizer["ModuleSettings.Heading"];
_pane = ModuleState.Pane;
@ -182,6 +185,7 @@
if (ModuleState.ModuleDefinition != null)
{
_module = ModuleState.ModuleDefinition.Name;
_permissionNames = ModuleState.ModuleDefinition?.PermissionNames;
if (!string.IsNullOrEmpty(ModuleState.ModuleDefinition.SettingsType))
@ -231,6 +235,8 @@
};
}
}
_initialized = true;
}
private async Task SaveModule()

View File

@ -156,7 +156,14 @@
<select id="theme" class="form-select" value="@_themetype" @onchange="(e => ThemeChanged(e))" required>
@foreach (var theme in _themes)
{
<option value="@theme.TypeName">@theme.Name</option>
@if (theme.TypeName == PageState.Site.DefaultThemeType)
{
<option value="@theme.TypeName">*@theme.Name*</option>
}
else
{
<option value="@theme.TypeName">@theme.Name</option>
}
}
</select>
</div>

View File

@ -172,7 +172,14 @@
<select id="theme" class="form-select" value="@_themetype" @onchange="(e => ThemeChanged(e))" required>
@foreach (var theme in _themes)
{
<option value="@theme.TypeName">@theme.Name</option>
@if (theme.TypeName == PageState.Site.DefaultThemeType)
{
<option value="@theme.TypeName">*@theme.Name*</option>
}
else
{
<option value="@theme.TypeName">@theme.Name</option>
}
}
</select>
</div>
@ -262,7 +269,14 @@
<select id="theme" class="form-select" @bind="@_themetype" required>
@foreach (var theme in _themes)
{
<option value="@theme.TypeName">@theme.Name</option>
@if (theme.TypeName == PageState.Site.DefaultThemeType)
{
<option value="@theme.TypeName">*@theme.Name*</option>
}
else
{
<option value="@theme.TypeName">@theme.Name</option>
}
}
</select>
</div>

View File

@ -11,65 +11,64 @@
{
if (!_userCreated)
{
<AuthorizeView Roles="@RoleNames.Registered">
<Authorizing>
<text>...</text>
</Authorizing>
<Authorized>
<ModuleMessage Message="@Localizer["Info.Registration.Exists"]" Type="MessageType.Info" />
</Authorized>
<NotAuthorized>
<ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" />
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="Your username. Note that this field can not be modified once it is saved." ResourceKey="Username"></Label>
<div class="col-sm-9">
<input id="username" class="form-control" @bind="@_username" maxlength="256" required />
</div>
if (PageState.User != null)
{
<ModuleMessage Message="@Localizer["Info.Registration.Exists"]" Type="MessageType.Info" />
}
else
{
<ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" />
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="Your username. Note that this field can not be modified once it is saved." ResourceKey="Username"></Label>
<div class="col-sm-9">
<input id="username" class="form-control" @bind="@_username" maxlength="256" required />
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Please choose a sufficiently secure password and enter it here" ResourceKey="Password"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirm" HelpText="Enter your password again to confirm it matches the value entered above" ResourceKey="Confirm"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="Your email address where you wish to receive notifications" ResourceKey="Email"></Label>
<div class="col-sm-9">
<input id="email" class="form-control" @bind="@_email" maxlength="256" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="Your full name" ResourceKey="DisplayName"></Label>
<div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@_displayname" maxlength="50" />
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Please choose a sufficiently secure password and enter it here" ResourceKey="Password"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirm" HelpText="Enter your password again to confirm it matches the value entered above" ResourceKey="Confirm"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="Your email address where you wish to receive notifications" ResourceKey="Email"></Label>
<div class="col-sm-9">
<input id="email" class="form-control" @bind="@_email" maxlength="256" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="Your full name" ResourceKey="DisplayName"></Label>
<div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@_displayname" maxlength="50" />
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-primary" @onclick="Register">@Localizer["Register"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
@if (_allowsitelogin)
{
<br />
<button type="button" class="btn btn-primary" @onclick="Register">@Localizer["Register"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
@if (_allowsitelogin)
{
<br /><br />
<NavLink href="@NavigateUrl("login")">@Localizer["Login"]</NavLink>
}
</form>
</NotAuthorized>
</AuthorizeView>
<br />
<NavLink href="@NavigateUrl("login")">@Localizer["Login"]</NavLink>
}
</form>
}
}
}
else

View File

@ -7,6 +7,7 @@
@inject ISettingService SettingService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@attribute [StreamRendering] // attribute allows the progress indicator to be displayed
<div class="search-result-container">
<div class="row">

View File

@ -207,14 +207,14 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="Enter the username for your SMTP account" ResourceKey="SmtpUsername">Username: </Label>
<div class="col-sm-9">
<input id="username" class="form-control" @bind="@_smtpusername" />
<input id="username" class="form-control" @bind="@_smtpusername" autocomplete="off"/>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Enter the password for your SMTP account" ResourceKey="SmtpPassword">Password: </Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_smtppasswordtype" class="form-control" @bind="@_smtppassword" />
<input id="password" type="@_smtppasswordtype" class="form-control" @bind="@_smtppassword" autocomplete="off"/>
<button type="button" class="btn btn-secondary" @onclick="@ToggleSMTPPassword" tabindex="-1">@_togglesmtppassword</button>
</div>
</div>

View File

@ -9,6 +9,8 @@
@inject INotificationService NotificationService
@inject IFileService FileService
@inject IFolderService FolderService
@inject IJSRuntime jsRuntime
@inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@ -84,6 +86,7 @@
<br />
<button type="button" class="btn btn-success" @onclick="Save">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
<button type="button" class="btn btn-danger" @onclick="Logout">@Localizer["Logout Everywhere"]</button>
</TabPanel>
<TabPanel Name="Profile" ResourceKey="Profile">
<div class="container">
@ -518,6 +521,32 @@
}
}
private async Task Logout()
{
await logger.LogInformation("User Logout Everywhere For Username {Username}", PageState.User?.Username);
var url = NavigateUrl(""); // home page
if (PageState.Runtime == Shared.Runtime.Hybrid)
{
if (PageState.User != null)
{
// hybrid apps utilize an interactive logout
await UserService.LogoutUserEverywhereAsync(PageState.User);
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider));
authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(url, true);
}
}
else
{
// post to the Logout page to complete the logout process
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, returnurl = url, everywhere = true };
var interop = new Interop(jsRuntime);
await interop.SubmitForm(Utilities.TenantUrl(PageState.Alias, "/pages/logout/"), fields);
}
}
private bool ValidateProfiles()
{
foreach (Profile profile in profiles)

View File

@ -14,7 +14,6 @@
<TabPanel Name="Identity" ResourceKey="Identity">
@if (profiles != null)
{
<ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" />
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="A unique username for a user. Note that this field can not be modified once it is saved." ResourceKey="Username"></Label>
@ -22,24 +21,6 @@
<input id="username" class="form-control" @bind="@_username" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="The user's password. Please choose a password which is sufficiently secure." ResourceKey="Password"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirm" HelpText="Please enter the password again to confirm it matches with the value above" ResourceKey="Confirm"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="The email address where the user will receive notifications" ResourceKey="Email"></Label>
<div class="col-sm-9">
@ -123,12 +104,7 @@
@code {
private bool _initialized = false;
private string _passwordrequirements;
private string _username = string.Empty;
private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string _confirm = string.Empty;
private string _email = string.Empty;
private string _displayname = string.Empty;
private string _notify = "True";
@ -142,8 +118,6 @@
{
try
{
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_togglepassword = SharedLocalizer["ShowPassword"];
profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
settings = new Dictionary<string, string>();
_initialized = true;
@ -169,39 +143,32 @@
{
try
{
if (_username != string.Empty && _password != string.Empty && _confirm != string.Empty && _email != string.Empty)
if (_username != string.Empty && _email != string.Empty)
{
if (_password == _confirm)
if (ValidateProfiles())
{
if (ValidateProfiles())
var user = new User();
user.SiteId = PageState.Site.SiteId;
user.Username = _username;
user.Password = ""; // will be auto generated
user.Email = _email;
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
user.PhotoFileId = null;
user.SuppressNotification = !bool.Parse(_notify);
user = await UserService.AddUserAsync(user);
if (user != null)
{
var user = new User();
user.SiteId = PageState.Site.SiteId;
user.Username = _username;
user.Password = _password;
user.Email = _email;
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
user.PhotoFileId = null;
user.SuppressNotification = !bool.Parse(_notify);
user = await UserService.AddUserAsync(user);
if (user != null)
{
await SettingService.UpdateUserSettingsAsync(settings, user.UserId);
await logger.LogInformation("User Created {User}", user);
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
await logger.LogError("Error Adding User {Username} {Email}", _username, _email);
AddModuleMessage(Localizer["Error.User.AddCheckPass"], MessageType.Error);
}
await SettingService.UpdateUserSettingsAsync(settings, user.UserId);
await logger.LogInformation("User Created {User}", user);
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
await logger.LogError("Error Adding User {Username} {Email}", _username, _email);
AddModuleMessage(Localizer["Error.User.AddCheckPass"], MessageType.Error);
}
}
else
{
AddModuleMessage(Localizer["Message.Password.NoMatch"], MessageType.Warning);
}
}
else
@ -252,18 +219,4 @@
var value = (string)e.Value;
settings = SettingService.SetSetting(settings, SettingName, value);
}
private void TogglePassword()
{
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = SharedLocalizer["ShowPassword"];
}
}
}

View File

@ -333,12 +333,29 @@ else
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="roleclaimtype" HelpText="The name of the role claim provided by the provider" ResourceKey="RoleClaimType">Role Claim:</Label>
<Label Class="col-sm-3" For="roleclaimtype" HelpText="The name of the roles claim provided by the provider" ResourceKey="RoleClaimType">Roles Claim:</Label>
<div class="col-sm-9">
<input id="roleclaimtype" class="form-control" @bind="@_roleclaimtype" />
</div>
</div>
<div class="row mb-1 align-items-center">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="roleclaimmappings" HelpText="Optionally provide a comma delimited list of role names provided by the identity provider, as well as mappings to your site roles." ResourceKey="RoleClaimMappings">Role Claim Mappings:</Label>
<div class="col-sm-9">
<input id="roleclaimmappings" class="form-control" @bind="@_roleclaimmappings" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="synchronizeroles" HelpText="This option will add or remove role assignments so that the site roles exactly match the roles provided by the identity provider" ResourceKey="SynchronizeRoles">Synchronize Roles?</Label>
<div class="col-sm-9">
<div class="input-group">
<select id="synchronizeroles" class="form-select" @bind="@_synchronizeroles" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="profileclaimtypes" HelpText="A comma delimited list of user profile claims provided by the provider, as well as mappings to your user profile definition. For example if the provider includes a 'given_name' claim and you have a 'FirstName' user profile definition you should specify 'given_name:FirstName'." ResourceKey="ProfileClaimTypes">User Profile Claims:</Label>
<div class="col-sm-9">
<input id="profileclaimtypes" class="form-control" @bind="@_profileclaimtypes" />
@ -457,6 +474,8 @@ else
private string _nameclaimtype;
private string _emailclaimtype;
private string _roleclaimtype;
private string _roleclaimmappings;
private string _synchronizeroles;
private string _profileclaimtypes;
private string _domainfilter;
private string _createusers;
@ -521,6 +540,8 @@ else
_nameclaimtype = SettingService.GetSetting(settings, "ExternalLogin:NameClaimType", "name");
_emailclaimtype = SettingService.GetSetting(settings, "ExternalLogin:EmailClaimType", "email");
_roleclaimtype = SettingService.GetSetting(settings, "ExternalLogin:RoleClaimType", "");
_roleclaimmappings = SettingService.GetSetting(settings, "ExternalLogin:RoleClaimMappings", "");
_synchronizeroles = SettingService.GetSetting(settings, "ExternalLogin:SynchronizeRoles", "false");
_profileclaimtypes = SettingService.GetSetting(settings, "ExternalLogin:ProfileClaimTypes", "");
_domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", "");
_createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true");
@ -614,7 +635,9 @@ else
settings = SettingService.SetSetting(settings, "ExternalLogin:NameClaimType", _nameclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:RoleClaimType", _roleclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ProfileClaimTypes", _profileclaimtypes, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:RoleClaimMappings", _roleclaimmappings, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:SynchronizeRoles", _synchronizeroles, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ProfileClaimTypes", _profileclaimtypes, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:VerifyUsers", _verifyusers, true);

View File

@ -173,6 +173,12 @@ else
_editmode = bool.Parse(EditMode);
}
Text = Localize(nameof(Text), Text);
Header = Localize(nameof(Header), Header);
Message = Localize(nameof(Message), Message);
_openText = Text;
if (!string.IsNullOrEmpty(IconName))
{
if (IconOnly)
@ -191,11 +197,6 @@ else
_iconSpan = $"<span class=\"{IconName}\"></span>&nbsp";
}
Text = Localize(nameof(Text), Text);
Header = Localize(nameof(Header), Header);
Message = Localize(nameof(Message), Message);
_openText = Text;
_permissions = (PermissionList == null) ? ModuleState.PermissionList : PermissionList;
_authorized = IsAuthorized();

View File

@ -359,12 +359,6 @@
}
if (restricted == "")
{
if (!ShowProgress)
{
_uploading = true;
StateHasChanged();
}
try
{
// upload the files
@ -374,7 +368,21 @@
if (PageState.Runtime == Shared.Runtime.Hybrid)
{
jwt = await UserService.GetTokenAsync();
if (string.IsNullOrEmpty(jwt))
{
await logger.LogInformation("File Upload Failed From .NET MAUI Due To Missing Security Token. Token Options Must Be Set In User Settings.");
_message = "Security Token Not Specified";
_messagetype = MessageType.Error;
return;
}
}
if (!ShowProgress)
{
_uploading = true;
StateHasChanged();
}
await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt);
// uploading is asynchronous so we need to poll to determine if uploads are completed
@ -387,7 +395,7 @@
var size = Int64.Parse(uploads[upload].Split(':')[1]); // bytes
var megabits = (size / 1048576.0) * 8; // binary conversion
var uploadspeed = 2; // 2 Mbps (3G ranges from 300Kbps to 3Mbps)
var uploadspeed = (PageState.Alias.Name.Contains("localhost")) ? 100 : 3; // 3 Mbps is FCC minimum for broadband upload
var uploadtime = (megabits / uploadspeed); // seconds
var maxattempts = 5; // polling (minimum timeout duration will be 5 seconds)
var sleep = (int)Math.Ceiling(uploadtime / maxattempts) * 1000; // milliseconds

View File

@ -111,7 +111,7 @@
[Parameter]
public List<Permission> PermissionList { get; set; }
protected override async Task OnParametersSetAsync()
protected override async Task OnInitializedAsync()
{
if (!string.IsNullOrEmpty(Permissions))
{

View File

@ -134,6 +134,7 @@ namespace Oqtane.Modules
// url methods
// navigate url
public string NavigateUrl()
{
return NavigateUrl(PageState.Page.Path);
@ -149,24 +150,65 @@ namespace Oqtane.Modules
return NavigateUrl(PageState.Page.Path, refresh);
}
public string NavigateUrl(string path, string parameters)
public string NavigateUrl(string path, string querystring)
{
return Utilities.NavigateUrl(PageState.Alias.Path, path, parameters);
return Utilities.NavigateUrl(PageState.Alias.Path, path, querystring);
}
public string NavigateUrl(string path, Dictionary<string, string> querystring)
{
return NavigateUrl(path, Utilities.CreateQueryString(querystring));
}
public string NavigateUrl(string path, bool refresh)
{
return Utilities.NavigateUrl(PageState.Alias.Path, path, refresh ? "refresh" : "");
return NavigateUrl(path, refresh ? "refresh" : "");
}
public string NavigateUrl(int moduleId, string action)
{
return EditUrl(PageState.Page.Path, moduleId, action, "");
}
public string NavigateUrl(int moduleId, string action, string querystring)
{
return EditUrl(PageState.Page.Path, moduleId, action, querystring);
}
public string NavigateUrl(int moduleId, string action, Dictionary<string, string> querystring)
{
return EditUrl(PageState.Page.Path, moduleId, action, querystring);
}
public string NavigateUrl(string path, int moduleId, string action)
{
return EditUrl(path, moduleId, action, "");
}
public string NavigateUrl(string path, int moduleId, string action, string querystring)
{
return EditUrl(path, moduleId, action, querystring);
}
public string NavigateUrl(string path, int moduleId, string action, Dictionary<string, string> querystring)
{
return EditUrl(path, moduleId, action, querystring);
}
// edit url
public string EditUrl(string action)
{
return EditUrl(ModuleState.ModuleId, action);
}
public string EditUrl(string action, string parameters)
public string EditUrl(string action, string querystring)
{
return EditUrl(ModuleState.ModuleId, action, parameters);
return EditUrl(ModuleState.ModuleId, action, querystring);
}
public string EditUrl(string action, Dictionary<string, string> querystring)
{
return EditUrl(ModuleState.ModuleId, action, querystring);
}
public string EditUrl(int moduleId, string action)
@ -174,16 +216,27 @@ namespace Oqtane.Modules
return EditUrl(moduleId, action, "");
}
public string EditUrl(int moduleId, string action, string parameters)
public string EditUrl(int moduleId, string action, string querystring)
{
return EditUrl(PageState.Page.Path, moduleId, action, parameters);
return EditUrl(PageState.Page.Path, moduleId, action, querystring);
}
public string EditUrl(string path, int moduleid, string action, string parameters)
public string EditUrl(int moduleId, string action, Dictionary<string, string> querystring)
{
return Utilities.EditUrl(PageState.Alias.Path, path, moduleid, action, parameters);
return EditUrl(PageState.Page.Path, moduleId, action, querystring);
}
public string EditUrl(string path, int moduleid, string action, string querystring)
{
return Utilities.EditUrl(PageState.Alias.Path, path, moduleid, action, querystring);
}
public string EditUrl(string path, int moduleid, string action, Dictionary<string, string> querystring)
{
return EditUrl(path, moduleid, action, Utilities.CreateQueryString(querystring));
}
// file url
public string FileUrl(string folderpath, string filename)
{
return FileUrl(folderpath, filename, false);
@ -203,6 +256,8 @@ namespace Oqtane.Modules
return Utilities.FileUrl(PageState.Alias, fileid, download);
}
// image url
public string ImageUrl(int fileid, int width, int height)
{
return ImageUrl(fileid, width, height, "");

View File

@ -4,7 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
<Configurations>Debug;Release</Configurations>
<Version>5.2.1</Version>
<Version>5.2.3</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -12,7 +12,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.1</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>

View File

@ -243,4 +243,7 @@
<data name="NoNotificationsSent.Text" xml:space="preserve">
<value>No notifications have been sent</value>
</data>
<data name="Logout Everywhere" xml:space="preserve">
<value>Logout Everywhere</value>
</data>
</root>

View File

@ -117,12 +117,6 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Error.User.AddCheckPass" xml:space="preserve">
<value>Error Adding User. Please Ensure Password Meets Complexity Requirements And Username And Email Is Not Already In Use.</value>
</data>
<data name="Message.Password.NoMatch" xml:space="preserve">
<value>Passwords Entered Do Not Match</value>
</data>
<data name="Error.User.Add" xml:space="preserve">
<value>Error Adding User</value>
</data>
@ -133,17 +127,11 @@
<value>Identity</value>
</data>
<data name="Message.Required.ProfileInfo" xml:space="preserve">
<value>You Must Provide A Username, Password, Email Address And All Required Profile Information</value>
<value>You Must Provide A Username, Email Address And All Required Profile Information</value>
</data>
<data name="Message.Username.Exists" xml:space="preserve">
<value>Username Already Exists</value>
</data>
<data name="Confirm.HelpText" xml:space="preserve">
<value>Please enter the password again to confirm it matches with the value above</value>
</data>
<data name="Confirm.Text" xml:space="preserve">
<value>Confirm Password:</value>
</data>
<data name="DisplayName.HelpText" xml:space="preserve">
<value>The full name of the user</value>
</data>
@ -156,21 +144,12 @@
<data name="Email.Text" xml:space="preserve">
<value>Email:</value>
</data>
<data name="Password.HelpText" xml:space="preserve">
<value>The user's password. Please choose a password which is sufficiently secure.</value>
</data>
<data name="Password.Text" xml:space="preserve">
<value>Password:</value>
</data>
<data name="Username.HelpText" xml:space="preserve">
<value>A unique username for a user. Note that this field can not be modified once it is saved.</value>
</data>
<data name="Username.Text" xml:space="preserve">
<value>Username:</value>
</data>
<data name="Password.Placeholder" xml:space="preserve">
<value>Password</value>
</data>
<data name="Notify.HelpText" xml:space="preserve">
<value>Indicate if new users should receive an email notification</value>
</data>

View File

@ -385,10 +385,22 @@
<value>Parameters:</value>
</data>
<data name="RoleClaimType.HelpText" xml:space="preserve">
<value>Optionally provide the type name of the role claim provided by the identity provider. These roles will be used in addition to any internal user roles assigned within the site.</value>
<value>Optionally provide the type name of the roles claim provided by the identity provider (the standard default is 'roles'). If role names from the identity provider do not exactly match your site role names, please use the Role Claim Mappings.</value>
</data>
<data name="RoleClaimType.Text" xml:space="preserve">
<value>Role Claim:</value>
<value>Roles Claim:</value>
</data>
<data name="RoleClaimMappings.HelpText" xml:space="preserve">
<value>Optionally provide a comma delimited list of role names provided by the identity provider, as well as mappings to your site roles. For example if the identity provider includes an 'Admin' role name and you want it to map to the 'Administrators' site role you should specify 'Admin:Administrators'.</value>
</data>
<data name="RoleClaimMappings.Text" xml:space="preserve">
<value>Role Claim Mappings:</value>
</data>
<data name="SynchronizeRoles.HelpText" xml:space="preserve">
<value>This option will add or remove role assignments so that the site roles exactly match the roles provided by the identity provider for a user</value>
</data>
<data name="SynchronizeRoles.Text" xml:space="preserve">
<value>Synchronize Roles?</value>
</data>
<data name="ProfileClaimTypes.HelpText" xml:space="preserve">
<value>Optionally provide a comma delimited list of user profile claim type names provided by the identity provider, as well as mappings to your user profile definition. For example if the identity provider includes a 'given_name' claim and you have a 'FirstName' user profile definition you should specify 'given_name:FirstName'.</value>

View File

@ -75,6 +75,13 @@ namespace Oqtane.Services
/// <returns></returns>
Task LogoutUserAsync(User user);
/// <summary>
/// Logout a <see cref="User"/>
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
Task LogoutUserEverywhereAsync(User user);
/// <summary>
/// Update e-mail verification status of a user.
/// </summary>

View File

@ -61,10 +61,14 @@ namespace Oqtane.Services
public async Task LogoutUserAsync(User user)
{
// best practices recommend post is preferrable to get for logout
await PostJsonAsync($"{Apiurl}/logout", user);
}
public async Task LogoutUserEverywhereAsync(User user)
{
await PostJsonAsync($"{Apiurl}/logouteverywhere", user);
}
public async Task<User> VerifyEmailAsync(User user, string token)
{
return await PostJsonAsync<User>($"{Apiurl}/verify?token={token}", user);

View File

@ -9,7 +9,7 @@
<div class="row flex-xl-nowrap gx-0">
<div class="sidebar">
<nav class="navbar">
<Logo />
<Logo UseSiteNameAsFallback="true" />
<Menu Orientation="Vertical" />
</nav>
</div>
@ -41,9 +41,7 @@
Integrity = "sha512-jnSuA4Ss2PkkikSOLtYs8BlYIeeIK1h99ty4YfvRPAlzr377vr3CXDb7sb7eEEBYjDtcYj+AjBH3FLv5uSJuXg==",
CrossOrigin = "anonymous" },
new Resource { ResourceType = ResourceType.Stylesheet, Url = ThemePath() + "Theme.css" },
new Resource { ResourceType = ResourceType.Script, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js",
Integrity = "sha512-7Pi/otdlbbCR+LnW+F7PwFcSDJOuUJB3OxtEHbg4vSMvzvJjde4Po1v4BR9Gdc9aXNUNFVUY+SK51wWT8WF0Gg==",
CrossOrigin = "anonymous", Location = ResourceLocation.Body },
new Resource { ResourceType = ResourceType.Script, Url = Constants.BootstrapScriptUrl, Integrity = Constants.BootstrapScriptIntegrity, CrossOrigin = "anonymous", Location = ResourceLocation.Body },
};
}

View File

@ -11,9 +11,6 @@ using System.Net;
using Microsoft.Extensions.Localization;
using Oqtane.UI;
// ReSharper disable UnassignedGetOnlyAutoProperty
// ReSharper disable MemberCanBePrivate.Global
namespace Oqtane.Themes.Controls
{
public class ModuleActionsBase : ComponentBase
@ -92,20 +89,21 @@ namespace Oqtane.Themes.Controls
return actionList;
}
private async Task<string> EditUrlAsync(string url, int moduleId, string import)
{
await Task.Yield();
return Utilities.EditUrl(PageState.Alias.Path, PageState.Page.Path, moduleId, import, "");
}
protected async Task ModuleAction(ActionViewModel action)
{
if (PageState.EditMode && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, ModuleState.PermissionList))
{
PageModule pagemodule = await PageModuleService.GetPageModuleAsync(ModuleState.PageModuleId);
string url = Utilities.NavigateUrl(PageState.Alias.Path, PageState.Page.Path, "edit=true&refresh");
var url = NavigationManager.Uri.Substring(NavigationManager.BaseUri.Length - 1);
if (!url.Contains("edit="))
{
url += (!url.Contains("?") ? "?" : "&") + "edit=true";
}
if (!url.Contains("refresh="))
{
url += (!url.Contains("?") ? "?" : "&") + "refresh=true";
}
var pagemodule = await PageModuleService.GetPageModuleAsync(ModuleState.PageModuleId);
if (action.Action != null)
{
url = await action.Action(url, pagemodule);
@ -115,31 +113,10 @@ namespace Oqtane.Themes.Controls
}
}
private async Task<string> MoveToPane(string url, string newPane, PageModule pagemodule)
private Task<string> Settings(string url, PageModule pagemodule)
{
string oldPane = pagemodule.Pane;
pagemodule.Pane = newPane;
pagemodule.Order = int.MaxValue; // add to bottom of pane
await PageModuleService.UpdatePageModuleAsync(pagemodule);
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane);
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, oldPane);
return url;
}
private async Task<string> DeleteModule(string url, PageModule pagemodule)
{
pagemodule.IsDeleted = true;
await PageModuleService.UpdatePageModuleAsync(pagemodule);
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane);
return url;
}
private async Task<string> Settings(string url, PageModule pagemodule)
{
await Task.Yield();
var returnurl = Utilities.NavigateUrl(PageState.Alias.Path, PageState.Page.Path, "edit=true");
url = Utilities.EditUrl(PageState.Alias.Path, PageState.Page.Path, pagemodule.ModuleId, "Settings", "returnurl=" + WebUtility.UrlEncode(returnurl));
return url;
url = Utilities.EditUrl(PageState.Alias.Path, PageState.Page.Path, pagemodule.ModuleId, "Settings", "returnurl=" + WebUtility.UrlEncode(url));
return Task.FromResult(url);
}
private async Task<string> Publish(string url, PageModule pagemodule)
@ -174,6 +151,20 @@ namespace Oqtane.Themes.Controls
return url;
}
private async Task<string> DeleteModule(string url, PageModule pagemodule)
{
pagemodule.IsDeleted = true;
await PageModuleService.UpdatePageModuleAsync(pagemodule);
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane);
return url;
}
private Task<string> EditUrlAsync(string url, int moduleId, string import)
{
url = Utilities.EditUrl(PageState.Alias.Path, PageState.Page.Path, moduleId, import, "returnurl=" + WebUtility.UrlEncode(url));
return Task.FromResult(url);
}
private async Task<string> MoveTop(string url, PageModule pagemodule)
{
pagemodule.Order = 0;
@ -206,6 +197,17 @@ namespace Oqtane.Themes.Controls
return url;
}
private async Task<string> MoveToPane(string url, string newPane, PageModule pagemodule)
{
string oldPane = pagemodule.Pane;
pagemodule.Pane = newPane;
pagemodule.Order = int.MaxValue; // add to bottom of pane
await PageModuleService.UpdatePageModuleAsync(pagemodule);
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane);
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, oldPane);
return url;
}
public class ActionViewModel
{
public string Icon { get; set; }

View File

@ -147,8 +147,7 @@
{
if (PageState.Page.IsPersonalizable && PageState.User != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Registered))
{
PageState.EditMode = true;
NavigationManager.NavigateTo(NavigateUrl(page.Path, "edit=" + ((PageState.EditMode) ? "true" : "false")));
NavigationManager.NavigateTo(NavigateUrl(page.Path, "edit=" + PageState.EditMode.ToString()));
}
}
}

View File

@ -331,7 +331,7 @@
if (_pageId != "-")
{
_modules = await ModuleService.GetModulesAsync(PageState.Page.SiteId);
_modules = _modules.Where(module => module.PageId == int.Parse(_pageId) &&
_modules = _modules.Where(module => module.PageId == int.Parse(_pageId) && module.IsDeleted == false &&
UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.PermissionList) &&
(_moduleType == "add" || module.ModuleDefinition.IsPortable))
.ToList();
@ -454,7 +454,7 @@
{
foreach (var permission in PageState.Page.PermissionList.Where(item => item.PermissionName == pagePermission))
{
permissions.Add(new Permission { SiteId = siteId, EntityName = EntityNames.Module, PermissionName = modulePermission, RoleId = permission.RoleId, UserId = permission.UserId, IsAuthorized = permission.IsAuthorized });
permissions.Add(new Permission { SiteId = siteId, EntityName = EntityNames.Module, PermissionName = modulePermission, RoleName = permission.RoleName, UserId = permission.UserId, IsAuthorized = permission.IsAuthorized });
}
return permissions;
}

View File

@ -4,31 +4,28 @@
@inject IStringLocalizer<SharedResources> SharedLocalizer
<span class="app-login">
<AuthorizeView Roles="@RoleNames.Registered">
<Authorizing>
<text>...</text>
</Authorizing>
<Authorized>
@if (PageState.Runtime == Runtime.Hybrid)
{
<button type="button" class="btn btn-primary" @onclick="LogoutUser">@Localizer["Logout"]</button>
}
else
{
<form method="post" class="app-form-inline" action="@logouturl" @formname="LogoutForm">
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<input type="hidden" name="returnurl" value="@returnurl" />
<button type="submit" class="btn btn-primary">@Localizer["Logout"]</button>
</form>
}
</Authorized>
<NotAuthorized>
@if (ShowLogin)
{
<a href="@loginurl" class="btn btn-primary">@SharedLocalizer["Login"]</a>
}
</NotAuthorized>
</AuthorizeView>
@if (PageState.User != null)
{
@if (PageState.Runtime == Runtime.Hybrid)
{
<button type="button" class="btn btn-primary" @onclick="LogoutUser">@Localizer["Logout"]</button>
}
else
{
<form method="post" class="app-form-inline" action="@logouturl" @formname="LogoutForm">
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<input type="hidden" name="returnurl" value="@returnurl" />
<button type="submit" class="btn btn-primary">@Localizer["Logout"]</button>
</form>
}
}
else
{
@if (ShowLogin)
{
<a href="@loginurl" class="btn btn-primary">@SharedLocalizer["Login"]</a>
}
}
</span>
@code

View File

@ -9,3 +9,18 @@
</a>
</span>
}
else
{
if (UseSiteNameAsFallback)
{
<span class="app-logo">
<a class="navbar-brand" href="@PageState.Alias.Path">@PageState.Site.Name</a>
</span>
}
}
@code {
[Parameter]
public bool UseSiteNameAsFallback { get; set; } = false; // indicates if the site name should be displayed in scenarios where a site does not have a logo defined
}

View File

@ -1,20 +1,23 @@
@namespace Oqtane.Themes.Controls
@using System.Net
@using Microsoft.AspNetCore.Http
@inherits ThemeControlBase
@inject ISettingService SettingService
@inject IStringLocalizer<Search> Localizer
@inject NavigationManager NavigationManager
@if (_searchResultsPage != null)
{
<span class="app-search @CssClass">
<span class="@_defaultCssClass @CssClass">
<form method="post" class="app-form-inline" @formname="@($"SearchForm")" @onsubmit="@PerformSearch" data-enhance>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<input type="text" name="keywords" maxlength="50"
class="form-control d-inline-block pe-5 shadow-none"
@bind="_keywords"
placeholder="@Localizer["SearchPlaceHolder"]"
aria-label="Search" />
@if (AllowTextInput)
{
<input type="text" name="keywords" maxlength="50"
class="form-control d-inline-block pe-5 shadow-none"
@bind="_keywords"
placeholder="@Localizer["SearchPlaceHolder"]"
aria-label="Search" />
}
<button type="submit" class="btn btn-search">
<span class="oi oi-magnifying-glass align-middle"></span>
</button>
@ -22,9 +25,8 @@
</span>
}
@code {
private string _defaultCssClass;
private Page _searchResultsPage;
private string _keywords = "";
@ -32,19 +34,23 @@
public string CssClass { get; set; }
[Parameter]
public string SearchResultPagePath { get; set; } = "search";
public bool AllowTextInput { get; set; } = true; // setting to false will display only the search icon button - not the textbox
[CascadingParameter]
HttpContext HttpContext { get; set; }
[Parameter]
public string SearchResultPagePath { get; set; } = "search"; // setting to "" will disable search
[SupplyParameterFromForm(FormName = "SearchForm")]
public string KeyWords { get => ""; set => _keywords = value; }
protected override void OnInitialized()
{
if(!string.IsNullOrEmpty(SearchResultPagePath))
if (bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "Search_Enabled", "True")))
{
_searchResultsPage = PageState.Pages.FirstOrDefault(i => i.Path == SearchResultPagePath);
_defaultCssClass = (AllowTextInput) ? "app-search" : "app-search-noinput";
if (!string.IsNullOrEmpty(SearchResultPagePath))
{
_searchResultsPage = PageState.Pages.FirstOrDefault(i => i.Path == SearchResultPagePath);
}
}
}

View File

@ -6,20 +6,17 @@
@inject NavigationManager NavigationManager
<span class="app-profile">
<AuthorizeView Roles="@RoleNames.Registered">
<Authorizing>
<text>...</text>
</Authorizing>
<Authorized>
<a href="@NavigateUrl("profile", "returnurl=" + _returnurl)" class="btn btn-primary">@context.User.Identity.Name</a>
</Authorized>
<NotAuthorized>
@if (ShowRegister && PageState.Site.AllowRegistration)
{
<a href="@NavigateUrl("register", "returnurl=" + _returnurl)" class="btn btn-primary">@Localizer["Register"]</a>
}
</NotAuthorized>
</AuthorizeView>
@if (PageState.User != null)
{
<a href="@NavigateUrl("profile", "returnurl=" + _returnurl)" class="btn btn-primary">@PageState.User.Username</a>
}
else
{
@if (ShowRegister && PageState.Site.AllowRegistration)
{
<a href="@NavigateUrl("register", "returnurl=" + _returnurl)" class="btn btn-primary">@Localizer["Register"]</a>
}
}
</span>
@code {

View File

@ -21,9 +21,7 @@ namespace Oqtane.Themes.OqtaneTheme
Integrity = "sha512-M+Wrv9LTvQe81gFD2ZE3xxPTN5V2n1iLCXsldIxXvfs6tP+6VihBCwCMBkkjkQUZVmEHBsowb9Vqsq1et1teEg==",
CrossOrigin = "anonymous" },
new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Theme.css" },
new Resource { ResourceType = ResourceType.Script, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js",
Integrity = "sha512-7Pi/otdlbbCR+LnW+F7PwFcSDJOuUJB3OxtEHbg4vSMvzvJjde4Po1v4BR9Gdc9aXNUNFVUY+SK51wWT8WF0Gg==",
CrossOrigin = "anonymous", Location = ResourceLocation.Body },
new Resource { ResourceType = ResourceType.Script, Url = Constants.BootstrapScriptUrl, Integrity = Constants.BootstrapScriptIntegrity, CrossOrigin = "anonymous", Location = ResourceLocation.Body }
}
};
}

View File

@ -4,7 +4,7 @@
<main role="main">
<nav class="navbar navbar-dark bg-primary fixed-top">
<Logo /><Menu Orientation="Horizontal" />
<Logo UseSiteNameAsFallback="true" /><Menu Orientation="Horizontal" />
<div class="controls ms-auto">
<div class="controls-group">
<Search CssClass="me-3 text-center bg-primary" />

View File

@ -93,6 +93,7 @@ namespace Oqtane.Themes
// url methods
// navigate url
public string NavigateUrl()
{
return NavigateUrl(PageState.Page.Path);
@ -108,31 +109,78 @@ namespace Oqtane.Themes
return NavigateUrl(PageState.Page.Path, refresh);
}
public string NavigateUrl(string path, string querystring)
{
return Utilities.NavigateUrl(PageState.Alias.Path, path, querystring);
}
public string NavigateUrl(string path, Dictionary<string, string> querystring)
{
return NavigateUrl(path, Utilities.CreateQueryString(querystring));
}
public string NavigateUrl(string path, bool refresh)
{
return Utilities.NavigateUrl(PageState.Alias.Path, path, refresh ? "refresh" : "");
return NavigateUrl(path, refresh ? "refresh" : "");
}
public string NavigateUrl(string path, string parameters)
public string NavigateUrl(int moduleid, string action)
{
return Utilities.NavigateUrl(PageState.Alias.Path, path, parameters);
return EditUrl(moduleid, action, "");
}
public string NavigateUrl(int moduleid, string action, string querystring)
{
return EditUrl(PageState.Page.Path, moduleid, action, querystring);
}
public string NavigateUrl(int moduleid, string action, Dictionary<string, string> querystring)
{
return EditUrl(PageState.Page.Path, moduleid, action, Utilities.CreateQueryString(querystring));
}
public string NavigateUrl(string path, int moduleId, string action)
{
return EditUrl(path, moduleId, action, "");
}
public string NavigateUrl(string path, int moduleid, string action, string querystring)
{
return EditUrl(path, moduleid, action, querystring);
}
public string NavigateUrl(string path, int moduleid, string action, Dictionary<string, string> querystring)
{
return EditUrl(path, moduleid, action, querystring);
}
// edit url
public string EditUrl(int moduleid, string action)
{
return EditUrl(moduleid, action, "");
}
public string EditUrl(int moduleid, string action, string parameters)
public string EditUrl(int moduleid, string action, string querystring)
{
return EditUrl(PageState.Page.Path, moduleid, action, parameters);
return EditUrl(PageState.Page.Path, moduleid, action, querystring);
}
public string EditUrl(string path, int moduleid, string action, string parameters)
public string EditUrl(int moduleid, string action, Dictionary<string, string> querystring)
{
return Utilities.EditUrl(PageState.Alias.Path, path, moduleid, action, parameters);
return EditUrl(PageState.Page.Path, moduleid, action, querystring);
}
public string EditUrl(string path, int moduleid, string action, string querystring)
{
return Utilities.EditUrl(PageState.Alias.Path, path, moduleid, action, querystring);
}
public string EditUrl(string path, int moduleid, string action, Dictionary<string, string> querystring)
{
return EditUrl(path, moduleid, action, Utilities.CreateQueryString(querystring));
}
// file url
public string FileUrl(string folderpath, string filename)
{
return FileUrl(folderpath, filename, false);
@ -152,6 +200,7 @@ namespace Oqtane.Themes
return Utilities.FileUrl(PageState.Alias, fileid, download);
}
// image url
public string ImageUrl(int fileid, int width, int height)
{
return ImageUrl(fileid, width, height, "");

View File

@ -61,7 +61,6 @@
{
SiteState.AntiForgeryToken = AntiForgeryToken;
SiteState.AuthorizationToken = AuthorizationToken;
SiteState.RemoteIPAddress = (_pageState != null) ? _pageState.RemoteIPAddress : "";
SiteState.Platform = Platform;
SiteState.IsPrerendering = (HttpContext != null) ? true : false;
@ -80,6 +79,7 @@
{
_pageState = PageState;
SiteState.Alias = PageState.Alias;
SiteState.RemoteIPAddress = (PageState != null) ? PageState.RemoteIPAddress : "";
_installed = true;
}
}

View File

@ -157,7 +157,7 @@
// verify user is authenticated for current site
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
if (authState.User.Identity.IsAuthenticated && authState.User.Claims.Any(item => item.Type == "sitekey" && item.Value == SiteState.Alias.SiteKey))
if (authState.User.Identity.IsAuthenticated && authState.User.Claims.Any(item => item.Type == Constants.SiteKeyClaimType && item.Value == SiteState.Alias.SiteKey))
{
// get user
var userid = int.Parse(authState.User.Claims.First(item => item.Type == ClaimTypes.NameIdentifier).Value);
@ -287,10 +287,10 @@
}
// load additional metadata for current page
page = ProcessPage(page, site, user, SiteState.Alias);
page = ProcessPage(page, site, user, SiteState.Alias, action);
// load additional metadata for modules
(page, modules) = ProcessModules(page, modules, moduleid, action, (!string.IsNullOrEmpty(page.DefaultContainerType)) ? page.DefaultContainerType : site.DefaultContainerType, SiteState.Alias);
(page, modules) = ProcessModules(site, page, modules, moduleid, action, (!string.IsNullOrEmpty(page.DefaultContainerType)) ? page.DefaultContainerType : site.DefaultContainerType, SiteState.Alias);
// populate page state (which acts as a client-side cache for subsequent requests)
_pagestate = new PageState
@ -366,7 +366,7 @@
}
}
private Page ProcessPage(Page page, Site site, User user, Alias alias)
private Page ProcessPage(Page page, Site site, User user, Alias alias, string action)
{
try
{
@ -403,6 +403,16 @@
page.Resources = ManagePageResources(page.Resources, themeobject.Resources, ResourceLevel.Page, alias, "Themes", themetype.Namespace);
}
}
// theme settings components are dynamically loaded within the framework Page Management module
if (page.Path == "admin/pages" && action.ToLower() == "edit" && theme != null && !string.IsNullOrEmpty(theme.ThemeSettingsType))
{
var settingsType = Type.GetType(theme.ThemeSettingsType);
if (settingsType != null)
{
var objSettings = Activator.CreateInstance(settingsType) as IModuleControl;
page.Resources = ManagePageResources(page.Resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace);
}
}
if (!string.IsNullOrEmpty(panes))
{
@ -426,7 +436,7 @@
return page;
}
private (Page Page, List<Module> Modules) ProcessModules(Page page, List<Module> modules, int moduleid, string action, string defaultcontainertype, Alias alias)
private (Page Page, List<Module> Modules) ProcessModules(Site site, Page page, List<Module> modules, int moduleid, string action, string defaultcontainertype, Alias alias)
{
var paneindex = new Dictionary<string, int>();
@ -494,15 +504,40 @@
module.Prerender = moduleobject.Prerender;
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace);
// settings components are dynamically loaded within the framework Settings module
if (action.ToLower() == "settings" && module.ModuleDefinition != null)
{
// settings components are embedded within a framework settings module
moduletype = Type.GetType(module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, action), false, true);
// module settings component
var settingsType = "";
if (!string.IsNullOrEmpty(module.ModuleDefinition.SettingsType))
{
// module settings type explicitly declared in IModule interface
settingsType = module.ModuleDefinition.SettingsType;
}
else
{
// legacy support - module settings type determined by convention
settingsType = module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, action);
}
moduletype = Type.GetType(settingsType, false, true);
if (moduletype != null)
{
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace);
}
// container settings component
var theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == page.ThemeType));
if (theme != null && !string.IsNullOrEmpty(theme.ContainerSettingsType))
{
moduletype = Type.GetType(theme.ContainerSettingsType);
if (moduletype != null)
{
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace);
}
}
}
// additional metadata needed for admin components

View File

@ -20,6 +20,13 @@
return;
}
// force authenticated user to provide email address (email may be missing if using external login)
if (PageState.User != null && PageState.User.IsAuthenticated && string.IsNullOrEmpty(PageState.User.Email) && PageState.Route.PagePath != "profile")
{
NavigationManager.NavigateTo(Utilities.NavigateUrl(PageState.Alias.Path, "profile", "returnurl=" + WebUtility.UrlEncode(PageState.Route.PathAndQuery)));
return;
}
// set page title
if (!string.IsNullOrEmpty(PageState.Page.Title))
{
@ -44,7 +51,6 @@
}
// head content
AddHeadContent(headcontent, PageState.Site.HeadContent);
if (!string.IsNullOrEmpty(PageState.Site.HeadContent))
{
headcontent = AddHeadContent(headcontent, PageState.Site.HeadContent);
@ -66,30 +72,24 @@
{
if (!string.IsNullOrEmpty(content))
{
if (PageState.RenderMode == RenderModes.Interactive)
var elements = content.Split('<', StringSplitOptions.RemoveEmptyEntries);
foreach (var element in elements)
{
// remove scripts
var index = content.IndexOf("<script");
while (index >= 0)
if (PageState.RenderMode == RenderModes.Static || (!element.ToLower().StartsWith("script") && !element.ToLower().StartsWith("/script")))
{
content = content.Remove(index, content.IndexOf("</script>") + 9 - index);
index = content.IndexOf("<script");
if (!headcontent.Contains("<" + element) || element.StartsWith("/"))
{
headcontent += "<" + element;
}
}
}
headcontent += content + "\n";
headcontent += "\n";
}
return headcontent;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// force authenticated user to provide email address (email may be missing if using external login)
if (PageState.User != null && PageState.User.IsAuthenticated && string.IsNullOrEmpty(PageState.User.Email) && PageState.Route.PagePath != "profile")
{
NavigationManager.NavigateTo(Utilities.NavigateUrl(PageState.Alias.Path, "profile", "returnurl=" + WebUtility.UrlEncode(PageState.Route.PathAndQuery)));
return;
}
if (!firstRender)
{
if (!string.IsNullOrEmpty(PageState.Page.HeadContent) && PageState.Page.HeadContent.Contains("<script"))

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>5.2.0</Version>
<Version>5.2.3</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>5.2.1</Version>
<Version>5.2.3</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.1</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>5.2.1</Version>
<Version>5.2.3</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.1</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>5.2.1</Version>
<Version>5.2.3</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.1</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

View File

@ -6,7 +6,7 @@
<!-- <TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks> -->
<!-- <TargetFrameworks>$(TargetFrameworks);net8.0-tizen</TargetFrameworks> -->
<OutputType>Exe</OutputType>
<Version>5.2.1</Version>
<Version>5.2.3</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -14,7 +14,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.1</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane.Maui</RootNamespace>
@ -31,7 +31,7 @@
<ApplicationIdGuid>0E29FC31-1B83-48ED-B6E0-9F3C67B775D4</ApplicationIdGuid>
<!-- Versions -->
<ApplicationDisplayVersion>5.2.1</ApplicationDisplayVersion>
<ApplicationDisplayVersion>5.2.3</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">14.2</SupportedOSPlatformVersion>

View File

@ -35,6 +35,9 @@ app {
}
/* Action Dialog */
.app-actiondialog{
position: absolute;
}
.app-actiondialog .modal {
position: fixed; /* Stay in place */
z-index: 9999; /* Sit on top */
@ -230,5 +233,41 @@ app {
}
.app-form-inline {
display: inline-block;
display: inline;
}
.app-search{
display: inline-block;
position: relative;
}
.app-search input + button{
background: none;
border: none;
position: absolute;
right: 0;
top: 0;
}
.app-search input + button .oi{
top: 0;
}
.app-search-noinput {
display: inline-block;
position: relative;
}
.app-search-noinput button {
background: none;
border: none;
color: var(--bs-heading-color);
}
.app-search-noinput button:hover {
color: var(--bs-heading-color);
}
/* Text Editor */
.text-area-editor > textarea {
width: 100%;
min-height: 250px;
}
.app-logo .navbar-brand {
padding: 5px 20px 5px 20px;
}

View File

@ -198,7 +198,9 @@ Oqtane.Interop = {
}
promises.push(new Promise((resolve, reject) => {
if (loadjs.isDefined(bundles[b])) {
resolve(true);
loadjs.ready(bundles[b], () => {
resolve(true);
});
}
else {
loadjs(urls, bundles[b], {
@ -206,18 +208,25 @@ Oqtane.Interop = {
returnPromise: true,
before: function (path, element) {
for (let s = 0; s < scripts.length; s++) {
if (path === scripts[s].href && scripts[s].integrity !== '') {
element.integrity = scripts[s].integrity;
}
if (path === scripts[s].href && scripts[s].crossorigin !== '') {
element.crossOrigin = scripts[s].crossorigin;
}
if (path === scripts[s].href && scripts[s].es6module === true) {
element.type = "module";
}
if (path === scripts[s].href && scripts[s].location === 'body') {
document.body.appendChild(element);
return false; // return false to bypass default DOM insertion mechanism
if (path === scripts[s].href) {
if (scripts[s].integrity !== '') {
element.integrity = scripts[s].integrity;
}
if (scripts[s].crossorigin !== '') {
element.crossOrigin = scripts[s].crossorigin;
}
if (scripts[s].es6module === true) {
element.type = "module";
}
if (typeof scripts[s].dataAttributes !== "undefined" && scripts[s].dataAttributes !== null) {
for (var key in scripts[s].dataAttributes) {
element.setAttribute(key, scripts[s].dataAttributes[key]);
}
}
if (scripts[s].location === 'body') {
document.body.appendChild(element);
return false; // return false to bypass default DOM insertion mechanism
}
}
}
}
@ -286,41 +295,49 @@ Oqtane.Interop = {
},
uploadFiles: function (posturl, folder, id, antiforgerytoken, jwt) {
var fileinput = document.getElementById('FileInput_' + id);
var files = fileinput.files;
var progressinfo = document.getElementById('ProgressInfo_' + id);
var progressbar = document.getElementById('ProgressBar_' + id);
if (progressinfo !== null && progressbar !== null) {
progressinfo.setAttribute("style", "display: inline;");
progressinfo.innerHTML = '';
progressbar.setAttribute("style", "width: 100%; display: inline;");
progressbar.value = 0;
}
var files = fileinput.files;
var totalSize = 0;
for (var i = 0; i < files.length; i++) {
var FileChunk = [];
var file = files[i];
var MaxFileSizeMB = 1;
var BufferChunkSize = MaxFileSizeMB * (1024 * 1024);
var FileStreamPos = 0;
var EndPos = BufferChunkSize;
var Size = file.size;
totalSize = totalSize + files[i].size;
}
while (FileStreamPos < Size) {
FileChunk.push(file.slice(FileStreamPos, EndPos));
FileStreamPos = EndPos;
EndPos = FileStreamPos + BufferChunkSize;
var maxChunkSizeMB = 1;
var bufferChunkSize = maxChunkSizeMB * (1024 * 1024);
var uploadedSize = 0;
for (var i = 0; i < files.length; i++) {
var fileChunk = [];
var file = files[i];
var fileStreamPos = 0;
var endPos = bufferChunkSize;
while (fileStreamPos < file.size) {
fileChunk.push(file.slice(fileStreamPos, endPos));
fileStreamPos = endPos;
endPos = fileStreamPos + bufferChunkSize;
}
var TotalParts = FileChunk.length;
var PartCount = 0;
var totalParts = fileChunk.length;
var partCount = 0;
while (Chunk = FileChunk.shift()) {
PartCount++;
var FileName = file.name + ".part_" + PartCount.toString().padStart(3, '0') + "_" + TotalParts.toString().padStart(3, '0');
while (chunk = fileChunk.shift()) {
partCount++;
var fileName = file.name + ".part_" + partCount.toString().padStart(3, '0') + "_" + totalParts.toString().padStart(3, '0');
var data = new FormData();
data.append('__RequestVerificationToken', antiforgerytoken);
data.append('folder', folder);
data.append('formfile', Chunk, FileName);
data.append('formfile', chunk, fileName);
var request = new XMLHttpRequest();
request.open('POST', posturl, true);
if (jwt !== "") {
@ -328,28 +345,36 @@ Oqtane.Interop = {
request.withCredentials = true;
}
request.upload.onloadstart = function (e) {
if (progressinfo !== null && progressbar !== null) {
progressinfo.innerHTML = file.name + ' 0%';
progressbar.value = 0;
if (progressinfo !== null && progressbar !== null && progressinfo.innerHTML === '') {
if (files.length === 1) {
progressinfo.innerHTML = file.name;
}
else {
progressinfo.innerHTML = file.name + ", ...";
}
}
};
request.upload.onprogress = function (e) {
if (progressinfo !== null && progressbar !== null) {
var percent = Math.ceil((e.loaded / e.total) * 100);
progressinfo.innerHTML = file.name + '[' + PartCount + '] ' + percent + '%';
var percent = Math.ceil(((uploadedSize + e.loaded) / totalSize) * 100);
progressbar.value = (percent / 100);
}
};
request.upload.onloadend = function (e) {
if (progressinfo !== null && progressbar !== null) {
progressinfo.innerHTML = file.name + ' 100%';
progressbar.value = 1;
uploadedSize = uploadedSize + e.total;
var percent = Math.ceil((uploadedSize / totalSize) * 100);
progressbar.value = (percent / 100);
}
};
request.upload.onerror = function() {
if (progressinfo !== null && progressbar !== null) {
progressinfo.innerHTML = file.name + ' Error: ' + request.statusText;
progressbar.value = 0;
if (files.length === 1) {
progressinfo.innerHTML = file.name + ' Error: ' + request.statusText;
}
else {
progressinfo.innerHTML = ' Error: ' + request.statusText;
}
}
};
request.send(data);

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<package>
<metadata>
<id>Oqtane.Client</id>
<version>5.2.1</version>
<version>5.2.3</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,8 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.1</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>
@ -20,5 +21,6 @@
<file src="..\Oqtane.Client\bin\Release\net8.0\Oqtane.Client.dll" target="lib\net8.0" />
<file src="..\Oqtane.Client\bin\Release\net8.0\Oqtane.Client.pdb" target="lib\net8.0" />
<file src="icon.png" target="" />
<file src="readme.md" target="" />
</files>
</package>

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<package>
<metadata>
<id>Oqtane.Framework</id>
<version>5.2.1</version>
<version>5.2.3</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -11,12 +11,14 @@
<copyright>.NET Foundation</copyright>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v5.2.1/Oqtane.Framework.5.2.1.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.1</releaseNotes>
<projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v5.2.3/Oqtane.Framework.5.2.3.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane framework</tags>
</metadata>
<files>
<file src="icon.png" target="" />
<file src="readme.md" target="" />
</files>
</package>

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<package>
<metadata>
<id>Oqtane.Server</id>
<version>5.2.1</version>
<version>5.2.3</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,8 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.1</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>
@ -20,5 +21,6 @@
<file src="..\Oqtane.Server\bin\Release\net8.0\Oqtane.Server.dll" target="lib\net8.0" />
<file src="..\Oqtane.Server\bin\Release\net8.0\Oqtane.Server.pdb" target="lib\net8.0" />
<file src="icon.png" target="" />
<file src="readme.md" target="" />
</files>
</package>

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<package>
<metadata>
<id>Oqtane.Shared</id>
<version>5.2.1</version>
<version>5.2.3</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,8 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.1</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>
@ -20,5 +21,6 @@
<file src="..\Oqtane.Shared\bin\Release\net8.0\Oqtane.Shared.dll" target="lib\net8.0" />
<file src="..\Oqtane.Shared\bin\Release\net8.0\Oqtane.Shared.pdb" target="lib\net8.0" />
<file src="icon.png" target="" />
<file src="readme.md" target="" />
</files>
</package>

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<package>
<metadata>
<id>Oqtane.Updater</id>
<version>5.2.1</version>
<version>5.2.3</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,12 +12,14 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.1</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>
<files>
<file src="..\Oqtane.Updater\bin\Release\net8.0\publish\*.*" target="lib\net8.0" />
<file src="icon.png" target="" />
<file src="readme.md" target="" />
</files>
</package>

View File

@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.2.1.Install.zip" -Force
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.2.3.Install.zip" -Force

Binary file not shown.

9
Oqtane.Package/readme.md Normal file
View File

@ -0,0 +1,9 @@
# Oqtane Framework
![Oqtane](https://github.com/oqtane/framework/blob/master/oqtane.png?raw=true "Oqtane")
Oqtane is an open source CMS and Application Framework that provides advanced functionality for developing web, mobile, and desktop applications on .NET. It leverages Blazor to compose a fully dynamic digital experience which can be hosted on Static Blazor, Blazor Server, Blazor WebAssembly, or Blazor Hybrid (via .NET MAUI).
Oqtane was created by [Shaun Walker](https://www.linkedin.com/in/shaunbrucewalker/) and was inspired by his earlier efforts with DotNetNuke... however Oqtane is a native Blazor application written from the ground up using modern .NET Core technology and a Single Page Application (SPA) architecture. It is a modular application framework offering a fully dynamic page compositing model, multi-site support, designer friendly themes, and extensibility via third party modules.
More information about Oqtane can be found at: [https://www.oqtane.org](https://www.oqtane.org)

View File

@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.2.1.Upgrade.zip" -Force
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.2.3.Upgrade.zip" -Force

View File

@ -522,7 +522,7 @@
" let currentUrl = window.location.pathname;" + Environment.NewLine +
" Blazor.addEventListener('enhancedload', () => {" + Environment.NewLine +
" let newUrl = window.location.pathname;" + Environment.NewLine +
" if (currentUrl != newUrl) {" + Environment.NewLine +
" if (currentUrl !== newUrl || window.location.hash === '') {" + Environment.NewLine +
" window.scrollTo({ top: 0, left: 0, behavior: 'instant' });" + Environment.NewLine +
" }" + Environment.NewLine +
" currentUrl = newUrl;" + Environment.NewLine +
@ -534,9 +534,9 @@
private string ParseScripts(string content)
{
// iterate scripts
var scripts = "";
if (!string.IsNullOrEmpty(content))
// in interactive render mode, parse scripts from content and inject into page
if (_renderMode == RenderModes.Interactive && !string.IsNullOrEmpty(content))
{
var index = content.IndexOf("<script");
while (index >= 0)
@ -644,6 +644,16 @@
resources = AddResources(resources, obj.Resources, ResourceLevel.Page, alias, "Themes", type.Namespace, site.RenderMode);
}
}
// theme settings components are dynamically loaded within the framework Page Management module
if (page.Path == "admin/pages" && action.ToLower() == "edit" && theme != null && !string.IsNullOrEmpty(theme.ThemeSettingsType))
{
var settingsType = Type.GetType(theme.ThemeSettingsType);
if (settingsType != null)
{
var objSettings = Activator.CreateInstance(settingsType) as IModuleControl;
resources = AddResources(resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace, site.RenderMode);
}
}
foreach (Module module in modules.Where(item => item.PageId == page.PageId || item.ModuleId == moduleid))
{
@ -686,25 +696,49 @@
// ensure component exists and implements IModuleControl
module.ModuleType = "";
Type moduletype = Type.GetType(typename, false, true); // case insensitive
var moduletype = Type.GetType(typename, false, true); // case insensitive
if (moduletype != null && moduletype.GetInterfaces().Contains(typeof(IModuleControl)))
{
module.ModuleType = Utilities.GetFullTypeName(moduletype.AssemblyQualifiedName); // get actual type name
}
if (moduletype != null && module.ModuleType != "")
{
var obj = Activator.CreateInstance(moduletype) as IModuleControl;
if (obj != null)
var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
if (moduleobject != null)
{
resources = AddResources(resources, obj.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode);
resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode);
// settings components are dynamically loaded within the framework Settings module
if (action.ToLower() == "settings" && module.ModuleDefinition != null)
{
// settings components are embedded within a framework settings module
moduletype = Type.GetType(module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, action), false, true);
// module settings component
var settingsType = "";
if (!string.IsNullOrEmpty(module.ModuleDefinition.SettingsType))
{
// module settings type explicitly declared in IModule interface
settingsType = module.ModuleDefinition.SettingsType;
}
else
{
// legacy support - module settings type determined by convention
settingsType = module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, action);
}
moduletype = Type.GetType(settingsType, false, true);
if (moduletype != null)
{
obj = Activator.CreateInstance(moduletype) as IModuleControl;
resources = AddResources(resources, obj.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode);
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode);
}
// container settings component
if (theme != null && !string.IsNullOrEmpty(theme.ContainerSettingsType))
{
moduletype = Type.GetType(theme.ContainerSettingsType);
if (moduletype != null)
{
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode);
}
}
}
}

View File

@ -425,11 +425,11 @@ namespace Oqtane.Controllers
// POST api/<controller>/upload
[EnableCors(Constants.MauiCorsPolicy)]
[HttpPost("upload")]
public async Task UploadFile(string folder, IFormFile formfile)
public async Task<IActionResult> UploadFile(string folder, IFormFile formfile)
{
if (formfile == null || formfile.Length <= 0)
{
return;
return NoContent();
}
// ensure filename is valid
@ -437,7 +437,7 @@ namespace Oqtane.Controllers
if (!formfile.FileName.IsPathOrFileValid() || !formfile.FileName.Contains(token) || !HasValidFileExtension(formfile.FileName.Substring(0, formfile.FileName.IndexOf(token))))
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Name Is Invalid Or Contains Invalid Extension {File}", formfile.FileName);
return;
return NoContent();
}
string folderPath = "";
@ -492,6 +492,8 @@ namespace Oqtane.Controllers
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Upload Attempt {Folder} {File}", folder, formfile.FileName);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
return NoContent();
}
private async Task<string> MergeFile(string folder, string filename)

View File

@ -6,7 +6,6 @@ using System.Threading.Tasks;
using System.Linq;
using System.Security.Claims;
using Oqtane.Shared;
using System;
using System.Net;
using Oqtane.Enums;
using Oqtane.Infrastructure;
@ -28,9 +27,10 @@ namespace Oqtane.Controllers
private readonly IUserPermissions _userPermissions;
private readonly IJwtManager _jwtManager;
private readonly IFileRepository _files;
private readonly ISettingRepository _settings;
private readonly ILogManager _logger;
public UserController(IUserRepository users, ITenantManager tenantManager, IUserManager userManager, ISiteRepository sites, IUserPermissions userPermissions, IJwtManager jwtManager, IFileRepository files, ILogManager logger)
public UserController(IUserRepository users, ITenantManager tenantManager, IUserManager userManager, ISiteRepository sites, IUserPermissions userPermissions, IJwtManager jwtManager, IFileRepository files, ISettingRepository settings, ILogManager logger)
{
_users = users;
_tenantManager = tenantManager;
@ -39,6 +39,7 @@ namespace Oqtane.Controllers
_userPermissions = userPermissions;
_jwtManager = jwtManager;
_files = files;
_settings = settings;
_logger = logger;
}
@ -110,31 +111,58 @@ namespace Oqtane.Controllers
private User Filter(User user)
{
// clone object to avoid mutating cache
User filtered = null;
if (user != null)
{
user.Password = "";
user.IsAuthenticated = false;
user.TwoFactorCode = "";
user.TwoFactorExpiry = null;
filtered = new User();
if (!_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) && User.Identity.Name?.ToLower() != user.Username.ToLower())
// public properties
filtered.SiteId = user.SiteId;
filtered.UserId = user.UserId;
filtered.Username = user.Username;
filtered.DisplayName = user.DisplayName;
// restricted properties
filtered.Password = "";
filtered.TwoFactorCode = "";
filtered.SecurityStamp = "";
// include private properties if authenticated user is accessing their own user account os is an administrator
if (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == user.UserId)
{
user.Email = "";
user.PhotoFileId = null;
user.LastLoginOn = DateTime.MinValue;
user.LastIPAddress = "";
user.Roles = "";
user.CreatedBy = "";
user.CreatedOn = DateTime.MinValue;
user.ModifiedBy = "";
user.ModifiedOn = DateTime.MinValue;
user.DeletedBy = "";
user.DeletedOn = DateTime.MinValue;
user.IsDeleted = false;
user.TwoFactorRequired = false;
filtered.Email = user.Email;
filtered.PhotoFileId = user.PhotoFileId;
filtered.LastLoginOn = user.LastLoginOn;
filtered.LastIPAddress = user.LastIPAddress;
filtered.TwoFactorRequired = false;
filtered.Roles = user.Roles;
filtered.CreatedBy = user.CreatedBy;
filtered.CreatedOn = user.CreatedOn;
filtered.ModifiedBy = user.ModifiedBy;
filtered.ModifiedOn = user.ModifiedOn;
filtered.DeletedBy = user.DeletedBy;
filtered.DeletedOn = user.DeletedOn;
filtered.IsDeleted = user.IsDeleted;
}
// if authenticated user is accessing their own user account
if (_userPermissions.GetUser(User).UserId == user.UserId)
{
// include all settings
filtered.Settings = user.Settings;
}
else
{
// include only public settings
filtered.Settings = _settings.GetSettings(EntityNames.User, user.UserId)
.Where(item => !item.IsPrivate)
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
}
}
return user;
return filtered;
}
// POST api/<controller>
@ -147,11 +175,13 @@ namespace Oqtane.Controllers
if (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin))
{
user.EmailConfirmed = true;
user.IsAuthenticated = true;
allowregistration = true;
}
else
{
user.EmailConfirmed = false;
user.IsAuthenticated = false;
allowregistration = _sites.GetSite(user.SiteId).AllowRegistration;
}
@ -233,8 +263,24 @@ namespace Oqtane.Controllers
[Authorize]
public async Task Logout([FromBody] User user)
{
await HttpContext.SignOutAsync(Constants.AuthenticationScheme);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Logout {Username}", (user != null) ? user.Username : "");
if (_userPermissions.GetUser(User).UserId == user.UserId)
{
await HttpContext.SignOutAsync(Constants.AuthenticationScheme);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Logout {Username}", (user != null) ? user.Username : "");
}
}
// POST api/<controller>/logout
[HttpPost("logouteverywhere")]
[Authorize]
public async Task LogoutEverywhere([FromBody] User user)
{
if (_userPermissions.GetUser(User).UserId == user.UserId)
{
await _userManager.LogoutUserEverywhere(user);
await HttpContext.SignOutAsync(Constants.AuthenticationScheme);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Logout Everywhere {Username}", (user != null) ? user.Username : "");
}
}
// POST api/<controller>/verify
@ -355,6 +401,7 @@ namespace Oqtane.Controllers
}
if (roles != "") roles = ";" + roles;
user.Roles = roles;
user.SecurityStamp = User.SecurityStamp();
}
return user;
}

View File

@ -1,6 +1,5 @@
using System.Linq;
using System.Security.Claims;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.Extensions
@ -41,9 +40,9 @@ namespace Oqtane.Extensions
public static string SiteKey(this ClaimsPrincipal claimsPrincipal)
{
if (claimsPrincipal.HasClaim(item => item.Type == "sitekey"))
if (claimsPrincipal.HasClaim(item => item.Type == Constants.SiteKeyClaimType))
{
return claimsPrincipal.Claims.FirstOrDefault(item => item.Type == "sitekey").Value;
return claimsPrincipal.Claims.FirstOrDefault(item => item.Type == Constants.SiteKeyClaimType).Value;
}
else
{
@ -71,6 +70,18 @@ namespace Oqtane.Extensions
return -1;
}
public static string SecurityStamp(this ClaimsPrincipal claimsPrincipal)
{
if (claimsPrincipal.HasClaim(item => item.Type == Constants.SecurityStampClaimType))
{
return claimsPrincipal.Claims.FirstOrDefault(item => item.Type == Constants.SecurityStampClaimType).Value;
}
else
{
return "";
}
}
public static bool IsOnlyInRole(this ClaimsPrincipal claimsPrincipal, string role)
{
var identity = claimsPrincipal.Identities.FirstOrDefault(item => item.AuthenticationType == Constants.AuthenticationScheme);

View File

@ -527,28 +527,63 @@ namespace Oqtane.Extensions
// manage user
if (user != null)
{
// create claims identity
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);
// external roles
// manage roles
var _userRoles = httpContext.RequestServices.GetRequiredService<IUserRoleRepository>();
var userRoles = _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList();
if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", "")))
{
if (claimsPrincipal.Claims.Any(item => item.Type == ClaimTypes.Role))
// external roles
if (claimsPrincipal.Claims.Any(item => item.Type == httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", "")))
{
foreach (var claim in claimsPrincipal.Claims.Where(item => item.Type == ClaimTypes.Role))
var _roles = httpContext.RequestServices.GetRequiredService<IRoleRepository>();
var roles = _roles.GetRoles(user.SiteId).ToList(); // global roles excluded ie. host users cannot be added/deleted
var mappings = httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimMappings", "").Split(',');
foreach (var claim in claimsPrincipal.Claims.Where(item => item.Type == httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", "")))
{
if (!identity.Claims.Any(item => item.Type == ClaimTypes.Role && item.Value == claim.Value))
var rolename = claim.Value;
if (mappings.Any(item => item.StartsWith(rolename + ":")))
{
identity.AddClaim(new Claim(ClaimTypes.Role, claim.Value));
rolename = mappings.First(item => item.StartsWith(rolename + ":")).Split(':')[1];
}
var role = roles.FirstOrDefault(item => item.Name == rolename);
if (role != null)
{
if (!userRoles.Any(item => item.RoleId == role.RoleId && item.UserId == user.UserId))
{
var userRole = new UserRole();
userRole.RoleId = role.RoleId;
userRole.UserId = user.UserId;
_userRoles.AddUserRole(userRole);
}
}
}
if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:SynchronizeRoles", "false")))
{
userRoles = _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList();
foreach (var userRole in userRoles)
{
var role = roles.FirstOrDefault(item => item.RoleId == userRole.RoleId);
if (role != null)
{
var rolename = role.Name;
if (mappings.Any(item => item.EndsWith(":" + rolename)))
{
rolename = mappings.First(item => item.EndsWith(":" + rolename)).Split(':')[0];
}
if (!claimsPrincipal.Claims.Any(item => item.Type == httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", "") && item.Value == rolename))
{
_userRoles.DeleteUserRole(userRole.UserRoleId);
}
}
}
}
userRoles = _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList();
}
else
{
@ -556,6 +591,12 @@ namespace Oqtane.Extensions
}
}
// create claims identity
identityuser = await _identityUserManager.FindByEmailAsync(user.Username);
user.SecurityStamp = identityuser.SecurityStamp;
identity = UserSecurity.CreateClaimsIdentity(alias, user, userRoles);
identity.Label = ExternalLoginStatus.Success;
// user profile claims
if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", "")))
{
@ -604,7 +645,7 @@ namespace Oqtane.Extensions
}
}
_logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} Using Provider {Provider}", user.Username, providerName);
_logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} From IP Address {IPAddress} Using Provider {Provider}", user.Username, httpContext.Connection.RemoteIpAddress, providerName);
}
}
else // claims invalid

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Oqtane.Enums;
using Oqtane.Models;
using Oqtane.Repository;
@ -20,8 +21,9 @@ namespace Oqtane.Infrastructure
private readonly IHttpContextAccessor _accessor;
private readonly IUserRoleRepository _userRoles;
private readonly INotificationRepository _notifications;
private readonly ILogger<LogManager> _filelogger;
public LogManager(ILogRepository logs, ITenantManager tenantManager, IConfigManager config, IUserPermissions userPermissions, IHttpContextAccessor accessor, IUserRoleRepository userRoles, INotificationRepository notifications)
public LogManager(ILogRepository logs, ITenantManager tenantManager, IConfigManager config, IUserPermissions userPermissions, IHttpContextAccessor accessor, IUserRoleRepository userRoles, INotificationRepository notifications, ILogger<LogManager> filelogger)
{
_logs = logs;
_tenantManager = tenantManager;
@ -30,24 +32,25 @@ namespace Oqtane.Infrastructure
_accessor = accessor;
_userRoles = userRoles;
_notifications = notifications;
_filelogger = filelogger;
}
public void Log(LogLevel level, object @class, LogFunction function, string message, params object[] args)
public void Log(Shared.LogLevel level, object @class, LogFunction function, string message, params object[] args)
{
Log(-1, level, @class, function, null, message, args);
}
public void Log(LogLevel level, object @class, LogFunction function, Exception exception, string message, params object[] args)
public void Log(Shared.LogLevel level, object @class, LogFunction function, Exception exception, string message, params object[] args)
{
Log(-1, level, @class, function, exception, message, args);
}
public void Log(int siteId, LogLevel level, object @class, LogFunction function, string message, params object[] args)
public void Log(int siteId, Shared.LogLevel level, object @class, LogFunction function, string message, params object[] args)
{
Log(siteId, level, @class, function, null, message, args);
}
public void Log(int siteId, LogLevel level, object @class, LogFunction function, Exception exception, string message, params object[] args)
public void Log(int siteId, Shared.LogLevel level, object @class, LogFunction function, Exception exception, string message, params object[] args)
{
Log log = new Log();
@ -60,7 +63,6 @@ namespace Oqtane.Infrastructure
log.SiteId = alias.SiteId;
}
}
if (log.SiteId == -1) return; // logs must be site specific
log.PageId = null;
log.ModuleId = null;
@ -92,7 +94,7 @@ namespace Oqtane.Infrastructure
log.Feature = log.Category;
}
log.Function = Enum.GetName(typeof(LogFunction), function);
log.Level = Enum.GetName(typeof(LogLevel), level);
log.Level = Enum.GetName(typeof(Shared.LogLevel), level);
if (exception != null)
{
log.Exception = exception.ToString();
@ -112,14 +114,14 @@ namespace Oqtane.Infrastructure
public void Log(Log log)
{
LogLevel minlevel = LogLevel.Information;
var minlevel = Shared.LogLevel.Information;
var section = _config.GetSection("Logging:LogLevel:Default");
if (section.Exists())
{
minlevel = Enum.Parse<LogLevel>(section.Value);
minlevel = Enum.Parse<Shared.LogLevel>(section.Value);
}
if (Enum.Parse<LogLevel>(log.Level) >= minlevel)
if (Enum.Parse<Shared.LogLevel>(log.Level) >= minlevel)
{
log.LogDate = DateTime.UtcNow;
log.Server = Environment.MachineName;
@ -127,12 +129,19 @@ namespace Oqtane.Infrastructure
log = ProcessStructuredLog(log);
try
{
_logs.AddLog(log);
SendNotification(log);
if (log.SiteId != -1)
{
_logs.AddLog(log);
SendNotification(log);
}
else // use file logger as fallback when site cannot be determined
{
_filelogger.Log(GetLogLevel(log.Level), "[" + log.Category + "] " + log.Message);
}
}
catch
{
// an error occurred writing to the database
// an error occurred writing the log
}
}
}
@ -156,17 +165,11 @@ namespace Oqtane.Infrastructure
names.Add(message.Substring(index + 1, message.IndexOf("}", index) - index - 1));
if (values.Length > (names.Count - 1))
{
if (values[names.Count - 1] == null)
{
message = message.Replace("{" + names[names.Count - 1] + "}", "null");
}
else
{
message = message.Replace("{" + names[names.Count - 1] + "}", values[names.Count - 1].ToString());
}
var value = (values[names.Count - 1] == null) ? "null" : values[names.Count - 1].ToString();
message = message.Replace("{" + names[names.Count - 1] + "}", value);
}
}
index = message.IndexOf("{", index + 1);
index = (index < message.Length - 1) ? message.IndexOf("{", index + 1) : -1;
}
// rebuild properties into dictionary
Dictionary<string, object> propertyDictionary = new Dictionary<string, object>();
@ -195,13 +198,13 @@ namespace Oqtane.Infrastructure
private void SendNotification(Log log)
{
LogLevel notifylevel = LogLevel.Error;
Shared.LogLevel notifylevel = Shared.LogLevel.Error;
var section = _config.GetSection("Logging:LogLevel:Notify");
if (section.Exists())
{
notifylevel = Enum.Parse<LogLevel>(section.Value);
notifylevel = Enum.Parse<Shared.LogLevel>(section.Value);
}
if (Enum.Parse<LogLevel>(log.Level) >= notifylevel)
if (Enum.Parse<Shared.LogLevel>(log.Level) >= notifylevel)
{
var subject = $"Site {log.Level} Notification";
string body = $"Log Message: {log.Message}";
@ -220,5 +223,26 @@ namespace Oqtane.Infrastructure
}
}
}
private Microsoft.Extensions.Logging.LogLevel GetLogLevel(string level)
{
switch (Enum.Parse<Shared.LogLevel>(level))
{
case Shared.LogLevel.Trace:
return Microsoft.Extensions.Logging.LogLevel.Trace;
case Shared.LogLevel.Debug:
return Microsoft.Extensions.Logging.LogLevel.Debug;
case Shared.LogLevel.Information:
return Microsoft.Extensions.Logging.LogLevel.Information;
case Shared.LogLevel.Warning:
return Microsoft.Extensions.Logging.LogLevel.Warning;
case Shared.LogLevel.Error:
return Microsoft.Extensions.Logging.LogLevel.Error;
case Shared.LogLevel.Critical:
return Microsoft.Extensions.Logging.LogLevel.Critical;
default:
return Microsoft.Extensions.Logging.LogLevel.None;
}
}
}
}

View File

@ -3,8 +3,7 @@ using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Oqtane.Extensions;
using Oqtane.Models;
using Oqtane.Repository;
using Oqtane.Managers;
using Oqtane.Security;
using Oqtane.Shared;
@ -59,19 +58,18 @@ namespace Oqtane.Infrastructure
if (userid != null && username != null)
{
// create user identity
var user = new User
var _users = context.RequestServices.GetService(typeof(IUserManager)) as IUserManager;
var user = _users.GetUser(userid, alias.SiteId); // cached
if (user != null && !user.IsDeleted)
{
UserId = int.Parse(userid),
Username = username
};
// set claims identity (note jwt already contains the roles - we are reloading to ensure most accurate permissions)
var _userRoles = context.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository;
var claimsidentity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, alias.SiteId).ToList());
context.User = new ClaimsPrincipal(claimsidentity);
logger.Log(alias.SiteId, LogLevel.Information, "TokenValidation", Enums.LogFunction.Security, "Token Validated For UserId {UserId} And Username {Username}", user.UserId, user.Username);
var claimsidentity = UserSecurity.CreateClaimsIdentity(alias, user);
context.User = new ClaimsPrincipal(claimsidentity);
logger.Log(alias.SiteId, LogLevel.Information, "TokenValidation", Enums.LogFunction.Security, "Token Validated For User {Username}", user.Username);
}
else
{
logger.Log(alias.SiteId, LogLevel.Error, "TokenValidation", Enums.LogFunction.Security, "Token Validated But User {Username} Does Not Exist Or Is Deleted", user.Username);
}
}
else
{

View File

@ -13,6 +13,7 @@ namespace Oqtane.Managers
Task<User> UpdateUser(User user);
Task DeleteUser(int userid, int siteid);
Task<User> LoginUser(User user, bool setCookie, bool isPersistent);
Task LogoutUserEverywhere(User user);
Task<User> VerifyEmail(User user, string token);
Task ForgotPassword(User user);
Task<User> ResetPassword(User user, string token);

View File

@ -64,8 +64,8 @@ namespace Oqtane.Managers
{
user.SiteId = siteid;
user.Roles = GetUserRoles(user.UserId, user.SiteId);
List<Setting> settings = _settings.GetSettings(EntityNames.User, user.UserId).ToList();
user.Settings = settings.Where(item => !item.IsPrivate || user.UserId == user.UserId)
user.SecurityStamp = _identityUserManager.FindByNameAsync(user.Username).GetAwaiter().GetResult()?.SecurityStamp;
user.Settings = _settings.GetSettings(EntityNames.User, user.UserId)
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
}
return user;
@ -145,13 +145,17 @@ namespace Oqtane.Managers
}
else
{
var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, false);
succeeded = result.Succeeded;
if (!succeeded)
succeeded = true;
if (!user.IsAuthenticated)
{
errors = "Password Not Valid For User";
var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, false);
succeeded = result.Succeeded;
if (!succeeded)
{
errors = "Password Not Valid For User";
}
user.EmailConfirmed = succeeded;
}
user.EmailConfirmed = succeeded;
}
if (succeeded)
@ -227,6 +231,7 @@ namespace Oqtane.Managers
{
identityuser.PasswordHash = _identityUserManager.PasswordHasher.HashPassword(identityuser, user.Password);
await _identityUserManager.UpdateAsync(identityuser);
await _identityUserManager.UpdateSecurityStampAsync(identityuser); // will force user to sign in again
}
else
{
@ -237,7 +242,8 @@ namespace Oqtane.Managers
if (user.Email != identityuser.Email)
{
await _identityUserManager.SetEmailAsync(identityuser, user.Email);
identityuser.Email = user.Email;
await _identityUserManager.UpdateAsync(identityuser); // security stamp not updated
// if email address changed and it is not confirmed, verification is required for new email address
if (!user.EmailConfirmed)
@ -259,7 +265,6 @@ namespace Oqtane.Managers
user = _users.UpdateUser(user);
_syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Update);
_syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Reload);
_cache.Remove($"user:{user.UserId}:{alias.SiteKey}");
user.Password = ""; // remove sensitive information
_logger.Log(LogLevel.Information, this, LogFunction.Update, "User Updated {User}", user);
}
@ -367,7 +372,7 @@ namespace Oqtane.Managers
user.LastLoginOn = DateTime.UtcNow;
user.LastIPAddress = LastIPAddress;
_users.UpdateUser(user);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful {Username}", user.Username);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful For {Username} From IP Address {IPAddress}", user.Username, LastIPAddress);
if (setCookie)
{
@ -414,6 +419,16 @@ namespace Oqtane.Managers
return user;
}
public async Task LogoutUserEverywhere(User user)
{
var identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null)
{
await _identityUserManager.UpdateSecurityStampAsync(identityuser);
_syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Update);
_syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Reload);
}
}
public async Task<User> VerifyEmail(User user, string token)
{
@ -469,6 +484,7 @@ namespace Oqtane.Managers
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null && !string.IsNullOrEmpty(token))
{
// note that ResetPasswordAsync checks password complexity rules
var result = await _identityUserManager.ResetPasswordAsync(identityuser, token, user.Password);
if (result.Succeeded)
{

View File

@ -127,6 +127,46 @@ namespace Oqtane.Migrations.EntityBuilders
return table.Column<DateTimeOffset>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue);
}
public void AddDateOnlyColumn(string name, bool nullable = false)
{
_migrationBuilder.AddColumn<DateOnly>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema);
}
public void AddDateOnlyColumn(string name, bool nullable, DateOnly defaultValue)
{
_migrationBuilder.AddColumn<DateOnly>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue, schema: Schema);
}
protected OperationBuilder<AddColumnOperation> AddDateOnlyColumn(ColumnsBuilder table, string name, bool nullable = false)
{
return table.Column<DateOnly>(name: RewriteName(name), nullable: nullable);
}
protected OperationBuilder<AddColumnOperation> AddDateOnlyColumn(ColumnsBuilder table, string name, bool nullable, DateOnly defaultValue)
{
return table.Column<DateOnly>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue);
}
public void AddTimeOnlyColumn(string name, bool nullable = false)
{
_migrationBuilder.AddColumn<TimeOnly>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema);
}
public void AddTimeOnlyColumn(string name, bool nullable, TimeOnly defaultValue)
{
_migrationBuilder.AddColumn<TimeOnly>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue, schema: Schema);
}
protected OperationBuilder<AddColumnOperation> AddTimeOnlyColumn(ColumnsBuilder table, string name, bool nullable = false)
{
return table.Column<TimeOnly>(name: RewriteName(name), nullable: nullable);
}
protected OperationBuilder<AddColumnOperation> AddTimeOnlyColumn(ColumnsBuilder table, string name, bool nullable, TimeOnly defaultValue)
{
return table.Column<TimeOnly>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue);
}
public void AddByteColumn(string name, bool nullable = false)
{
_migrationBuilder.AddColumn<byte>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema);

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Configurations>Debug;Release</Configurations>
<Version>5.2.1</Version>
<Version>5.2.3</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -11,7 +11,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.1</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>

View File

@ -23,7 +23,7 @@ namespace Oqtane.Pages
_syncManager = syncManager;
}
public async Task<IActionResult> OnPostAsync(string returnurl)
public async Task<IActionResult> OnPostAsync(string returnurl, string everywhere)
{
if (HttpContext.User != null)
{
@ -31,6 +31,10 @@ namespace Oqtane.Pages
var user = _userManager.GetUser(HttpContext.User.Identity.Name, alias.SiteId);
if (user != null)
{
if (everywhere == "true")
{
await _userManager.LogoutUserEverywhere(user);
}
_syncManager.AddSyncEvent(alias, EntityNames.User, user.UserId, SyncEventActions.Reload);
}

View File

@ -40,7 +40,7 @@ namespace Oqtane.Providers
}
else
{
return true;
return authState.User.SecurityStamp() == user.SecurityStamp;
}
}
}

View File

@ -48,6 +48,12 @@ namespace Oqtane.Repository
public void AddLog(Log log)
{
if (log.Url.Length > 2048) log.Url = log.Url.Substring(0, 2048);
if (log.Server.Length > 200) log.Server = log.Server.Substring(0, 200);
if (log.Category.Length > 200) log.Category = log.Category.Substring(0, 200);
if (log.Feature.Length > 200) log.Feature = log.Feature.Substring(0, 200);
if (log.Function.Length > 20) log.Function = log.Function.Substring(0, 20);
if (log.Level.Length > 20) log.Level = log.Level.Substring(0, 20);
using var db = _dbContextFactory.CreateDbContext();
db.Log.Add(log);
db.SaveChanges();

View File

@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.Extensions.Caching.Memory;
using Oqtane.Infrastructure;
using Oqtane.Models;

View File

@ -441,7 +441,7 @@ namespace Oqtane.Repository
pageModule.Module.PermissionList = new List<Permission>();
foreach (var permission in pageTemplateModule.PermissionList)
{
pageModule.Module.PermissionList.Add(permission.Clone(permission));
pageModule.Module.PermissionList.Add(permission.Clone());
}
pageModule.Module.AllPages = false;
pageModule.Module.IsDeleted = false;

View File

@ -5,15 +5,11 @@ using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using System.Security;
using Microsoft.Extensions.Caching.Memory;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Shared;
using Oqtane.Themes;
using System.Reflection.Metadata;
using Oqtane.Migrations.Master;
using Oqtane.Modules;
namespace Oqtane.Repository
{

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Oqtane.Infrastructure;
@ -14,13 +15,15 @@ namespace Oqtane.Repository
private readonly IDbContextFactory<TenantDBContext> _dbContextFactory;
private readonly IRoleRepository _roles;
private readonly ITenantManager _tenantManager;
private readonly UserManager<IdentityUser> _identityUserManager;
private readonly IMemoryCache _cache;
public UserRoleRepository(IDbContextFactory<TenantDBContext> dbContextFactory, IRoleRepository roles, ITenantManager tenantManager, IMemoryCache cache)
public UserRoleRepository(IDbContextFactory<TenantDBContext> dbContextFactory, IRoleRepository roles, ITenantManager tenantManager, UserManager<IdentityUser> identityUserManager, IMemoryCache cache)
{
_dbContextFactory = dbContextFactory;
_roles = roles;
_tenantManager = tenantManager;
_identityUserManager = identityUserManager;
_cache = cache;
}
@ -69,9 +72,7 @@ namespace Oqtane.Repository
DeleteUserRoles(userRole.UserId);
}
var alias = _tenantManager.GetAlias();
_cache.Remove($"user:{userRole.UserId}:{alias.SiteKey}");
_cache.Remove($"userroles:{userRole.UserId}:{alias.SiteKey}");
UpdateSecurityStamp(userRole.UserId);
return userRole;
}
@ -82,9 +83,7 @@ namespace Oqtane.Repository
db.Entry(userRole).State = EntityState.Modified;
db.SaveChanges();
var alias = _tenantManager.GetAlias();
_cache.Remove($"user:{userRole.UserId}:{alias.SiteKey}");
_cache.Remove($"userroles:{userRole.UserId}:{alias.SiteKey}");
UpdateSecurityStamp(userRole.UserId);
return userRole;
}
@ -144,9 +143,7 @@ namespace Oqtane.Repository
db.UserRole.Remove(userRole);
db.SaveChanges();
var alias = _tenantManager.GetAlias();
_cache.Remove($"user:{userRole.UserId}:{alias.SiteKey}");
_cache.Remove($"userroles:{userRole.UserId}:{alias.SiteKey}");
UpdateSecurityStamp(userRole.UserId);
}
public void DeleteUserRoles(int userId)
@ -158,9 +155,30 @@ namespace Oqtane.Repository
}
db.SaveChanges();
UpdateSecurityStamp(userId);
}
private void UpdateSecurityStamp(int userId)
{
// update user security stamp
using var db = _dbContextFactory.CreateDbContext();
var user = db.User.Find(userId);
if (user != null)
{
var identityuser = _identityUserManager.FindByNameAsync(user.Username).GetAwaiter().GetResult();
if (identityuser != null)
{
_identityUserManager.UpdateSecurityStampAsync(identityuser);
}
}
// refresh cache
var alias = _tenantManager.GetAlias();
_cache.Remove($"user:{userId}:{alias.SiteKey}");
_cache.Remove($"userroles:{userId}:{alias.SiteKey}");
if (alias != null)
{
_cache.Remove($"user:{userId}:{alias.SiteKey}");
_cache.Remove($"userroles:{userId}:{alias.SiteKey}");
}
}
}
}

View File

@ -13,14 +13,17 @@ namespace Oqtane.Security
public class ClaimsPrincipalFactory<TUser> : UserClaimsPrincipalFactory<TUser> where TUser : IdentityUser
{
private readonly ITenantManager _tenants;
// cannot utilize IUserManager due to circular references - which is fine as this method is only called on login
private readonly IUserRepository _users;
private readonly IUserRoleRepository _userRoles;
private readonly UserManager<TUser> _userManager;
public ClaimsPrincipalFactory(UserManager<TUser> userManager, IOptions<IdentityOptions> optionsAccessor, ITenantManager tenants, IUserRepository users, IUserRoleRepository userroles) : base(userManager, optionsAccessor)
{
_tenants = tenants;
_users = users;
_userRoles = userroles;
_userManager = userManager;
}
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(TUser identityuser)
@ -33,6 +36,7 @@ namespace Oqtane.Security
Alias alias = _tenants.GetAlias();
if (alias != null)
{
user.SecurityStamp = await _userManager.GetSecurityStampAsync(identityuser);
List<UserRole> userroles = _userRoles.GetUserRoles(user.UserId, alias.SiteId).ToList();
identity = UserSecurity.CreateClaimsIdentity(alias, user, userroles);
}

View File

@ -3,12 +3,11 @@ using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.Cookies;
using Oqtane.Infrastructure;
using Oqtane.Repository;
using Oqtane.Models;
using System.Collections.Generic;
using Oqtane.Extensions;
using Oqtane.Shared;
using System.IO;
using Oqtane.Managers;
namespace Oqtane.Security
{
@ -24,49 +23,38 @@ namespace Oqtane.Security
// check if framework is installed
if (config.IsInstalled() && !path.StartsWith("/_")) // ignore Blazor framework requests
{
// get current site
var _logger = context.HttpContext.RequestServices.GetService(typeof(ILogManager)) as ILogManager;
var alias = context.HttpContext.GetAlias();
if (alias != null)
{
var claims = context.Principal.Claims;
var userManager = context.HttpContext.RequestServices.GetService(typeof(IUserManager)) as IUserManager;
var user = userManager.GetUser(context.Principal.UserId(), alias.SiteId); // cached
// check if principal has roles and matches current site
if (!claims.Any(item => item.Type == ClaimTypes.Role) || !claims.Any(item => item.Type == "sitekey" && item.Value == alias.SiteKey))
// check if user is valid, not deleted, has roles, and security stamp has not changed
if (user != null && !user.IsDeleted && !string.IsNullOrEmpty(user.Roles) && context.Principal.SecurityStamp() == user.SecurityStamp)
{
var userRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRepository)) as IUserRepository;
var userRoleRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository;
var _logger = context.HttpContext.RequestServices.GetService(typeof(ILogManager)) as ILogManager;
User user = userRepository.GetUser(context.Principal.Identity.Name);
if (user != null)
// validate sitekey in case user has changed sites in installation
if (context.Principal.SiteKey() != alias.SiteKey || !context.Principal.Roles().Any())
{
// replace principal with roles for current site
List<UserRole> userroles = userRoleRepository.GetUserRoles(user.UserId, alias.SiteId).ToList();
if (userroles.Any())
{
var identity = UserSecurity.CreateClaimsIdentity(alias, user, userroles);
context.ReplacePrincipal(new ClaimsPrincipal(identity));
context.ShouldRenew = true;
Log(_logger, alias, "Permissions Updated For User {Username} Accessing {Url}", context.Principal.Identity.Name, path);
}
else
{
// user has no roles - remove principal
Log(_logger, alias, "Permissions Removed For User {Username} Accessing {Url}", context.Principal.Identity.Name, path);
context.RejectPrincipal();
}
}
else
{
// user does not exist - remove principal
Log(_logger, alias, "Permissions Removed For User {Username} Accessing {Url}", context.Principal.Identity.Name, path);
context.RejectPrincipal();
// refresh principal
var identity = UserSecurity.CreateClaimsIdentity(alias, user);
context.ReplacePrincipal(new ClaimsPrincipal(identity));
context.ShouldRenew = true;
Log(_logger, alias, "Permissions Refreshed For User {Username} Accessing {Url}", context.Principal.Identity.Name, path);
}
}
else
{
// remove principal (ie. log user out)
Log(_logger, alias, "Permissions Removed For User {Username} Accessing {Url}", context.Principal.Identity.Name, path);
context.RejectPrincipal();
}
}
else
{
// user is signed in but tenant cannot be determined
// user is signed in but site cannot be determined
Log(_logger, alias, "Alias Could Not Be Resolved For User {Username} Accessing {Url}", context.Principal.Identity.Name, path);
}
}
}
@ -77,7 +65,8 @@ namespace Oqtane.Security
{
if (!path.StartsWith("/api/")) // reduce log verbosity
{
logger.Log(alias.SiteId, LogLevel.Information, "LoginValidation", Enums.LogFunction.Security, message, username, path);
var siteId = (alias != null) ? alias.SiteId : -1;
logger.Log(siteId, LogLevel.Information, "UserValidation", Enums.LogFunction.Security, message, username, path);
}
}
}

View File

@ -32,6 +32,7 @@ namespace Oqtane.Services
private readonly ILogManager _logger;
private readonly IMemoryCache _cache;
private readonly IHttpContextAccessor _accessor;
private readonly string _private = "[PRIVATE]";
public ServerSiteService(ISiteRepository sites, IPageRepository pages, IThemeRepository themes, IPageModuleRepository pageModules, IModuleDefinitionRepository moduleDefinitions, ILanguageRepository languages, IUserPermissions userPermissions, ISettingRepository settings, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger, IMemoryCache cache, IHttpContextAccessor accessor)
{
@ -69,18 +70,26 @@ namespace Oqtane.Services
return GetSite(siteId);
});
// clone object so that cache is not mutated
site = site.Clone();
// trim site settings based on user permissions
site.Settings = site.Settings
.Where(item => !item.Value.StartsWith(_private) || _accessor.HttpContext.User.IsInRole(RoleNames.Admin))
.ToDictionary(setting => setting.Key, setting => setting.Value.Replace(_private, ""));
// trim pages based on user permissions
var pages = new List<Page>();
foreach (Page page in site.Pages)
{
if (!page.IsDeleted && _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.View, page.PermissionList) && (Utilities.IsEffectiveAndNotExpired(page.EffectiveDate, page.ExpiryDate) || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, page.PermissionList)))
{
page.Settings = page.Settings
.Where(item => !item.Value.StartsWith(_private) || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, page.PermissionList))
.ToDictionary(setting => setting.Key, setting => setting.Value.Replace(_private, ""));
pages.Add(page);
}
}
// clone object so that cache is not mutated
site = site.Clone(site);
site.Pages = pages;
return Task.FromResult(site);
@ -94,14 +103,13 @@ namespace Oqtane.Services
{
// site settings
site.Settings = _settings.GetSettings(EntityNames.Site, site.SiteId)
.Where(item => !item.IsPrivate || _accessor.HttpContext.User.IsInRole(RoleNames.Admin))
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
.ToDictionary(setting => setting.SettingName, setting => (setting.IsPrivate ? _private : "") + setting.SettingValue);
// populate File Extensions
// populate file extensions
site.ImageFiles = site.Settings.ContainsKey("ImageFiles") && !string.IsNullOrEmpty(site.Settings["ImageFiles"])
? site.Settings["ImageFiles"] : Constants.ImageFiles;
site.UploadableFiles = site.Settings.ContainsKey("UploadableFiles") && !string.IsNullOrEmpty(site.Settings["UploadableFiles"])
? site.Settings["UploadableFiles"] : Constants.UploadableFiles;
? site.Settings["UploadableFiles"] : Constants.UploadableFiles;
// pages
List<Setting> settings = _settings.GetSettings(EntityNames.Page).ToList();
@ -109,14 +117,13 @@ namespace Oqtane.Services
foreach (Page page in _pages.GetPages(site.SiteId))
{
page.Settings = settings.Where(item => item.EntityId == page.PageId)
.Where(item => !item.IsPrivate || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, page.PermissionList))
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
.ToDictionary(setting => setting.SettingName, setting => (setting.IsPrivate ? _private : "") + setting.SettingValue);
site.Pages.Add(page);
}
site.Pages = GetPagesHierarchy(site.Pages);
// framework modules
var modules = GetModules(site.SiteId);
var modules = GetPageModules(site.SiteId);
site.Settings.Add(Constants.AdminDashboardModule, modules.FirstOrDefault(item => item.ModuleDefinitionName == Constants.AdminDashboardModule).ModuleId.ToString());
site.Settings.Add(Constants.PageManagementModule, modules.FirstOrDefault(item => item.ModuleDefinitionName == Constants.PageManagementModule).ModuleId.ToString());
@ -249,31 +256,28 @@ namespace Oqtane.Services
public Task<List<Module>> GetModulesAsync(int siteId, int pageId)
{
var alias = _tenantManager.GetAlias();
var sitemodules = _cache.GetOrCreate($"modules:{alias.SiteKey}", entry =>
{
entry.SlidingExpiration = TimeSpan.FromMinutes(30);
return GetModules(siteId);
});
var modules = new List<Module>();
foreach (Module module in sitemodules.Where(item => (item.PageId == pageId || pageId == -1) && !item.IsDeleted && _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.View, item.PermissionList)))
{
if (Utilities.IsEffectiveAndNotExpired(module.EffectiveDate, module.ExpiryDate) || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, module.PermissionList))
{
modules.Add(module);
}
}
return Task.FromResult(modules);
}
private List<Module> GetModules(int siteId)
{
var alias = _tenantManager.GetAlias();
return _cache.GetOrCreate($"modules:{alias.SiteKey}", entry =>
var modules = _cache.GetOrCreate($"modules:{alias.SiteKey}", entry =>
{
entry.SlidingExpiration = TimeSpan.FromMinutes(30);
return GetPageModules(siteId);
});
// clone object so that cache is not mutated
modules = modules.ConvertAll(module => module.Clone());
// trim modules for current page based on user permissions
var pagemodules = new List<Module>();
foreach (Module module in modules.Where(item => (item.PageId == pageId || pageId == -1) && !item.IsDeleted && _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.View, item.PermissionList)))
{
if (Utilities.IsEffectiveAndNotExpired(module.EffectiveDate, module.ExpiryDate) || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, module.PermissionList))
{
module.Settings = module.Settings
.Where(item => !item.Value.StartsWith(_private) || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, module.PermissionList))
.ToDictionary(setting => setting.Key, setting => setting.Value.Replace(_private, ""));
pagemodules.Add(module);
}
}
return Task.FromResult(pagemodules);
}
private List<Module> GetPageModules(int siteId)
@ -311,8 +315,7 @@ namespace Oqtane.Services
ModuleDefinition = _moduleDefinitions.FilterModuleDefinition(moduledefinitions.Find(item => item.ModuleDefinitionName == pagemodule.Module.ModuleDefinitionName)),
Settings = settings.Where(item => item.EntityId == pagemodule.ModuleId)
.Where(item => !item.IsPrivate || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, pagemodule.Module.PermissionList))
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue)
.ToDictionary(setting => setting.SettingName, setting => (setting.IsPrivate ? _private : "") + setting.SettingValue)
};
modules.Add(module);

View File

@ -0,0 +1,14 @@
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Services;
using [Owner].Module.[Module].Services;
namespace [Owner].Module.[Module].Startup
{
public class ClientStartup : IClientStartup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<I[Module]Service, [Module]Service>();
}
}
}

View File

@ -0,0 +1,28 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Infrastructure;
using [Owner].Module.[Module].Repository;
using [Owner].Module.[Module].Services;
namespace [Owner].Module.[Module].Startup
{
public class ServerStartup : IServerStartup
{
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// not implemented
}
public void ConfigureMvc(IMvcBuilder mvcBuilder)
{
// not implemented
}
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<I[Module]Service, Server[Module]Service>();
services.AddDbContextFactory<[Module]Context>(opt => { }, ServiceLifetime.Transient);
}
}
}

View File

@ -117,6 +117,10 @@
margin: .5rem;
}
.app-logo .navbar-brand {
color: white;
}
@media (max-width: 767.98px) {
.main .top-row {
display: none;

View File

@ -5,24 +5,30 @@
<meta name="viewport" content="width=device-width">
<title>Upgrade Framework</title>
<base href="/" />
<link rel="stylesheet" href="https://www.oqtane.net/css/app.css">
<link rel="stylesheet" type="text/css" href="[BOOTSTRAPCSSURL]" integrity="[BOOTSTRAPCSSINTEGRITY]" crossorigin="anonymous" />
<link rel="stylesheet" type="text/css" href="https://www.oqtane.net/css/app.css">
</head>
<body onload="forceWait()">
<body onload="refresh()">
<div>
<br /><br />
<h1 align="center">Please Wait... Upgrade In Progress...</h1>
<p align="center">(this process can take a few minutes... please be patient)</p>
</div>
<div class="app-progress-indicator"></div>
<div class="w-50 mx-auto mt-5">
<div class="progress" role="progressbar" aria-label="Basic example" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar progress-bar-striped progress-bar-animated [PROGRESSCLASS]" style="width: [PROGRESS]%"></div>
</div>
<div class="fs-6 fst-italic mt-1">
[STATUS]
</div>
</div>
<script>
function forceWait() {
setInterval(function () {
window.location.href = "/";
}, 120 * 1000);
function refresh() {
setTimeout(function () {
window.location.href = "/?reload";
}, 1000);
}
</script>
</body>
</html>

View File

@ -235,23 +235,40 @@ app {
.app-form-inline {
display: inline;
}
.app-search{
.app-search {
display: inline-block;
position: relative;
}
.app-search input + button{
.app-search input + button {
background: none;
border: none;
position: absolute;
right: 0;
top: 0;
}
.app-search input + button .oi{
.app-search input + button .oi {
top: 0;
}
.app-search-noinput {
display: inline-block;
position: relative;
}
.app-search-noinput button {
background: none;
border: none;
color: var(--bs-heading-color);
}
.app-search-noinput button:hover {
color: var(--bs-heading-color);
}
/* Text Editor */
.text-area-editor > textarea {
width: 100%;
min-height: 250px;
}
.app-logo .navbar-brand {
padding: 5px 20px 5px 20px;
}

View File

@ -198,7 +198,9 @@ Oqtane.Interop = {
}
promises.push(new Promise((resolve, reject) => {
if (loadjs.isDefined(bundles[b])) {
resolve(true);
loadjs.ready(bundles[b], () => {
resolve(true);
});
}
else {
loadjs(urls, bundles[b], {
@ -293,41 +295,49 @@ Oqtane.Interop = {
},
uploadFiles: function (posturl, folder, id, antiforgerytoken, jwt) {
var fileinput = document.getElementById('FileInput_' + id);
var files = fileinput.files;
var progressinfo = document.getElementById('ProgressInfo_' + id);
var progressbar = document.getElementById('ProgressBar_' + id);
if (progressinfo !== null && progressbar !== null) {
progressinfo.setAttribute("style", "display: inline;");
progressinfo.innerHTML = '';
progressbar.setAttribute("style", "width: 100%; display: inline;");
progressbar.value = 0;
}
var files = fileinput.files;
var totalSize = 0;
for (var i = 0; i < files.length; i++) {
var FileChunk = [];
var file = files[i];
var MaxFileSizeMB = 1;
var BufferChunkSize = MaxFileSizeMB * (1024 * 1024);
var FileStreamPos = 0;
var EndPos = BufferChunkSize;
var Size = file.size;
totalSize = totalSize + files[i].size;
}
while (FileStreamPos < Size) {
FileChunk.push(file.slice(FileStreamPos, EndPos));
FileStreamPos = EndPos;
EndPos = FileStreamPos + BufferChunkSize;
var maxChunkSizeMB = 1;
var bufferChunkSize = maxChunkSizeMB * (1024 * 1024);
var uploadedSize = 0;
for (var i = 0; i < files.length; i++) {
var fileChunk = [];
var file = files[i];
var fileStreamPos = 0;
var endPos = bufferChunkSize;
while (fileStreamPos < file.size) {
fileChunk.push(file.slice(fileStreamPos, endPos));
fileStreamPos = endPos;
endPos = fileStreamPos + bufferChunkSize;
}
var TotalParts = FileChunk.length;
var PartCount = 0;
var totalParts = fileChunk.length;
var partCount = 0;
while (Chunk = FileChunk.shift()) {
PartCount++;
var FileName = file.name + ".part_" + PartCount.toString().padStart(3, '0') + "_" + TotalParts.toString().padStart(3, '0');
while (chunk = fileChunk.shift()) {
partCount++;
var fileName = file.name + ".part_" + partCount.toString().padStart(3, '0') + "_" + totalParts.toString().padStart(3, '0');
var data = new FormData();
data.append('__RequestVerificationToken', antiforgerytoken);
data.append('folder', folder);
data.append('formfile', Chunk, FileName);
data.append('formfile', chunk, fileName);
var request = new XMLHttpRequest();
request.open('POST', posturl, true);
if (jwt !== "") {
@ -335,28 +345,36 @@ Oqtane.Interop = {
request.withCredentials = true;
}
request.upload.onloadstart = function (e) {
if (progressinfo !== null && progressbar !== null) {
progressinfo.innerHTML = file.name + ' 0%';
progressbar.value = 0;
if (progressinfo !== null && progressbar !== null && progressinfo.innerHTML === '') {
if (files.length === 1) {
progressinfo.innerHTML = file.name;
}
else {
progressinfo.innerHTML = file.name + ", ...";
}
}
};
request.upload.onprogress = function (e) {
if (progressinfo !== null && progressbar !== null) {
var percent = Math.ceil((e.loaded / e.total) * 100);
progressinfo.innerHTML = file.name + '[' + PartCount + '] ' + percent + '%';
var percent = Math.ceil(((uploadedSize + e.loaded) / totalSize) * 100);
progressbar.value = (percent / 100);
}
};
request.upload.onloadend = function (e) {
if (progressinfo !== null && progressbar !== null) {
progressinfo.innerHTML = file.name + ' 100%';
progressbar.value = 1;
uploadedSize = uploadedSize + e.total;
var percent = Math.ceil((uploadedSize / totalSize) * 100);
progressbar.value = (percent / 100);
}
};
request.upload.onerror = function() {
if (progressinfo !== null && progressbar !== null) {
progressinfo.innerHTML = file.name + ' Error: ' + request.statusText;
progressbar.value = 0;
if (files.length === 1) {
progressinfo.innerHTML = file.name + ' Error: ' + request.statusText;
}
else {
progressinfo.innerHTML = ' Error: ' + request.statusText;
}
}
};
request.send(data);

View File

@ -16,7 +16,7 @@ namespace Oqtane.Themes
string Thumbnail { get; }
/// <summary>
/// Identifies all panes in a theme ( delimited by "," or ";") - assumed to be a layout if no panes specified
/// Comma delimited list of all panes in a theme
/// </summary>
string Panes { get; }

View File

@ -1,4 +1,3 @@
using System;
using System.ComponentModel.DataAnnotations.Schema;
namespace Oqtane.Models
@ -40,5 +39,18 @@ namespace Oqtane.Models
/// Version of the satellite assembly
/// </summary>
public string Version { get; set; }
public Language Clone()
{
return new Language
{
LanguageId = LanguageId,
SiteId = SiteId,
Name = Name,
Code = Code,
IsDefault = IsDefault,
Version = Version
};
}
}
}

View File

@ -2,6 +2,7 @@ using Oqtane.Shared;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
@ -28,11 +29,18 @@ namespace Oqtane.Models
public string ModuleDefinitionName { get; set; }
/// <summary>
/// Determines if this Module Instance should be shown on all pages of the current <see cref="Site"/>
/// Determines if this module should be shown on all pages of the current <see cref="Site"/>
/// </summary>
public bool AllPages { get; set; }
#region IDeletable Properties (note that these are NotMapped and are only used for storing PageModule properties)
/// <summary>
/// Reference to the <see cref="ModuleDefinition"/> used for this module.
/// </summary>
[NotMapped]
public ModuleDefinition ModuleDefinition { get; set; }
#region IDeletable Properties
[NotMapped]
public string DeletedBy { get; set; }
@ -43,14 +51,23 @@ namespace Oqtane.Models
#endregion
/// <summary>
/// list of permissions for this module
/// </summary>
[NotMapped]
public List<Permission> PermissionList { get; set; }
/// <summary>
/// List of settings for this module
/// </summary>
[NotMapped]
public Dictionary<string, string> Settings { get; set; }
#region PageModule properties
/// <summary>
/// The id of the PageModule instance
/// </summary>
[NotMapped]
public int PageModuleId { get; set; }
@ -60,24 +77,39 @@ namespace Oqtane.Models
[NotMapped]
public int PageId { get; set; }
/// <summary>
/// Title of the pagemodule instance
/// </summary>
[NotMapped]
public string Title { get; set; }
/// <summary>
/// The Pane this module is shown in.
/// The pane where this pagemodule instance will be injected on the page
/// </summary>
[NotMapped]
public string Pane { get; set; }
/// <summary>
/// The order of the pagemodule instance within the Pane
/// </summary>
[NotMapped]
public int Order { get; set; }
/// <summary>
/// The container for the pagemodule instance
/// </summary>
[NotMapped]
public string ContainerType { get; set; }
/// <summary>
/// Start of when this module is visible. See also <see cref="ExpiryDate"/>
/// </summary>
[NotMapped]
public DateTime? EffectiveDate { get; set; }
/// <summary>
/// End of when this module is visible. See also <see cref="EffectiveDate"/>
/// </summary>
[NotMapped]
public DateTime? ExpiryDate { get; set; }
@ -85,38 +117,67 @@ namespace Oqtane.Models
#region SiteRouter properties
/// <summary>
/// Stores the type name for the module component being rendered
/// </summary>
[NotMapped]
public string ModuleType { get; set; }
/// <summary>
/// The position of the module instance in a pane
/// </summary>
[NotMapped]
public int PaneModuleIndex { get; set; }
/// <summary>
/// The number of modules in a pane
/// </summary>
[NotMapped]
public int PaneModuleCount { get; set; }
/// <summary>
/// A unique id to help determine if a component should be rendered
/// </summary>
[NotMapped]
public Guid RenderId { get; set; }
#endregion
#region ModuleDefinition
#region IModuleControl properties
/// <summary>
/// Reference to the <see cref="ModuleDefinition"/> used for this module.
/// TODO: todoc - unclear if this is always populated
/// The minimum access level to view the component being rendered
/// </summary>
[NotMapped]
public ModuleDefinition ModuleDefinition { get; set; }
#endregion
#region IModuleControl properties
[NotMapped]
public SecurityAccessLevel SecurityAccessLevel { get; set; }
/// <summary>
/// An optional title for the component
/// </summary>
[NotMapped]
public string ControlTitle { get; set; }
/// <summary>
/// Optional mapping of Url actions to a component
/// </summary>
[NotMapped]
public string Actions { get; set; }
/// <summary>
/// Optionally indicate if a compoent should not be rendered with the default modal admin container
/// </summary>
[NotMapped]
public bool UseAdminContainer { get; set; }
/// <summary>
/// Optionally specify the render mode for the component (overrides the Site setting)
/// </summary>
[NotMapped]
public string RenderMode{ get; set; }
public string RenderMode { get; set; }
/// <summary>
/// Optionally specify id the component should be prerendered (overrides the Site setting)
/// </summary>
[NotMapped]
public bool? Prerender { get; set; }
@ -140,5 +201,34 @@ namespace Oqtane.Models
}
#endregion
public Module Clone()
{
return new Module
{
ModuleId = ModuleId,
SiteId = SiteId,
ModuleDefinitionName = ModuleDefinitionName,
AllPages = AllPages,
PageModuleId = PageModuleId,
PageId = PageId,
Title = Title,
Pane = Pane,
Order = Order,
ContainerType = ContainerType,
EffectiveDate = EffectiveDate,
ExpiryDate = ExpiryDate,
CreatedBy = CreatedBy,
CreatedOn = CreatedOn,
ModifiedBy = ModifiedBy,
ModifiedOn = ModifiedOn,
DeletedBy = DeletedBy,
DeletedOn = DeletedOn,
IsDeleted = IsDeleted,
ModuleDefinition = ModuleDefinition,
PermissionList = PermissionList.ConvertAll(permission => permission.Clone()),
Settings = Settings.ToDictionary(setting => setting.Key, setting => setting.Value)
};
}
}
}
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
@ -75,33 +76,68 @@ namespace Oqtane.Models
public string BodyContent { get; set; }
/// <summary>
/// Icon file for this page.
/// TODO: unclear what this is for, and what icon library is used. Probably FontAwesome?
/// Icon class name for this page
/// </summary>
public string Icon { get; set; }
/// <summary>
/// Indicates if this page should be included in navigation menu
/// </summary>
public bool IsNavigation { get; set; }
/// <summary>
/// Indicates if this page should be clickable in navigation menu
/// </summary>
public bool IsClickable { get; set; }
public int? UserId { get; set; }
/// <summary>
/// Start of when this assignment is valid. See also <see cref="ExpiryDate"/>
/// Indicates if page is personalizable ie. allows users to create custom versions of the page
/// </summary>
public DateTime? EffectiveDate { get; set; }
/// <summary>
/// End of when this assignment is valid. See also <see cref="EffectiveDate"/>
/// </summary>
public DateTime? ExpiryDate { get; set; }
public bool IsPersonalizable { get; set; }
#region IDeletable Properties
public string DeletedBy { get; set; }
public DateTime? DeletedOn { get; set; }
public bool IsDeleted { get; set; }
#endregion
/// <summary>
/// Reference to the user <see cref="User"/> who owns the personalized page
/// </summary>
public int? UserId { get; set; }
/// <summary>
/// List of Pane-names which this Page has.
/// Start of when this page is visible. See also <see cref="ExpiryDate"/>
/// </summary>
public DateTime? EffectiveDate { get; set; }
/// <summary>
/// End of when this page is visible. See also <see cref="EffectiveDate"/>
/// </summary>
public DateTime? ExpiryDate { get; set; }
/// <summary>
/// The hierarchical level of the page
/// </summary>
[NotMapped]
public int Level { get; set; }
/// <summary>
/// Determines if there are sub-pages. True if this page has sub-pages.
/// </summary>
[NotMapped]
public bool HasChildren { get; set; }
/// <summary>
/// List of permissions for this page
/// </summary>
[NotMapped]
public List<Permission> PermissionList { get; set; }
/// <summary>
/// List of settings for this page
/// </summary>
[NotMapped]
public Dictionary<string, string> Settings { get; set; }
#region SiteRouter properties
/// <summary>
/// List of Pane names for the Theme assigned to this page
/// </summary>
[NotMapped]
public List<string> Panes { get; set; }
@ -112,20 +148,15 @@ namespace Oqtane.Models
[NotMapped]
public List<Resource> Resources { get; set; }
[NotMapped]
public List<Permission> PermissionList { get; set; }
#endregion
[NotMapped]
public Dictionary<string, string> Settings { get; set; }
#region IDeletable Properties
[NotMapped]
public int Level { get; set; }
public string DeletedBy { get; set; }
public DateTime? DeletedOn { get; set; }
public bool IsDeleted { get; set; }
/// <summary>
/// Determines if there are sub-pages. True if this page has sub-pages.
/// </summary>
[NotMapped]
public bool HasChildren { get; set; }
#endregion
#region Deprecated Properties
@ -152,5 +183,42 @@ namespace Oqtane.Models
}
#endregion
public Page Clone()
{
return new Page
{
PageId = PageId,
SiteId = SiteId,
Path = Path,
ParentId = ParentId,
Name = Name,
Title = Title,
Order = Order,
Url = Url,
ThemeType = ThemeType,
DefaultContainerType = DefaultContainerType,
HeadContent = HeadContent,
BodyContent = BodyContent,
Icon = Icon,
IsNavigation = IsNavigation,
IsClickable = IsClickable,
UserId = UserId,
IsPersonalizable = IsPersonalizable,
EffectiveDate = EffectiveDate,
ExpiryDate = ExpiryDate,
Level = Level,
HasChildren = HasChildren,
CreatedBy = CreatedBy,
CreatedOn = CreatedOn,
ModifiedBy = ModifiedBy,
ModifiedOn = ModifiedOn,
DeletedBy = DeletedBy,
DeletedOn = DeletedOn,
IsDeleted = IsDeleted,
PermissionList = PermissionList.ConvertAll(permission => permission.Clone()),
Settings = Settings.ToDictionary(setting => setting.Key, setting => setting.Value)
};
}
}
}

View File

@ -101,17 +101,22 @@ namespace Oqtane.Models
IsAuthorized = isAuthorized;
}
public Permission Clone(Permission permission)
public Permission Clone()
{
return new Permission
{
SiteId = permission.SiteId,
EntityName = permission.EntityName,
EntityId = permission.EntityId,
PermissionName = permission.PermissionName,
RoleName = permission.RoleName,
UserId = permission.UserId,
IsAuthorized = permission.IsAuthorized
SiteId = SiteId,
EntityName = EntityName,
EntityId = EntityId,
PermissionName = PermissionName,
RoleName = RoleName,
RoleId = RoleId,
UserId = UserId,
IsAuthorized = IsAuthorized,
CreatedBy = CreatedBy,
CreatedOn = CreatedOn,
ModifiedBy = ModifiedBy,
ModifiedOn = ModifiedOn
};
}

View File

@ -187,47 +187,47 @@ namespace Oqtane.Models
[NotMapped]
public List<Theme> Themes { get; set; }
public Site Clone(Site site)
public Site Clone()
{
return new Site
{
SiteId = site.SiteId,
TenantId = site.TenantId,
Name = site.Name,
LogoFileId = site.LogoFileId,
FaviconFileId = site.FaviconFileId,
DefaultThemeType = site.DefaultThemeType,
DefaultContainerType = site.DefaultContainerType,
AdminContainerType = site.AdminContainerType,
PwaIsEnabled = site.PwaIsEnabled,
PwaAppIconFileId = site.PwaAppIconFileId,
PwaSplashIconFileId = site.PwaSplashIconFileId,
AllowRegistration = site.AllowRegistration,
VisitorTracking = site.VisitorTracking,
CaptureBrokenUrls = site.CaptureBrokenUrls,
SiteGuid = site.SiteGuid,
RenderMode = site.RenderMode,
Runtime = site.Runtime,
Prerender = site.Prerender,
Hybrid = site.Hybrid,
Version = site.Version,
HomePageId = site.HomePageId,
HeadContent = site.HeadContent,
BodyContent = site.BodyContent,
IsDeleted = site.IsDeleted,
DeletedBy = site.DeletedBy,
DeletedOn = site.DeletedOn,
ImageFiles = site.ImageFiles,
UploadableFiles = site.UploadableFiles,
SiteTemplateType = site.SiteTemplateType,
CreatedBy = site.CreatedBy,
CreatedOn = site.CreatedOn,
ModifiedBy = site.ModifiedBy,
ModifiedOn = site.ModifiedOn,
Settings = site.Settings.ToDictionary(),
Pages = site.Pages.ToList(),
Languages = site.Languages.ToList(),
Themes = site.Themes.ToList()
SiteId = SiteId,
TenantId = TenantId,
Name = Name,
LogoFileId = LogoFileId,
FaviconFileId = FaviconFileId,
DefaultThemeType = DefaultThemeType,
DefaultContainerType = DefaultContainerType,
AdminContainerType = AdminContainerType,
PwaIsEnabled = PwaIsEnabled,
PwaAppIconFileId = PwaAppIconFileId,
PwaSplashIconFileId = PwaSplashIconFileId,
AllowRegistration = AllowRegistration,
VisitorTracking = VisitorTracking,
CaptureBrokenUrls = CaptureBrokenUrls,
SiteGuid = SiteGuid,
RenderMode = RenderMode,
Runtime = Runtime,
Prerender = Prerender,
Hybrid = Hybrid,
Version = Version,
HomePageId = HomePageId,
HeadContent = HeadContent,
BodyContent = BodyContent,
IsDeleted = IsDeleted,
DeletedBy = DeletedBy,
DeletedOn = DeletedOn,
ImageFiles = ImageFiles,
UploadableFiles = UploadableFiles,
SiteTemplateType = SiteTemplateType,
CreatedBy = CreatedBy,
CreatedOn = CreatedOn,
ModifiedBy = ModifiedBy,
ModifiedOn = ModifiedOn,
Settings = Settings.ToDictionary(setting => setting.Key, setting => setting.Value),
Pages = Pages.ConvertAll(page => page.Clone()),
Languages = Languages.ConvertAll(language => language.Clone()),
Themes = Themes
};
}

View File

@ -59,6 +59,12 @@ namespace Oqtane.Models
/// </summary>
public DateTime? TwoFactorExpiry { get; set; }
/// <summary>
/// A token indicating if a user's security properties have been modified
/// </summary>
[NotMapped]
public string SecurityStamp { get; set; }
/// <summary>
/// Reference to the <see cref="Site"/> this user belongs to.
/// </summary>
@ -66,8 +72,7 @@ namespace Oqtane.Models
public int SiteId { get; set; }
/// <summary>
/// Role names this user has.
/// TODO: todoc - is this comma separated?
/// Semi-colon delimited list of role names for the user
/// </summary>
[NotMapped]
public string Roles { get; set; }

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Configurations>Debug;Release</Configurations>
<Version>5.2.1</Version>
<Version>5.2.3</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -11,7 +11,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.1</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>

View File

@ -100,7 +100,7 @@ namespace Oqtane.Security
{
identity.AddClaim(new Claim(ClaimTypes.Name, user.Username));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.UserId.ToString()));
identity.AddClaim(new Claim("sitekey", alias.SiteKey));
identity.AddClaim(new Claim(Constants.SiteKeyClaimType, alias.SiteKey));
if (user.Roles.Contains(RoleNames.Host))
{
// host users are site admins by default
@ -115,6 +115,7 @@ namespace Oqtane.Security
identity.AddClaim(new Claim(ClaimTypes.Role, role));
}
}
identity.AddClaim(new Claim(Constants.SecurityStampClaimType, user.SecurityStamp));
}
return identity;
}

View File

@ -4,8 +4,8 @@ namespace Oqtane.Shared
{
public class Constants
{
public static readonly string Version = "5.2.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,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1";
public static readonly string Version = "5.2.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,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3";
public const string PackageId = "Oqtane.Framework";
public const string ClientId = "Oqtane.Client";
public const string UpdaterPackageId = "Oqtane.Updater";
@ -67,6 +67,9 @@ namespace Oqtane.Shared
public static readonly string AntiForgeryTokenHeaderName = "X-XSRF-TOKEN-HEADER";
public static readonly string AntiForgeryTokenCookieName = "X-XSRF-TOKEN-COOKIE";
public static readonly string SecurityStampClaimType = "AspNet.Identity.SecurityStamp";
public static readonly string SiteKeyClaimType = "Oqtane.Identity.SiteKey";
public static readonly string DefaultVisitorFilter = "bot,crawler,slurp,spider,(none),??";
public static readonly string HttpContextAliasKey = "Alias";
@ -83,6 +86,11 @@ namespace Oqtane.Shared
public static readonly string[] InternalPagePaths = { "login", "register", "reset", "404" };
public const string DefaultTextEditor = "Oqtane.Modules.Controls.QuillJSTextEditor, Oqtane.Client";
public const string BootstrapScriptUrl = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js";
public const string BootstrapScriptIntegrity = "sha512-7Pi/otdlbbCR+LnW+F7PwFcSDJOuUJB3OxtEHbg4vSMvzvJjde4Po1v4BR9Gdc9aXNUNFVUY+SK51wWT8WF0Gg==";
public const string BootstrapStylesheetUrl = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css";
public const string BootstrapStylesheetIntegrity = "sha512-jnSuA4Ss2PkkikSOLtYs8BlYIeeIK1h99ty4YfvRPAlzr377vr3CXDb7sb7eEEBYjDtcYj+AjBH3FLv5uSJuXg==";
// Obsolete constants
const string RoleObsoleteMessage = "Use the corresponding member from Oqtane.Shared.RoleNames";

View File

@ -23,12 +23,13 @@ namespace Oqtane.Shared
public static (string UrlParameters, string Querystring, string Fragment) ParseParameters(string parameters)
{
// /urlparameters /urlparameters?Id=1 /urlparameters#5 /urlparameters?Id=1#5 /urlparameters?reload#5
// Id=1 Id=1#5 reload#5 reload
// /urlparameters /urlparameters?id=1 /urlparameters#5 /urlparameters?id=1#5 /urlparameters?reload#5
// ?id=1 ?id=1#5 ?reload#5 ?reload
// id=1 id=1#5 reload#5 reload
// #5
// create absolute url to convert to Uri
parameters = (!parameters.StartsWith("/") && !parameters.StartsWith("#") ? "?" : "") + parameters;
parameters = (!parameters.StartsWith("/") && !parameters.StartsWith("#") && !parameters.StartsWith("?") ? "?" : "") + parameters;
parameters = Constants.PackageRegistryUrl + parameters;
var uri = new Uri(parameters);
var querystring = uri.Query.Replace("?", "");

View File

@ -1,4 +1,4 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.28822.285
@ -12,6 +12,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Updater", "Oqtane.Updater\Oqtane.Updater.csproj", "{2E8C6889-37CF-4C8D-88B1-505547F25098}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Shared", "Oqtane.Shared\Oqtane.Shared.csproj", "{E2512C17-291F-460A-A6D1-741C301DA184}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -22,6 +24,10 @@ Global
{2E8C6889-37CF-4C8D-88B1-505547F25098}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2E8C6889-37CF-4C8D-88B1-505547F25098}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2E8C6889-37CF-4C8D-88B1-505547F25098}.Release|Any CPU.Build.0 = Release|Any CPU
{E2512C17-291F-460A-A6D1-741C301DA184}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E2512C17-291F-460A-A6D1-741C301DA184}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E2512C17-291F-460A-A6D1-741C301DA184}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E2512C17-291F-460A-A6D1-741C301DA184}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
<Version>5.2.1</Version>
<Version>5.2.3</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -11,11 +11,15 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.1</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Oqtane.Shared\Oqtane.Shared.csproj" />
</ItemGroup>
</Project>

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Threading;
using Oqtane.Shared;
namespace Oqtane.Updater
{
@ -31,10 +32,6 @@ namespace Oqtane.Updater
if (Directory.Exists(deployfolder))
{
string log = "Upgrade Process Started: " + DateTime.UtcNow.ToString() + Environment.NewLine;
log += "ContentRootPath: " + contentrootfolder + Environment.NewLine;
log += "WebRootPath: " + webrootfolder + Environment.NewLine;
string packagename = "";
string[] packages = Directory.GetFiles(deployfolder, "Oqtane.Framework.*.Upgrade.zip");
if (packages.Length > 0)
@ -42,15 +39,27 @@ namespace Oqtane.Updater
packagename = packages[packages.Length - 1]; // use highest version
}
// create upgrade log file
var logFilePath = Path.Combine(deployfolder, $"{Path.GetFileNameWithoutExtension(packagename)}.log");
if (File.Exists(logFilePath))
{
File.Delete(logFilePath);
}
WriteLog(logFilePath, "Upgrade Process Started: " + DateTime.UtcNow.ToString() + Environment.NewLine);
WriteLog(logFilePath, "ContentRootPath: " + contentrootfolder + Environment.NewLine);
WriteLog(logFilePath, "WebRootPath: " + webrootfolder + Environment.NewLine);
if (packagename != "" && File.Exists(Path.Combine(webrootfolder, "app_offline.bak")))
{
log += "Located Upgrade Package: " + packagename + Environment.NewLine;
WriteLog(logFilePath, "Located Upgrade Package: " + packagename + Environment.NewLine);
log += "Stopping Application Using: " + Path.Combine(contentrootfolder, "app_offline.htm") + Environment.NewLine;
File.Copy(Path.Combine(webrootfolder, "app_offline.bak"), Path.Combine(contentrootfolder, "app_offline.htm"), true);
WriteLog(logFilePath, "Stopping Application Using: " + Path.Combine(contentrootfolder, "app_offline.htm") + Environment.NewLine);
var offlineTemplate = File.ReadAllText(Path.Combine(webrootfolder, "app_offline.bak"));
var offlineFilePath = Path.Combine(contentrootfolder, "app_offline.htm");
// get list of files in package with local paths
log += "Retrieving List Of Files From Upgrade Package..." + Environment.NewLine;
UpdateOfflineContent(offlineFilePath, offlineTemplate, 5, "Retrieving List Of Files From Upgrade Package");
WriteLog(logFilePath, "Retrieving List Of Files From Upgrade Package..." + Environment.NewLine);
List<string> files = new List<string>();
using (ZipArchive archive = ZipFile.OpenRead(packagename))
{
@ -59,15 +68,18 @@ namespace Oqtane.Updater
if (!string.IsNullOrEmpty(entry.Name))
{
files.Add(Path.Combine(contentrootfolder, entry.FullName));
WriteLog(logFilePath, "Check File: " + entry.FullName + Environment.NewLine);
}
}
}
bool success = true;
// ensure files are not locked
if (CanAccessFiles(files))
{
log += "Preparing Backup Folder: " + backupfolder + Environment.NewLine;
bool success = true;
UpdateOfflineContent(offlineFilePath, offlineTemplate, 10, "Preparing Backup Folder");
WriteLog(logFilePath, "Preparing Backup Folder: " + backupfolder + Environment.NewLine);
try
{
// clear out backup folder
@ -79,14 +91,16 @@ namespace Oqtane.Updater
}
catch (Exception ex)
{
log += "Error Creating Backup Folder: " + ex.Message + Environment.NewLine;
UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Error Creating Backup Folder", "bg-danger");
WriteLog(logFilePath, "Error Creating Backup Folder: " + ex.Message + Environment.NewLine);
success = false;
}
// backup files
if (success)
{
log += "Backing Up Files..." + Environment.NewLine;
UpdateOfflineContent(offlineFilePath, offlineTemplate, 15, "Backing Up Files");
WriteLog(logFilePath, "Backing Up Files..." + Environment.NewLine);
foreach (string file in files)
{
string filename = Path.Combine(backupfolder, file.Replace(contentrootfolder + Path.DirectorySeparatorChar, ""));
@ -99,12 +113,15 @@ namespace Oqtane.Updater
Directory.CreateDirectory(Path.GetDirectoryName(filename));
}
File.Copy(file, filename);
WriteLog(logFilePath, "Copy File: " + filename + Environment.NewLine);
}
}
catch (Exception ex)
{
log += "Error Backing Up Files: " + ex.Message + Environment.NewLine;
UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Error Backing Up Files", "bg-danger");
WriteLog(logFilePath, "Error Backing Up Files: " + ex.Message + Environment.NewLine);
success = false;
break;
}
}
}
@ -112,7 +129,8 @@ namespace Oqtane.Updater
// extract files
if (success)
{
log += "Extracting Files From Upgrade Package..." + Environment.NewLine;
UpdateOfflineContent(offlineFilePath, offlineTemplate, 50, "Extracting Files From Upgrade Package");
WriteLog(logFilePath, "Extracting Files From Upgrade Package..." + Environment.NewLine);
try
{
using (ZipArchive archive = ZipFile.OpenRead(packagename))
@ -127,6 +145,7 @@ namespace Oqtane.Updater
Directory.CreateDirectory(Path.GetDirectoryName(filename));
}
entry.ExtractToFile(filename, true);
WriteLog(logFilePath, "Exact File: " + filename + Environment.NewLine);
}
}
}
@ -134,12 +153,14 @@ namespace Oqtane.Updater
catch (Exception ex)
{
success = false;
log += "Error Extracting Files From Upgrade Package: " + ex.Message + Environment.NewLine;
UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Error Extracting Files From Upgrade Package", "bg-danger");
WriteLog(logFilePath, "Error Extracting Files From Upgrade Package: " + ex.Message + Environment.NewLine);
}
if (success)
{
log += "Removing Backup Folder..." + Environment.NewLine;
UpdateOfflineContent(offlineFilePath, offlineTemplate, 90, "Removing Backup Folder");
WriteLog(logFilePath, "Removing Backup Folder..." + Environment.NewLine);
try
{
// clean up backup
@ -149,12 +170,14 @@ namespace Oqtane.Updater
}
catch (Exception ex)
{
log += "Error Removing Backup Folder: " + ex.Message + Environment.NewLine;
UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Error Extracting Files From Upgrade Package", "bg-warning");
WriteLog(logFilePath, "Error Removing Backup Folder: " + ex.Message + Environment.NewLine);
}
}
else
{
log += "Restoring Files From Backup Folder..." + Environment.NewLine;
UpdateOfflineContent(offlineFilePath, offlineTemplate, 50, "Upgrade Failed, Restoring Files From Backup Folder", "bg-warning");
WriteLog(logFilePath, "Restoring Files From Backup Folder..." + Environment.NewLine);
try
{
// restore on failure
@ -165,6 +188,7 @@ namespace Oqtane.Updater
if (File.Exists(filename))
{
File.Copy(filename, file);
WriteLog(logFilePath, "Restore File: " + filename + Environment.NewLine);
}
}
// clean up backup
@ -172,41 +196,38 @@ namespace Oqtane.Updater
}
catch (Exception ex)
{
log += "Error Restoring Files From Backup Folder: " + ex.Message + Environment.NewLine;
UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Error Restoring Files From Backup Folder", "bg-danger");
WriteLog(logFilePath, "Error Restoring Files From Backup Folder: " + ex.Message + Environment.NewLine);
}
}
}
else
{
log += "Upgrade Failed: Could Not Backup Files" + Environment.NewLine;
UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Upgrade Failed: Could Not Backup Files", "bg-danger");
WriteLog(logFilePath, "Upgrade Failed: Could Not Backup Files" + Environment.NewLine);
}
}
else
{
log += "Upgrade Failed: Some Files Are Locked By The Hosting Environment" + Environment.NewLine;
UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Upgrade Failed: Some Files Are Locked By The Hosting Environment", "bg-danger");
WriteLog(logFilePath, "Upgrade Failed: Some Files Are Locked By The Hosting Environment" + Environment.NewLine);
}
UpdateOfflineContent(offlineFilePath, offlineTemplate, 100, "Upgrade Process Finished, Reloading", success ? "" : "bg-danger");
Thread.Sleep(3000); //wait for 3 seconds to complete the upgrade process.
// bring the app back online
if (File.Exists(Path.Combine(contentrootfolder, "app_offline.htm")))
{
log += "Restarting Application By Removing: " + Path.Combine(contentrootfolder, "app_offline.htm") + Environment.NewLine;
WriteLog(logFilePath, "Restarting Application By Removing: " + Path.Combine(contentrootfolder, "app_offline.htm") + Environment.NewLine);
File.Delete(Path.Combine(contentrootfolder, "app_offline.htm"));
}
}
else
{
log += "Framework Upgrade Package Not Found Or " + Path.Combine(webrootfolder, "app_offline.bak") + " Does Not Exist" + Environment.NewLine;
WriteLog(logFilePath, "Framework Upgrade Package Not Found Or " + Path.Combine(webrootfolder, "app_offline.bak") + " Does Not Exist" + Environment.NewLine);
}
log += "Upgrade Process Ended: " + DateTime.UtcNow.ToString() + Environment.NewLine;
// create upgrade log file
string logfile = Path.Combine(deployfolder, Path.GetFileNameWithoutExtension(packagename) + ".log");
if (File.Exists(logfile))
{
File.Delete(logfile);
}
File.WriteAllText(logfile, log);
WriteLog(logFilePath, "Upgrade Process Ended: " + DateTime.UtcNow.ToString() + Environment.NewLine);
}
else
{
@ -269,5 +290,21 @@ namespace Oqtane.Updater
}
return canAccess;
}
private static void UpdateOfflineContent(string filePath, string contentTemplate, int progress, string status, string progressClass = "")
{
var content = contentTemplate
.Replace("[BOOTSTRAPCSSURL]", Constants.BootstrapStylesheetUrl)
.Replace("[BOOTSTRAPCSSINTEGRITY]", Constants.BootstrapStylesheetIntegrity)
.Replace("[PROGRESS]", progress.ToString())
.Replace("[PROGRESSCLASS]", progressClass)
.Replace("[STATUS]", status);
File.WriteAllText(filePath, content);
}
private static void WriteLog(string logFilePath, string logContent)
{
File.AppendAllText(logFilePath, $"[{DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff")}] {logContent}");
}
}
}

View File

@ -1,6 +1,6 @@
# Latest Release
[5.2.0](https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0) was released on July 25, 2024 and is a major release including 109 pull requests by 8 different contributors, pushing the total number of project commits all-time to over 5600. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers.
[5.2.2](https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.2) was released on September 23, 2024 and is a maintenance release including 55 pull requests by 8 different contributors, pushing the total number of project commits all-time to over 5800. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers.
[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Foqtane%2Foqtane.framework%2Fmaster%2Fazuredeploy.json)
@ -14,26 +14,46 @@ Oqtane is being developed based on some fundamental principles which are outline
Please note that this project is owned by the .NET Foundation and is governed by the **[.NET Foundation Contributor Covenant Code of Conduct](https://dotnetfoundation.org/code-of-conduct)**
# Getting Started
# Getting Started (Version 5.x)
**Using Version 5:**
**Installing using source code from the Dev/Master branch:**
- Install **[.NET 8.0.7 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)**.
- Install **[.NET 8.0.8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)**.
- Install the latest edition (v17.9 or higher) of [Visual Studio 2022](https://visualstudio.microsoft.com/downloads) with the **ASP.NET and web development** workload enabled. Oqtane works with ALL editions of Visual Studio from Community to Enterprise. If you wish to use LocalDB for development ( not a requirement as Oqtane supports SQLite, mySQL, and PostgreSQL ) you must also install the **Data storage and processing**.
- Clone the Oqtane dev branch source code to your local system.
- Clone (or download) the Oqtane Master or Dev branch source code to your local system.
- Open the **Oqtane.sln** solution file.
- **Important:** Rebuild the entire solution before running it.
- **Important:** Rebuild the entire solution before running it (ie. Build / Rebuild Solution).
- Make sure you specify Oqtane.Server as the Startup Project
- Make sure you specify Oqtane.Server as the Startup Project.
- Run the application.
- Run the application... an Installation Wizard screen will be displayed which will allow you to configure your preferred database and create a host user account.
**Developing a custom module:**
- follow the instructions for installing using source code outlined above
- login as the host user
- navigate to Control Panel (gear icon at top-right of page), Admin Dashboard, Module Management
- select Create Module
- enter information corresponding to the module you wish to create and then select the Create button
- make note of the Location where the code was generated and open the solution file in Visual Studio
- Build / Rebuild Solution, ensure the Oqtane.Server is set as the Startup Project, and hit F5 to run the solution
**Installing an official release:**
- all official releases of Oqtane are distributed on [GitHub](https://github.com/oqtane/oqtane.framework/releases). Releases include an Install.zip package for new installations and an Upgrade.zip for existing installations.
- A detailed set of instructions for installing Oqtane on Azure is located here: [Installing Oqtane on Azure](https://blazorhelpwebsite.com/ViewBlogPost/1)
- A detailed set of instructions for installing Oqtane on IIS is located here: [Installing Oqtane on IIS](https://www.oqtane.org/Resources/Blog/PostId/542/installing-oqtane-on-iis)
- Instructions for upgrading Oqtane are located here: [Upgrading Oqtane](https://www.oqtane.org/Resources/Blog/PostId/543/upgrading-oqtane)
@ -63,6 +83,15 @@ Backlog (TBD)
- [ ] Folder Providers
- [ ] Generative AI Integration
[5.2.2](https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.2) (Sep 23, 2024)
- [x] Stabilization improvements
- [x] Support for Security Stamp to faciliate Logout Everywhere
- [x] Role synchronization from External Login identity providers
[5.2.1](https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.1) (Aug 22, 2024)
- [x] Stabilization improvements
- [x] Unzip support in File Management
[5.2.0](https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0) (Jul 25, 2024)
- [x] Site Content Search
- [x] RichTextEditor extensibility
@ -90,7 +119,7 @@ Backlog (TBD)
➡️ Full list and older versions can be found in the [docs roadmap](https://docs.oqtane.org/guides/roadmap/index.html)
# Background
Oqtane was created by [Shaun Walker](https://www.linkedin.com/in/shaunbrucewalker/) and is inspired by the DotNetNuke web application framework. Initially created as a proof of concept, Oqtane is a native Blazor application written from the ground up using modern .NET Core technology and a Single Page Application (SPA) architecture. It is a modular application framework offering a fully dynamic page compositing model, multi-site support, designer friendly themes, and extensibility via third party modules.
Oqtane was created by [Shaun Walker](https://www.linkedin.com/in/shaunbrucewalker/) and is inspired by the DotNetNuke web application framework. Oqtane is a native Blazor application written from the ground up using modern .NET Core technology and a Single Page Application (SPA) architecture. It is a modular application framework offering a fully dynamic page compositing model, multi-site support, designer friendly themes, and extensibility via third party modules.
# Reference Implementations
@ -106,7 +135,7 @@ The following diagram visualizes the client and server components in the Oqtane
# Databases
As of version 2.1, Oqtane supports multiple relational database providers.
As of version 2.1 (June 2021) Oqtane supports multiple relational database providers - SQL Server, SQLite, MySQL, PostgreSQL
![Databases](https://github.com/oqtane/framework/blob/dev/screenshots/databases.png?raw=true "Oqtane Databases")