Compare commits

...

626 Commits

Author SHA1 Message Date
7a9941fe66 Add entrypoint, modify Dockerfile: Overwrite volume files 2025-05-30 17:40:18 +02:00
b3b39f583a Merge mirror with local history. 2025-05-30 11:51:43 +02:00
92c554e854 New: Register Component that Renders a Link (secondary) to the register URL including the redirect property. 2025-05-30 11:45:26 +02:00
29fe3dfd0b Merge pull request #5344 from sbwalker/dev
update Azure ARM template to 6.1.3
2025-05-29 17:05:04 -04:00
985e50d415 update Azure ARM template to 6.1.3 2025-05-29 17:04:45 -04:00
11150b6a10 Update README.md 2025-05-29 17:03:20 -04:00
c499acdc4a Merge pull request #5342 from oqtane/master
Merge pull request #5341 from oqtane/dev
2025-05-29 15:59:32 -04:00
b24e3252d9 Merge pull request #5341 from oqtane/dev
6.1.3 Release
2025-05-29 15:58:55 -04:00
e15787b1e4 Merge pull request #5340 from sbwalker/dev
change id for header/footer
2025-05-29 15:04:28 -04:00
d5f19d97e2 change id for header/footer 2025-05-29 15:04:12 -04:00
5543a4aeed Merge pull request #5339 from sbwalker/dev
fix #5329 - clear Options after updating User Settings
2025-05-29 11:53:29 -04:00
9c333232e2 fix #5329 - clear Options after updating User Settings 2025-05-29 11:53:14 -04:00
d52b95ea23 Merge pull request #5333 from leigh-pointer/TokenReplace
Fix for ModuleBase ReplaceTokens #5332
2025-05-28 13:11:28 -04:00
ef4fbcbb8a Update ModuleBase.cs
This method replaces all tokens in the format [Object:Property] or [Object:SubObject:Property] within a string.
Efficient string parsing and reflection ensure flexibility with performance.
It supports deeply nested properties, optional default fallback values (e.g. [PageState:User:Email|default@email.com]), and uses caching to optimize repeated token resolution without regex.
2025-05-28 17:30:19 +02:00
aa454b411f Merge pull request #5335 from thabaum/Update-Swashbuckle.AspNetCore-Package-Dependency-to-8.1.2
Fixes #5331: Updates Swashbuckle.AspNetCore Package Dependency to 8.1.2
2025-05-28 08:23:39 -04:00
543e9339c7 Update Swashbuckle.AspNetCore Package Dependency to 8.1.2 2025-05-25 09:34:09 -07:00
7fff5c0d18 Fix for ModuleBase ReplaceTokens #5332
Replaced the ReplaceTokens logic to replace all tokens in the string
2025-05-25 10:55:49 +02:00
fa79f3f6fa Merge pull request #5326 from sbwalker/dev
fix #5205 add support for inheritance when loading Resources from ModuleBase or ThemeBase
2025-05-19 21:01:02 -07:00
c098839881 fix #5205 add support for inheritance when loading Resources from ModuleBase or ThemeBase 2025-05-19 21:00:35 -07:00
8d0d88c1b9 Merge pull request #5325 from sbwalker/dev
ensure Content folder is empty when packaging
2025-05-19 18:32:42 -04:00
2b6ba0f410 ensure Content folder is empty when packaging 2025-05-19 15:32:27 -07:00
11235009c0 Merge pull request #5324 from sbwalker/dev
imprvoe help text
2025-05-19 18:15:03 -04:00
4b05f7fdad imprvoe help text 2025-05-19 15:14:49 -07:00
338b0ae509 Merge pull request #5320 from sbwalker/dev
use consistent authorization method
2025-05-16 12:11:16 -04:00
a437082952 use consistent authorization method 2025-05-16 12:11:03 -04:00
ca9aba7b3b Merge pull request #5319 from sbwalker/dev
improve comment
2025-05-16 11:53:17 -04:00
fe9f189734 improve comment 2025-05-16 11:53:04 -04:00
018ac612f4 Merge pull request #5318 from sbwalker/dev
improve messaging
2025-05-16 11:47:06 -04:00
5bde40ec2b improve messaging 2025-05-16 11:46:53 -04:00
4d5780c192 Merge pull request #5317 from sbwalker/dev
Fix #4789 - allow user email verification to be managed by administrator
2025-05-16 11:13:20 -04:00
ff6a810ad5 Fix #4789 - allow user email verification to be managed by administrator 2025-05-16 11:13:03 -04:00
feec01ba00 Merge pull request #5316 from sbwalker/dev
fix spelling mistake
2025-05-16 09:40:10 -04:00
1f05d12ef5 fix spelling mistake 2025-05-16 09:39:57 -04:00
31aba14507 Merge pull request #5315 from sbwalker/dev
fix initialization issue related to time zones
2025-05-16 09:09:21 -04:00
bbd6f13f36 fix initialization issue related to time zones 2025-05-16 09:09:07 -04:00
68edbbbdb9 Merge pull request #5314 from sbwalker/dev
improve filename validation in module content export
2025-05-16 08:26:04 -04:00
eb5a0dc1c9 improve filename validation in module content export 2025-05-16 08:25:50 -04:00
7cea4f1792 Merge pull request #5312 from sbwalker/dev
update module export resource info
2025-05-15 11:06:20 -04:00
c57c6abb1b update module export resource info 2025-05-15 11:06:04 -04:00
a25b706c7b Merge pull request #5311 from sbwalker/dev
allow filename to be provided during module export
2025-05-15 10:59:10 -04:00
5d077e843d allow filename to be provided during module export 2025-05-15 10:58:55 -04:00
65bf3e9899 Merge pull request #5310 from sbwalker/dev
allow module import from a file
2025-05-15 09:34:37 -04:00
51ba3a01f5 allow module import from a file 2025-05-15 09:34:19 -04:00
f9ca611b8b Merge pull request #5309 from sbwalker/dev
improve module export so that content can be saved to a file
2025-05-15 08:56:37 -04:00
a49b8728fd improve module export so that content can be saved to a file 2025-05-15 08:56:21 -04:00
9234b91089 Merge pull request #5306 from sbwalker/dev
fix #5200 - sort folders alphabetically, display folders hierarchically
2025-05-14 15:52:04 -04:00
f3fcef52dd fix #5200 - sort folders alphabetically, display folders hierarchically 2025-05-14 15:51:51 -04:00
7f8b741981 Merge pull request #5305 from sbwalker/dev
fix issue with module header/footer
2025-05-14 14:20:54 -04:00
f1791a709c fix issue with module header/footer 2025-05-14 14:20:44 -04:00
30307fb05e Merge pull request #5304 from sbwalker/dev
support for module header and footer content
2025-05-14 12:18:51 -04:00
57d443be8d support for module header and footer content 2025-05-14 12:18:37 -04:00
84844c5043 Merge pull request #5303 from sbwalker/dev
move ConfigureOqtaneAssemblies to occur before UseEndpoints
2025-05-14 11:13:12 -04:00
9000f05961 move ConfigureOqtaneAssemblies to occur before UseEndpoints 2025-05-14 11:12:58 -04:00
6bef9b5c6e Merge pull request #5302 from sbwalker/dev
update default templates to .NET SDK 9.0.5
2025-05-13 16:52:51 -04:00
ffef1f4820 update default templates to .NET SDK 9.0.5 2025-05-13 16:52:39 -04:00
10c7bdbcaa Merge pull request #5301 from sbwalker/dev
upgrade to .NET SDK 9.0.5
2025-05-13 16:49:59 -04:00
e8f9888a41 upgrade to .NET SDK 9.0.5 2025-05-13 16:49:46 -04:00
8bac702be6 Merge pull request #5300 from sbwalker/dev
rollback change which moved ConfigureOqtaneAssemblies
2025-05-13 16:39:08 -04:00
128bcecfe3 rollback change which moved ConfigureOqtaneAssemblies(env); 2025-05-13 16:38:48 -04:00
1390b3c489 Merge pull request #5299 from sbwalker/dev
fix #5398 - editing page permissions
2025-05-13 15:49:30 -04:00
a0f41341ac fix #5398 - editing page permissions 2025-05-13 15:49:16 -04:00
8ffa7ef7ff Merge pull request #5297 from sbwalker/dev
adding time zone support to admin modules
2025-05-13 13:55:15 -04:00
deb4607081 adding time zone support to admin modules 2025-05-13 13:55:01 -04:00
49c62f80e8 Merge pull request #5296 from sbwalker/dev
display local datetimes in the Job Scheduler (using time zones)
2025-05-13 11:29:40 -04:00
139793f3c0 display local datetimes in the Job Scheduler (using time zones) 2025-05-13 11:29:26 -04:00
f237cb9655 Merge pull request #5295 from sbwalker/dev
add time zone support for sites and users
2025-05-13 09:24:33 -04:00
9f18c460d8 add time zone support for sites and users 2025-05-13 09:24:17 -04:00
306a41b442 Merge pull request #5293 from sbwalker/dev
fix #5292 - fix External Login Provider Info link
2025-05-12 11:32:42 -04:00
b53f54295d fix #5292 - fix External Login Provider Info link 2025-05-12 11:32:27 -04:00
7e4f066694 Merge pull request #5291 from sbwalker/dev
fix #5287 - allow deletion of folder which contains files
2025-05-12 08:42:12 -04:00
90d72489d9 fix #5287 - allow deletion of folder which contains files 2025-05-12 08:41:57 -04:00
045c455324 Merge pull request #5286 from sbwalker/dev
update version to 6.1.3
2025-05-08 16:09:12 -04:00
60da903360 update version to 6.1.3 2025-05-08 16:09:00 -04:00
6d2a71f37e Merge pull request #5283 from thabaum/update-6.1.3
Fixes #5267: Updates Project Dependencies and Version to 6.1.3
2025-05-08 16:07:48 -04:00
c8f60a12a4 Merge pull request #5277 from ohba-ikuo/dev
Modify the module's server-side resource path.
2025-05-07 17:27:15 -04:00
11b2d3aa43 Merge pull request #5284 from sbwalker/dev
change Synchronize button to Check For Updates to improve clarity
2025-05-07 17:12:26 -04:00
3d9c81d850 change Synchronize button to Check For Updates to improve clarity 2025-05-07 17:12:14 -04:00
8ccb1a24f8 Update Version to 6.1.3 2025-05-07 10:25:27 -07:00
48c6796128 Update Version to 6.1.3 2025-05-07 10:24:36 -07:00
fc403f920b Update Version to 6.1.3 2025-05-07 10:24:13 -07:00
ca19496b5c Update Version to 6.1.3 2025-05-07 10:23:32 -07:00
0ae38f8a40 Update Version to 6.1.3 2025-05-07 10:23:11 -07:00
ad868ba841 Update Version to 6.1.3 2025-05-07 10:22:47 -07:00
f2aa39aa85 Update Version to 6.1.3 2025-05-07 10:22:04 -07:00
e76e0fc351 Update Version to 6.1.3 2025-05-07 10:20:52 -07:00
a348913888 Update Version to 6.1.3 2025-05-07 10:20:15 -07:00
5507006c53 Update Version to 6.1.3 2025-05-07 10:19:05 -07:00
994429f098 Update Version to 6.1.3 2025-05-07 10:18:40 -07:00
8efdcb9c49 Update Version to 6.1.3 2025-05-07 10:17:54 -07:00
db9a40db2b Update Version to 6.1.3 2025-05-07 10:17:17 -07:00
25667499e6 Update Package Dependencies & Version to 6.1.3 2025-05-07 10:16:38 -07:00
a728cd2d91 Update Package Dependencies & Version to 6.1.3 2025-05-07 10:14:12 -07:00
3f5f3ef10b Update Version to 6.1.3 2025-05-07 10:10:58 -07:00
0d708124c2 Undo Oqtane.Server.csproj 2025-05-07 08:31:34 +09:00
d31f73df14 Merge branch 'oqtane:dev' into dev 2025-05-07 06:20:48 +09:00
3fed45438b Merge pull request #5281 from thabaum/HtmlText.cs-Unused-Namespaces
Fixes #5280: Remove Unused Namespaces from HtmlText.cs in HtmlText Module
2025-05-06 16:50:10 -04:00
8aa967fa1b Remove Unnecessary Namespaces
Removes Unnecessary Namespaces 'System' and 'System.ComponentModel.DataAnnotations.Schema;'.
2025-05-06 13:29:26 -07:00
6eaa3e342c Merge branch 'dev' of https://github.com/ohba-ikuo/oqtane.framework into dev 2025-05-02 23:45:18 +09:00
6fc9e60f62 fixed serverside resource path 2025-05-02 23:45:13 +09:00
c39ffcf51c Merge pull request #5275 from sbwalker/dev
add new Register Url and Profile Url options to User Management / Settings
2025-05-01 23:33:10 -04:00
6f60a91f4c add new Register Url and Profile Url options to User Management / Settings 2025-05-01 23:32:37 -04:00
8031df6f28 Merge pull request #5274 from sbwalker/dev
use new GetSettingValue() method
2025-04-30 14:35:11 -04:00
da1e859fda use new GetSettingValue() method 2025-04-30 14:34:54 -04:00
8d4d25f1d1 Merge pull request #5273 from sbwalker/dev
use new GetSettingValue() method
2025-04-30 14:18:44 -04:00
6aff27778d use new GetSettingValue() method 2025-04-30 14:18:29 -04:00
ca2dcbfec0 Merge pull request #5272 from sbwalker/dev
remove unecessary using statment
2025-04-30 13:55:24 -04:00
24b666a382 remove unecessary using statment 2025-04-30 13:55:11 -04:00
9e34295529 Merge pull request #5270 from leigh-pointer/ModBase
GetUrlParameters crash
2025-04-30 13:47:21 -04:00
10c55d056b Merge pull request #5271 from sbwalker/dev
resolve issue with host setting overrides
2025-04-30 13:47:06 -04:00
753ab3bdd7 resolve issue with host setting overrides 2025-04-30 13:46:52 -04:00
feee8def6f GetUrlParameters crash
The _urlparametersstate variable is not initialized so in GetUrlParameters it causes a crash
2025-04-29 09:50:15 +02:00
c208f12f8c Merge pull request #5266 from sbwalker/dev
add a convenience method to get a setting value server-side
2025-04-28 12:43:04 -04:00
dc926bf838 add a convenience method to get a setting value server-side 2025-04-28 12:42:50 -04:00
55a76b5204 Merge pull request #5265 from sbwalker/dev
fix #5229 - move IServerStartup.Configure execution until later so that it is possible to register custom endpoints
2025-04-28 12:35:49 -04:00
53b837e763 fix #5229 - move IServerStartup.Configure execution until later so that it is possible to register custom endpoints 2025-04-28 12:34:48 -04:00
3a551cdf25 Merge pull request #5257 from leigh-pointer/AddMDfiles
Updated UploadableFiles constant
2025-04-22 17:00:03 -04:00
c0c7f87dc9 Merge pull request #5256 from leigh-pointer/Packages
Packages updated
2025-04-22 16:59:49 -04:00
ac77fd138b Updated UploadableFiles constant
Updated UploadableFiles constant to allow for Markdown files to be uploaded.
2025-04-22 10:08:20 +02:00
30d6e9d67c Merge branch 'dev' into Packages 2025-04-22 09:13:15 +02:00
47db4334a1 Packages updated
MySql.Data to 9.3.0
HtmlAgilityPack to 1.12.1
washbuckle.AspNetCore to 8.1.1
2025-04-22 09:11:53 +02:00
fce97179e1 Merge pull request #5255 from sbwalker/dev
url mapping improvements
2025-04-21 15:14:52 -04:00
4f16cd2d01 url mapping improvements 2025-04-21 15:14:39 -04:00
fa587691b1 Merge pull request #5254 from sbwalker/dev
improve validation in Url Mapping
2025-04-21 14:14:03 -04:00
e0044658f9 improve validation in Url Mapping 2025-04-21 14:13:49 -04:00
53de1ddb36 Merge pull request #5253 from mdmontesinos/files-optimization
Files server optimization
2025-04-21 13:57:41 -04:00
da7b046092 Remove extra using 2025-04-21 18:47:34 +02:00
d888d83a98 Simplify files etag calculation 2025-04-21 18:44:16 +02:00
430f83e8e9 Only compute hash when file has query string 2025-04-21 16:46:55 +02:00
e7acd14faa Replace MD5 hash with a longer simple hash 2025-04-21 15:51:25 +02:00
1b00fa74bc Compute file server etag with MD5 and always include ModifiedOn 2025-04-21 11:14:24 +02:00
4d572d8173 Allow earlier return in files server 2025-04-21 10:48:48 +02:00
dbda85d8d9 Merge pull request #5248 from sbwalker/dev
UX improvements for System Update
2025-04-15 09:20:35 -04:00
95cb5dd66c UX improvements for System Update 2025-04-15 09:20:18 -04:00
f64e1c3a6a Merge pull request #5245 from sbwalker/dev
.NET MAUI client was changed from 0.0.0.0 to 0.0.0.1 in .NET 9
2025-04-11 15:54:17 -04:00
26a686c412 .NET MAUI client was changed from 0.0.0.0 to 0.0.0.1 in .NET 9 2025-04-11 15:54:02 -04:00
2505383f53 Merge pull request #5244 from leigh-pointer/Swash
Swagger Updated to latest
2025-04-11 09:03:29 -04:00
1a1e9ac6be Swagger Updated to latest
Swashbuckle.AspNetCore update from 8.1.0 > 8.1.1
2025-04-11 09:24:18 +02:00
7f20a3179e Merge pull request #5241 from sbwalker/dev
fix issue with new UserProfile parameters
2025-04-10 14:36:57 -04:00
46431f0187 fix issue with new UserProfile parameters 2025-04-10 14:36:42 -04:00
1ff8ec78c9 Merge pull request #5240 from sbwalker/dev
backup parameter needs to be backward compatible
2025-04-10 12:27:39 -04:00
7840230c62 backup parameter needs to be backward compatible 2025-04-10 12:27:21 -04:00
523db0a005 Merge pull request #5239 from sbwalker/dev
update Deploy To Azure to the 6.1.2 release
2025-04-10 12:04:40 -04:00
cc906d49ba update Deploy To Azure to the 6.1.2 release 2025-04-10 12:04:26 -04:00
fc23af89d3 Update README.md 2025-04-10 11:54:58 -04:00
5d8829ba63 Merge pull request #5238 from oqtane/master
6.1.2 Release
2025-04-10 11:48:58 -04:00
31ccd80894 Merge pull request #5237 from oqtane/dev
6.1.2 Release
2025-04-10 11:48:36 -04:00
bac2234616 Delete Oqtane.Server/wwwroot/Packages/Oqtane.Database.Sqlite.nupkg 2025-04-10 07:49:01 -04:00
bd61db76a3 Delete Oqtane.Server/wwwroot/Packages/Oqtane.Database.SqlServer.nupkg 2025-04-10 07:48:52 -04:00
bc99e3b992 Delete Oqtane.Server/wwwroot/Packages/Oqtane.Database.MySQL.nupkg 2025-04-10 07:48:42 -04:00
b7314b0813 Delete Oqtane.Server/wwwroot/Packages/Oqtane.Database.PostgreSQL.nupkg 2025-04-10 07:48:31 -04:00
4759bd569f Merge pull request #5235 from sbwalker/dev
fix incorrect path in theme template
2025-04-09 17:21:36 -04:00
b88c28f864 fix incorrect path in theme template 2025-04-09 17:21:23 -04:00
774ccb05f8 Merge pull request #5234 from sbwalker/dev
removing ShutdownTimeout specification as it was changed in .NET 7 to 30 seconds (https://github.com/dotnet/runtime/pull/63712)
2025-04-09 14:58:56 -04:00
0ac48cba34 removing ShutdownTimeout specification as it was changed in .NET 7 to 30 seconds (https://github.com/dotnet/runtime/pull/63712) 2025-04-09 14:58:30 -04:00
e36880fe3a Merge pull request #5233 from sbwalker/dev
prepare for 6.1.2 release
2025-04-09 11:46:30 -04:00
713cf5de2c prepare for 6.1.2 release 2025-04-09 11:46:16 -04:00
0fa336411f Merge pull request #5232 from sbwalker/dev
update to .NET 9.0.4
2025-04-09 11:41:08 -04:00
8ebdb09d68 update to .NET 9.0.4 2025-04-09 11:40:54 -04:00
40bc53001e Merge pull request #5231 from sbwalker/dev
improve sitemap detection in robots.txt
2025-04-09 11:26:53 -04:00
b1656d1eea improve sitemap detection in robots.txt 2025-04-09 11:26:33 -04:00
7aa54bf979 Merge pull request #5230 from sbwalker/dev
resolve issue with host role support  in external login
2025-04-09 10:55:32 -04:00
231f9bca84 resolve issue with host role support in external login 2025-04-09 10:55:16 -04:00
e4f8596c19 Merge pull request #5227 from sbwalker/dev
fix #5223 - allow robots.txt to be customized for each site
2025-04-08 09:23:35 -04:00
020b7233d0 fix #5223 - allow robots.txt to be customized for each site 2025-04-08 09:23:22 -04:00
85fc0b3e2f Merge pull request #5226 from sbwalker/dev
optimize the System Update process
2025-04-07 13:19:07 -04:00
5dcc7c14f3 optimize the System Update process 2025-04-07 13:18:52 -04:00
7993d27b11 Merge pull request #5224 from sbwalker/dev
added new Azure SQL database provider
2025-04-07 10:48:20 -04:00
1f8c54ce74 added new Azure SQL database provider 2025-04-07 10:48:02 -04:00
73a414a34b Merge pull request #5221 from sbwalker/dev
improve help text
2025-04-02 09:47:31 -04:00
8fa19c4a51 improve help text 2025-04-02 09:47:16 -04:00
0667ae3e15 Merge pull request #5220 from sbwalker/dev
update help text
2025-04-02 09:36:49 -04:00
db1d00cd07 update help text 2025-04-02 09:36:32 -04:00
b27f092bef Merge pull request #5219 from sbwalker/dev
use dynamic .NET major version value
2025-04-01 15:29:26 -04:00
4eaea8e586 use dynamic .NET major version value 2025-04-01 15:29:10 -04:00
89cd7d3bbb Update README.md 2025-04-01 14:30:49 -04:00
2fff1d8d21 Merge pull request #5218 from sbwalker/dev
removing connection string section
2025-04-01 13:55:09 -04:00
850631f00e removing connection string section 2025-04-01 13:54:39 -04:00
1cea8846cf Merge pull request #5217 from sbwalker/dev
fix azuredeploy
2025-04-01 11:04:02 -04:00
af48a48559 fix azuredeploy 2025-04-01 11:03:46 -04:00
655c1762aa Merge pull request #5216 from sbwalker/dev
azuredeploy changes to use ZIP Deploy
2025-04-01 10:56:47 -04:00
f706ccfd87 new version using ZIP Deploy 2025-04-01 10:56:04 -04:00
71e4c7f117 Merge pull request #5214 from sbwalker/dev
remove .deployment file as we are deploying from a package rather than from source code
2025-03-31 16:22:08 -04:00
ad6182f4bd remove .deployment file as we are deploying from a package rather than from source code 2025-03-31 16:21:45 -04:00
86bf0f65b0 Merge pull request #5213 from sbwalker/dev
modifications to use WEBSITE_RUN_FROM_PACKAGE
2025-03-31 16:06:24 -04:00
7742f7747d modifications to use WEBSITE_RUN_FROM_PACKAGE 2025-03-31 16:06:08 -04:00
eb998c41f2 Merge pull request #5212 from sbwalker/dev
fix #4929 deploy to azure
2025-03-31 13:59:12 -04:00
657bd7c97c fix #4929 deploy to azure 2025-03-31 13:58:03 -04:00
c8286148c1 Merge pull request #5211 from sbwalker/dev
fix #5194 - improve performance of retrieving scheduled job logs
2025-03-31 13:16:52 -04:00
e6ba2cce62 fix #5194 - improve performance of retrieving scheduled job logs 2025-03-31 13:16:35 -04:00
6105ff44b4 Merge pull request #5210 from sbwalker/dev
fix #5207 add support for username and displayname in permissions grid
2025-03-31 10:04:43 -04:00
72da77be01 fix #5207 add support for username and displayname in permissions grid 2025-03-31 10:04:26 -04:00
4c29b31f1b Merge pull request #5209 from sbwalker/dev
delete files before deleting folder
2025-03-31 08:58:39 -04:00
6e640108ed delete files before deleting folder 2025-03-31 08:58:23 -04:00
157322441d Merge pull request #5206 from zyhfish/task/fix-5205
Fix #5205: specific the date time as UTC kind.
2025-03-31 08:40:28 -04:00
61d967e6af Merge pull request #5208 from sbwalker/dev
allow custom urls in UserProfile component
2025-03-31 08:38:46 -04:00
99f2158e55 allow custom urls in UserProfile component 2025-03-31 08:38:30 -04:00
Ben
1cba78cc4e Fix #5205: specific the date time as UTC kind. 2025-03-29 09:29:49 +08:00
1770c1ee11 Merge pull request #5201 from sbwalker/dev
include external login support for host role
2025-03-26 17:11:52 -04:00
a57fbea0cc include external login support for host role 2025-03-26 17:11:29 -04:00
f0c27c83f1 Merge pull request #5197 from zyhfish/task/clean-build
suppress build warnings.
2025-03-26 11:16:47 -04:00
Ben
7873ca564c supress build warnings. 2025-03-26 08:12:10 +08:00
5ebc1fec24 Merge pull request #5195 from zyhfish/task/fix-5191
Fix #5191: trigger event when folder changed.
2025-03-25 19:46:24 -04:00
f2559b7d4d Merge pull request #5196 from sbwalker/dev
fix #5193 - prevent scheduled jobs from blocking startup
2025-03-25 19:07:24 -04:00
1ee92a248e fix #5193 - prevent scheduled jobs from blocking startup 2025-03-25 19:07:01 -04:00
Ben
8376f98f21 Fix #5191: trigger event when folder changed. 2025-03-25 22:23:51 +08:00
810a3e0171 Merge pull request #5188 from sbwalker/dev
prevent stylesheet resources from being duplicated
2025-03-21 17:34:21 -04:00
2eac9c3795 prevent stylesheet resources from being duplicated 2025-03-21 17:34:07 -04:00
75f2425668 Merge pull request #5187 from sbwalker/dev
remove unnecessary using statements
2025-03-21 10:10:32 -04:00
2dd1d7e926 remove unnecessary using statements 2025-03-21 10:10:19 -04:00
5bb05a0a51 Merge pull request #5185 from sbwalker/dev
fix page order for new Privacy and Terms pages
2025-03-21 08:19:26 -04:00
bc2c5b00c6 fix page order for new Privacy and Terms pages 2025-03-21 08:19:09 -04:00
09f5e158dd Merge pull request #5181 from sbwalker/dev
add ability to Synchronize local modules and themes with Marketplace
2025-03-19 14:37:50 -04:00
4656471a0a add ability to Synchronize local modules and themes with Marketplace 2025-03-19 14:37:36 -04:00
69d58a4273 Merge pull request #5179 from leigh-pointer/SwashbuckleUpdate8
Swashbuckle Update
2025-03-19 08:42:40 -04:00
53a27677d4 Swashbuckle Update
Update Swashbuckle  to version 8.0
2025-03-19 12:07:15 +01:00
f243ad0348 Merge pull request #5178 from sbwalker/dev
add caching support for ImageUrl
2025-03-18 14:29:23 -04:00
b4ce6bbb42 add caching support for ImageUrl 2025-03-18 14:29:09 -04:00
fa32937045 Merge pull request #5177 from sbwalker/dev
MySql.Data is still required for raw query operations
2025-03-18 09:24:53 -04:00
812e5f3c8e MySql.Data is still required for raw query operations 2025-03-18 09:24:39 -04:00
e2981e802c Merge pull request #5176 from sbwalker/dev
fix #5173 - MySQL Database Provider not incuding MySqlConnector dependency
2025-03-18 09:08:07 -04:00
4ae4705c73 fix #5173 - MySQL Database Provider not incuding MySqlConnector dependency 2025-03-18 09:07:23 -04:00
fbf4b12713 Merge pull request #5168 from zyhfish/task/fix-cookie-consent-layout
adjust the cookie consent layout in small screen.
2025-03-17 13:45:14 -04:00
e4ece3e0dc Merge pull request #5174 from sbwalker/dev
notifications should only convert line breaks to HTML for plain text messages
2025-03-17 13:45:02 -04:00
9f231421be notifications should only convert line breaks to HTML for plain text messages 2025-03-17 13:44:40 -04:00
391827222e Update README.md 2025-03-15 18:08:36 +00:00
c1721bd1a1 Update README.md 2025-03-15 18:08:00 +00:00
f6630ae241 Update README.md 2025-03-15 18:07:14 +00:00
424cab64a8 NEW: Docker builds 2025-03-15 18:59:18 +01:00
Ben
b4fdbb5e48 adjust the layout in small screen. 2025-03-13 21:54:43 +08:00
Ben
fbf62ca30d adjust the cookie consent layout in small screen. 2025-03-13 20:54:50 +08:00
05d2096fb8 Update README.md 2025-03-12 14:34:31 -04:00
7683af81bc Update README.md 2025-03-12 14:22:43 -04:00
bad10b3812 6.1.1 Release
6.1.1 Release
2025-03-12 14:21:41 -04:00
9f9522c2ed 6.1.1 Release
6.1.1 Release
2025-03-12 14:21:15 -04:00
62879c3e52 Merge pull request #5162 from sbwalker/dev
upgrade to .NET 9.0.3
2025-03-11 14:36:14 -04:00
45610f8dd7 upgrade to .NET 9.0.3 2025-03-11 14:35:59 -04:00
18102cbd78 Merge pull request #5160 from sbwalker/dev
sort endpoints by route
2025-03-11 13:11:34 -04:00
262d6a1529 sort endpoints by route 2025-03-11 13:11:19 -04:00
1124ddaf90 Merge pull request #5157 from zyhfish/task/fix-5156
Fix #5156: update the bind event to oninput.
2025-03-11 11:51:38 -04:00
981add3872 Merge pull request #5158 from sbwalker/dev
rename Cache service to OutputCache
2025-03-11 11:48:58 -04:00
8d4b30140e rename Cache service to OutputCache 2025-03-11 11:48:43 -04:00
Ben
b9c59137a8 Fix #5156: update the bind event to oninput. 2025-03-11 22:58:06 +08:00
0b1c7e06ca Update README.md 2025-03-10 10:56:41 -04:00
fcaf80cba6 Update README.md 2025-03-10 10:56:11 -04:00
6358b9eabb Merge pull request #5155 from sbwalker/dev
added Logout Everywhere option to User Settings
2025-03-10 10:53:25 -04:00
70a3fab1ff added Logout Everywhere option to User Settings 2025-03-10 10:53:05 -04:00
bdf86ace86 Merge pull request #5152 from sbwalker/dev
upgrade to ImageSharp 3.1.7 due to security vulnerability
2025-03-07 14:17:02 -05:00
d57132d1e4 upgrade to ImageSharp 3.1.7 due to security vulnerability 2025-03-07 14:16:47 -05:00
a6e87abf99 Merge pull request #5151 from sbwalker/dev
allow site settings to be overidden at host level
2025-03-07 14:15:31 -05:00
f1771610fe allow site settings to be overidden at host level 2025-03-07 14:15:16 -05:00
a88ea9780f Update README.md 2025-03-06 15:45:30 -05:00
bca0866d72 Update README.md 2025-03-06 15:43:59 -05:00
cebed93abf Update README.md 2025-03-06 15:42:48 -05:00
ee2b2e3569 Update README.md 2025-03-06 15:40:07 -05:00
cb8e9ee244 Update README.md 2025-03-06 15:38:18 -05:00
486184b16c Update README.md 2025-03-06 15:36:28 -05:00
9f9bd1988f Merge pull request #5149 from sbwalker/dev
update based on changes suggested by @adefwebserver
2025-03-06 15:25:37 -05:00
ba1bfd1bc0 update based on changes suggested by @adefwebserver 2025-03-06 15:25:25 -05:00
e12926e971 Merge pull request #5148 from sbwalker/dev
allow page and module settings to be included in site templates, improve terms and privacy default content, add Settings for HtmlText module
2025-03-06 14:46:38 -05:00
5b4db0de3b allow page and module settings to be included in site templates, improve terms and privacy default content, add Settings for HtmlText module 2025-03-06 14:46:17 -05:00
70ff55faa6 Merge pull request #5147 from sbwalker/dev
modify terminology
2025-03-06 09:43:55 -05:00
f2bd47d8bc modify terminology 2025-03-06 09:43:40 -05:00
e2b9c9e98e Merge pull request #5146 from sbwalker/dev
prepare for 6.1.1
2025-03-05 15:02:39 -05:00
b0791a594f prepare for 6.1.1 2025-03-05 15:02:12 -05:00
ea5eaa6ed2 Merge pull request #5145 from sbwalker/dev
update to .NET 9.0.2
2025-03-05 14:55:00 -05:00
ec3fd1d585 update to .NET 9.0.2 2025-03-05 14:54:46 -05:00
d76de22977 Merge pull request #5144 from sbwalker/dev
modify localization text
2025-03-05 10:45:17 -05:00
c0e3483cc7 modify localization text 2025-03-05 10:44:34 -05:00
0994cdf3b6 Merge pull request #5141 from sbwalker/dev
sync interop.js changes with .NET MAUI
2025-03-04 16:25:24 -05:00
a76fd82262 sync interop.js changes with .NET MAUI 2025-03-04 16:25:10 -05:00
2f919c7d69 Merge pull request #5140 from sbwalker/dev
fix regression issue with Search component
2025-03-04 15:45:16 -05:00
f12592731b fix regression issue with Search component 2025-03-04 15:45:02 -05:00
4a20e1a25d Merge pull request #5138 from zyhfish/task/improve-middle-screen-view
improve the styles in middle screen size.
2025-03-04 15:42:37 -05:00
cc7111c3ff Merge pull request #5139 from sbwalker/dev
change module title for Terms
2025-03-04 15:40:58 -05:00
81972aed62 change module title for Terms 2025-03-04 15:40:44 -05:00
Ben
5e2092c6d4 improve the styles in middle screen size. 2025-03-04 23:31:21 +08:00
d136f8ac91 Merge pull request #5137 from sbwalker/dev
add nonce support
2025-03-03 16:49:41 -05:00
5b23917940 add nonce support 2025-03-03 16:49:28 -05:00
2cda0a3798 Merge pull request #5136 from sbwalker/dev
modify cookie consent text
2025-03-03 15:37:46 -05:00
f315ad1ce9 modify cookie consent text 2025-03-03 15:37:31 -05:00
49f1c273c2 Merge pull request #5134 from sbwalker/dev
add terms to upgrademanager
2025-03-03 13:36:47 -05:00
8518476c87 add terms to upgrademanager 2025-03-03 13:36:32 -05:00
a34ed756db Merge pull request #5132 from mdmontesinos/sitemap-cache
resolves #4899: output cache for sitemap
2025-03-03 13:22:46 -05:00
a48232c4e3 Merge pull request #5133 from sbwalker/dev
add default privacy and terms
2025-03-03 13:22:33 -05:00
ac65e38390 add default privacy and terms 2025-03-03 13:22:17 -05:00
eab3a753f5 resolves #4899: output cache for sitemap 2025-03-03 17:54:33 +01:00
9e5922e121 Merge pull request #5130 from zyhfish/task/fix-4936
Fix #4936: set the allow cookie value when refresh state.
2025-03-03 07:57:42 -05:00
Ben
bf57b23776 update the page state after policy changed. 2025-03-01 15:05:23 +08:00
Ben
8f4a20fd46 Fix #4936: set the allow cookie value when refresh state. 2025-03-01 14:16:42 +08:00
b3716da5ac Merge pull request #5128 from zyhfish/task/fix-4936-new
move the styles to app.css.
2025-02-28 13:36:58 -05:00
2c129fd800 Merge pull request #5129 from sbwalker/dev
localization text change
2025-02-28 13:36:49 -05:00
6fb18e7a25 localization text change 2025-02-28 13:36:34 -05:00
Ben
38d28d6944 move the styles to app.css. 2025-02-28 23:53:53 +08:00
a187e1a7a2 Merge pull request #5127 from sbwalker/dev
fix RESX issue
2025-02-28 10:52:18 -05:00
7d7a19c7c2 fix RESX issue 2025-02-28 10:52:06 -05:00
6a2ae2153a Merge pull request #5119 from zyhfish/task/fix-4936
update the cookie consent control.
2025-02-28 10:48:21 -05:00
f50ba1a91e Merge branch 'dev' into task/fix-4936 2025-02-28 10:47:24 -05:00
b09575dbd6 Merge pull request #5126 from sbwalker/dev
provide option to assign a theme to a site
2025-02-28 10:45:40 -05:00
c52ee3d91d provide option to assign a theme to a site 2025-02-28 10:45:25 -05:00
Ben
1ced5c0425 update the cookie consent control. 2025-02-28 23:32:17 +08:00
e399a5c9b1 Merge pull request #5124 from sbwalker/dev
add missing maxlength attributes
2025-02-27 10:51:37 -05:00
08dff5fb67 add missing maxlength attributes 2025-02-27 10:51:22 -05:00
912760f2a7 Update SECURITY.md 2025-02-26 15:29:49 -05:00
4b62fdbf93 Update SECURITY.md 2025-02-26 15:27:33 -05:00
df593d43a7 Update SECURITY.md 2025-02-26 15:25:25 -05:00
89b1fba771 Merge pull request #5123 from sbwalker/dev
remove package validation logic
2025-02-26 12:54:06 -05:00
5505c91ae0 remove package validation logic 2025-02-26 12:53:53 -05:00
cc720ff399 Merge pull request #5122 from sbwalker/dev
remove unnecessary log message
2025-02-26 12:12:14 -05:00
29f07f6c56 remove unnecessary log message 2025-02-26 12:11:56 -05:00
a69e197a1f Merge pull request #5121 from zyhfish/task/fix-5116
Fix #5116: parse the value as UTC time.
2025-02-26 07:45:55 -05:00
Ben
6dddd8eff8 only allow essential cookies when cookie consent not granted. 2025-02-26 19:41:58 +08:00
Ben
51aada8922 Fix #5116: parse the value as UTC time. 2025-02-26 18:25:24 +08:00
Ben
b47bf40e8f update the cookie consent control. 2025-02-25 11:36:47 +08:00
48151bf365 Merge pull request #5118 from sbwalker/dev
remove IJSRuntime reference as it was causing a compilation warning
2025-02-24 16:15:51 -05:00
659950996d remove IJSRuntime reference as it was causing a compilation warning 2025-02-24 16:15:35 -05:00
6e656a4d0a Merge pull request #5114 from zyhfish/task/fix-4936
Fix #4936: add the cookie consent theme control.
2025-02-24 16:08:22 -05:00
Ben
bf308dd13d enable child component of cookie consent control. 2025-02-24 22:32:19 +08:00
Ben
982f3b1943 Fix #4936: add the cookie consent theme control. 2025-02-22 09:49:33 +08:00
7a4ea8cf1b Merge pull request #5094 from zyhfish/task/set-fa-ir-culture
Fix #5054: resolve the issue in fa-IR language.
2025-02-19 07:36:16 -05:00
7c0482a87c Merge pull request #5111 from sbwalker/dev
remove warning message related to no jobs being registered
2025-02-18 11:50:57 -05:00
101ededd89 remove warning message related to no jobs being registered 2025-02-18 11:50:36 -05:00
a8cbc0040e Merge pull request #5108 from zyhfish/task/fix-5103
Fix #5103: return a copy of the assembly list.
2025-02-18 11:40:30 -05:00
ed91bb445b Merge pull request #5110 from sbwalker/dev
improve HostedServiceBase so that scheduled jobs can be registered during installation
2025-02-18 11:36:00 -05:00
f158a222f4 improve HostedServiceBase so that scheduled jobs can be registered during installation 2025-02-18 11:35:38 -05:00
46bcad1fca Merge pull request #5109 from sbwalker/dev
clean up scheduled jobs which have been uninstalled
2025-02-18 09:12:48 -05:00
5e147afb9f clean up scheduled jobs which have been uninstalled 2025-02-18 09:12:26 -05:00
Ben
b061d4593f Fix #5103: return a copy of the assembly list. 2025-02-18 22:02:25 +08:00
3fa520b4ef Merge pull request #5107 from sbwalker/dev
make purge job output more readable
2025-02-18 08:53:38 -05:00
2df05b4afd make purge job output more readable 2025-02-18 08:53:23 -05:00
e0569a6748 Merge pull request #5104 from sbwalker/dev
fix visitor purge logic
2025-02-17 13:22:11 -05:00
2e6ab398d9 fix visitor purge logic 2025-02-17 13:21:58 -05:00
Ben
94b03d2a6b update settings for all RTL languages. 2025-02-16 10:25:43 +08:00
f84fe30bb6 Merge pull request #5097 from sbwalker/dev
synchronize BlazorScriptReload changes
2025-02-14 09:17:58 -05:00
049ddef531 synchronize BlazorScriptReload changes 2025-02-14 09:17:44 -05:00
a1a214c742 Merge pull request #5096 from sbwalker/dev
add another constructor for Script class
2025-02-14 09:09:14 -05:00
c40a483ffa add another constructor for Script class 2025-02-14 09:09:00 -05:00
Ben
aff99acfae Fix #5054: resolve the issue in fa-IR language. 2025-02-12 20:18:10 +08:00
628129c08d Update README.md 2025-02-11 12:00:41 -05:00
679d34dfdf Merge pull request #5093 from oqtane/master
6.1.0 Release
2025-02-11 11:49:29 -05:00
b2f65903ae Merge pull request #5092 from oqtane/dev
6.1.0 Release
2025-02-11 11:49:06 -05:00
2daefe0382 Merge pull request #5091 from tvatavuk/patch-1
Minor fix in ThemeController.cs
2025-02-11 11:41:52 -05:00
1214a11704 Minor fix in ThemeController.cs
update `SharedReference` to use "Oqtane.Shared" for PackageReference code
2025-02-11 14:35:13 +01:00
e55e0118c2 Merge pull request #5090 from sbwalker/dev
fix #5089 - remove upgrade cleanup logic as .NET 9.0.1 moves assemblies back to /bin folder
2025-02-10 16:53:42 -05:00
a1ac81e907 fix #5089 - remove upgrade cleanup logic as .NET 9.0.1 moves assemblies back to /bin folder 2025-02-10 16:53:22 -05:00
14ad68bf69 Merge pull request #5088 from sbwalker/dev
modify RemoveAssemblies method so that it only runs once - not for every tenant
2025-02-10 16:27:24 -05:00
e3118c6e99 modify RemoveAssemblies method so that it only runs once - not for every tenant 2025-02-10 16:27:05 -05:00
b41aeab8f8 Merge pull request #5087 from sbwalker/dev
update Provider property to Pomelo
2025-02-10 10:47:07 -05:00
1a738b358e update Provider property to Pomelo 2025-02-10 10:46:44 -05:00
f4b1e8035b Merge pull request #5086 from sbwalker/dev
improve notification add and update methods
2025-02-10 09:50:35 -05:00
324e985247 improve notification add and update methods 2025-02-10 09:50:21 -05:00
60faacd7d0 Merge pull request #5085 from sbwalker/dev
fix comment
2025-02-10 08:21:13 -05:00
d4a4d7c346 fix comment 2025-02-10 08:20:58 -05:00
189f8f1d27 Merge pull request #5082 from sbwalker/dev
enhance purge job to trim broken urls based on retention policy
2025-02-09 13:02:21 -05:00
ed353461da enhance purge job to trim broken urls based on retention policy 2025-02-09 13:02:07 -05:00
e9330d6c62 Merge pull request #5080 from zyhfish/task/fix-5079
Fix #5079: Retrieve all Url Mapping records
2025-02-09 12:35:28 -05:00
f53e7cc3f6 Merge pull request #5081 from sbwalker/dev
fix #5072 - administrators should be allowed to send system notifications
2025-02-09 12:34:38 -05:00
4f4258d532 fix #5072 - administrators should be allowed to send system notifications 2025-02-09 12:34:17 -05:00
Ben
c80910f355 Fix #5079: remove the records limit. 2025-02-08 11:51:38 +08:00
12470ab178 Merge pull request #5075 from mdmontesinos/dev
fix #5074: generate cancellation token for file upload
2025-02-07 07:53:16 -05:00
704e091e9b Merge pull request #5076 from sbwalker/dev
synchronize interop script changes with .NET MAUI
2025-02-07 07:52:58 -05:00
f30f1e5c1f synchronize interop script changes with .NET MAUI 2025-02-07 07:52:43 -05:00
0741ce2197 fix #5074: generate cancellation token for file upload 2025-02-07 09:11:52 +01:00
fc81bae9b7 Update README.md 2025-02-06 15:16:50 -05:00
3fa68e4f96 Merge pull request #5071 from sbwalker/dev
moved file setting to File Management and added Max Chunk Size
2025-02-06 15:10:29 -05:00
05a767c7be moved file setting to File Management and added Max Chunk Size 2025-02-06 15:10:14 -05:00
8c1e8f6377 Merge pull request #5070 from sbwalker/dev
fix #5067 - add support for Guid data types
2025-02-06 14:17:06 -05:00
0fbae8d7da fix #5067 - add support for Guid data types 2025-02-06 14:16:53 -05:00
cec4b339f5 Merge pull request #5069 from mdmontesinos/dev-test
fix #5058: ensure sequential file and chunk uploads to avoid overload
2025-02-06 13:56:35 -05:00
1a7656d8ee fix #5058: ensure sequential file and chunk uploads to avoid overload 2025-02-06 19:21:51 +01:00
e173815810 Merge pull request #5068 from sbwalker/dev
fix LogLevel for file upload error
2025-02-06 12:21:51 -05:00
620c768e05 fix LogLevel for file upload error 2025-02-06 12:21:34 -05:00
7740679077 Merge pull request #5066 from sbwalker/dev
fix #5061 - configure Page Management for personalizable pages
2025-02-06 11:16:46 -05:00
1ebc8ebff3 fix #5061 - configure Page Management for personalizable pages 2025-02-06 11:16:30 -05:00
fa4fac70d5 Merge pull request #5065 from sbwalker/dev
improve file upload validation and error handling on server
2025-02-06 08:20:13 -05:00
8c83a18f93 improve file upload validation and error handling on server 2025-02-06 08:19:57 -05:00
a151ecfda0 Merge pull request #5064 from sbwalker/dev
modified file upload error message to reflect new behavior
2025-02-05 19:10:10 -05:00
dec0c0649c modified file upload error message to reflect new behavior 2025-02-05 19:09:55 -05:00
a356f893ac Merge pull request #5063 from sbwalker/dev
remove uploadFile() method as it is not used
2025-02-05 17:08:56 -05:00
e2af4f74c3 remove uploadFile() method as it is not used 2025-02-05 17:08:32 -05:00
99022b76e5 Merge pull request #5062 from sbwalker/dev
file upload improvements
2025-02-05 16:48:59 -05:00
9dd6dc7523 file upload improvements 2025-02-05 16:48:34 -05:00
6f588200d7 Merge pull request #5057 from sbwalker/dev
fix #5044 - improve file part removal logic
2025-02-03 11:00:37 -05:00
f3dbeae28e fix #5044 - improve file part removal logic 2025-02-03 11:00:22 -05:00
9f70361298 Merge pull request #5056 from sbwalker/dev
add additional Script class constructors
2025-02-03 10:35:32 -05:00
534353ce13 add additional Script class constructors 2025-02-03 10:35:17 -05:00
3f391a7354 Merge pull request #5052 from sbwalker/dev
modify button spacing
2025-01-31 14:51:06 -05:00
0dd0752710 modify button spacing 2025-01-31 14:50:54 -05:00
710fae4b0e Merge pull request #5051 from sbwalker/dev
add user impersonation
2025-01-31 11:14:28 -05:00
de6c57a7ee add user impersonation 2025-01-31 11:14:13 -05:00
7eee1fcd6a Merge pull request #5050 from sbwalker/dev
make Kestrel the default profile in launchjSettings.json
2025-01-31 09:18:59 -05:00
1fd2aedf96 make Kestrel the default profile in launchjSettings.json 2025-01-31 09:18:44 -05:00
ffdd7c063b Merge pull request #5049 from sbwalker/dev
added a ScriptsLoaded property in ModuleBase and ThemeBase for flow control in Interactive rendering scenarios
2025-01-31 08:42:56 -05:00
a87af264eb added a ScriptsLoaded property in ModuleBase and ThemeBase for flow control in Interactive rendering scenarios 2025-01-31 08:42:36 -05:00
3640cd2fdd Merge pull request #5046 from sbwalker/dev
fix upgrade issue which can occur in development environments
2025-01-30 08:40:08 -05:00
f7cf25c4bb fix upgrade issue which can occur in development environments 2025-01-30 08:39:49 -05:00
77dbd0d4c7 Merge pull request #5045 from sbwalker/dev
update version to 6.1.0
2025-01-30 08:08:34 -05:00
5a77c83e68 update version to 6.1.0 2025-01-30 08:08:16 -05:00
0a6763e08c Merge pull request #5043 from sbwalker/dev
change ResourceLoadBehavior Never to None
2025-01-29 19:05:20 -05:00
b5aa206670 change ResourceLoadBehavior Never to None 2025-01-29 19:05:05 -05:00
dbb4d9b64b Merge pull request #5042 from sbwalker/dev
fix logic to retrieve access token
2025-01-29 16:03:11 -05:00
6775edfd66 fix logic to retrieve access token 2025-01-29 16:02:55 -05:00
b06750ed65 Merge pull request #5040 from sbwalker/dev
remove method which was relocated to PageRepository
2025-01-29 13:12:59 -05:00
57879c1891 remove method which was relocated to PageRepository 2025-01-29 13:12:46 -05:00
65b55a76f2 Merge pull request #5039 from sbwalker/dev
improve asset caching help text
2025-01-29 12:40:02 -05:00
8562a68306 improve asset caching help text 2025-01-29 12:39:47 -05:00
160da46b5a Merge pull request #5038 from sbwalker/dev
remove Environment.IsDevelopment logic
2025-01-29 12:27:57 -05:00
ae5f70a739 remove Environment.IsDevelopment logic 2025-01-29 12:27:42 -05:00
4456e57466 Merge pull request #5036 from sbwalker/dev
use fingerprint term consistently
2025-01-29 10:42:34 -05:00
24cd090c61 use fingerprint term consistently 2025-01-29 10:42:19 -05:00
4f849f5d5f Merge pull request #5035 from sbwalker/dev
performance improvement when loading files within a folder
2025-01-29 10:23:11 -05:00
db2e86e84c performance improvement when loading files within a folder 2025-01-29 10:22:56 -05:00
14748ce2b3 Merge pull request #5034 from sbwalker/dev
use Configuration service as it already exists
2025-01-29 08:28:08 -05:00
527509732c use Configuration service as it already exists 2025-01-29 08:22:21 -05:00
aa9214477c Merge pull request #5033 from sbwalker/dev
fix #5018 - redirect file download to login page
2025-01-28 16:31:03 -05:00
db24ed8b55 fix #5018 - redirect file download to login page 2025-01-28 16:30:49 -05:00
349d1849d9 Merge pull request #5032 from sbwalker/dev
use deterministic hash in file server image generation
2025-01-28 15:55:56 -05:00
188be2fa8c use deterministic hash in file server image generation 2025-01-28 15:55:40 -05:00
5c4d7df734 Merge pull request #5031 from sbwalker/dev
remove Oqtane.Server.staticwebassets.endpoints.json from release packages
2025-01-28 14:39:48 -05:00
37de18c670 remove Oqtane.Server.staticwebassets.endpoints.json from release packages 2025-01-28 14:39:30 -05:00
0001e3844b Merge pull request #5030 from sbwalker/dev
provides options to control caching for static assets
2025-01-28 14:30:13 -05:00
65f171f701 provides options to control caching for static assets 2025-01-28 14:29:58 -05:00
c4308c239c Merge pull request #5007 from RahulKaushik007/fix-static-file-caching
Fixes #5005: Add Browser Caching for Static Assets
2025-01-28 13:21:29 -05:00
2b6af3cb37 Merge pull request #5029 from sbwalker/dev
fix localization text
2025-01-28 13:18:32 -05:00
c5a16fbbc1 fix localization text 2025-01-28 13:18:18 -05:00
1db83f509b Merge pull request #5028 from sbwalker/dev
add caching support for folders
2025-01-28 12:47:37 -05:00
2a06304a2c add caching support for folders 2025-01-28 12:47:23 -05:00
7bbe684135 Merge pull request #5027 from sbwalker/dev
improve terminology consistency
2025-01-28 08:56:19 -05:00
a996a88fc4 improve terminology consistency 2025-01-28 08:56:05 -05:00
8cf9e7db51 Merge pull request #5026 from sbwalker/dev
add Fingerprint property to ModuleBase and ThemeBase
2025-01-28 08:44:01 -05:00
ed981c67b7 add Fingerprint property to ModuleBase and ThemeBase 2025-01-28 08:43:48 -05:00
6a77a0a5b9 Merge pull request #5025 from sbwalker/dev
improve terminology
2025-01-28 08:34:31 -05:00
bfb4b4431b improve terminology 2025-01-28 08:34:15 -05:00
3de44c0335 Merge pull request #5024 from sbwalker/dev
add ThemeState property to ThemeBase
2025-01-28 08:27:25 -05:00
37afd1aec9 add ThemeState property to ThemeBase 2025-01-28 08:27:10 -05:00
b2ac561673 Merge pull request #5021 from thabaum/update-package-dependencies-9.0.1
Fixes #5020: Updates package dependencies 9.0.1 (Latest)
2025-01-27 16:44:33 -05:00
c9bf7d9138 Merge pull request #5022 from sbwalker/dev
fix #5005 - adds versioning (ie. fingerprinting) for static assets - core, modules, and themes.
2025-01-27 16:35:07 -05:00
153a689bdb fix #5005 - adds versioning (ie. fingerprinting) for static assets - core, modules, and themes. 2025-01-27 16:34:47 -05:00
26b88f1a22 Update Package Dependencies to 9.0.1 2025-01-27 05:13:31 -08:00
c66a5d028f Update Package Dependencies to 9.0.1 2025-01-27 05:13:01 -08:00
8441c95a5c Update Package Dependencies to 9.0.1 2025-01-27 05:11:49 -08:00
e0e32b0199 Update Package Dependencies to 9.0.1 and 9.0.30 2025-01-27 05:09:19 -08:00
2bb76564e9 Update Package Dependencies to 9.0.1 2025-01-27 05:06:46 -08:00
86ec25d4de Update Package Dependencies to 9.0.1 2025-01-27 05:05:42 -08:00
ed9929963c Update Package Dependencies to 9.0.1 2025-01-27 05:04:34 -08:00
36f50118ac Update Package Dependencies to 9.0.1 2025-01-27 05:03:46 -08:00
72ddf27504 Update Package Dependency to 9.2.0 2025-01-27 05:03:03 -08:00
9bd36931ff Update Package Dependencies to 9.0.1 2025-01-27 05:02:10 -08:00
056ef7a3d5 Update Package Dependencies to 9.0.1 2025-01-27 05:00:08 -08:00
e483945d05 Merge pull request #5017 from sbwalker/dev
fix #5014 - page content scripts not loading on initial page request in Interactive rendering
2025-01-24 14:29:42 -05:00
7a9c637e03 fix #5014 - page content scripts not loading on initial page request in Interactive rendering 2025-01-24 14:29:23 -05:00
09ce543ea6 Merge pull request #5013 from sbwalker/dev
allow packages to be managed across installations
2025-01-23 09:08:16 -05:00
0ef24ebc3f allow packages to be managed across installations 2025-01-23 09:08:02 -05:00
a4419d3af6 Merge pull request #5011 from sbwalker/dev
remove GetButtonSize method
2025-01-22 07:43:38 -05:00
935983c02a remove GetButtonSize method 2025-01-22 07:43:23 -05:00
bd54ce5017 Merge pull request #4998 from leigh-pointer/ActionDialogSize
Update ActionDialog Add method to ensure consistent button sizing
2025-01-22 07:42:04 -05:00
af6ed78b8e Update ActionDialog.razor 2025-01-22 07:57:52 +01:00
f72438996d Merge remote-tracking branch 'upstream/dev' into ActionDialogSize 2025-01-22 07:36:01 +01:00
9db2a55a5a Merge pull request #5010 from sbwalker/dev
fix #4964 - use bearer token if it already exists
2025-01-21 16:55:19 -05:00
950d90badb fix #4964 - use bearer token if it already exists 2025-01-21 16:55:02 -05:00
1864d180af Merge pull request #5003 from sdi2121/patch-1
Update Oqtane.Server.csproj - MySQL deploy to Azure error
2025-01-21 15:58:44 -05:00
0e82e98382 Merge pull request #5006 from mdmontesinos/patch-1
FIX: File server MimeType not updated after image conversion
2025-01-21 15:58:33 -05:00
46023d35dc Merge pull request #5009 from sbwalker/dev
fix #4976 - manage hierarchical path updates and page deletion
2025-01-21 15:58:08 -05:00
90d2e0a40b fix #4976 - manage hierarchical path updates and page deletion 2025-01-21 15:57:48 -05:00
5f884e0796 Merge pull request #5008 from sbwalker/dev
fix #4965 - improve user/site management
2025-01-21 12:21:50 -05:00
16477052e2 fix #4965 - improve user/site management 2025-01-21 12:21:27 -05:00
66a05603f7 Fix static file caching headers 2025-01-21 17:42:08 +05:30
fe2a883386 Fix static file caching headers 2025-01-21 17:26:47 +05:30
ca7fdaa125 FIX: File server MimeType not updated after image conversion 2025-01-20 12:17:22 +01:00
1283ec2008 Update Oqtane.Server.csproj
Fixes Azure manual deployment build from local code build. May need an additional fix in MySQL library (Line 24 - MySQLDatabase.cs)
2025-01-18 13:53:04 -05:00
d1f78f9048 Update LICENSE 2025-01-17 12:57:32 -05:00
45f43bfade Merge pull request #5002 from sbwalker/dev
update copyright year
2025-01-17 12:57:14 -05:00
4793ab4bc9 update copyright year 2025-01-17 12:56:59 -05:00
88b174dea8 Merge pull request #5001 from sbwalker/dev
remove unused method
2025-01-17 12:50:42 -05:00
06ca382bd7 remove unused method 2025-01-17 12:50:26 -05:00
b09175a8db Merge pull request #5000 from sbwalker/dev
allow entry of name during installation
2025-01-17 11:14:49 -05:00
677f68b08d allow entry of name during installation 2025-01-17 11:14:35 -05:00
8058b8dba4 Merge pull request #4999 from sbwalker/dev
improve error messages
2025-01-17 07:54:49 -05:00
4bc26f13c1 improve error messages 2025-01-17 07:54:34 -05:00
e6cf77e724 Update ActionDialog Add method to ensure consistent button sizing
This PR introduces a new private method GetButtonSize() to enhance the consistency of button sizing within our UI. The method specifically checks for the presence of the "btn-sm" class in the Action Button's class list and applies the same sizing to the Cancel Button if found.
2025-01-17 13:34:16 +01:00
f8737c112e Merge pull request #4997 from sbwalker/dev
reload the script if data-reload is "always" or if the script has not been loaded previously and data-reload is "once" or "true"
2025-01-16 15:06:36 -05:00
74b72ed9d4 reload the script if data-reload is "always" or if the script has not been loaded previously and data-reload is "once" or "true" 2025-01-16 15:06:15 -05:00
4950391201 Merge pull request #4996 from sbwalker/dev
allow data-reload to support true or always
2025-01-16 14:22:06 -05:00
64a38d6e45 allow data-reload to support true or always 2025-01-16 14:21:52 -05:00
e2d8ee53f8 Merge pull request #4995 from sbwalker/dev
script reload improvements
2025-01-16 14:06:28 -05:00
0204ff8dd5 script reload improvements 2025-01-16 14:06:13 -05:00
4630ee6e93 Merge pull request #4992 from beolafsen/dev
Trim ModuleOwner and ModuleName before create
2025-01-16 12:29:00 -05:00
c4f2abf143 Merge pull request #4994 from sbwalker/dev
fix #4986 - allow Resources which have Reload specified to be used in Interactive rendering
2025-01-16 12:26:30 -05:00
e7444a0194 fix #4986 - allow Resources which have Reload specified to be used in Interactive rendering 2025-01-16 12:26:10 -05:00
ffed7305ed Merge pull request #4993 from sbwalker/dev
fix #4984 - path mapping for personalized pages
2025-01-16 09:25:43 -05:00
334054bcd4 fix #4984 - path mapping for personalized pages 2025-01-16 09:25:27 -05:00
5bb98eb5b2 Trim ModuleOwner and ModuleName before create 2025-01-16 09:32:18 +01:00
e842bd882a Merge pull request #4991 from sbwalker/dev
fix #4984 - redirect not working for personalized pages
2025-01-15 11:57:00 -05:00
74bfb46f73 fix #4984 - redirect not working for personalized pages 2025-01-15 11:56:44 -05:00
cd45bf4b70 Merge pull request #4989 from sbwalker/dev
fix #4984 - ensure personalized page path does not contain illegal characters
2025-01-14 15:43:00 -05:00
51600bbcb0 fix #4984 - ensure personalized page path does not contain illegal characters 2025-01-14 15:42:40 -05:00
8811a9bcaa Merge pull request #4988 from sbwalker/dev
introduce RemoveAssemblies() method in UpgradeManager
2025-01-14 08:43:38 -05:00
4521f8a774 introduce RemoveAssemblies() method in UpgradeManager 2025-01-14 08:43:23 -05:00
5b427783f8 Merge pull request #4987 from zyhfish/task/fix-4954
Fix #4954: use Pomelo.EntityFrameworkCore.MySql package.
2025-01-14 08:34:34 -05:00
Ben
9508983b15 Fix #4954: use Pomelo.EntityFrameworkCore.MySql package. 2025-01-14 19:58:56 +08:00
fd09912cd7 Merge pull request #4978 from beolafsen/dev
Issue #4977
2025-01-13 16:00:06 -05:00
01cc8584b6 Merge pull request #4968 from leigh-pointer/unused
Removed unused Using statements from the SiteTemplates
2025-01-13 15:56:39 -05:00
c0b104e7c8 Merge pull request #4962 from thabaum/prepare-6.0.2-update-dependencies
Fixes #4961 - Updates Oqtane version to 6.0.2 and update related project dependencies to latest.
2025-01-13 15:55:13 -05:00
9a82021a82 Merge pull request #4983 from sbwalker/dev
include option for external login to save tokens
2025-01-13 15:14:27 -05:00
1fb54a0b0f include option for external login to save tokens 2025-01-13 15:14:13 -05:00
aa5ea61638 Merge pull request #4982 from sbwalker/dev
improve filtering logic in UserRole API
2025-01-13 14:42:36 -05:00
a59ec0258b improve filtering logic in UserRole API 2025-01-13 14:42:19 -05:00
b403f5cf71 Merge pull request #4980 from sbwalker/dev
fix comment spelling
2025-01-13 07:50:21 -05:00
0ac6a62b86 fix comment spelling 2025-01-13 07:50:05 -05:00
ed3743d3b6 Merge pull request #4979 from sbwalker/dev
fix #4969 - improve feedback and flow when connection string points to an invalid database
2025-01-13 07:48:50 -05:00
3468cba000 fix #4969 - improve feedback and flow when connection string points to an invalid database 2025-01-13 07:48:30 -05:00
5a4cdc5354 Issue #4977 2025-01-13 07:31:35 +01:00
af4e19a57e Removed unused Using statements from the SiteTemplates 2025-01-07 04:14:31 +01:00
26bb743679 Prepare 6.0.2 2024-12-31 10:37:27 -08:00
96cc726e22 Prepare 6.0.2 2024-12-31 10:35:00 -08:00
f4b00b01d0 Prepare 6.0.2 2024-12-31 10:34:21 -08:00
127b2ca86d Prepare 6.0.2 2024-12-31 10:32:09 -08:00
4b8b93e1b8 Prepare 6.0.2 2024-12-31 10:31:47 -08:00
3aea412fe9 Prepare 6.0.2 2024-12-31 10:31:23 -08:00
2aef96ad4f Prepare 6.0.2 2024-12-31 10:29:52 -08:00
ec0a77230c Prepare 6.0.2 2024-12-31 10:29:32 -08:00
b35e4bddd0 Prepare 6.0.2 2024-12-31 10:28:48 -08:00
aa32beb341 Update Oqtane.Shared.csproj 2024-12-31 10:28:32 -08:00
efafe89b42 Prepare 6.0.2 2024-12-31 10:26:45 -08:00
5ef2e49d9c Prepare 6.0.2 2024-12-31 10:25:34 -08:00
1cfbf61a30 Prepare 6.0.2 2024-12-31 10:23:15 -08:00
2bb5494b84 Prepare 6.0.2 2024-12-31 10:22:10 -08:00
e8a41ccb47 Prepare 6.0.2 2024-12-31 10:21:10 -08:00
7184f7f635 Prepare 6.0.2 and update package dependencies 2024-12-31 10:20:13 -08:00
cc5727b7fa Prepare 6.0.2 and update package dependencies 2024-12-31 10:16:35 -08:00
7f3d6ef6a5 Merge pull request #4959 from sbwalker/dev
fix #4957 - unable to login after password reset
2024-12-31 08:18:14 -05:00
44ce68097b fix #4957 - unable to login after password reset 2024-12-31 08:17:58 -05:00
d976cc6c19 Update SECURITY.md 2024-12-25 10:04:25 -05:00
d19d7d2a43 Merge pull request #4948 from zyhfish/task/fix-4947
Fix #4947: check the 2FA settings.
2024-12-24 08:46:43 -05:00
9bfaa02f97 Merge pull request #4953 from sbwalker/dev
add back System.Text.Json to Shared project (#4929)
2024-12-23 14:48:10 -05:00
2d9396b245 add back System.Text.Json to Shared project (#4929) 2024-12-23 14:47:17 -05:00
56e0da64ee Merge pull request #4952 from sbwalker/dev
updated default module template to use Service consistently
2024-12-23 14:10:07 -05:00
997e9213f2 updated default module template to use Service consistently 2024-12-23 14:09:54 -05:00
366569a23b Merge pull request #4951 from sbwalker/dev
update package references
2024-12-23 11:24:38 -05:00
36d5747b4f update package references 2024-12-23 11:24:24 -05:00
ea026c726c Merge pull request #4950 from sbwalker/dev
remove unnecessary reference to System.Text.Json in Shared project
2024-12-23 11:16:49 -05:00
1e71e32c74 remove unnecessary reference to System.Text.Json in Shared project 2024-12-23 11:16:34 -05:00
ed729bbd4f Merge pull request #4949 from sbwalker/dev
fix #4946 - allow administrators to access user roles via API
2024-12-23 09:27:04 -05:00
1a925221b7 fix #4946 - allow administrators to access user roles via API 2024-12-23 09:26:50 -05:00
Ben
af7b4db062 Fix #4947: check the 2FA settings. 2024-12-23 22:10:51 +08:00
cfefe35e3f Update README.md 2024-12-20 16:56:05 -05:00
8d7845a44d Merge pull request #4944 from oqtane/master
6.0.1 Release
2024-12-20 16:52:31 -05:00
3a15e6e5e9 Merge pull request #4943 from oqtane/dev
6.0.1 Release
2024-12-20 16:52:10 -05:00
3b8a51e855 Merge pull request #4942 from sbwalker/dev
fix reload script to use static array rather than a live HtmlCollection
2024-12-20 15:39:50 -05:00
f2cb34cc35 fix reload script to use static array rather than a live HtmlCollection 2024-12-20 15:39:35 -05:00
723ce62a34 Merge pull request #4940 from sbwalker/dev
write upgrade errors to log rather than console
2024-12-20 14:38:34 -05:00
2c9a2ea021 write upgrade errors to log rather than console 2024-12-20 14:38:21 -05:00
2be008d6d1 Merge pull request #4939 from sbwalker/dev
fix compilation issues in PR #4782
2024-12-20 12:36:24 -05:00
7fb51bdd0a fix compilation issues in PR #4782 2024-12-20 12:36:10 -05:00
abdbe3694f Merge pull request #4782 from thabaum/edit-add-page-scrolltotoppage-error
Fixes #4781: Adds Edit + Add Page ScrollToTopPage() On Settings Tab Form Error Messages
2024-12-20 12:26:55 -05:00
bd87e5012f Merge pull request #4938 from sbwalker/dev
fix documentation
2024-12-20 11:55:50 -05:00
55e18f2364 fix documentation 2024-12-20 11:55:35 -05:00
655e84072d Merge pull request #4937 from sbwalker/dev
use CompressionEnabled switch to disable static asset compression during publish - eliminates need to cleanup files manually in release.cmd
2024-12-20 11:46:52 -05:00
ab5409d5b6 use CompressionEnabled switch to disable static asset compression during publish - eliminates need to cleanup files manually in release.cmd 2024-12-20 11:46:30 -05:00
5a5da6486c Merge pull request #4933 from sbwalker/dev
remove *.br, *.gz files from wwwroot content folders in published release (as MapStaticAssets is not enabled)
2024-12-19 19:56:36 -05:00
7e99252429 remove *.br, *.gz files from wwwroot content folders in published release (as MapStaticAssets is not enabled) 2024-12-19 19:56:13 -05:00
10e0dcef8b Merge pull request #4931 from sbwalker/dev
ensure Pages collection is always returned in the same order by moving GetPagesHierarchy method to the repository
2024-12-19 14:45:34 -05:00
80c8433aad ensure Pages collection is always returned in the same order by moving GetPagesHierarchy method to the repository. 2024-12-19 14:45:12 -05:00
5b0ae372f8 Merge pull request #4930 from sbwalker/dev
add support for data-reload=false attribute in Interactive rendering
2024-12-19 13:04:59 -05:00
b5a1b529ab add support for data-reload=false attribute in Interactive rendering 2024-12-19 13:04:43 -05:00
af821dcd9a Merge pull request #4914 from leigh-pointer/userSettings
User Profile Update: Utilizing User Settings Object
2024-12-18 15:27:34 -05:00
10d3c81520 Merge pull request #4918 from leigh-pointer/ProjectNameTemplates
Implement Dynamic ProjectName Parameter Across Build Process
2024-12-18 15:27:24 -05:00
e3811b453a Merge pull request #4927 from sbwalker/dev
Improvements to add support for script type and data-* attributes. Also added Script and Stylesheet classes to simplify Resource declarations.
2024-12-18 15:16:16 -05:00
ca0fb05baa Improvements to add support for script type and data-* attributes. Also added Script and Stylesheet classes to simplify Resource declarations. 2024-12-18 15:15:54 -05:00
2b4b01bf6e Merge pull request #4926 from sbwalker/dev
always render page-script elements in body
2024-12-18 11:56:18 -05:00
3a1244bddc always render page-script elements in body 2024-12-18 11:56:03 -05:00
b8fd922b19 Merge pull request #4925 from sbwalker/dev
improve reload script to replicate all attributes
2024-12-18 10:32:12 -05:00
03f856025e improve reload script to replicate all attributes 2024-12-18 10:31:59 -05:00
45f04d24c3 Merge pull request #4924 from sbwalker/dev
refactor Static Blazor script processing
2024-12-18 10:09:20 -05:00
2435d610c7 refactor Static Blazor script processing 2024-12-18 10:09:02 -05:00
06572bcd14 Merge pull request #4922 from sbwalker/dev
page-script optimization
2024-12-17 14:57:17 -05:00
3fab79afc0 page-script optimization 2024-12-17 14:57:01 -05:00
2b7dd3fed5 Merge pull request #4921 from sbwalker/dev
render page-script elements at end of head content
2024-12-17 12:31:40 -05:00
d65efed032 render page-script elements at end of head content 2024-12-17 12:31:26 -05:00
c6896ea936 Merge pull request #4920 from sbwalker/dev
fix page-script to support type attribute on inline scripts
2024-12-17 09:13:42 -05:00
b7a41bddec fix page-script to support type attribute on inline scripts 2024-12-17 09:13:26 -05:00
6fd80c3737 Merge pull request #4919 from sbwalker/dev
page-script improvements
2024-12-17 08:34:55 -05:00
0aa690b3b1 page-script improvements 2024-12-17 08:34:39 -05:00
6d5bcfc6ed Implement Dynamic ProjectName Parameter Across Build Process
This pull request introduces a dynamic ProjectName parameter across our build process, enhancing flexibility and reducing hardcoding in our module and theme development workflow. The changes affect the project file, NuSpec file, command scripts.
2024-12-17 10:35:14 +01:00
b60de69fa5 Merge pull request #4917 from sbwalker/dev
improvements to page-script
2024-12-16 12:44:23 -05:00
d991b57d08 improvements to page-script 2024-12-16 12:44:07 -05:00
1133d7fcba User Profile Update: Utilizing User Settings Object
This pull request updates the User Profile implementation to utilize the existing User Settings object on the user object. While the current implementation is functional, this change offers several benefits:
Improved code consistency
Better guidance for developers accessing User Settings
Alignment with the framework's best practices
2024-12-14 11:09:54 +01:00
6fbf0383bb Merge pull request #4911 from leigh-pointer/FixGlow
This is a cosmetic fix to the Oqtane Glow image
2024-12-13 16:46:49 -05:00
0296230219 Merge pull request #4913 from sbwalker/dev
page-script enhancements
2024-12-13 16:46:23 -05:00
dedfbba27a page-script enhancements 2024-12-13 16:46:08 -05:00
dc5441da07 This is a cosmetic fix to the Oqtane Glow image
The glow on the original is cropped at the left by a few pixcels.  This update fixes that and sizes the image to a 600 x 150
2024-12-13 11:45:13 +01:00
585648b7f3 Merge pull request #4907 from leigh-pointer/ProfileMaxLength
FIX for #4906 Profile Field does not allow max length
2024-12-10 09:12:52 -05:00
cd0ee1c26d FIX for #4906 Profile Field does not allow max length
This update applies the maxlength attribute fix to all input and textarea elements. The maxlength attribute will only be added if p.MaxLength is greater than 0, allowing unlimited characters when it's 0 or less.
2024-12-10 09:25:01 +01:00
d7a7be5af4 Merge pull request #4904 from sbwalker/dev
add sync events for user login/logout
2024-12-09 10:55:55 -05:00
13e4267c11 add sync events for user login/logout 2024-12-09 10:55:40 -05:00
15bc47e3e8 Merge pull request #4898 from leigh-pointer/TemplateParam
Implement Dynamic TargetFramework in Debug Scripts #4897
2024-12-09 07:36:00 -05:00
1a4380dcd7 Variable update 2024-12-07 15:02:27 +01:00
5ace34b5cd Add the $TargetFramework macro to the Release Builds
Add the $TargetFramework macro to the Release Builds
2024-12-06 13:37:37 +01:00
f010c0f1fa Implement Dynamic TargetFramework in Debug Scripts #4897
This PR updates our debug scripts (both .cmd and .sh) to dynamically use the current TargetFramework passed from the build process. This change improves flexibility and future-proofs our build process for different .NET versions.
2024-12-05 12:43:38 +01:00
2c721ad5dd Merge pull request #4895 from tvatavuk/enh-4883-control-panel
Add ShowEditMode parameter to ControlPanel to allow hiding the Edit Mode toggle button
2024-12-04 08:25:17 -05:00
8a7c2ce2c2 Merge pull request #4894 from W6HBR/dev
Fix #4885 - Pass userid as int to GetUser for JWT authentication
2024-12-04 08:24:27 -05:00
b2a7b813de Remove redundant IServiceProvider injection 2024-12-04 09:39:02 +01:00
e85cf04b99 Fix #4883: Add ShowEditMode parameter to ControlPanel to allow hiding the Edit Mode toggle button 2024-12-04 09:37:21 +01:00
ab6fa48172 Fix #4885 - Pass userid as int to GetUser for JWT authentication 2024-12-03 10:34:44 -08:00
c81905882f Merge pull request #4881 from sbwalker/dev
improve security of UserRole API
2024-11-27 14:59:41 -05:00
f0d31c1114 improve security of UserRole API 2024-11-27 14:59:25 -05:00
497b255216 Merge pull request #4880 from sbwalker/dev
User Settings should only be accessible to individual users or administrators
2024-11-27 13:16:04 -05:00
d96286d771 User Settings should only be accessible to individual users or administrators 2024-11-27 13:15:43 -05:00
2441647d75 Merge pull request #4879 from sbwalker/dev
update EFCore.NamingConventions to .NET 9 package version
2024-11-27 13:06:53 -05:00
77b780d631 update EFCore.NamingConventions to .NET 9 package version 2024-11-27 13:06:37 -05:00
cdd03bf3d4 Merge pull request #4878 from sbwalker/dev
User Settings should only be accessible to individual users or administrators
2024-11-27 13:04:27 -05:00
e786c35f7d User Settings should only be accessible to individual users or administrators 2024-11-27 13:04:06 -05:00
e83399acb1 Merge pull request #4876 from sbwalker/dev
prevent notifications from being accessed by other users
2024-11-26 14:30:55 -05:00
ffea9e3210 prevent notifications from being accessed by other users 2024-11-26 14:30:41 -05:00
f71a3a1ce3 Merge pull request #4875 from oqtane/revert-4828-TabChange
Revert "Fix for Tabpanel is not updating the UI. #4778"
2024-11-26 13:36:18 -05:00
a5ccc23604 Revert "Fix for Tabpanel is not updating the UI. #4778" 2024-11-26 13:36:05 -05:00
1ed4c8a094 Merge pull request #4874 from oqtane/revert-4871-HTMLTabError
Revert "Rework for Tabstrip regression issue"
2024-11-26 13:35:37 -05:00
784f3771b3 Remove Blank Page Specific Error Message 2024-10-24 13:22:08 -07:00
80316824f7 Remove Blank Page Specific Error Message 2024-10-24 13:21:40 -07:00
cbaebb7b7c Remove Blank Page Specific Error Message 2024-10-24 13:21:15 -07:00
97d3764b6e Remove Blank Page Specific Error Message 2024-10-24 13:20:52 -07:00
4c937be884 Clarify Page Name Required Message 2024-10-24 12:38:51 -07:00
c25cce4398 Clarify Page Name Required Message 2024-10-24 12:38:16 -07:00
58c422285a Adds Message.Required.PageName 2024-10-24 12:28:17 -07:00
f2f22f35e8 Adds Message.Required.PageName 2024-10-24 12:26:36 -07:00
15867a7807 Adds await ScrollToPageTop(); to error messages + Page Name Error Message 2024-10-24 12:23:29 -07:00
e2c404d8bb Adds await ScrollToPageTop(); to error messages + Page Name Error Message
- Adds await ScrollToPageTop(); to error messages
- Adds Blank Page Name Error Message
2024-10-24 12:18:30 -07:00
265 changed files with 6985 additions and 2979 deletions

View File

@ -1,2 +0,0 @@
[config]
project = Oqtane.Server/Oqtane.Server.csproj

View File

@ -0,0 +1,25 @@
name: build-docker-imge
on:
- push
jobs:
build:
name: Build the docker container
runs-on: ubuntu-latest
steps:
- name: "Git clone"
run: git clone ${{ gitea.server_url }}/${{ gitea.repository }}.git .
- name: "Git checkout"
run: git checkout "${{ gitea.sha }}"
- uses: aevea/action-kaniko@master
name: Run Kaniko to build our api docker container.
with:
image: kocoded/oqtane.framework
tag: ${{ git.workflow_sha }}
tag_with_latest: github.ref == 'refs/heads/master'
registry: git.kocoder.xyz
username: ${{ secrets.CI_RUNNER_USER }}
password: ${{ secrets.CI_RUNNER_TOKEN }}
build_file: Dockerfile
target: deploy

27
Dockerfile Normal file
View File

@ -0,0 +1,27 @@
# Build
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /source
COPY --link . .
RUN dotnet restore /source/Oqtane.sln
RUN dotnet build "/source/Oqtane.sln" -c Release -o /source/build/
# Publish
FROM build AS publish
RUN dotnet publish "Oqtane.Server/Oqtane.Server.csproj" -c Release -o /source/publish/
# Deploy
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS deploy
WORKDIR /codefiles
COPY --from=publish /source/publish/ /codefiles/
COPY entrypoint.sh .
RUN chmod +x entrypoint.sh
ENTRYPOINT ["./entrypoint.sh"]

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2018-2024 .NET Foundation
Copyright (c) 2018-2025 .NET Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -52,6 +52,9 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<IVisitorService, VisitorService>();
services.AddScoped<ISyncService, SyncService>();
services.AddScoped<ILocalizationCookieService, LocalizationCookieService>();
services.AddScoped<ICookieConsentService, CookieConsentService>();
services.AddScoped<IOutputCacheService, OutputCacheService>();
services.AddScoped<ITimeZoneService, TimeZoneService>();
// providers
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>();

View File

@ -0,0 +1,119 @@
@namespace Oqtane.Installer.Controls
@implements Oqtane.Interfaces.IDatabaseConfigControl
@inject IStringLocalizer<SqlServerConfig> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="server" HelpText="Enter the database server name. This might include a port number as well if you are using a cloud service." ResourceKey="Server">Server:</Label>
<div class="col-sm-9">
<input id="server" type="text" class="form-control" @bind="@_server" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="database" HelpText="Enter the name of the database" ResourceKey="Database">Database:</Label>
<div class="col-sm-9">
<input id="database" type="text" class="form-control" @bind="@_database" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="security" HelpText="Select your security method" ResourceKey="Security">Security:</Label>
<div class="col-sm-9">
<select id="security" class="form-select custom-select" @bind="@_security">
<option value="integrated" selected>@Localizer["Integrated"]</option>
<option value="custom">@Localizer["Custom"]</option>
</select>
</div>
</div>
@if (_security == "custom")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="uid" HelpText="Enter the username to use for the database" ResourceKey="Uid">User Id:</Label>
<div class="col-sm-9">
<input id="uid" type="text" class="form-control" @bind="@_uid" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pwd" HelpText="Enter the password to use for the database" ResourceKey="Pwd">Password:</Label>
<div class="col-sm-9">
<div class="input-group">
<input id="pwd" type="@_passwordType" class="form-control" @bind="@_pwd" autocomplete="new-password" />
<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="encryption" HelpText="Specify if you are using an encrypted database connection. It is highly recommended to use encryption in a production environment." ResourceKey="Encryption">Encryption:</Label>
<div class="col-sm-9">
<select id="encryption" class="form-select custom-select" @bind="@_encryption">
<option value="true">@SharedLocalizer["True"]</option>
<option value="false">@SharedLocalizer["False"]</option>
</select>
</div>
</div>
@if (_encryption == "true")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="trustservercertificate" HelpText="Specify the type of certificate you are using for encryption. Verifiable is equivalent to False. Self Signed is equivalent to True." ResourceKey="TrustServerCertificate">Certificate:</Label>
<div class="col-sm-9">
<select id="encryption" class="form-select custom-select" @bind="@_trustservercertificate">
<option value="true">@Localizer["Self Signed"]</option>
<option value="false">@Localizer["Verifiable"]</option>
</select>
</div>
</div>
}
@code {
private string _server = "tcp:{SQL Server Name}.database.windows.net,1433";
private string _database = "{SQL Database Name}";
private string _security = "custom";
private string _uid = "{SQL Administrator Login}";
private string _pwd = String.Empty;
private string _passwordType = "password";
private string _togglePassword = string.Empty;
private string _encryption = "true";
private string _trustservercertificate = "false";
protected override void OnInitialized()
{
_togglePassword = SharedLocalizer["ShowPassword"];
}
public string GetConnectionString()
{
var connectionString = String.Empty;
if (!String.IsNullOrEmpty(_server) && !String.IsNullOrEmpty(_database))
{
connectionString = $"Data Source={_server};Initial Catalog={_database};";
}
if (_security == "integrated")
{
connectionString += "Integrated Security=SSPI;";
}
else
{
connectionString += $"User ID={_uid};Password={_pwd};";
}
connectionString += $"Encrypt={_encryption};";
connectionString += $"TrustServerCertificate={_trustservercertificate};";
return connectionString;
}
private void TogglePassword()
{
if (_passwordType == "password")
{
_passwordType = "text";
_togglePassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordType = "password";
_togglePassword = SharedLocalizer["ShowPassword"];
}
}
}

View File

@ -4,7 +4,7 @@
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="server" HelpText="Enter the database server name. This might include a port number as well if you are using a cloud service (ie. servername.database.windows.net,1433) " ResourceKey="Server">Server:</Label>
<Label Class="col-sm-3" For="server" HelpText="Enter the database server name. This might include a port number as well if you are using a cloud service." ResourceKey="Server">Server:</Label>
<div class="col-sm-9">
<input id="server" type="text" class="form-control" @bind="@_server" />
</div>

View File

@ -15,7 +15,7 @@
<div class="row">
<div class="mx-auto text-center">
<img src="oqtane-black.png" />
<div style="font-weight: bold">@SharedLocalizer["Version"] @Constants.Version (.NET 9)</div>
<div style="font-weight: bold">@SharedLocalizer["Version"] @Constants.Version (.NET @Environment.Version.Major)</div>
</div>
</div>
<hr class="app-rule" />
@ -71,14 +71,14 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="Provide a username for the primary user account" ResourceKey="Username">Username:</Label>
<div class="col-sm-9">
<input id="username" type="text" class="form-control" @bind="@_hostUsername" />
<input id="username" type="text" class="form-control" maxlength="256" @bind="@_hostUsername" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Provide a password for the primary user account" ResourceKey="Password">Password:</Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_passwordType" class="form-control" @bind="@_hostPassword" autocomplete="new-password" />
<input id="password" type="@_passwordType" class="form-control" maxlength="256" @bind="@_hostPassword" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglePassword</button>
</div>
</div>
@ -87,7 +87,7 @@
<Label Class="col-sm-3" For="confirm" HelpText="Please confirm the password entered above by entering it again" ResourceKey="Confirm">Confirm:</Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_confirmPasswordType" class="form-control" @bind="@_confirmPassword" autocomplete="new-password" />
<input id="confirm" type="@_confirmPasswordType" class="form-control" maxlength="256" @bind="@_confirmPassword" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleConfirmPassword" tabindex="-1">@_toggleConfirmPassword</button>
</div>
</div>
@ -95,7 +95,13 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="Provide the email address for the host user account" ResourceKey="Email">Email:</Label>
<div class="col-sm-9">
<input type="text" class="form-control" @bind="@_hostEmail" />
<input type="text" class="form-control" maxlength="256" @bind="@_hostEmail" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Provide the full name of the host user" ResourceKey="Name">Full Name:</Label>
<div class="col-sm-9">
<input type="text" class="form-control" maxlength="50" @bind="@_hostName" />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -153,6 +159,7 @@
private string _toggleConfirmPassword = string.Empty;
private string _confirmPassword = string.Empty;
private string _hostEmail = string.Empty;
private string _hostName = string.Empty;
private List<SiteTemplate> _templates;
private string _template = Constants.DefaultSiteTemplate;
private bool _register = true;
@ -236,7 +243,7 @@
}
}
if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && !string.IsNullOrEmpty(_hostPassword) && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@"))
if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && !string.IsNullOrEmpty(_hostPassword) && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@") && !string.IsNullOrEmpty(_hostName))
{
var result = await UserService.ValidateUserAsync(_hostUsername, _hostEmail, _hostPassword);
if (result.Succeeded)
@ -256,7 +263,7 @@
HostUsername = _hostUsername,
HostPassword = _hostPassword,
HostEmail = _hostEmail,
HostName = _hostUsername,
HostName = _hostName,
TenantName = TenantNames.Master,
IsNewTenant = true,
SiteName = Constants.DefaultSite,

View File

@ -15,22 +15,34 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="parent" HelpText="Select the parent folder" ResourceKey="Parent">Parent: </Label>
<div class="col-sm-9">
<select id="parent" class="form-select" @bind="@_parentId" required>
@if (PageState.QueryString.ContainsKey("id"))
{
@if (_parentId == -1)
{
<select id="parent" class="form-select" @bind="@_parentId" required>
<option value="-1">&lt;@Localizer["NoParent"]&gt;</option>
}
</select>
}
else
{
<select id="parent" class="form-select" @bind="@_parentId" required>
@foreach (Folder folder in _folders)
{
<option value="@(folder.FolderId)">@(new string('-', folder.Level * 2))@(folder.Name)</option>
}
</select>
</select>
}
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the folder name" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" maxlength="256" required />
@if (_isSystem)
{
<input id="name" class="form-control" @bind="@_name" readonly />
}
else
{
<input id="name" class="form-control" @bind="@_name" maxlength="256" required />
}
</div>
</div>
<div class="row mb-1 align-items-center">
@ -49,18 +61,24 @@
}
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="imagesizes" HelpText="Enter a list of image sizes which can be generated dynamically from uploaded images (ie. 200x200,400x400). Use * to indicate the folder supports all image sizes." ResourceKey="ImageSizes">Image Sizes: </Label>
<div class="col-sm-9">
<input id="imagesizes" class="form-control" @bind="@_imagesizes" maxlength="512" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="capacity" HelpText="Enter the maximum folder capacity (in megabytes). Specify zero if the capacity is unlimited." ResourceKey="Capacity">Capacity: </Label>
<div class="col-sm-9">
<input id="capacity" class="form-control" @bind="@_capacity" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cachecontrol" HelpText="Optionally provide a Cache-Control directive for this folder. For example 'public, max-age=60' indicates that files in this folder should be cached for 60 seconds. Please note that when caching is enabled, changes to files will not be immediately reflected in the UI." ResourceKey="CacheControl">Caching: </Label>
<div class="col-sm-9">
<input id="cachecontrol" class="form-control" @bind="@_cachecontrol" maxlength="50" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="imagesizes" HelpText="Optionally enter a list of image sizes which can be generated dynamically from uploaded images (ie. 200x200,400x400). Use * to indicate the folder supports all image sizes (not recommended)." ResourceKey="ImageSizes">Image Sizes: </Label>
<div class="col-sm-9">
<input id="imagesizes" class="form-control" @bind="@_imagesizes" maxlength="512" />
</div>
</div>
</div>
@if (PageState.QueryString.ContainsKey("id"))
{
@ -100,8 +118,9 @@
private int _parentId = -1;
private string _name;
private string _type = FolderTypes.Private;
private string _imagesizes = string.Empty;
private string _capacity = "0";
private string _cachecontrol = string.Empty;
private string _imagesizes = string.Empty;
private bool _isSystem;
private List<Permission> _permissions = null;
private string _createdBy;
@ -132,8 +151,9 @@
_parentId = folder.ParentId ?? -1;
_name = folder.Name;
_type = folder.Type;
_imagesizes = folder.ImageSizes;
_capacity = folder.Capacity.ToString();
_cachecontrol = folder.CacheControl;
_imagesizes = folder.ImageSizes;
_isSystem = folder.IsSystem;
_permissions = folder.PermissionList;
_createdBy = folder.CreatedBy;
@ -193,7 +213,7 @@
{
folder.ParentId = _parentId;
}
// check for duplicate folder names
if (_folders.Any(item => item.ParentId == folder.ParentId && item.Name == _name && item.FolderId != _folderId))
{
@ -204,8 +224,9 @@
folder.SiteId = PageState.Site.SiteId;
folder.Name = _name;
folder.Type = _type;
folder.ImageSizes = _imagesizes;
folder.Capacity = int.Parse(_capacity);
folder.CacheControl = _cachecontrol;
folder.ImageSizes = _imagesizes;
folder.IsSystem = _isSystem;
folder.PermissionList = _permissionGrid.GetPermissionList();
@ -220,7 +241,6 @@
if (folder != null)
{
await FolderService.UpdateFolderOrderAsync(folder.SiteId, folder.FolderId, folder.ParentId);
await logger.LogInformation("Folder Saved {Folder}", folder);
NavigationManager.NavigateTo(NavigateUrl());
}
@ -256,17 +276,9 @@
}
if (!isparent)
{
var files = await FileService.GetFilesAsync(_folderId);
if (files.Count == 0)
{
await FolderService.DeleteFolderAsync(_folderId);
await logger.LogInformation("Folder Deleted {Folder}", _folderId);
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
AddModuleMessage(Localizer["Message.Folder.Files.InvalidDelete"], MessageType.Warning);
}
await FolderService.DeleteFolderAsync(_folderId);
await logger.LogInformation("Folder Deleted {Folder}", _folderId);
NavigationManager.NavigateTo(NavigateUrl());
}
else
{

View File

@ -3,54 +3,92 @@
@inject NavigationManager NavigationManager
@inject IFolderService FolderService
@inject IFileService FileService
@inject ISettingService SettingService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_files != null)
@if (_files == null)
{
<div class="row">
<div class="col-md mb-1">
<ActionLink Action="Edit" Text="Add Folder" Class="btn btn-secondary" ResourceKey="AddFolder" />
</div>
<div class="col-md-8 mb-1">
<div class="input-group">
<span class="input-group-text">@Localizer["Folder"]:</span>
<select class="form-select" @onchange="(e => FolderChanged(e))">
@foreach (Folder folder in _folders)
{
<option value="@(folder.FolderId)">@(new string('-', folder.Level * 2))@(folder.Name)</option>
}
</select>
<ActionLink Action="Edit" Text="Edit Folder" Class="btn btn-secondary" Parameters="@($"id=" + _folderId.ToString())" ResourceKey="EditFolder" />&nbsp;
<p>
<em>@SharedLocalizer["Loading"]</em>
</p>
}
else
{
<TabStrip>
<TabPanel Name="Files" Heading="Files" ResourceKey="Files">
<div class="row">
<div class="col-md mb-1">
<ActionLink Action="Edit" Text="Add Folder" Class="btn btn-secondary" ResourceKey="AddFolder" />
</div>
<div class="col-md-8 mb-1">
<div class="input-group">
<span class="input-group-text">@Localizer["Folder"]:</span>
<select class="form-select" @onchange="(e => FolderChanged(e))">
@foreach (Folder folder in _folders)
{
<option value="@(folder.FolderId)">@(new string('-', folder.Level * 2))@(folder.Name)</option>
}
</select>
<ActionLink Action="Edit" Text="Edit Folder" Class="btn btn-secondary" Parameters="@($"id=" + _folderId.ToString())" ResourceKey="EditFolder" />&nbsp;
</div>
</div>
<div class="col-md mb-1 text-end">
<ActionLink Action="Add" Text="Upload Files" Parameters="@($"id=" + _folderId.ToString())" ResourceKey="UploadFiles" />
</div>
</div>
</div>
<div class="col-md mb-1 text-end">
<ActionLink Action="Add" Text="Upload Files" Parameters="@($"id=" + _folderId.ToString())" ResourceKey="UploadFiles" />
</div>
</div>
<Pager Items="@_files" SearchProperties="Name">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th>
<th>@Localizer["Modified"]</th>
<th>@Localizer["Type"]</th>
<th>@Localizer["Size"]</th>
</Header>
<Row>
<td><ActionLink Action="Details" Text="Edit" Parameters="@($"id=" + context.FileId.ToString())" ResourceKey="Details" /></td>
<td><ActionDialog Header="Delete File" Message="@string.Format(Localizer["Confirm.File.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteFile(context))" ResourceKey="DeleteFile" /></td>
<td><a href="@context.Url" target="_new">@context.Name</a></td>
<td>@context.ModifiedOn</td>
<td>@context.Extension.ToUpper() @SharedLocalizer["File"]</td>
<td>@string.Format("{0:0.00}", ((decimal)context.Size / 1000)) KB</td>
</Row>
</Pager>
@if (_files.Count == 0)
{
<div class="text-center">@Localizer["NoFiles"]</div>
}
@if (_files.Count != 0)
{
<Pager Items="@_files" SearchProperties="Name">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th>
<th>@Localizer["Modified"]</th>
<th>@Localizer["Type"]</th>
<th>@Localizer["Size"]</th>
</Header>
<Row>
<td><ActionLink Action="Details" Text="Edit" Parameters="@($"id=" + context.FileId.ToString())" ResourceKey="Details" /></td>
<td><ActionDialog Header="Delete File" Message="@string.Format(Localizer["Confirm.File.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteFile(context))" ResourceKey="DeleteFile" /></td>
<td><a href="@context.Url" target="_new">@context.Name</a></td>
<td>@UtcToLocal(context.ModifiedOn)</td>
<td>@context.Extension.ToUpper() @SharedLocalizer["File"]</td>
<td>@string.Format("{0:0.00}", ((decimal)context.Size / 1000)) KB</td>
</Row>
</Pager>
}
else
{
<div class="text-center">@Localizer["NoFiles"]</div>
}
</TabPanel>
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings" Security="SecurityAccessLevel.Admin">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="imageExt" HelpText="Enter a comma separated list of image file extensions" ResourceKey="ImageExtensions">Image Extensions: </Label>
<div class="col-sm-9">
<input id="imageExt" spellcheck="false" class="form-control" @bind="@_imageFiles" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="uploadableFileExt" HelpText="Enter a comma separated list of uploadable file extensions" ResourceKey="UploadableFileExtensions">Uploadable File Extensions: </Label>
<div class="col-sm-9">
<input id="uploadableFileExt" spellcheck="false" class="form-control" @bind="@_uploadableFiles" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="maxChunkSize" HelpText="Files are split into chunks to streamline the upload process. Specify the maximum chunk size in MB (note that higher chunk sizes should only be used on faster networks)." ResourceKey="MaxChunkSize">Max Upload Chunk Size (MB): </Label>
<div class="col-sm-9">
<input id="maxChunkSize" type="number" min="1" max="10" step="1" class="form-control" @bind="@_maxChunkSize" />
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
</TabPanel>
</TabStrip>
}
@code {
@ -58,6 +96,10 @@
private int _folderId = -1;
private List<File> _files;
private string _imageFiles = string.Empty;
private string _uploadableFiles = string.Empty;
private int _maxChunkSize = 1;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
protected override async Task OnParametersSetAsync()
@ -71,6 +113,13 @@
_folderId = _folders[0].FolderId;
await GetFiles();
}
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_imageFiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles);
_imageFiles = (string.IsNullOrEmpty(_imageFiles)) ? Constants.ImageFiles : _imageFiles;
_uploadableFiles = SettingService.GetSetting(settings, "UploadableFiles", Constants.UploadableFiles);
_uploadableFiles = (string.IsNullOrEmpty(_uploadableFiles)) ? Constants.UploadableFiles : _uploadableFiles;
_maxChunkSize = int.Parse(SettingService.GetSetting(settings, "MaxChunkSize", "1"));
}
catch (Exception ex)
{
@ -115,4 +164,23 @@
AddModuleMessage(string.Format(Localizer["Error.File.Delete"], file.Name), MessageType.Error);
}
}
private async Task SaveSiteSettings()
{
try
{
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
settings = SettingService.SetSetting(settings, "ImageFiles", (_imageFiles != Constants.ImageFiles) ? _imageFiles.Replace(" ", "") : "", false);
settings = SettingService.SetSetting(settings, "UploadableFiles", (_uploadableFiles != Constants.UploadableFiles) ? _uploadableFiles.Replace(" ", "") : "", false);
settings = SettingService.SetSetting(settings, "MaxChunkSize", _maxChunkSize.ToString(), false);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Site Settings {Error}", ex.Message);
AddModuleMessage(Localizer["Error.SaveSiteSettings"], MessageType.Error);
}
}
}

View File

@ -45,7 +45,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of log entries to retain for this job" ResourceKey="RetentionLog">Retention Log (Items): </Label>
<div class="col-sm-9">
<input id="retention" type="number" min="0" step ="1" class="form-control" @bind="@_retentionHistory" maxlength="4" required />
<input id="retention" type="number" min="0" step ="1" class="form-control" @bind="@_retentionHistory" maxlength="3" required />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -132,13 +132,13 @@
_isEnabled = job.IsEnabled.ToString();
_interval = job.Interval.ToString();
_frequency = job.Frequency;
_startDate = Utilities.UtcAsLocalDate(job.StartDate);
_startTime = Utilities.UtcAsLocalDateTime(job.StartDate);
_endDate = Utilities.UtcAsLocalDate(job.EndDate);
_endTime = Utilities.UtcAsLocalDateTime(job.EndDate);
_startDate = UtcToLocal(job.StartDate);
_startTime = UtcToLocal(job.StartDate);
_endDate = UtcToLocal(job.EndDate);
_endTime = UtcToLocal(job.EndDate);
_retentionHistory = job.RetentionHistory.ToString();
_nextDate = Utilities.UtcAsLocalDate(job.NextExecution);
_nextTime = Utilities.UtcAsLocalDateTime(job.NextExecution);
_nextDate = UtcToLocal(job.NextExecution);
_nextTime = UtcToLocal(job.NextExecution);
createdby = job.CreatedBy;
createdon = job.CreatedOn;
modifiedby = job.ModifiedBy;
@ -176,10 +176,10 @@
{
job.Interval = int.Parse(_interval);
}
job.StartDate = Utilities.LocalDateAndTimeAsUtc(_startDate, _startTime);
job.EndDate = Utilities.LocalDateAndTimeAsUtc(_endDate, _endTime);
job.StartDate = LocalToUtc(_startDate.Value.Date.Add(_startTime.Value.TimeOfDay));
job.EndDate = LocalToUtc(_endDate.Value.Date.Add(_endTime.Value.TimeOfDay));
job.RetentionHistory = int.Parse(_retentionHistory);
job.NextExecution = Utilities.LocalDateAndTimeAsUtc(_nextDate, _nextTime);
job.NextExecution = LocalToUtc(_nextDate.Value.Date.Add(_nextTime.Value.TimeOfDay));
try
{

View File

@ -29,7 +29,7 @@ else
<td>@context.Name</td>
<td>@DisplayStatus(context.IsEnabled, context.IsExecuting)</td>
<td>@DisplayFrequency(context.Interval, context.Frequency)</td>
<td>@context.NextExecution?.ToLocalTime()</td>
<td>@UtcToLocal(context.NextExecution)</td>
<td>
@if (context.IsStarted)
{
@ -55,10 +55,6 @@ else
protected override async Task OnInitializedAsync()
{
await GetJobs();
if (_jobs.Count == 0)
{
AddModuleMessage(string.Format(Localizer["Message.NoJobs"], NavigateUrl("admin/system")), MessageType.Warning);
}
}
private async Task GetJobs()

View File

@ -23,8 +23,8 @@ else
<Row>
<td>@context.Job.Name</td>
<td>@DisplayStatus(context.Job.IsExecuting, context.Succeeded)</td>
<td>@context.StartDate</td>
<td>@context.FinishDate</td>
<td>@UtcToLocal(context.StartDate)</td>
<td>@UtcToLocal(context.FinishDate)</td>
</Row>
<Detail>
<td colspan="4">@((MarkupString)context.Notes)</td>
@ -44,14 +44,12 @@ else
private async Task GetJobLogs()
{
_jobLogs = await JobLogService.GetJobLogsAsync();
var jobId = -1;
if (PageState.QueryString.ContainsKey("id"))
{
_jobLogs = _jobLogs.Where(item => item.JobId == Int32.Parse(PageState.QueryString["id"])).ToList();
jobId = int.Parse(PageState.QueryString["id"]);
}
_jobLogs = _jobLogs.OrderByDescending(item => item.JobLogId).ToList();
_jobLogs = await JobLogService.GetJobLogsAsync(jobId);
}
private string DisplayStatus(bool isExecuting, bool? succeeded)

View File

@ -29,12 +29,12 @@ else
{
<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 />
<input id="username" type="text" @ref="username" class="form-control" placeholder="@Localizer["Username.Placeholder"]" @bind="@_username" @bind:event="oninput" 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 />
<input id="password" type="@_passwordtype" name="Password" class="form-control" placeholder="@Localizer["Password.Placeholder"]" @bind="@_password" @bind:event="oninput" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
@ -144,7 +144,7 @@ else
user = await UserService.VerifyEmailAsync(user, PageState.QueryString["token"]);
if (user != null)
{
await logger.LogInformation(LogFunction.Security, "Email Verified For For Username {Username}", _username);
await logger.LogInformation(LogFunction.Security, "Email Verified For Username {Username}", _username);
AddModuleMessage(Localizer["Success.Account.Verified"], MessageType.Info);
}
else

View File

@ -141,7 +141,7 @@
var log = await LogService.GetLogAsync(_logId);
if (log != null)
{
_logDate = log.LogDate.ToString(CultureInfo.CurrentCulture);
_logDate = UtcToLocal(log.LogDate).Value.ToString(CultureInfo.CurrentCulture);
_level = log.Level;
_feature = log.Feature;
_function = log.Function;

View File

@ -64,7 +64,7 @@ else
</Header>
<Row>
<td class="@GetClass(context.Function)"><ActionLink Action="Detail" Text="Details" Parameters="@($"/{context.LogId}")" ReturnUrl="@(NavigateUrl(PageState.Page.Path, AddUrlParameters(_level, _function, _rows, _page)))" ResourceKey="LogDetails" /></td>
<td class="@GetClass(context.Function)">@context.LogDate</td>
<td class="@GetClass(context.Function)">@UtcToLocal(context.LogDate)</td>
<td class="@GetClass(context.Function)">@context.Level</td>
<td class="@GetClass(context.Function)">@context.Feature</td>
<td class="@GetClass(context.Function)">@context.Function</td>

View File

@ -111,6 +111,8 @@
private async Task CreateModule()
{
validated = true;
_owner = _owner.Trim();
_module = _module.Trim();
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{

View File

@ -63,24 +63,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packagename" HelpText="The unique name of the package from which this module was installed. This value must be specified within the module's IModule interface specification." ResourceKey="PackageName">Package Name: </Label>
<div class="col-sm-9">
@if (!string.IsNullOrEmpty(_packagename))
{
<div class="input-group">
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
@if (string.IsNullOrEmpty(_packageurl))
{
<button type="button" class="btn btn-secondary" @onclick="ValidatePackage">@Localizer["Validate"]</button>
}
else
{
<a href="@_packageurl" target="_blank" class="btn btn-primary">@SharedLocalizer["Download"]</a>
}
</div>
}
else
{
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
}
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -90,7 +73,7 @@
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="The reference url of the module" ResourceKey="ReferenceUrl">Reference Url: </Label>
<Label Class="col-sm-3" For="url" HelpText="The url of the module" ResourceKey="Url">Url: </Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" disabled />
</div>
@ -244,7 +227,6 @@
private string _moduledefinitionname = "";
private string _version;
private string _packagename = "";
private string _packageurl = "";
private string _owner = "";
private string _url = "";
private string _contact = "";
@ -445,27 +427,5 @@
}
}
private async Task ValidatePackage()
{
try
{
var package = await PackageService.GetPackageAsync(_packagename, _version, true);
if (package == null || string.IsNullOrEmpty(package.PackageUrl))
{
AddModuleMessage(Localizer["Message.Validate"], MessageType.Warning);
}
else
{
_packageurl = package.PackageUrl;
AddModuleMessage(Localizer["Message.Download"], MessageType.Info);
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Package {PackageId} {Version}", _packagename, _version);
AddModuleMessage(Localizer["Error.Validate"], MessageType.Error);
}
}
private string Browse(Page page) => string.IsNullOrEmpty(page.Url) ? NavigateUrl(page.Path) : page.Url;
}

View File

@ -13,32 +13,32 @@
}
else
{
<div class="container">
<div class="row mb-3 align-items-center">
<div class="col-sm-6">
<ActionLink Action="Add" Text="Install Module" ResourceKey="InstallModule" />
@((MarkupString)"&nbsp;")
<ActionLink Action="Create" Text="Create Module" ResourceKey="CreateModule" Class="btn btn-secondary" />
</div>
<div class="col-sm-6">
<select class="form-select" @onchange="(e => CategoryChanged(e))">
@foreach (var category in _categories)
{
if (category == _category)
{
<option value="@category" selected>@category @Localizer["Modules"]</option>
}
else
{
<option value="@category">@category @Localizer["Modules"]</option>
}
}
</select>
</div>
</div>
</div>
<Pager Items="@_moduleDefinitions">
<div class="container">
<div class="row mb-3 align-items-center">
<div class="col-sm-6">
<ActionLink Action="Add" Text="Install Module" ResourceKey="InstallModule" />
<ActionLink Action="Create" Text="Create Module" ResourceKey="CreateModule" Class="btn btn-secondary ps-2" />
<button type="button" class="btn btn-secondary pw-2" @onclick="@Synchronize">@Localizer["Synchronize"]</button>
</div>
<div class="col-sm-6">
<select class="form-select" @onchange="(e => CategoryChanged(e))">
@foreach (var category in _categories)
{
if (category == _category)
{
<option value="@category" selected>@category @Localizer["Modules"]</option>
}
else
{
<option value="@category">@category @Localizer["Modules"]</option>
}
}
</select>
</div>
</div>
</div>
<Pager Items="@_moduleDefinitions">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
@ -61,17 +61,17 @@ else
<td>@context.Name</td>
<td>@context.Version</td>
<td>
@if (context.IsEnabled)
{
<span>@SharedLocalizer["Yes"]</span>
}
else
{
<span>@SharedLocalizer["No"]</span>
}
@if (context.IsEnabled)
{
<span>@SharedLocalizer["Yes"]</span>
}
else
{
<span>@SharedLocalizer["No"]</span>
}
</td>
<td>
@if (context.AssemblyName == Constants.ClientId || _modules.Where(m => m.ModuleDefinition?.ModuleDefinitionId == context.ModuleDefinitionId).FirstOrDefault() != null)
@if (context.AssemblyName == Constants.ClientId || _modules.Where(m => m.ModuleDefinition?.ModuleDefinitionId == context.ModuleDefinitionId).FirstOrDefault() != null)
{
<span>@SharedLocalizer["Yes"]</span>
}
@ -87,9 +87,9 @@ else
@((MarkupString)PurchaseLink(context.PackageName))
</td>
<td>
@{
var version = UpgradeAvailable(context.PackageName, context.Version);
}
@{
var version = UpgradeAvailable(context.PackageName, context.Version);
}
@if (version != context.Version)
{
<button type="button" class="btn btn-success" @onclick=@(async () => await DownloadModule(context.PackageName, version))>@SharedLocalizer["Upgrade"]</button>
@ -153,10 +153,10 @@ else
link = "<a class=\"btn btn-primary\" style=\"text-decoration: none !important\" href=\"" + package.PaymentUrl + "\" target=\"_new\">" + package.ExpiryDate.Value.Date.ToString("MMM dd, yyyy") + "</a>";
}
}
}
}
return link;
}
}
}
return link;
}
private string SupportLink(string packagename, string version)
{
@ -172,52 +172,75 @@ else
return link;
}
private string UpgradeAvailable(string packagename, string version)
{
if (!string.IsNullOrEmpty(packagename) && _packages != null)
{
var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault();
if (package != null && Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0)
{
return package.Version;
}
}
return version;
}
private string UpgradeAvailable(string packagename, string version)
{
if (!string.IsNullOrEmpty(packagename) && _packages != null)
{
var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault();
if (package != null && Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0)
{
return package.Version;
}
}
return version;
}
private async Task DownloadModule(string packagename, string version)
{
try
{
await PackageService.DownloadPackageAsync(packagename, version);
await logger.LogInformation("Module Downloaded {ModuleDefinitionName} {Version}", packagename, version);
AddModuleMessage(string.Format(Localizer["Success.Module.Install"], NavigateUrl("admin/system")), MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Module {ModuleDefinitionName} {Version} {Error}", packagename, version, ex.Message);
AddModuleMessage(Localizer["Error.Module.Download"], MessageType.Error);
}
}
private async Task DownloadModule(string packagename, string version)
{
try
{
await PackageService.DownloadPackageAsync(packagename, version);
await logger.LogInformation("Module Downloaded {ModuleDefinitionName} {Version}", packagename, version);
AddModuleMessage(string.Format(Localizer["Success.Module.Install"], NavigateUrl("admin/system")), MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Module {ModuleDefinitionName} {Version} {Error}", packagename, version, ex.Message);
AddModuleMessage(Localizer["Error.Module.Download"], MessageType.Error);
}
}
private async Task DeleteModule(ModuleDefinition moduleDefinition)
{
try
{
await ModuleDefinitionService.DeleteModuleDefinitionAsync(moduleDefinition.ModuleDefinitionId, moduleDefinition.SiteId);
AddModuleMessage(Localizer["Success.Module.Delete"], MessageType.Success);
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true));
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Module {ModuleDefinition} {Error}", moduleDefinition, ex.Message);
AddModuleMessage(Localizer["Error.Module.Delete"], MessageType.Error);
}
}
private async Task DeleteModule(ModuleDefinition moduleDefinition)
{
try
{
await ModuleDefinitionService.DeleteModuleDefinitionAsync(moduleDefinition.ModuleDefinitionId, moduleDefinition.SiteId);
AddModuleMessage(Localizer["Success.Module.Delete"], MessageType.Success);
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true));
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Module {ModuleDefinition} {Error}", moduleDefinition, ex.Message);
AddModuleMessage(Localizer["Error.Module.Delete"], MessageType.Error);
}
}
private async Task CategoryChanged(ChangeEventArgs e)
{
_category = (string)e.Value;
private async Task CategoryChanged(ChangeEventArgs e)
{
_category = (string)e.Value;
await LoadModuleDefinitions();
}
}
private async Task Synchronize()
{
try
{
ShowProgressIndicator();
foreach (var moduleDefinition in _moduleDefinitions)
{
if (!string.IsNullOrEmpty(moduleDefinition.PackageName) && !_packages.Any(item => item.PackageId == moduleDefinition.PackageName))
{
var package = await PackageService.GetPackageAsync(moduleDefinition.PackageName, moduleDefinition.Version, false);
}
}
HideProgressIndicator();
AddModuleMessage(Localizer["Success.Module.Synchronize"], MessageType.Success);
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true));
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Synchronizing Modules {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Module.Synchronize"], MessageType.Error);
}
}
}

View File

@ -5,24 +5,57 @@
@inject IStringLocalizer<Export> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="content" HelpText="The Exported Module Content" ResourceKey="Content">Content: </Label>
<div class="col-sm-9">
<textarea id="content" class="form-control" @bind="@_content" rows="5" readonly></textarea>
<TabStrip>
<TabPanel Name="Content" Heading="Content" ResourceKey="Content">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="content" HelpText="Select the Export option and you will be able to view the module content" ResourceKey="Content">Content: </Label>
<div class="col-sm-9">
<textarea id="content" class="form-control" @bind="@_content" rows="5" readonly></textarea>
</div>
</div>
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="ExportText">@Localizer["Export"]</button>
<NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink>
</TabPanel>
<TabPanel Name="File" Heading="File" ResourceKey="File">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="folder" HelpText="Select a folder where you wish to save the exported content" ResourceKey="Folder">Folder: </Label>
<div class="col-sm-9">
<FileManager ShowFiles="false" ShowUpload="false" @ref="_filemanager" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="filename" HelpText="Specify a name for the file (without an extension)" ResourceKey="Filename">Filename: </Label>
<div class="col-sm-9">
<input id="content" type="text" class="form-control" @bind="@_filename" />
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="ExportFile">@Localizer["Export"]</button>
<NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink>
</TabPanel>
</TabStrip>
<button type="button" class="btn btn-success" @onclick="ExportModule">@Localizer["Export"]</button>
<NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink>
@code {
private string _content = string.Empty;
private FileManager _filemanager;
private string _filename = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Title => "Export Content";
private async Task ExportModule()
protected override void OnInitialized()
{
_filename = Utilities.GetFriendlyUrl(ModuleState.Title);
}
private async Task ExportText()
{
try
{
@ -35,4 +68,34 @@
AddModuleMessage(Localizer["Error.Module.Export"], MessageType.Error);
}
}
private async Task ExportFile()
{
try
{
var folderid = _filemanager.GetFolderId();
if (folderid != -1 && !string.IsNullOrEmpty(_filename))
{
var fileid = await ModuleService.ExportModuleAsync(ModuleState.ModuleId, PageState.Page.PageId, folderid, _filename);
if (fileid != -1)
{
AddModuleMessage(Localizer["Success.Content.Export"], MessageType.Success);
}
else
{
AddModuleMessage(Localizer["Error.Module.Export"], MessageType.Error);
}
}
else
{
AddModuleMessage(Localizer["Message.Content.Export"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Exporting Module {ModuleId} {Error}", ModuleState.ModuleId, ex.Message);
AddModuleMessage(Localizer["Error.Module.Export"], MessageType.Error);
}
}
}

View File

@ -2,20 +2,27 @@
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IModuleService ModuleService
@inject IFileService FileService
@inject IStringLocalizer<Import> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<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="content" HelpText="Enter The Module Content To Import" ResourceKey="Content">Content: </Label>
<Label Class="col-sm-3" For="file" HelpText="Optionally upload or select a file to import for this module" ResourceKey="File">File: </Label>
<div class="col-sm-9">
<FileManager Filter="json" OnSelectFile="OnSelectFile" />
</div>
</div>
<hr />
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="content" HelpText="Provide the module content to import" ResourceKey="Content">Content: </Label>
<div class="col-sm-9">
<textarea id="content" class="form-control" @bind="@_content" rows="5" required></textarea>
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="ImportModule">@Localizer["Import"]</button>
<NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink>
</form>
@ -28,6 +35,12 @@
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Title => "Import Content";
private async Task OnSelectFile(int fileId)
{
var bytes = await FileService.DownloadFileAsync(fileId);
_content = System.Text.Encoding.UTF8.GetString(bytes, 0, bytes.Length);
}
private async Task ImportModule()
{
validated = true;

View File

@ -97,6 +97,23 @@
</div>
</div>
</div>
<br />
<Section Name="ModuleContent" Heading="Content" ResourceKey="ModuleContent">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="moduleheader" HelpText="Optionally provide content to be injected above the module instance" ResourceKey="Header">Header: </Label>
<div class="col-sm-9">
<textarea id="moduleheader" class="form-control" @bind="@_header" rows="3" maxlength="4000"></textarea>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="modulefooter" HelpText="Optionally provide content to be injected below the module instance" ResourceKey="Footer">Footer: </Label>
<div class="col-sm-9">
<textarea id="modulefooter" class="form-control" @bind="@_footer" rows="3" maxlength="4000"></textarea>
</div>
</div>
</div>
</Section>
}
</TabPanel>
<TabPanel Name="Permissions" Heading="Permissions" ResourceKey="Permissions">
@ -144,6 +161,8 @@
private string _pane;
private string _containerType;
private string _allPages = "false";
private string _header = "";
private string _footer = "";
private string _permissionNames = "";
private List<Permission> _permissions = null;
private string _pageId;
@ -167,37 +186,47 @@
protected override async Task OnInitializedAsync()
{
SetModuleTitle(Localizer["ModuleSettings.Title"]);
_title = ModuleState.Title;
_moduleSettingsTitle = Localizer["ModuleSettings.Heading"];
_pane = ModuleState.Pane;
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, PageState.Page.ThemeType);
_containerType = ModuleState.ContainerType;
_allPages = ModuleState.AllPages.ToString();
_permissions = ModuleState.PermissionList;
_pageId = ModuleState.PageId.ToString();
createdby = ModuleState.CreatedBy;
createdon = ModuleState.CreatedOn;
modifiedby = ModuleState.ModifiedBy;
modifiedon = ModuleState.ModifiedOn;
_effectivedate = Utilities.UtcAsLocalDate(ModuleState.EffectiveDate);
_expirydate = Utilities.UtcAsLocalDate(ModuleState.ExpiryDate);
_pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
if (ModuleState.ModuleDefinition != null)
{
_module = ModuleState.ModuleDefinition.Name;
_permissionNames = ModuleState.ModuleDefinition?.PermissionNames;
var pagemodule = await PageModuleService.GetPageModuleAsync(ModuleState.PageModuleId);
if (!string.IsNullOrEmpty(ModuleState.ModuleDefinition.SettingsType))
_pageId = pagemodule.PageId.ToString();
_title = pagemodule.Title;
_pane = pagemodule.Pane;
_containerType = pagemodule.ContainerType;
if (string.IsNullOrEmpty(_containerType))
{
_containerType = (!string.IsNullOrEmpty(PageState.Page.DefaultContainerType)) ? PageState.Page.DefaultContainerType : PageState.Site.DefaultContainerType;
}
_header = pagemodule.Header;
_footer = pagemodule.Footer;
_effectivedate = Utilities.UtcAsLocalDate(pagemodule.EffectiveDate);
_expirydate = Utilities.UtcAsLocalDate(pagemodule.ExpiryDate);
_allPages = pagemodule.Module.AllPages.ToString();
createdby = pagemodule.Module.CreatedBy;
createdon = pagemodule.Module.CreatedOn;
modifiedby = pagemodule.Module.ModifiedBy;
modifiedon = pagemodule.Module.ModifiedOn;
_permissions = pagemodule.Module.PermissionList;
if (pagemodule.Module.ModuleDefinition != null)
{
_module = pagemodule.Module.ModuleDefinition.Name;
_permissionNames = pagemodule.Module.ModuleDefinition?.PermissionNames;
if (!string.IsNullOrEmpty(pagemodule.Module.ModuleDefinition.SettingsType))
{
// module settings type explicitly declared in IModule interface
_moduleSettingsType = Type.GetType(ModuleState.ModuleDefinition.SettingsType);
_moduleSettingsType = Type.GetType(pagemodule.Module.ModuleDefinition.SettingsType);
}
else
{
// legacy support - module settings type determined by convention ( ie. existence of a "Settings.razor" component in module )
_moduleSettingsType = Type.GetType(ModuleState.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, PageState.Action), false, true);
_moduleSettingsType = Type.GetType(pagemodule.Module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, PageState.Action), false, true);
}
if (_moduleSettingsType != null)
{
@ -218,7 +247,7 @@
}
else
{
AddModuleMessage(string.Format(Localizer["Error.Module.Load"], ModuleState.ModuleDefinitionName), MessageType.Error);
AddModuleMessage(string.Format(Localizer["Error.Module.Load"], pagemodule.Module.ModuleDefinitionName), MessageType.Error);
}
var theme = PageState.Site.Themes.FirstOrDefault(item => item.Containers.Any(themecontrol => themecontrol.TypeName.Equals(_containerType)));
@ -270,10 +299,12 @@
{
pagemodule.ContainerType = string.Empty;
}
pagemodule.Header = _header;
pagemodule.Footer = _footer;
await PageModuleService.UpdatePageModuleAsync(pagemodule);
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane);
var module = ModuleState;
var module = await ModuleService.GetModuleAsync(ModuleState.ModuleId);
module.AllPages = bool.Parse(_allPages);
module.PageModuleId = ModuleState.PageModuleId;
module.PermissionList = _permissionGrid.GetPermissionList();

View File

@ -10,13 +10,13 @@
@if (_initialized)
{
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<TabStrip Refresh="@_refresh" ActiveTab="@_activetab">
<TabStrip Refresh="@_refresh">
<TabPanel Name="Settings" ResourceKey="Settings" Heading="Settings">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the page name" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" required />
<input id="name" class="form-control" @bind="@_name" maxlength="50" required />
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
@ -101,13 +101,13 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="path" HelpText="Optionally enter a url path for this page (ie. home ). If you do not provide a url path, the page name will be used. If the page is intended to be the root path specify '/'." ResourceKey="UrlPath">Url Path: </Label>
<div class="col-sm-9">
<input id="path" class="form-control" @bind="@_path" />
<input id="path" class="form-control" @bind="@_path" maxlength="256" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="Optionally enter a url which this page should redirect to when a user navigates to it" ResourceKey="Redirect">Redirect: </Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" />
<input id="url" class="form-control" @bind="@_url" maxlength="500" />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -147,7 +147,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="title" HelpText="Optionally enter the page title. If you do not provide a page title, the page name will be used." ResourceKey="Title">Title: </Label>
<div class="col-sm-9">
<input id="title" class="form-control" @bind="@_title" />
<input id="title" class="form-control" @bind="@_title" maxlength="200" />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -186,13 +186,13 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="headcontent" HelpText="Optionally enter content to be included in the page head (ie. meta, link, or script tags)" ResourceKey="HeadContent">Head Content: </Label>
<div class="col-sm-9">
<textarea id="headcontent" class="form-control" @bind="@_headcontent" rows="3"></textarea>
<textarea id="headcontent" class="form-control" @bind="@_headcontent" rows="3" maxlength="4000"></textarea>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="bodycontent" HelpText="Optionally enter content to be included in the page body (ie. script tags)" ResourceKey="BodyContent">Body Content: </Label>
<div class="col-sm-9">
<textarea id="bodycontent" class="form-control" @bind="@_bodycontent" rows="3"></textarea>
<textarea id="bodycontent" class="form-control" @bind="@_bodycontent" rows="3" maxlength="4000"></textarea>
</div>
</div>
</div>
@ -246,7 +246,6 @@
private string _iconresources = "";
private DateTime? _effectivedate = null;
private DateTime? _expirydate = null;
private string _activetab = "";
protected override async Task OnInitializedAsync()
{
@ -317,10 +316,11 @@
{
await logger.LogError(ex, "Error Loading Child Pages For Parent {PageId} {Error}", _parentid, ex.Message);
AddModuleMessage(Localizer["Error.ChildPage.Load"], MessageType.Error);
await ScrollToPageTop();
}
}
private void ThemeChanged(ChangeEventArgs e)
private async Task ThemeChanged(ChangeEventArgs e)
{
_themetype = (string)e.Value;
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype);
@ -331,12 +331,12 @@
if (ThemeService.GetTheme(PageState.Site.Themes, _themetype)?.ThemeName != ThemeService.GetTheme(PageState.Site.Themes, PageState.Site.DefaultThemeType)?.ThemeName)
{
AddModuleMessage(Localizer["ThemeChanged.Message"], MessageType.Warning);
await ScrollToPageTop();
}
}
private async Task SavePage()
{
_activetab = "Settings";
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
@ -347,6 +347,7 @@
if (!Utilities.ValidateEffectiveExpiryDates(_effectivedate, _expirydate))
{
AddModuleMessage(SharedLocalizer["Message.EffectiveExpiryDateError"], MessageType.Warning);
await ScrollToPageTop();
return;
}
if (!string.IsNullOrEmpty(_themetype) && !string.IsNullOrEmpty(_containertype))
@ -397,12 +398,14 @@
if (_pages.Any(item => item.Path == page.Path))
{
AddModuleMessage(string.Format(Localizer["Message.Page.Exists"], _path), MessageType.Warning);
await ScrollToPageTop();
return;
}
if (page.ParentId == null && Constants.ReservedRoutes.Contains(page.Name.ToLower()))
{
AddModuleMessage(string.Format(Localizer["Message.Page.Reserved"], page.Name), MessageType.Warning);
await ScrollToPageTop();
return;
}
@ -470,6 +473,7 @@
else
{
AddModuleMessage(Localizer["Message.Required.PageInfo"], MessageType.Warning);
await ScrollToPageTop();
}
}
@ -477,11 +481,13 @@
{
await logger.LogError(ex, "Error Saving Page {Page} {Error}", page, ex.Message);
AddModuleMessage(Localizer["Error.Page.Save"], MessageType.Error);
await ScrollToPageTop();
}
}
else
{
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
await ScrollToPageTop();
}
}

View File

@ -16,7 +16,7 @@
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
@if (_page.UserId == null)
{
<TabStrip Refresh="@_refresh" ActiveTab="@_activetab">
<TabStrip Refresh="@_refresh">
<TabPanel Name="Settings" ResourceKey="Settings" Heading="Settings">
<div class="container">
<div class="row mb-1 align-items-center">
@ -116,7 +116,7 @@
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="path" HelpText="Optionally enter a url path for this page (ie. home ). If you do not provide a url path, the page name will be used. If the page is intended to be the root path specify '/'." ResourceKey="UrlPath">Url Path: </Label>
<Label Class="col-sm-3" For="path" HelpText="Optionally enter a url path for this page (ie. home ). If you do not provide a url path, the page name will be used. Please note that spaces and punctuation will be replaced by a dash. If the page is intended to be the root path specify '/'." ResourceKey="UrlPath">Url Path: </Label>
<div class="col-sm-9">
<input id="path" class="form-control" @bind="@_path" maxlength="256" />
</div>
@ -205,18 +205,21 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="headcontent" HelpText="Optionally enter content to be included in the page head (ie. meta, link, or script tags)" ResourceKey="HeadContent">Head Content: </Label>
<div class="col-sm-9">
<textarea id="headcontent" class="form-control" @bind="@_headcontent" rows="3"></textarea>
<textarea id="headcontent" class="form-control" @bind="@_headcontent" rows="3" maxlength="4000"></textarea>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="bodycontent" HelpText="Optionally enter content to be included in the page body (ie. script tags)" ResourceKey="BodyContent">Body Content: </Label>
<div class="col-sm-9">
<textarea id="bodycontent" class="form-control" @bind="@_bodycontent" rows="3"></textarea>
<textarea id="bodycontent" class="form-control" @bind="@_bodycontent" rows="3" maxlength="4000"></textarea>
</div>
</div>
</div>
</Section>
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
<br />
<br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo>
</TabPanel>
@ -225,6 +228,19 @@
<div class="row mb-1 align-items-center">
<PermissionGrid EntityName="@EntityNames.Page" PermissionList="@_permissions" @ref="_permissionGrid" />
</div>
<br /><br />
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="updatemodulepermissions" HelpText="Specify if changes made to page permissions should be propagated to the modules on this page" ResourceKey="UpdateModulePermissions">Update Module Permissions? </Label>
<div class="col-sm-9">
<select id="updatemodulepermissions" class="form-select" @bind="@_updatemodulepermissions" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</div>
</TabPanel>
<TabPanel Name="PageModules" Heading="Modules" ResourceKey="PageModules">
@ -247,8 +263,10 @@
{
<TabPanel Name="ThemeSettings" Heading="Theme Settings" ResourceKey="ThemeSettings">
@_themeSettingsComponent
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</TabPanel>
<br />
}
</TabStrip>
}
@ -263,6 +281,12 @@
<input id="title" class="form-control" @bind="@_title" maxlength="200" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="path" HelpText="Provide a url path for your personalized page. Please note that spaces and punctuation will be replaced by a dash." ResourceKey="PersonalizedUrlPath">Url Path: </Label>
<div class="col-sm-9">
<input id="path" class="form-control" @bind="@_path" maxlength="256" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="theme" HelpText="Select the theme for this page" ResourceKey="Theme">Theme: </Label>
<div class="col-sm-9">
@ -293,19 +317,21 @@
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</TabPanel>
@if (_themeSettingsType != null)
{
<TabPanel Name="ThemeSettings" Heading="Theme Settings" ResourceKey="ThemeSettings">
@_themeSettingsComponent
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</TabPanel>
<br />
}
</TabStrip>
}
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</form>
}
@ -342,6 +368,7 @@
private string _bodycontent;
private List<Permission> _permissions = null;
private PermissionGrid _permissionGrid;
private string _updatemodulepermissions;
private List<Module> _pageModules;
private string _createdby;
private DateTime _createdon;
@ -356,7 +383,6 @@
private string _iconresources = "";
private DateTime? _effectivedate = null;
private DateTime? _expirydate = null;
private string _activetab = "";
protected override async Task OnInitializedAsync()
{
@ -431,6 +457,7 @@
// permissions
_permissions = _page.PermissionList;
_updatemodulepermissions = "True";
// page modules
var modules = await ModuleService.GetModulesAsync(PageState.Site.SiteId);
@ -480,10 +507,11 @@
{
await logger.LogError(ex, "Error Loading Child Pages For Parent {PageId} {Error}", _parentid, ex.Message);
AddModuleMessage(Localizer["Error.ChildPage.Load"], MessageType.Error);
await ScrollToPageTop();
}
}
private void ThemeChanged(ChangeEventArgs e)
private async Task ThemeChanged(ChangeEventArgs e)
{
_themetype = (string)e.Value;
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype);
@ -495,6 +523,7 @@
if (ThemeService.GetTheme(PageState.Site.Themes, _themetype)?.ThemeName != ThemeService.GetTheme(PageState.Site.Themes, PageState.Site.DefaultThemeType)?.ThemeName)
{
AddModuleMessage(Localizer["ThemeChanged.Message"], MessageType.Warning);
await ScrollToPageTop();
}
}
@ -513,7 +542,7 @@
builder.OpenComponent(0, _themeSettingsType);
builder.AddAttribute(1, "RenderModeBoundary", RenderModeBoundary);
builder.AddComponentReferenceCapture(2, inst => { _themeSettings = Convert.ChangeType(inst, _themeSettingsType); });
builder.CloseComponent();
};
}
@ -523,7 +552,6 @@
private async Task SavePage()
{
_activetab = "Settings";
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
@ -533,6 +561,7 @@
if (!Utilities.ValidateEffectiveExpiryDates(_effectivedate, _expirydate))
{
AddModuleMessage(SharedLocalizer["Message.EffectiveExpiryDateError"], MessageType.Warning);
await ScrollToPageTop();
return;
}
if (!string.IsNullOrEmpty(_themetype) && _containertype != "-")
@ -583,12 +612,14 @@
if (_pages.Any(item => item.Path == _page.Path && item.PageId != _page.PageId))
{
AddModuleMessage(string.Format(Localizer["Mesage.Page.PathExists"], _path), MessageType.Warning);
await ScrollToPageTop();
return;
}
if (_page.ParentId == null && Constants.ReservedRoutes.Contains(_page.Name.ToLower()))
{
AddModuleMessage(string.Format(Localizer["Message.Page.Reserved"], _page.Name), MessageType.Warning);
await ScrollToPageTop();
return;
}
@ -642,6 +673,7 @@
if (_page.UserId == null)
{
_page.PermissionList = _permissionGrid.GetPermissionList();
_page.UpdateModulePermissions = bool.Parse(_updatemodulepermissions);
}
_page = await PageService.UpdatePageAsync(_page);
@ -672,18 +704,21 @@
}
else
{
AddModuleMessage(Localizer["Message.Required.PageInfo"], MessageType.Warning);
AddModuleMessage(Localizer["Message.Required.PageInfo"], MessageType.Warning);
await ScrollToPageTop();
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Page {Page} {Error}", _page, ex.Message);
AddModuleMessage(Localizer["Error.Page.Save"], MessageType.Error);
await ScrollToPageTop();
}
}
else
{
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
await ScrollToPageTop();
}
}

View File

@ -13,71 +13,71 @@
}
else
{
<TabStrip>
<TabPanel Name="Pages" ResourceKey="Pages" Heading="Pages">
@if (!_pages.Where(item => item.IsDeleted).Any())
{
<br />
<p>@Localizer["NoPage.Deleted"]</p>
}
else
{
<Pager Items="@_pages.Where(item => item.IsDeleted).OrderByDescending(item => item.DeletedOn)" CurrentPage="@_pagePage.ToString()" OnPageChange="OnPageChangePage">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th>
<th>@Localizer["DeletedBy"]</th>
<th>@Localizer["DeletedOn"]</th>
</Header>
<Row>
<TabStrip>
<TabPanel Name="Pages" ResourceKey="Pages" Heading="Pages">
@if (!_pages.Where(item => item.IsDeleted).Any())
{
<br />
<p>@Localizer["NoPage.Deleted"]</p>
}
else
{
<Pager Items="@_pages.Where(item => item.IsDeleted).OrderByDescending(item => item.DeletedOn)" CurrentPage="@_pagePage.ToString()" OnPageChange="OnPageChangePage">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Path"]</th>
<th>@Localizer["DeletedBy"]</th>
<th>@Localizer["DeletedOn"]</th>
</Header>
<Row>
<td><button type="button" @onclick="@(() => RestorePage(context))" class="btn btn-success" title="Restore">@Localizer["Restore"]</button></td>
<td><ActionDialog Header="Delete Page" Message="@string.Format(Localizer["Confirm.Page.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeletePage(context))" ResourceKey="DeletePage" /></td>
<td>@context.Name</td>
<td>@context.DeletedBy</td>
<td>@context.DeletedOn</td>
</Row>
</Pager>
<br />
<ActionDialog Header="Remove All Deleted Pages" Message="Are You Sure You Wish To Permanently Remove All Deleted Pages?" Action="Remove All Deleted Pages" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllPages())" ResourceKey="DeleteAllPages" />
}
</TabPanel>
<TabPanel Name="Modules" ResourceKey="Modules" Heading="Modules">
@if (!_modules.Where(item => item.IsDeleted).Any())
{
<br />
<p>@Localizer["NoModule.Deleted"]</p>
}
else
{
<Pager Items="@_modules.Where(item => item.IsDeleted).OrderByDescending(item => item.DeletedOn)" CurrentPage="@_pageModule.ToString()" OnPageChange="OnPageChangeModule">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Page"]</th>
<th>@Localizer["Module"]</th>
<th>@Localizer["DeletedBy"]</th>
<th>@Localizer["DeletedOn"]</th>
</Header>
<Row>
<td><button type="button" @onclick="@(() => RestoreModule(context))" class="btn btn-success" title="Restore">@Localizer["Restore"]</button></td>
<td><ActionDialog Header="Delete Module" Message="@string.Format(Localizer["Confirm.Module.Delete"], context.Title)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" /></td>
<td>@_pages.Find(item => item.PageId == context.PageId).Name</td>
<td>@context.Title</td>
<td>@context.DeletedBy</td>
<td>@context.DeletedOn</td>
</Row>
</Pager>
<br />
<ActionDialog Header="Remove All Deleted Modules" Message="Are You Sure You Wish To Permanently Remove All Deleted Modules?" Action="Remove All Deleted Modules" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllModules())" ResourceKey="DeleteAllModules" />
}
</TabPanel>
</TabStrip>
<td><ActionDialog Header="Delete Page" Message="@string.Format(Localizer["Confirm.Page.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeletePage(context))" ResourceKey="DeletePage" /></td>
<td>@context.Path</td>
<td>@context.DeletedBy</td>
<td>@context.DeletedOn</td>
</Row>
</Pager>
<br />
<ActionDialog Header="Remove All Deleted Pages" Message="Are You Sure You Wish To Permanently Remove All Deleted Pages?" Action="Remove All Deleted Pages" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllPages())" ResourceKey="DeleteAllPages" />
}
</TabPanel>
<TabPanel Name="Modules" ResourceKey="Modules" Heading="Modules">
@if (!_modules.Where(item => item.IsDeleted).Any())
{
<br />
<p>@Localizer["NoModule.Deleted"]</p>
}
else
{
<Pager Items="@_modules.Where(item => item.IsDeleted).OrderByDescending(item => item.DeletedOn)" CurrentPage="@_pageModule.ToString()" OnPageChange="OnPageChangeModule">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Page"]</th>
<th>@Localizer["Module"]</th>
<th>@Localizer["DeletedBy"]</th>
<th>@Localizer["DeletedOn"]</th>
</Header>
<Row>
<td><button type="button" @onclick="@(() => RestoreModule(context))" class="btn btn-success" title="Restore">@Localizer["Restore"]</button></td>
<td><ActionDialog Header="Delete Module" Message="@string.Format(Localizer["Confirm.Module.Delete"], context.Title)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" /></td>
<td>@_pages.Find(item => item.PageId == context.PageId).Name</td>
<td>@context.Title</td>
<td>@context.DeletedBy</td>
<td>@context.DeletedOn</td>
</Row>
</Pager>
<br />
<ActionDialog Header="Remove All Deleted Modules" Message="Are You Sure You Wish To Permanently Remove All Deleted Modules?" Action="Remove All Deleted Modules" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllModules())" ResourceKey="DeleteAllModules" />
}
</TabPanel>
</TabStrip>
}
@code {
private List<Page> _pages;
private List<Module> _modules;
private List<Page> _pages;
private List<Module> _modules;
private int _pagePage = 1;
private int _pageModule = 1;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
@ -105,12 +105,25 @@ else
{
try
{
page.IsDeleted = false;
await PageService.UpdatePageAsync(page);
await logger.LogInformation("Page Restored {Page}", page);
await Load();
StateHasChanged();
NavigationManager.NavigateTo(NavigateUrl());
var validated = true;
if (page.ParentId != null)
{
var parent = _pages.Find(item => item.PageId == page.ParentId);
validated = !parent.IsDeleted;
}
if (validated)
{
page.IsDeleted = false;
await PageService.UpdatePageAsync(page);
await logger.LogInformation("Page Restored {Page}", page);
AddModuleMessage(Localizer["Success.Page.Restore"], MessageType.Success);
await Load();
StateHasChanged();
}
else
{
AddModuleMessage(Localizer["Message.Page.Restore"], MessageType.Warning);
}
}
catch (Exception ex)
{
@ -125,9 +138,9 @@ else
{
await PageService.DeletePageAsync(page.PageId);
await logger.LogInformation("Page Permanently Deleted {Page}", page);
AddModuleMessage(Localizer["Success.Page.Delete"], MessageType.Success);
await Load();
StateHasChanged();
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
@ -148,10 +161,10 @@ else
}
await logger.LogInformation("Pages Permanently Deleted");
AddModuleMessage(Localizer["Success.Pages.Delete"], MessageType.Success);
await Load();
HideProgressIndicator();
StateHasChanged();
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
@ -169,6 +182,7 @@ else
pagemodule.IsDeleted = false;
await PageModuleService.UpdatePageModuleAsync(pagemodule);
await logger.LogInformation("Module Restored {Module}", module);
AddModuleMessage(Localizer["Success.Module.Restore"], MessageType.Success);
await Load();
StateHasChanged();
}
@ -185,6 +199,7 @@ else
{
await PageModuleService.DeletePageModuleAsync(module.PageModuleId);
await logger.LogInformation("Module Permanently Deleted {Module}", module);
AddModuleMessage(Localizer["Success.Module.Delete"], MessageType.Success);
await Load();
StateHasChanged();
}
@ -205,6 +220,7 @@ else
await PageModuleService.DeletePageModuleAsync(module.PageModuleId);
}
await logger.LogInformation("Modules Permanently Deleted");
AddModuleMessage(Localizer["Success.Modules.Delete"], MessageType.Success);
await Load();
HideProgressIndicator();
StateHasChanged();

View File

@ -3,80 +3,98 @@
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IUserService UserService
@inject ITimeZoneService TimeZoneService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@inject ISettingService SettingService
@if (PageState.Site.AllowRegistration)
@if (_initialized)
{
if (!_userCreated)
@if (PageState.Site.AllowRegistration)
{
if (PageState.User != null)
if (!_userCreated)
{
<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 />
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>
</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 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 class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="timezone" HelpText="Your time zone" ResourceKey="TimeZone">Time Zone:</Label>
<div class="col-sm-9">
<select id="timezone" class="form-select" @bind="@_timezoneid">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var timezone in _timezones)
{
<option value="@timezone.Id">@timezone.DisplayName</option>
}
</select>
</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>
<br />
<NavLink href="@NavigateUrl("login")">@Localizer["Login"]</NavLink>
}
</form>
}
}
}
}
else
{
<ModuleMessage Message="@Localizer["Info.Registration.Disabled"]" Type="MessageType.Info" />
}
else
{
<ModuleMessage Message="@Localizer["Info.Registration.Disabled"]" Type="MessageType.Info" />
}
}
@code {
private bool _initialized = false;
private List<Models.TimeZone> _timezones;
private string _passwordrequirements;
private string _username = string.Empty;
private ElementReference form;
@ -87,6 +105,7 @@ else
private string _confirm = string.Empty;
private string _email = string.Empty;
private string _displayname = string.Empty;
private string _timezoneid = string.Empty;
private bool _userCreated = false;
private bool _allowsitelogin = true;
@ -96,6 +115,9 @@ else
{
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true"));
_timezones = await TimeZoneService.GetTimeZonesAsync();
_timezoneid = PageState.Site.TimeZoneId;
_initialized = true;
}
protected override void OnParametersSet()
@ -124,6 +146,7 @@ else
Password = _password,
Email = _email,
DisplayName = (_displayname == string.Empty ? _username : _displayname),
TimeZoneId = _timezoneid,
PhotoFileId = null
};
user = await UserService.AddUserAsync(user);

View File

@ -58,7 +58,7 @@ else
<td>@context.EffectiveDate</td>
<td>@context.ExpiryDate</td>
<td>
<ActionDialog Header="Remove User" Message="@string.Format(Localizer["Confirm.User.DeleteRole"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.IsAutoAssigned || context.User.Username == UserNames.Host || context.User.UserId == PageState.User.UserId)" ResourceKey="DeleteUserRole" />
<ActionDialog Header="Remove User" Message="@string.Format(Localizer["Confirm.User.DeleteRole"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.User.Username == UserNames.Host || context.User.UserId == PageState.User.UserId)" ResourceKey="DeleteUserRole" />
</td>
</Row>
</Pager>
@ -180,27 +180,28 @@ else
private async Task DeleteUserRole(int UserRoleId)
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
try
{
try
var userrole = await UserRoleService.GetUserRoleAsync(UserRoleId);
if (userrole.Role.Name == RoleNames.Registered)
{
userrole.ExpiryDate = DateTime.UtcNow;
await UserRoleService.UpdateUserRoleAsync(userrole);
await logger.LogInformation("User {Username} Expired From Role {Role}", userrole.User.Username, userrole.Role.Name);
}
else
{
await UserRoleService.DeleteUserRoleAsync(UserRoleId);
await logger.LogInformation("User Removed From Role {UserRoleId}", UserRoleId);
AddModuleMessage(Localizer["Confirm.User.RoleRemoved"], MessageType.Success);
await GetUserRoles();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Removing User From Role {UserRoleId} {Error}", UserRoleId, ex.Message);
AddModuleMessage(Localizer["Error.User.RemoveRole"], MessageType.Error);
await logger.LogInformation("User {Username} Removed From Role {Role}", userrole.User.Username, userrole.Role.Name);
}
AddModuleMessage(Localizer["Confirm.User.RoleRemoved"], MessageType.Success);
await GetUserRoles();
StateHasChanged();
}
else
catch (Exception ex)
{
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
await logger.LogError(ex, "Error Removing User From Role {UserRoleId} {Error}", UserRoleId, ex.Message);
AddModuleMessage(Localizer["Error.User.RemoveRole"], MessageType.Error);
}
}
}

View File

@ -10,10 +10,12 @@
@inject IAliasService AliasService
@inject IThemeService ThemeService
@inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService
@inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Index> Localizer
@inject INotificationService NotificationService
@inject IStringLocalizer<SharedResources> SharedLocalizer
@inject IOutputCacheService CacheService
@if (_initialized)
{
@ -40,6 +42,18 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="timezone" HelpText="Your time zone" ResourceKey="TimeZone">Time Zone:</Label>
<div class="col-sm-9">
<select id="timezone" class="form-select" @bind="@_timezoneid">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var timezone in _timezones)
{
<option value="@timezone.Id">@timezone.DisplayName</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isDeleted" HelpText="Is this site deleted?" ResourceKey="IsDeleted">Deleted? </Label>
<div class="col-sm-9">
@ -50,11 +64,12 @@
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="sitemap" HelpText="The site map url for this site which can be submitted to search engines for indexing" ResourceKey="SiteMap">Site Map: </Label>
<Label Class="col-sm-3" For="sitemap" HelpText="The site map url for this site which can be submitted to search engines for indexing. The sitemap is cached for 5 minutes and the cache can be manually cleared." ResourceKey="SiteMap">Site Map: </Label>
<div class="col-sm-9">
<div class="input-group">
<input id="sitemap" class="form-control" @bind="@_sitemap" disabled />
<a href="@_sitemap" class="btn btn-secondary" target="_new">@Localizer["Browse"]</a>
<button type="button" class="btn btn-danger" @onclick="EvictSitemapOutputCache">@Localizer["SiteMap.EvictCache"]</button>
</div>
</div>
</div>
@ -72,20 +87,8 @@
</div>
</div>
<br />
<Section Name="Appearance" Heading="Appearance" ResourceKey="Appearance">
<Section Name="Theme" Heading="Theme" ResourceKey="Theme">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="logo" HelpText="Specify a logo for the site" ResourceKey="Logo">Logo: </Label>
<div class="col-sm-9">
<FileManager FileId="@_logofileid" Filter="@_imageFiles" @ref="_logofilemanager" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="favicon" HelpText="Specify a Favicon" ResourceKey="FavoriteIcon">Favicon: </Label>
<div class="col-sm-9">
<FileManager FileId="@_faviconfileid" Filter="ico,png,gif" @ref="_faviconfilemanager" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="defaultTheme" HelpText="Select the sites default theme" ResourceKey="DefaultTheme">Default Theme: </Label>
<div class="col-sm-9">
@ -126,6 +129,32 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cookieconsent" HelpText="Specify if cookie consent is enabled on this site. Please note this option must be used in conjunction with a Theme which supports cookie consent." ResourceKey="CookieConsent">Cookie Consent: </Label>
<div class="col-sm-9">
<select id="cookieconsent" class="form-select" @bind="@_cookieconsent">
<option value="">@SharedLocalizer["Disabled"]</option>
<option value="optin">@Localizer["OptIn"]</option>
<option value="optout">@Localizer["OptOut"]</option>
</select>
</div>
</div>
</div>
</Section>
<Section Name="Appearance" Heading="Appearance" ResourceKey="Appearance">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="logo" HelpText="Specify a logo for the site" ResourceKey="Logo">Logo: </Label>
<div class="col-sm-9">
<FileManager FileId="@_logofileid" Filter="@_imagefiles" @ref="_logofilemanager" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="favicon" HelpText="Specify a Favicon" ResourceKey="FavoriteIcon">Favicon: </Label>
<div class="col-sm-9">
<FileManager FileId="@_faviconfileid" Filter="ico,png,gif" @ref="_faviconfilemanager" />
</div>
</div>
</div>
</Section>
<Section Name="Functionality" Heading="Functionality" ResourceKey="Functionality">
@ -144,18 +173,6 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="imageExt" HelpText="Enter a comma separated list of image file extensions" ResourceKey="ImageExtensions">Image Extensions: </Label>
<div class="col-sm-9">
<input id="imageExt" spellcheck="false" class="form-control" @bind="@_imageFiles" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="uploadableFileExt" HelpText="Enter a comma separated list of uploadable file extensions" ResourceKey="UploadableFileExtensions">Uploadable File Extensions: </Label>
<div class="col-sm-9">
<input id="uploadableFileExt" spellcheck="false" class="form-control" @bind="@_uploadableFiles" />
</div>
</div>
</div>
</Section>
<Section Name="PageContent" Heading="Page Content" ResourceKey="PageContent">
@ -412,9 +429,11 @@
private List<ThemeControl> _themes = new List<ThemeControl>();
private List<ThemeControl> _containers = new List<ThemeControl>();
private List<Page> _pages;
private List<Models.TimeZone> _timezones;
private string _name = string.Empty;
private string _homepageid = "-";
private string _timezoneid = string.Empty;
private string _isdeleted;
private string _sitemap = "";
private string _siteguid = "";
@ -427,11 +446,11 @@
private string _themetype = "";
private string _containertype = "";
private string _admincontainertype = "";
private string _cookieconsent = "";
private Dictionary<string, string> _textEditors = new Dictionary<string, string>();
private string _textEditor = "";
private string _imageFiles = string.Empty;
private string _uploadableFiles = string.Empty;
private string _imagefiles = string.Empty;
private string _headcontent = string.Empty;
private string _bodycontent = string.Empty;
@ -489,11 +508,13 @@
Site site = await SiteService.GetSiteAsync(PageState.Site.SiteId);
if (site != null)
{
_timezones = await TimeZoneService.GetTimeZonesAsync();
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
_pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
_name = site.Name;
_timezoneid = site.TimeZoneId;
if (site.HomePageId != null)
{
_homepageid = site.HomePageId.Value.ToString();
@ -518,6 +539,7 @@
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype);
_containertype = (!string.IsNullOrEmpty(site.DefaultContainerType)) ? site.DefaultContainerType : Constants.DefaultContainer;
_admincontainertype = (!string.IsNullOrEmpty(site.AdminContainerType)) ? site.AdminContainerType : Constants.DefaultAdminContainer;
_cookieconsent = SettingService.GetSetting(settings, "CookieConsent", string.Empty);
// functionality
var textEditors = ServiceProvider.GetServices<ITextEditor>();
@ -526,10 +548,8 @@
_textEditors.Add(textEditor.Name, Utilities.GetFullTypeName(textEditor.GetType().AssemblyQualifiedName));
}
_textEditor = SettingService.GetSetting(settings, "TextEditor", Constants.DefaultTextEditor);
_imageFiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles);
_imageFiles = (string.IsNullOrEmpty(_imageFiles)) ? Constants.ImageFiles : _imageFiles;
_uploadableFiles = SettingService.GetSetting(settings, "UploadableFiles", Constants.UploadableFiles);
_uploadableFiles = (string.IsNullOrEmpty(_uploadableFiles)) ? Constants.UploadableFiles : _uploadableFiles;
_imagefiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles);
_imagefiles = (string.IsNullOrEmpty(_imagefiles)) ? Constants.ImageFiles : _imagefiles;
// page content
_headcontent = site.HeadContent;
@ -647,6 +667,7 @@
if (site != null)
{
site.Name = _name;
site.TimeZoneId = _timezoneid;
site.HomePageId = (_homepageid != "-" ? int.Parse(_homepageid) : null);
site.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted));
@ -732,10 +753,11 @@
settings = SettingService.SetSetting(settings, "SiteGuid", _siteguid, true);
settings = SettingService.SetSetting(settings, "NotificationRetention", _retention.ToString(), true);
//cookie consent
settings = SettingService.SetSetting(settings, "CookieConsent", _cookieconsent);
// functionality
settings = SettingService.SetSetting(settings, "TextEditor", _textEditor);
settings = SettingService.SetSetting(settings, "ImageFiles", (_imageFiles != Constants.ImageFiles) ? _imageFiles.Replace(" ", "") : "", false);
settings = SettingService.SetSetting(settings, "UploadableFiles", (_uploadableFiles != Constants.UploadableFiles) ? _uploadableFiles.Replace(" ", "") : "", false);
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
@ -930,4 +952,9 @@
_aliasname = "";
StateHasChanged();
}
private async Task EvictSitemapOutputCache() {
await CacheService.EvictByTag(Constants.SitemapOutputCacheTag);
AddModuleMessage(Localizer["Success.SiteMap.CacheEvicted"], MessageType.Success);
}
}

View File

@ -133,16 +133,30 @@
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packageregistryurl" HelpText="Specify The Package Manager Service For Installing Modules, Themes, And Translations. If This Field Is Blank It Means The Package Manager Service Is Disabled For This Installation." ResourceKey="PackageManager">Package Manager: </Label>
<Label Class="col-sm-3" For="cachecontrol" HelpText="Provide a Cache-Control directive for static assets. For example 'public, max-age=60' indicates that static assets should be cached for 60 seconds. A blank value indicates caching is not enabled." ResourceKey="CacheControl">Static Asset Caching: </Label>
<div class="col-sm-9">
<input id="cachecontrol" class="form-control" @bind="@_cachecontrol" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packageregistryurl" HelpText="Specify The Url Of The Package Manager Service For Installing Modules, Themes, And Translations. If This Field Is Blank It Means The Package Manager Service Is Disabled For This Installation." ResourceKey="PackageManager">Package Manager Url: </Label>
<div class="col-sm-9">
<input id="packageregistryurl" class="form-control" @bind="@_packageregistryurl" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packageregistryemail" HelpText="Specify The Email Address Of The User Account Used For Interacting With The Package Manager Service. This Account Is Used For Managing Packages Across Multiple Installations." ResourceKey="PackageManagerEmail">Package Manager Email: </Label>
<div class="col-sm-9">
<input id="packageregistryemail" class="form-control" @bind="@_packageregistryemail" />
</div>
</div>
</div>
<br /><br />
<button type="button" class="btn btn-success" @onclick="SaveConfig">@SharedLocalizer["Save"]</button>&nbsp;
<a class="btn btn-primary" href="swagger/index.html" target="_new">@Localizer["Access.ApiFramework"]</a>&nbsp;
<ActionDialog Header="Restart Application" Message="Are You Sure You Wish To Restart The Application?" Action="Restart Application" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await RestartApplication())" ResourceKey="RestartApplication" />
<br /><br />
<a class="btn btn-primary" href="swagger/index.html" target="_new">@Localizer["Swagger"]</a>&nbsp;
<a class="btn btn-secondary" href="api/endpoint" target="_new">@Localizer["Endpoints"]</a>
</TabPanel>
<TabPanel Name="Log" Heading="Log" ResourceKey="Log">
<div class="container">
@ -179,9 +193,11 @@
private string _logginglevel = string.Empty;
private string _notificationlevel = string.Empty;
private string _swagger = string.Empty;
private string _cachecontrol = string.Empty;
private string _packageregistryurl = string.Empty;
private string _packageregistryemail = string.Empty;
private string _log = string.Empty;
private string _log = string.Empty;
protected override async Task OnInitializedAsync()
{
@ -209,9 +225,11 @@
_detailederrors = systeminfo["DetailedErrors"].ToString();
_logginglevel = systeminfo["Logging:LogLevel:Default"].ToString();
_notificationlevel = systeminfo["Logging:LogLevel:Notify"].ToString();
_swagger = systeminfo["UseSwagger"].ToString();
_swagger = systeminfo["UseSwagger"].ToString();
_cachecontrol = systeminfo["CacheControl"].ToString();
_packageregistryurl = systeminfo["PackageRegistryUrl"].ToString();
}
_packageregistryemail = systeminfo["PackageRegistryEmail"].ToString();
}
systeminfo = await SystemService.GetSystemInfoAsync("log");
if (systeminfo != null)
@ -229,8 +247,10 @@
settings.Add("Logging:LogLevel:Default", _logginglevel);
settings.Add("Logging:LogLevel:Notify", _notificationlevel);
settings.Add("UseSwagger", _swagger);
settings.Add("PackageRegistryUrl", _packageregistryurl);
await SystemService.UpdateSystemInfoAsync(settings);
settings.Add("CacheControl", _cachecontrol);
settings.Add("PackageRegistryUrl", _packageregistryurl);
settings.Add("PackageRegistryEmail", _packageregistryemail);
await SystemService.UpdateSystemInfoAsync(settings);
AddModuleMessage(Localizer["Success.UpdateConfig.Restart"], MessageType.Success);
}
catch (Exception ex)

View File

@ -45,24 +45,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packagename" HelpText="The unique name of the package from which this theme was installed. This value must be specified within the theme's ITheme interface specification." ResourceKey="PackageName">Package Name: </Label>
<div class="col-sm-9">
@if (!string.IsNullOrEmpty(_packagename))
{
<div class="input-group">
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
@if (string.IsNullOrEmpty(_packageurl))
{
<button type="button" class="btn btn-secondary" @onclick="ValidatePackage">@Localizer["Validate"]</button>
}
else
{
<a href="@_packageurl" target="_blank" class="btn btn-primary">@SharedLocalizer["Download"]</a>
}
</div>
}
else
{
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
}
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -72,7 +55,7 @@
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="The reference url of the theme" ResourceKey="ReferenceUrl">Reference Url: </Label>
<Label Class="col-sm-3" For="url" HelpText="The url of the theme" ResourceKey="Url">Url: </Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" disabled />
</div>
@ -116,7 +99,6 @@
private string _name;
private string _version;
private string _packagename = "";
private string _packageurl = "";
private string _owner = "";
private string _url = "";
private string _contact = "";
@ -185,27 +167,4 @@
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
private async Task ValidatePackage()
{
try
{
var package = await PackageService.GetPackageAsync(_packagename, _version, true);
if (package == null || string.IsNullOrEmpty(package.PackageUrl))
{
AddModuleMessage(Localizer["Message.Validate"], MessageType.Warning);
}
else
{
_packageurl = package.PackageUrl;
AddModuleMessage(Localizer["Message.Download"], MessageType.Info);
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Package {PackageId} {Version}", _packagename, _version);
AddModuleMessage(Localizer["Error.Validate"], MessageType.Error);
}
}
}

View File

@ -4,6 +4,7 @@
@inject NavigationManager NavigationManager
@inject IThemeService ThemeService
@inject IPackageService PackageService
@inject ISiteService SiteService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@ -14,11 +15,12 @@
else
{
<ActionLink Action="Add" Text="Install Theme" ResourceKey="InstallTheme" />
@((MarkupString)"&nbsp;")
<ActionLink Action="Create" Text="Create Theme" ResourceKey="CreateTheme" Class="btn btn-secondary" />
<ActionLink Action="Create" Text="Create Theme" ResourceKey="CreateTheme" Class="btn btn-secondary ps-2" />
<button type="button" class="btn btn-secondary pw-2" @onclick="@Synchronize">@Localizer["Synchronize"]</button>
<Pager Items="@_themes">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th>
@ -32,10 +34,11 @@ else
<td><ActionLink Action="Edit" Text="Edit" Parameters="@($"id=" + context.ThemeId.ToString())" ResourceKey="EditTheme" /></td>
<td>
@if (context.AssemblyName != Constants.ClientId)
{
{
<ActionDialog Header="Delete Theme" Message="@string.Format(Localizer["Confirm.Theme.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await DeleteTheme(context))" ResourceKey="DeleteTheme" />
}
}
</td>
<td><NavLink class="btn btn-secondary" href="@NavigateUrl("admin/site")">@Localizer["Assign"]</NavLink></td>
<td>@context.Name</td>
<td>@context.Version</td>
<td>
@ -170,4 +173,27 @@ else
AddModuleMessage(Localizer["Error.Theme.Delete"], MessageType.Error);
}
}
private async Task Synchronize()
{
try
{
ShowProgressIndicator();
foreach (var theme in _themes)
{
if (!string.IsNullOrEmpty(theme.PackageName) && !_packages.Any(item => item.PackageId == theme.PackageName))
{
await PackageService.GetPackageAsync(theme.PackageName, theme.Version, false);
}
}
HideProgressIndicator();
AddModuleMessage(Localizer["Success.Theme.Synchronize"], MessageType.Success);
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true));
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Synchronizing Themes {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Theme.Synchronize"], MessageType.Error);
}
}
}

View File

@ -13,9 +13,26 @@
<TabPanel Name="Download" ResourceKey="Download">
@if (_package != null && _upgradeavailable)
{
<ModuleMessage Type="MessageType.Info" Message="Select The Download Button To Download The Framework Upgrade Package And Then Select Upgrade"></ModuleMessage>
<button type="button" class="btn btn-primary" @onclick=@(async () => await Download(Constants.PackageId, @_package.Version))>@SharedLocalizer["Download"] @_package.Version</button>
<button type="button" class="btn btn-success" @onclick="Upgrade">@SharedLocalizer["Upgrade"]</button>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" HelpText="Specify if you want to backup files during the upgrade process. Disabling this option will reduce the time required for the upgrade." ResourceKey="Backup">Backup Files? </Label>
<div class="col-sm-9">
<select id="backup" class="form-select" @bind="@_backup">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
<br />
@if (!_downloaded)
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await Download(Constants.PackageId, @_package.Version))>@SharedLocalizer["Download"] @_package.Version</button>
}
else
{
<button type="button" class="btn btn-success" @onclick="Upgrade">@SharedLocalizer["Upgrade"]</button>
}
}
else
{
@ -23,7 +40,6 @@
}
</TabPanel>
<TabPanel Name="Upload" ResourceKey="Upload">
<ModuleMessage Type="MessageType.Info" Message=@Localizer["MessageUpgrade.Text"]></ModuleMessage>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" HelpText="Upload A Framework Package And Then Select Upgrade" ResourceKey="Framework">Framework: </Label>
@ -31,7 +47,17 @@
<FileManager Folder="@Constants.PackagesFolder" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" HelpText="Specify if you want to backup files during the upgrade process. Disabling this option will reduce the time required for the upgrade." ResourceKey="Backup">Backup Files? </Label>
<div class="col-sm-9">
<select id="backup" class="form-select" @bind="@_backup">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="Upgrade">@SharedLocalizer["Upgrade"]</button>
</TabPanel>
</TabStrip>
@ -39,8 +65,10 @@
@code {
private bool _initialized = false;
private bool _downloaded = false;
private Package _package;
private bool _upgradeavailable = false;
private string _backup = "True";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
@ -86,7 +114,7 @@
ShowProgressIndicator();
var interop = new Interop(JSRuntime);
await interop.RedirectBrowser(NavigateUrl(), 10);
await InstallationService.Upgrade();
await InstallationService.Upgrade(bool.Parse(_backup));
}
catch (Exception ex)
{
@ -102,6 +130,7 @@
ShowProgressIndicator();
await PackageService.DownloadPackageAsync(packageid, version);
await PackageService.DownloadPackageAsync(Constants.UpdaterPackageId, version);
_downloaded = true;
HideProgressIndicator();
AddModuleMessage(Localizer["Success.Framework.Download"], MessageType.Success);
}

View File

@ -8,13 +8,16 @@
<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="url" HelpText="The fully qualified Url for this site" ResourceKey="Url">Url:</Label>
<Label Class="col-sm-3" For="url" HelpText="A Url identifying a path to a specific page in the site (absolute or relative)" ResourceKey="Url">Url:</Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" maxlength="500" required />
<div class="input-group">
<input id="url" class="form-control" @bind="@_url" maxlength="500" required />
<button type="button" class="btn btn-primary" @onclick="GenerateUrl">@Localizer["Generate"]</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="mappedurl" HelpText="A fully qualified Url where the user will be redirected" ResourceKey="MappedUrl">Redirect To:</Label>
<Label Class="col-sm-3" For="mappedurl" HelpText="A Url where the user will be redirected (absolute or relative). Use '/' for site root path." ResourceKey="MappedUrl">Redirect To:</Label>
<div class="col-sm-9">
<input id="mappedurl" class="form-control" @bind="@_mappedurl" maxlength="500" required />
</div>
@ -26,64 +29,80 @@
</form>
@code {
private ElementReference form;
private bool validated = false;
private ElementReference form;
private bool validated = false;
private string _url = string.Empty;
private string _mappedurl = string.Empty;
private string _url = string.Empty;
private string _mappedurl = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
private async Task SaveUrlMapping()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
if (_url != _mappedurl)
{
var url = PageState.Uri.Scheme + "://" + PageState.Uri.Authority + "/";
url = url + (!string.IsNullOrEmpty(PageState.Alias.Path) ? PageState.Alias.Path + "/" : "");
private async Task SaveUrlMapping()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
if (_url != _mappedurl)
{
var url = PageState.Uri.Scheme + "://" + PageState.Uri.Authority + "/";
url = url + (!string.IsNullOrEmpty(PageState.Alias.Path) ? PageState.Alias.Path + "/" : "");
_url = (_url.StartsWith("/")) ? _url.Substring(1) : _url;
_url = (!_url.StartsWith("http")) ? url + _url : _url;
_url = (_url.StartsWith("/")) ? _url.Substring(1) : _url;
_url = (!_url.StartsWith("http")) ? url + _url : _url;
if (_url.StartsWith(url))
{
var urlmapping = new UrlMapping();
urlmapping.SiteId = PageState.Site.SiteId;
var route = new Route(_url, PageState.Alias.Path);
urlmapping.Url = route.PagePath;
urlmapping.MappedUrl = _mappedurl.Replace(url, "");
urlmapping.Requests = 0;
urlmapping.CreatedOn = DateTime.UtcNow;
urlmapping.RequestedOn = DateTime.UtcNow;
_mappedurl = _mappedurl.Replace(url, "");
_mappedurl = (_mappedurl.StartsWith("/") && _mappedurl != "/") ? _mappedurl.Substring(1) : _mappedurl;
try
{
urlmapping = await UrlMappingService.AddUrlMappingAsync(urlmapping);
await logger.LogInformation("UrlMapping Saved {UrlMapping}", urlmapping);
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving UrlMapping {UrlMapping} {Error}", urlmapping, ex.Message);
AddModuleMessage(Localizer["Error.SaveUrlMapping"], MessageType.Error);
}
}
else
{
AddModuleMessage(Localizer["Message.SaveUrlMapping"], MessageType.Warning);
}
}
else
{
if (_url.StartsWith(url))
{
var urlmapping = new UrlMapping();
urlmapping.SiteId = PageState.Site.SiteId;
urlmapping.Url = new Route(_url, PageState.Alias.Path).PagePath;
urlmapping.MappedUrl = _mappedurl;
urlmapping.Requests = 0;
urlmapping.CreatedOn = DateTime.UtcNow;
urlmapping.RequestedOn = DateTime.UtcNow;
try
{
urlmapping = await UrlMappingService.AddUrlMappingAsync(urlmapping);
await logger.LogInformation("UrlMapping Saved {UrlMapping}", urlmapping);
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving UrlMapping {UrlMapping} {Error}", urlmapping, ex.Message);
AddModuleMessage(Localizer["Error.SaveUrlMapping"], MessageType.Error);
}
}
else
{
AddModuleMessage(Localizer["Message.SaveUrlMapping"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["Message.DuplicateUrlMapping"], MessageType.Warning);
}
}
}
else
{
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
private void GenerateUrl()
{
var url = PageState.Uri.Scheme + "://" + PageState.Uri.Authority + "/";
url = url + (!string.IsNullOrEmpty(PageState.Alias.Path) ? PageState.Alias.Path + "/" : "");
var chars = "abcdefghijklmnopqrstuvwxyz";
Random rnd = new Random();
for (int i = 0; i < 5; i++)
{
url += chars.Substring(rnd.Next(0, chars.Length - 1), 1);
}
_url = url;
}
}

View File

@ -8,13 +8,13 @@
<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="url" HelpText="A fully qualified Url for this site" ResourceKey="Url">Url:</Label>
<Label Class="col-sm-3" For="url" HelpText="A Url identifying a path to a specific page in the site (absolute or relative)" ResourceKey="Url">Url:</Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" maxlength="500" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="mappedurl" HelpText="A fully qualified Url where the user will be redirected" ResourceKey="MappedUrl">Redirect To:</Label>
<Label Class="col-sm-3" For="mappedurl" HelpText="A Url where the user will be redirected (absolute or relative). Use '/' for site root path." ResourceKey="MappedUrl">Redirect To:</Label>
<div class="col-sm-9">
<input id="mappedurl" class="form-control" @bind="@_mappedurl" maxlength="500" required />
</div>
@ -67,8 +67,11 @@
var url = PageState.Uri.Scheme + "://" + PageState.Uri.Authority + "/";
url = url + (!string.IsNullOrEmpty(PageState.Alias.Path) ? PageState.Alias.Path + "/" : "");
_mappedurl = _mappedurl.Replace(url, "");
_mappedurl = (_mappedurl.StartsWith("/") && _mappedurl != "/") ? _mappedurl.Substring(1) : _mappedurl;
var urlmapping = await UrlMappingService.GetUrlMappingAsync(_urlmappingid);
urlmapping.MappedUrl = _mappedurl.Replace(url, "");
urlmapping.MappedUrl = _mappedurl;
urlmapping = await UrlMappingService.UpdateUrlMappingAsync(urlmapping);
await logger.LogInformation("UrlMapping Saved {UrlMapping}", urlmapping);
NavigationManager.NavigateTo(NavigateUrl());

View File

@ -3,6 +3,7 @@
@inject NavigationManager NavigationManager
@inject IUrlMappingService UrlMappingService
@inject ISiteService SiteService
@inject ISettingService SettingService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@ -47,7 +48,7 @@ else
}
</td>
<td>@context.Requests</td>
<td>@context.RequestedOn</td>
<td>@UtcToLocal(context.RequestedOn)</td>
</Row>
</Pager>
</TabPanel>
@ -62,7 +63,13 @@ else
</select>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of days of broken urls to retain" ResourceKey="Retention">Retention (Days): </Label>
<div class="col-sm-9">
<input id="retention" class="form-control" type="number" min="0" step="1" @bind="@_retention" />
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
</TabPanel>
@ -73,6 +80,7 @@ else
private bool _mapped = true;
private List<UrlMapping> _urlMappings;
private string _capturebrokenurls;
private int _retention = 30;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
@ -80,7 +88,10 @@ else
{
await GetUrlMappings();
_capturebrokenurls = PageState.Site.CaptureBrokenUrls.ToString();
}
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_retention = int.Parse(SettingService.GetSetting(settings, "UrlMappingRetention", "30"));
}
private async void MappedChanged(ChangeEventArgs e)
{
@ -124,7 +135,12 @@ else
var site = PageState.Site;
site.CaptureBrokenUrls = bool.Parse(_capturebrokenurls);
await SiteService.UpdateSiteAsync(site);
AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
settings = SettingService.SetSetting(settings, "UrlMappingRetention", _retention.ToString(), true);
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);
}
catch (Exception ex)
{

View File

@ -9,6 +9,7 @@
@inject INotificationService NotificationService
@inject IFileService FileService
@inject IFolderService FolderService
@inject ITimeZoneService TimeZoneService
@inject IJSRuntime jsRuntime
@inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Index> Localizer
@ -16,9 +17,9 @@
@if (_initialized)
{
@if (PageState.User != null && photo != null)
@if (PageState.User != null && _photo != null)
{
<img src="@ImageUrl(photofileid, 400, 400)" alt="@displayname" style="max-width: 400px" class="rounded-circle mx-auto d-block">
<img src="@ImageUrl(_photofileid, 400, 400)" alt="@_displayname" style="max-width: 400px" class="rounded-circle mx-auto d-block">
}
else
{
@ -31,7 +32,7 @@
<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." ResourceKey="Username"></Label>
<div class="col-sm-9">
<input id="username" class="form-control" @bind="@username" readonly />
<input id="username" class="form-control" @bind="@_username" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -47,17 +48,17 @@
<Label Class="col-sm-3" For="confirm" HelpText="If you are changing your password you must enter it again to confirm it matches" ResourceKey="Confirm"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@confirm" autocomplete="new-password" />
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
</div>
@if (allowtwofactor)
@if (_allowtwofactor)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="twofactor" HelpText="Indicates if you are using two factor authentication. Two factor authentication requires you to enter a verification code sent via email after you sign in." ResourceKey="TwoFactor"></Label>
<div class="col-sm-9">
<select id="twofactor" class="form-select" @bind="@twofactor" required>
<select id="twofactor" class="form-select" @bind="@_twofactor" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
@ -67,19 +68,31 @@
<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" />
<input id="email" class="form-control" @bind="@_email" />
</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" />
<input id="displayname" class="form-control" @bind="@_displayname" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@photofileid.ToString()" HelpText="A photo of yourself" ResourceKey="Photo"></Label>
<Label Class="col-sm-3" For="timezone" HelpText="Your time zone" ResourceKey="TimeZone">Time Zone:</Label>
<div class="col-sm-9">
<FileManager FileId="@photofileid" Filter="@PageState.Site.ImageFiles" ShowFolders="false" ShowFiles="true" UploadMultiple="false" FolderId="@folderid" @ref="filemanager" />
<select id="timezone" class="form-select" @bind="@_timezoneid">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var timezone in _timezones)
{
<option value="@timezone.Id">@timezone.DisplayName</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@_photofileid.ToString()" HelpText="A photo of yourself" ResourceKey="Photo"></Label>
<div class="col-sm-9">
<FileManager FileId="@_photofileid" Filter="@PageState.Site.ImageFiles" ShowFolders="false" ShowFiles="true" UploadMultiple="false" FolderId="@_folderid" @ref="_filemanager" />
</div>
</div>
</div>
@ -91,21 +104,21 @@
<TabPanel Name="Profile" ResourceKey="Profile">
<div class="container">
<div class="row mb-1 align-items-center">
@foreach (Profile profile in profiles)
@foreach (Profile profile in _profiles)
{
var p = profile;
if (!p.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
if (p.Category != category)
if (p.Category != _category)
{
<div class="col text-center pb-2">
@p.Category
</div>
category = p.Category;
_category = p.Category;
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label>
<div class="col-sm-9">
<div class="col-sm-9">
@if (!string.IsNullOrEmpty(p.Options))
{
@if (!string.IsNullOrEmpty(p.Autocomplete))
@ -149,22 +162,26 @@
{
@if (p.IsRequired)
{
<input id="@p.Name" class="form-control" maxlength="@p.MaxLength" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))" autocomplete="@p.Autocomplete" />
<input id="@p.Name" class="form-control" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))" autocomplete="@p.Autocomplete"
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)" />
}
else
{
<input id="@p.Name" class="form-control" maxlength="@p.MaxLength" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" autocomplete="@p.Autocomplete" />
<input id="@p.Name" class="form-control" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" autocomplete="@p.Autocomplete"
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)" />
}
}
else
{
@if (p.IsRequired)
{
<input id="@p.Name" class="form-control" maxlength="@p.MaxLength" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))" />
<input id="@p.Name" class="form-control" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))"
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)" />
}
else
{
<input id="@p.Name" class="form-control" maxlength="@p.MaxLength" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" />
<input id="@p.Name" class="form-control" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))"
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)" />
}
}
}
@ -174,22 +191,26 @@
{
@if (p.IsRequired)
{
<textarea id="@p.Name" class="form-control" maxlength="@p.MaxLength" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))" autocomplete="@p.Autocomplete"></textarea>
<textarea id="@p.Name" class="form-control" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))" autocomplete="@p.Autocomplete"
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)"></textarea>
}
else
{
<textarea id="@p.Name" class="form-control" maxlength="@p.MaxLength" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" autocomplete="@p.Autocomplete"></textarea>
<textarea id="@p.Name" class="form-control" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" autocomplete="@p.Autocomplete"
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)"></textarea>
}
}
else
{
@if (p.IsRequired)
{
<textarea id="@p.Name" class="form-control" maxlength="@p.MaxLength" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))"></textarea>
<textarea id="@p.Name" class="form-control" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))"
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)"></textarea>
}
else
{
<textarea id="@p.Name" class="form-control" maxlength="@p.MaxLength" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))"></textarea>
<textarea id="@p.Name" class="form-control" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))"
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)"></textarea>
}
}
}
@ -212,17 +233,17 @@
<option value="from">@Localizer["Items.Sent"]</option>
</select>
<br />
@if (filter == "to")
@if (_filter == "to")
{
@if (notifications.Any())
@if (_notifications.Any())
{
<Pager Items="@notifications">
<Pager Items="@_notifications">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["From"]</th>
<th>@Localizer["Subject"]</th>
<th>@Localizer["Received"]</th>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["From"]</th>
<th>@Localizer["Subject"]</th>
<th>@Localizer["Received"]</th>
</Header>
<Row>
<td><ActionLink Action="View" Parameters="@($"id=" + context.NotificationId.ToString())" Security="SecurityAccessLevel.View" EditMode="false" ResourceKey="ViewNotification" ReturnUrl="@NavigateUrl(PageState.Page.Path, "tab=Notifications")" /></td>
@ -230,13 +251,13 @@
@if (context.IsRead)
{
<td>@context.FromDisplayName</td>
<td>@(string.IsNullOrEmpty(context.FromDisplayName) ? SharedLocalizer["System"] : context.FromDisplayName)</td>
<td>@context.Subject</td>
<td>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", @context.CreatedOn)</td>
}
else
{
<td><b>@context.FromDisplayName</b></td>
<td><b>@(string.IsNullOrEmpty(context.FromDisplayName) ? SharedLocalizer["System"] : context.FromDisplayName)</b></td>
<td><b>@context.Subject</b></td>
<td><b>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", @context.CreatedOn)</b></td>
}
@ -252,15 +273,15 @@
context.Body = context.Body.Replace("\n", "");
context.Body = context.Body.Replace("\r", "");
}
notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body;
_notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body;
}
@if (context.IsRead)
{
@notificationSummary
@_notificationSummary
}
else
{
<b>@notificationSummary</b>
<b>@_notificationSummary</b>
}
</td>
</Detail>
@ -277,15 +298,15 @@
}
else
{
@if (notifications.Any())
@if (_notifications.Any())
{
<Pager Items="@notifications">
<Pager Items="@_notifications">
<Header>
<th style="width: 1px;"></th>
<th style="width: 1px;"></th>
<th>@Localizer["To"]</th>
<th>@Localizer["Subject"]</th>
<th>@Localizer["Sent"]</th>
<th style="width: 1px;"></th>
<th style="width: 1px;"></th>
<th>@Localizer["To"]</th>
<th>@Localizer["Subject"]</th>
<th>@Localizer["Sent"]</th>
</Header>
<Row>
<td><ActionLink Action="View" Parameters="@($"id=" + context.NotificationId.ToString())" Security="SecurityAccessLevel.View" EditMode="false" ResourceKey="ViewNotification" ReturnUrl="@NavigateUrl(PageState.Page.Path, "tab=Notifications")" /></td>
@ -316,15 +337,15 @@
context.Body = context.Body.Replace("\n", "");
context.Body = context.Body.Replace("\r", "");
}
notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body;
_notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body;
}
@if (context.IsRead)
{
@notificationSummary
@_notificationSummary
}
else
{
<b>@notificationSummary</b>
<b>@_notificationSummary</b>
}
</td>
</Detail>
@ -346,29 +367,32 @@
}
@code {
private List<Models.TimeZone> _timezones;
private bool _initialized = false;
private string _passwordrequirements;
private string username = string.Empty;
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 bool allowtwofactor = false;
private string twofactor = "False";
private string email = string.Empty;
private string displayname = string.Empty;
private FileManager filemanager;
private int folderid = -1;
private int photofileid = -1;
private File photo = null;
private string _ImageFiles = string.Empty;
private List<Profile> profiles;
private Dictionary<string, string> settings;
private string category = string.Empty;
private string _confirm = string.Empty;
private bool _allowtwofactor = false;
private string _twofactor = "False";
private string _email = string.Empty;
private string _displayname = string.Empty;
private FileManager _filemanager;
private int _folderid = -1;
private string _timezoneid = string.Empty;
private int _photofileid = -1;
private File _photo = null;
private string _imagefiles = string.Empty;
private string filter = "to";
private List<Notification> notifications;
private string notificationSummary = string.Empty;
private List<Profile> _profiles;
private Dictionary<string, string> _userSettings;
private string _category = string.Empty;
private string _filter = "to";
private List<Notification> _notifications;
private string _notificationSummary = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
@ -378,17 +402,19 @@
{
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_togglepassword = SharedLocalizer["ShowPassword"];
allowtwofactor = (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "true");
profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
_allowtwofactor = (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "true");
_profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
_timezones = await TimeZoneService.GetTimeZonesAsync();
if (PageState.User != null)
{
username = PageState.User.Username;
twofactor = PageState.User.TwoFactorRequired.ToString();
email = PageState.User.Email;
displayname = PageState.User.DisplayName;
_username = PageState.User.Username;
_twofactor = PageState.User.TwoFactorRequired.ToString();
_email = PageState.User.Email;
_displayname = PageState.User.DisplayName;
_timezoneid = PageState.User.TimeZoneId;
if (string.IsNullOrEmpty(email))
if (string.IsNullOrEmpty(_email))
{
AddModuleMessage(Localizer["Message.User.NoEmail"], MessageType.Warning);
}
@ -397,24 +423,24 @@
var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath);
if (folder != null)
{
folderid = folder.FolderId;
_folderid = folder.FolderId;
}
if (PageState.User.PhotoFileId != null)
{
photofileid = PageState.User.PhotoFileId.Value;
photo = await FileService.GetFileAsync(photofileid);
_photofileid = PageState.User.PhotoFileId.Value;
_photo = await FileService.GetFileAsync(_photofileid);
}
else
{
photofileid = -1;
photo = null;
_photofileid = -1;
_photo = null;
}
settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
_userSettings = PageState.User.Settings;
var sitesettings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_ImageFiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles);
_ImageFiles = (string.IsNullOrEmpty(_ImageFiles)) ? Constants.ImageFiles : _ImageFiles;
_imagefiles = SettingService.GetSetting(_userSettings, "ImageFiles", Constants.ImageFiles);
_imagefiles = (string.IsNullOrEmpty(_imagefiles)) ? Constants.ImageFiles : _imagefiles;
await LoadNotificationsAsync();
@ -434,13 +460,13 @@
private async Task LoadNotificationsAsync()
{
notifications = await NotificationService.GetNotificationsAsync(PageState.Site.SiteId, filter, PageState.User.UserId);
notifications = notifications.Where(item => item.DeletedBy != PageState.User.Username).ToList();
_notifications = await NotificationService.GetNotificationsAsync(PageState.Site.SiteId, _filter, PageState.User.UserId);
_notifications = _notifications.Where(item => item.DeletedBy != PageState.User.Username).ToList();
}
private string GetProfileValue(string SettingName, string DefaultValue)
{
string value = SettingService.GetSetting(settings, SettingName, DefaultValue);
string value = SettingService.GetSetting(_userSettings, SettingName, DefaultValue);
if (value.Contains("]"))
{
value = value.Substring(value.IndexOf("]") + 1);
@ -452,38 +478,39 @@
{
try
{
if (username != string.Empty && email != string.Empty)
if (_username != string.Empty && _email != string.Empty)
{
if (_password == confirm)
if (_password == _confirm)
{
if (ValidateProfiles())
{
var user = PageState.User;
user.Username = username;
user.Username = _username;
user.Password = _password;
user.TwoFactorRequired = bool.Parse(twofactor);
user.Email = email;
user.DisplayName = (displayname == string.Empty ? username : displayname);
user.PhotoFileId = filemanager.GetFileId();
user.TwoFactorRequired = bool.Parse(_twofactor);
user.Email = _email;
user.DisplayName = (_displayname == string.Empty ? _username : _displayname);
user.TimeZoneId = _timezoneid;
user.PhotoFileId = _filemanager.GetFileId();
if (user.PhotoFileId == -1)
{
user.PhotoFileId = null;
}
if (user.PhotoFileId != null)
{
photofileid = user.PhotoFileId.Value;
photo = await FileService.GetFileAsync(photofileid);
_photofileid = user.PhotoFileId.Value;
_photo = await FileService.GetFileAsync(_photofileid);
}
else
{
photofileid = -1;
photo = null;
_photofileid = -1;
_photo = null;
}
user = await UserService.UpdateUserAsync(user);
if (user != null)
{
await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId);
await SettingService.UpdateUserSettingsAsync(_userSettings, PageState.User.UserId);
await logger.LogInformation("User Profile Saved");
if (!string.IsNullOrEmpty(PageState.ReturnUrl))
@ -549,12 +576,12 @@
private bool ValidateProfiles()
{
foreach (Profile profile in profiles)
foreach (Profile profile in _profiles)
{
var value = GetProfileValue(profile.Name, string.Empty);
if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(profile.DefaultValue))
{
settings = SettingService.SetSetting(settings, profile.Name, profile.DefaultValue);
_userSettings = SettingService.SetSetting(_userSettings, profile.Name, profile.DefaultValue);
}
if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
@ -586,7 +613,7 @@
private void ProfileChanged(ChangeEventArgs e, string SettingName)
{
var value = (string)e.Value;
settings = SettingService.SetSetting(settings, SettingName, value);
_userSettings = SettingService.SetSetting(_userSettings, SettingName, value);
}
private async Task Delete(Notification Notification)
@ -616,7 +643,7 @@
private async void FilterChanged(ChangeEventArgs e)
{
filter = (string)e.Value;
_filter = (string)e.Value;
await LoadNotificationsAsync();
StateHasChanged();
}
@ -626,7 +653,7 @@
try
{
ShowProgressIndicator();
foreach(var Notification in notifications)
foreach(var Notification in _notifications)
{
if (!Notification.IsDeleted)
{

View File

@ -128,7 +128,7 @@
createdon = notification.CreatedOn.ToString();
body = notification.Body;
if (title == "From")
if (title == "From" && !notification.IsRead)
{
notification.IsRead = true;
notification = await NotificationService.UpdateNotificationAsync(notification);

View File

@ -5,6 +5,7 @@
@inject IUserService UserService
@inject IProfileService ProfileService
@inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService
@inject IStringLocalizer<Add> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@ -12,7 +13,7 @@
{
<TabStrip>
<TabPanel Name="Identity" ResourceKey="Identity">
@if (profiles != null)
@if (_profiles != null)
{
<div class="container">
<div class="row mb-1 align-items-center">
@ -33,6 +34,18 @@
<input id="displayname" class="form-control" @bind="@_displayname" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="timezone" HelpText="Your time zone" ResourceKey="TimeZone">Time Zone:</Label>
<div class="col-sm-9">
<select id="timezone" class="form-select" @bind="@_timezoneid">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var timezone in _timezones)
{
<option value="@timezone.Id">@timezone.DisplayName</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="notify" HelpText="Indicate if new users should receive an email notification" ResourceKey="Notify">Notify? </Label>
<div class="col-sm-9">
@ -48,20 +61,20 @@
<TabPanel Name="Profile" ResourceKey="Profile">
<div class="container">
<div class="row mb-1 align-items-center">
@foreach (Profile profile in profiles)
@foreach (Profile profile in _profiles)
{
var p = profile;
if (p.Category != category)
if (p.Category != _category)
{
<div class="col text-center pb-2">
<strong>@p.Category</strong>
</div>
category = p.Category;
_category = p.Category;
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label>
<div class="col-sm-9">
@if (!string.IsNullOrEmpty(p.Options))
<div class="col-sm-9">
@if (!string.IsNullOrEmpty(p.Options))
{
<select id="@p.Name" class="form-select" @onchange="@(e => ProfileChanged(e, p.Name))">
@foreach (var option in p.Options.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
@ -81,11 +94,11 @@
{
@if (p.Rows == 1)
{
<input id="@p.Name" class="form-control" maxlength="@p.MaxLength" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" />
<input id="@p.Name" class="form-control" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" @attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)" />
}
else
{
<textarea id="@p.Name" class="form-control" maxlength="@p.MaxLength" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))"></textarea>
<textarea id="@p.Name" class="form-control" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" @attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)"></textarea>
}
}
</div>
@ -103,14 +116,16 @@
@code {
private List<Models.TimeZone> _timezones;
private bool _initialized = false;
private string _username = string.Empty;
private string _email = string.Empty;
private string _displayname = string.Empty;
private string _timezoneid = string.Empty;
private string _notify = "True";
private List<Profile> profiles;
private Dictionary<string, string> settings;
private string category = string.Empty;
private List<Profile> _profiles;
private Dictionary<string, string> _settings;
private string _category = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
@ -118,8 +133,10 @@
{
try
{
profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
settings = new Dictionary<string, string>();
_timezones = await TimeZoneService.GetTimeZonesAsync();
_profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
_settings = new Dictionary<string, string>();
_timezoneid = PageState.Site.TimeZoneId;
_initialized = true;
}
catch (Exception ex)
@ -131,7 +148,7 @@
private string GetProfileValue(string SettingName, string DefaultValue)
{
string value = SettingService.GetSetting(settings, SettingName, DefaultValue);
string value = SettingService.GetSetting(_settings, SettingName, DefaultValue);
if (value.Contains("]"))
{
value = value.Substring(value.IndexOf("]") + 1);
@ -153,6 +170,7 @@
user.Password = ""; // will be auto generated
user.Email = _email;
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
user.TimeZoneId = _timezoneid;
user.PhotoFileId = null;
user.SuppressNotification = !bool.Parse(_notify);
@ -160,7 +178,7 @@
if (user != null)
{
await SettingService.UpdateUserSettingsAsync(settings, user.UserId);
await SettingService.UpdateUserSettingsAsync(_settings, user.UserId);
await logger.LogInformation("User Created {User}", user);
NavigationManager.NavigateTo(NavigateUrl());
}
@ -185,12 +203,12 @@
private bool ValidateProfiles()
{
foreach (Profile profile in profiles)
foreach (Profile profile in _profiles)
{
var value = GetProfileValue(profile.Name, string.Empty);
if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(profile.DefaultValue))
{
settings = SettingService.SetSetting(settings, profile.Name, profile.DefaultValue);
_settings = SettingService.SetSetting(_settings, profile.Name, profile.DefaultValue);
}
if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
@ -217,6 +235,6 @@
private void ProfileChanged(ChangeEventArgs e, string SettingName)
{
var value = (string)e.Value;
settings = SettingService.SetSetting(settings, SettingName, value);
_settings = SettingService.SetSetting(_settings, SettingName, value);
}
}

View File

@ -6,6 +6,8 @@
@inject IProfileService ProfileService
@inject ISettingService SettingService
@inject IFileService FileService
@inject ITimeZoneService TimeZoneService
@inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@ -16,13 +18,13 @@
<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="The unique username for a user. Note that this field can not be modified." ResourceKey="Username"></Label>
<Label Class="col-sm-3" For="username" HelpText="The unique username for a user. Note that this field can not be modified." ResourceKey="Username">Username:</Label>
<div class="col-sm-9">
<input id="username" class="form-control" @bind="@username" readonly />
<input id="username" class="form-control" @bind="@_username" readonly />
</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>
<Label Class="col-sm-3" For="password" HelpText="The user's password. Please choose a password which is sufficiently secure." ResourceKey="Password">Password:</Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" />
@ -31,45 +33,69 @@
</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>
<Label Class="col-sm-3" For="confirm" HelpText="Please enter the password again to confirm it matches with the value above" ResourceKey="Confirm">Confirm Password:</Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@confirm" autocomplete="new-password" />
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" />
<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>
<Label Class="col-sm-3" For="email" HelpText="The email address where the user will receive notifications" ResourceKey="Email">Email:</Label>
<div class="col-sm-9">
<input id="email" class="form-control" @bind="@email" />
<input id="email" class="form-control" @bind="@_email" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="The full name of the user" ResourceKey="DisplayName"></Label>
<Label Class="col-sm-3" For="confirmed" HelpText="Indicates if the user's email is verified" ResourceKey="Confirmed">Confirmed?</Label>
<div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@displayname" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isdeleted" HelpText="Indicate if the user is active" ResourceKey="IsDeleted"></Label>
<div class="col-sm-9">
<select id="isdeleted" class="form-select" @bind="@isdeleted">
<select id="confirmed" class="form-select" @bind="@_confirmed">
<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="lastlogin" HelpText="The date and time when the user last signed in" ResourceKey="LastLogin"></Label>
<Label Class="col-sm-3" For="displayname" HelpText="The full name of the user" ResourceKey="DisplayName">Full Name:</Label>
<div class="col-sm-9">
<input id="lastlogin" class="form-control" @bind="@lastlogin" readonly />
<input id="displayname" class="form-control" @bind="@_displayname" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lastipaddress" HelpText="The IP Address of the user recorded during their last login" ResourceKey="LastIPAddress"></Label>
<Label Class="col-sm-3" For="timezone" HelpText="Your time zone" ResourceKey="TimeZone">Time Zone:</Label>
<div class="col-sm-9">
<input id="lastipaddress" class="form-control" @bind="@lastipaddress" readonly />
<select id="timezone" class="form-select" @bind="@_timezoneid">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var timezone in _timezones)
{
<option value="@timezone.Id">@timezone.DisplayName</option>
}
</select>
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isdeleted" HelpText="Indicate if the user is active" ResourceKey="IsDeleted">Deleted?</Label>
<div class="col-sm-9">
<select id="isdeleted" class="form-select" @bind="@_isdeleted">
<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="lastlogin" HelpText="The date and time when the user last signed in" ResourceKey="LastLogin">Last Login:</Label>
<div class="col-sm-9">
<input id="lastlogin" class="form-control" @bind="@_lastlogin" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lastipaddress" HelpText="The IP Address of the user recorded during their last login" ResourceKey="LastIPAddress">Last IP Address:</Label>
<div class="col-sm-9">
<input id="lastipaddress" class="form-control" @bind="@_lastipaddress" readonly />
</div>
</div>
</div>
@ -77,20 +103,20 @@
<TabPanel Name="Profile" ResourceKey="Profile">
<div class="container">
<div class="row mb-1 align-items-center">
@foreach (Profile profile in profiles)
@foreach (Profile profile in _profiles)
{
var p = profile;
if (p.Category != category)
if (p.Category != _category)
{
<div class="col text-center pb-2">
<strong>@p.Category</strong>
</div>
category = p.Category;
_category = p.Category;
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label>
<div class="col-sm-9">
@if (!string.IsNullOrEmpty(p.Options))
<div class="col-sm-9">
@if (!string.IsNullOrEmpty(p.Options))
{
<select id="@p.Name" class="form-select" @onchange="@(e => ProfileChanged(e, p.Name))">
@foreach (var option in p.Options.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
@ -110,11 +136,11 @@
{
@if (p.Rows == 1)
{
<input id="@p.Name" class="form-control" maxlength="@p.MaxLength" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" />
<input id="@p.Name" class="form-control" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" @attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)" />
}
else
{
<textarea id="@p.Name" class="form-control" maxlength="@p.MaxLength" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))"></textarea>
<textarea id="@p.Name" class="form-control" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" @attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)"></textarea>
}
}
</div>
@ -124,39 +150,50 @@
</div>
</TabPanel>
</TabStrip>
<br />
<button type="button" class="btn btn-success" @onclick="SaveUser">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon" DeletedBy="@deletedby" DeletedOn="@deletedon"></AuditInfo>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) && PageState.Runtime != Shared.Runtime.Hybrid && !_ishost)
{
<button type="button" class="btn btn-primary ms-1" @onclick="ImpersonateUser">@Localizer["Impersonate"]</button>
}
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _isdeleted == "True")
{
<ActionDialog Header="Delete User" Message="Are You Sure You Wish To Permanently Delete This User?" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await DeleteUser())" ResourceKey="DeleteUser" />
}
<br /><br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo>
}
@code {
@code {
private List<Models.TimeZone> _timezones;
private bool _initialized = false;
private string _passwordrequirements;
private int userid;
private string username = string.Empty;
private int _userid;
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 isdeleted;
private string lastlogin;
private string lastipaddress;
private string _confirm = string.Empty;
private string _email = string.Empty;
private string _confirmed = string.Empty;
private string _displayname = string.Empty;
private string _timezoneid = string.Empty;
private string _isdeleted;
private string _lastlogin;
private string _lastipaddress;
private bool _ishost = false;
private List<Profile> profiles;
private Dictionary<string, string> settings;
private string category = string.Empty;
private List<Profile> _profiles;
private Dictionary<string, string> _settings;
private string _category = string.Empty;
private string createdby;
private DateTime createdon;
private string modifiedby;
private DateTime modifiedon;
private string deletedby;
private DateTime? deletedon;
private string _createdby;
private DateTime _createdon;
private string _modifiedby;
private DateTime _modifiedon;
private string _deletedby;
private DateTime? _deletedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
@ -166,28 +203,32 @@
{
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_togglepassword = SharedLocalizer["ShowPassword"];
profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
_profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
_timezones = await TimeZoneService.GetTimeZonesAsync();
if (PageState.QueryString.ContainsKey("id") && int.TryParse(PageState.QueryString["id"], out int UserId))
{
userid = UserId;
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId);
_userid = UserId;
var user = await UserService.GetUserAsync(_userid, PageState.Site.SiteId);
if (user != null)
{
username = user.Username;
email = user.Email;
displayname = user.DisplayName;
isdeleted = user.IsDeleted.ToString();
lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", user.LastLoginOn);
lastipaddress = user.LastIPAddress;
_username = user.Username;
_email = user.Email;
_confirmed = user.EmailConfirmed.ToString();
_displayname = user.DisplayName;
_timezoneid = PageState.User.TimeZoneId;
_isdeleted = user.IsDeleted.ToString();
_lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", UtcToLocal(user.LastLoginOn));
_lastipaddress = user.LastIPAddress;
_ishost = UserSecurity.ContainsRole(user.Roles, RoleNames.Host);
settings = await SettingService.GetUserSettingsAsync(user.UserId);
createdby = user.CreatedBy;
createdon = user.CreatedOn;
modifiedby = user.ModifiedBy;
modifiedon = user.ModifiedOn;
deletedby = user.DeletedBy;
deletedon = user.DeletedOn;
_settings = user.Settings;
_createdby = user.CreatedBy;
_createdon = user.CreatedOn;
_modifiedby = user.ModifiedBy;
_modifiedon = user.ModifiedOn;
_deletedby = user.DeletedBy;
_deletedon = user.DeletedOn;
}
}
@ -195,14 +236,14 @@
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading User {UserId} {Error}", userid, ex.Message);
await logger.LogError(ex, "Error Loading User {UserId} {Error}", _userid, ex.Message);
AddModuleMessage(Localizer["Error.User.Load"], MessageType.Error);
}
}
private string GetProfileValue(string SettingName, string DefaultValue)
{
string value = SettingService.GetSetting(settings, SettingName, DefaultValue);
string value = SettingService.GetSetting(_settings, SettingName, DefaultValue);
if (value.Contains("]"))
{
value = value.Substring(value.IndexOf("]") + 1);
@ -214,25 +255,29 @@
{
try
{
if (username != string.Empty && email != string.Empty)
if (_username != string.Empty && _email != string.Empty)
{
if (_password == confirm)
if (_password == _confirm)
{
if (ValidateProfiles())
{
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId);
var user = await UserService.GetUserAsync(_userid, PageState.Site.SiteId);
user.SiteId = PageState.Site.SiteId;
user.Username = username;
user.Username = _username;
user.Password = _password;
user.Email = email;
user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname;
user.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted));
user.Email = _email;
user.EmailConfirmed = bool.Parse(_confirmed);
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
user.TimeZoneId = _timezoneid;
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
user.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted));
}
user = await UserService.UpdateUserAsync(user);
if (user != null)
{
await SettingService.UpdateUserSettingsAsync(settings, user.UserId);
await SettingService.UpdateUserSettingsAsync(_settings, user.UserId);
await logger.LogInformation("User Saved {User}", user);
NavigationManager.NavigateTo(NavigateUrl());
}
@ -254,19 +299,57 @@
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving User {Username} {Email} {Error}", username, email, ex.Message);
await logger.LogError(ex, "Error Saving User {Username} {Email} {Error}", _username, _email, ex.Message);
AddModuleMessage(Localizer["Error.User.Save"], MessageType.Error);
}
}
private async Task ImpersonateUser()
{
try
{
await logger.LogInformation(LogFunction.Security, "User {Username} Impersonated By Administrator {Administrator}", _username, PageState.User.Username);
// post back to the server so that the cookies are set correctly
var interop = new Interop(JSRuntime);
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, returnurl = PageState.Alias.Path };
string url = Utilities.TenantUrl(PageState.Alias, "/pages/impersonate/");
await interop.SubmitForm(url, fields);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Impersonating User {Username} {Error}", _username, ex.Message);
AddModuleMessage(Localizer["Error.User.Impersonate"], MessageType.Error);
}
}
private async Task DeleteUser()
{
try
{
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _userid != PageState.User.UserId)
{
var user = await UserService.GetUserAsync(_userid, PageState.Site.SiteId);
await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId);
await logger.LogInformation("User Permanently Deleted {User}", user);
NavigationManager.NavigateTo(NavigateUrl());
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Permanently Deleting User {UserId} {Error}", _userid, ex.Message);
AddModuleMessage(Localizer["Error.DeleteUser"], MessageType.Error);
}
}
private bool ValidateProfiles()
{
foreach (Profile profile in profiles)
foreach (Profile profile in _profiles)
{
var value = GetProfileValue(profile.Name, string.Empty);
if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(profile.DefaultValue))
{
settings = SettingService.SetSetting(settings, profile.Name, profile.DefaultValue);
_settings = SettingService.SetSetting(_settings, profile.Name, profile.DefaultValue);
}
if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
@ -293,7 +376,7 @@
private void ProfileChanged(ChangeEventArgs e, string SettingName)
{
var value = (string)e.Value;
settings = SettingService.SetSetting(settings, SettingName, value);
_settings = SettingService.SetSetting(_settings, SettingName, value);
}
private void TogglePassword()

View File

@ -17,171 +17,174 @@ else
{
<TabStrip>
<TabPanel Name="Users" Heading="Users" ResourceKey="Users">
<ActionLink Action="Add" Text="Add User" Security="SecurityAccessLevel.Edit" ResourceKey="AddUser" />&nbsp;
<ActionLink Action="Add" Text="Add User" Security="SecurityAccessLevel.Edit" ResourceKey="AddUser" />&nbsp;
<ActionLink Text="Import Users" Class="btn btn-secondary ms-2" Action="Users" Security="SecurityAccessLevel.Admin" ResourceKey="ImportUsers"/>
<Pager Items="@users" RowClass="align-middle" SearchProperties="User.Username,User.Email,User.DisplayName">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th class="app-sort-th link-primary text-decoration-underline" @onclick="@(() => SortTable("Username"))">@Localizer["Username"]<i class="@(SetSortIcon("Username"))"></i></th>
<th class="app-sort-th link-primary text-decoration-underline" @onclick="@(() => SortTable("DisplayName"))">@Localizer["Name"]<i class="@(SetSortIcon("DisplayName"))"></i></th>
<th class="app-sort-th link-primary text-decoration-underline" @onclick="@(() => SortTable("Email"))">@Localizer["Email"]<i class="@(SetSortIcon("Email"))"></i></th>
<th class="app-sort-th link-primary text-decoration-underline" @onclick="@(() => SortTable("LastLoginOn"))">@Localizer["LastLoginOn"]<i class="@(SetSortIcon("LastLoginOn"))"></i></th>
</Header>
<Row>
<td>
</Header>
<Row>
<td>
<ActionLink Action="Edit" Text="Edit" Parameters="@($"id=" + context.UserId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="EditUser" />
</td>
<td>
<ActionDialog Header="Delete User" Message="@string.Format(Localizer["Confirm.User.Delete"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUser(context))" Disabled="@(context.UserId == PageState.User.UserId)" ResourceKey="DeleteUser" />
</td>
<td>
</td>
<td>
<ActionDialog Header="Delete User" Message="@string.Format(Localizer["Confirm.User.Delete"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUser(context))" Disabled="@(context.UserId == PageState.User.UserId || context.User.IsDeleted)" ResourceKey="DeleteUser" />
</td>
<td>
<ActionLink Action="Roles" Text="Roles" Parameters="@($"id=" + context.UserId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="Roles" />
</td>
<td>@context.User.Username</td>
<td>@context.User.DisplayName</td>
<td>@((MarkupString)string.Format("<a href=\"mailto:{0}\">{1}</a>", @context.User.Email, @context.User.Email))</td>
<td>@((context.User.LastLoginOn != DateTime.MinValue) ? string.Format("{0:dd-MMM-yyyy HH:mm:ss}", context.User.LastLoginOn) : "")</td>
</Row>
</Pager>
</td>
<td>@context.User.Username</td>
<td>@context.User.DisplayName</td>
<td>@((MarkupString)string.Format("<a href=\"mailto:{0}\">{1}</a>", @context.User.Email, @context.User.Email))</td>
<td>@((context.User.LastLoginOn != DateTime.MinValue) ? string.Format("{0:dd-MMM-yyyy HH:mm:ss}", UtcToLocal(context.User.LastLoginOn)) : "")</td>
</Row>
</Pager>
</TabPanel>
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings" Security="SecurityAccessLevel.Admin">
<div class="container">
<Section Name="User" Heading="User Settings" ResourceKey="UserSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowregistration" HelpText="Do you want anonymous visitors to be able to register for an account on the site" ResourceKey="AllowRegistration">Allow User Registration?</Label>
<div class="col-sm-9">
<select id="allowregistration" class="form-select" @bind="@_allowregistration">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
@if (_providertype != "")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowsitelogin" HelpText="Do you want to allow users to sign in using a username and password that is managed locally on this site? Note that you should only disable this option if you have already sucessfully configured an external login provider, or else you may lock yourself out of the site." ResourceKey="AllowSiteLogin">Allow Login?</Label>
<div class="col-sm-9">
<select id="allowsitelogin" class="form-select" @bind="@_allowsitelogin">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
else
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowsitelogin" HelpText="Do you want to allow users to sign in using a username and password that is managed locally on this site? Note that you should only disable this option if you have already sucessfully configured an external login provider, or else you may lock yourself out of the site." ResourceKey="AllowSiteLogin">Allow Login?</Label>
<div class="col-sm-9">
<input id="allowsitelogin" class="form-control" value="@SharedLocalizer["Yes"]" readonly />
</div>
</div>
}
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="twofactor" HelpText="Do you want users to use two factor authentication? Note that you should use the Disabled option until you have successfully verified that the Notification Job in Scheduled Jobs is enabled and your SMTP options in Site Settings are configured or else you will lock yourself out." ResourceKey="TwoFactor">Two Factor?</Label>
<div class="col-sm-9">
<select id="twofactor" class="form-select" @bind="@_twofactor">
<option value="false">@Localizer["Disabled"]</option>
<option value="true">@Localizer["Optional"]</option>
<option value="required">@Localizer["Required"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cookiename" HelpText="You can choose to use a custom authentication cookie name for each site. However please be aware that if you want to share an authentication cookie between sites on the same domain they need to use a consistent cookie name. Also be aware that changing the authentication cookie name will logout all current users." ResourceKey="CookieName">Cookie Name:</Label>
<div class="col-sm-9">
<input id="cookiename" class="form-control" @bind="@_cookiename" />
</div>
</div>
<div class="row mb-1 align-items-center">
<div class="container">
<Section Name="User" Heading="User Settings" ResourceKey="UserSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowregistration" HelpText="Do you want anonymous visitors to be able to register for an account on the site" ResourceKey="AllowRegistration">Allow User Registration?</Label>
<div class="col-sm-9">
<select id="allowregistration" class="form-select" @bind="@_allowregistration">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
@if (_allowregistration == "true")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="registerurl" HelpText="Optionally provide a custom registration url" ResourceKey="RegisterUrl">Register Url:</Label>
<div class="col-sm-9">
<input id="registerurl" class="form-control" @bind="@_registerurl" />
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="profileurl" HelpText="Optionally provide a custom profile url" ResourceKey="ProfileUrl">Profile Url:</Label>
<div class="col-sm-9">
<input id="profileurl" class="form-control" @bind="@_profileurl" />
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="twofactor" HelpText="Do you want users to use two factor authentication? Note that you should use the Disabled option until you have successfully verified that the Notification Job in Scheduled Jobs is enabled and your SMTP options in Site Settings are configured or else you will lock yourself out." ResourceKey="TwoFactor">Two Factor?</Label>
<div class="col-sm-9">
<select id="twofactor" class="form-select" @bind="@_twofactor">
<option value="false">@Localizer["Disabled"]</option>
<option value="true">@Localizer["Optional"]</option>
<option value="required">@Localizer["Required"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cookiename" HelpText="You can choose to use a custom authentication cookie name for each site. However please be aware that if you want to share an authentication cookie between sites on the same domain they need to use a consistent cookie name. Also be aware that changing the authentication cookie name will logout all current users." ResourceKey="CookieName">Cookie Name:</Label>
<div class="col-sm-9">
<input id="cookiename" class="form-control" @bind="@_cookiename" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cookieexpiration" HelpText="You can choose to use a custom authentication cookie expiration timespan for each site (e.g. '08:00:00' for 8 hours). The default is 14 days if not specified." ResourceKey="CookieExpiration">Cookie Expiration Timespan:</Label>
<div class="col-sm-9">
<input id="cookieexpiration" class="form-control" @bind="@_cookieexpiration" />
</div>
</div>
<div class="row mb-1 align-items-center">
<div class="col-sm-9">
<input id="cookieexpiration" class="form-control" @bind="@_cookieexpiration" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="alwaysremember" HelpText="Enabling this option will set a permanent cookie in conjunction with the Cookie Expiration Timespan, which will automatically sign in users the next time they visit the site. By default the site will use session cookies." ResourceKey="AlwaysRemember">Always Remember User?</Label>
<div class="col-sm-9">
<div class="col-sm-9">
<select id="alwaysremember" class="form-select" @bind="@_alwaysremember">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
</Section>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<Section Name="Password" Heading="Password Settings" ResourceKey="PasswordSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="minimumlength" HelpText="The Minimum Length For A Password" ResourceKey="RequiredLength">Minimum Length:</Label>
<div class="col-sm-9">
<input id="minimumlength" class="form-control" @bind="@_minimumlength" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="uniquecharacters" HelpText="The Minimum Number Of Unique Characters Which A Password Must Contain" ResourceKey="UniqueCharacters">Unique Characters:</Label>
<div class="col-sm-9">
<input id="uniquecharacters" class="form-control" @bind="@_uniquecharacters" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requiredigit" HelpText="Indicate If Passwords Must Contain A Digit" ResourceKey="RequireDigit">Require Digit?</Label>
<div class="col-sm-9">
<select id="requiredigit" class="form-select" @bind="@_requiredigit" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requireupper" HelpText="Indicate If Passwords Must Contain An Upper Case Character" ResourceKey="RequireUpper">Require Uppercase?</Label>
<div class="col-sm-9">
<select id="requireupper" class="form-select" @bind="@_requireupper" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requirelower" HelpText="Indicate If Passwords Must Contain A Lower Case Character" ResourceKey="RequireLower">Require Lowercase?</Label>
<div class="col-sm-9">
<select id="requirelower" class="form-select" @bind="@_requirelower" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requirepunctuation" HelpText="Indicate if Passwords Must Contain A Non-alphanumeric Character (ie. Punctuation)" ResourceKey="RequirePunctuation">Require Punctuation?</Label>
<div class="col-sm-9">
<select id="requirepunctuation" class="form-select" @bind="@_requirepunctuation" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</Section>
<Section Name="Lockout" Heading="Lockout Settings" ResourceKey="LockoutSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="maximum" HelpText="The Maximum Number Of Sign In Attempts Before A User Is Locked Out" ResourceKey="MaximumFailures">Maximum Failures:</Label>
<div class="col-sm-9">
<input id="maximum" class="form-control" @bind="@_maximumfailures" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lockoutduration" HelpText="The Number Of Minutes A User Should Be Locked Out" ResourceKey="LockoutDuration">Lockout Duration:</Label>
<div class="col-sm-9">
<input id="lockoutduration" class="form-control" @bind="@_lockoutduration" required />
</div>
</div>
</Section>
<Section Name="ExternalLogin" Heading="External Login Settings" ResourceKey="ExternalLoginSettings">
<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="logouteverywhere" HelpText="Do you want users to be logged out of every active session on any device, or only their current session?" ResourceKey="LogoutEverywhere">Logout Everywhere?</Label>
<div class="col-sm-9">
<select id="logouteverywhere" class="form-select" @bind="@_logouteverywhere">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
</Section>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<Section Name="Password" Heading="Password Settings" ResourceKey="PasswordSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="minimumlength" HelpText="The Minimum Length For A Password" ResourceKey="RequiredLength">Minimum Length:</Label>
<div class="col-sm-9">
<input id="minimumlength" class="form-control" @bind="@_minimumlength" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="uniquecharacters" HelpText="The Minimum Number Of Unique Characters Which A Password Must Contain" ResourceKey="UniqueCharacters">Unique Characters:</Label>
<div class="col-sm-9">
<input id="uniquecharacters" class="form-control" @bind="@_uniquecharacters" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requiredigit" HelpText="Indicate If Passwords Must Contain A Digit" ResourceKey="RequireDigit">Require Digit?</Label>
<div class="col-sm-9">
<select id="requiredigit" class="form-select" @bind="@_requiredigit" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requireupper" HelpText="Indicate If Passwords Must Contain An Upper Case Character" ResourceKey="RequireUpper">Require Uppercase?</Label>
<div class="col-sm-9">
<select id="requireupper" class="form-select" @bind="@_requireupper" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requirelower" HelpText="Indicate If Passwords Must Contain A Lower Case Character" ResourceKey="RequireLower">Require Lowercase?</Label>
<div class="col-sm-9">
<select id="requirelower" class="form-select" @bind="@_requirelower" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requirepunctuation" HelpText="Indicate if Passwords Must Contain A Non-alphanumeric Character (ie. Punctuation)" ResourceKey="RequirePunctuation">Require Punctuation?</Label>
<div class="col-sm-9">
<select id="requirepunctuation" class="form-select" @bind="@_requirepunctuation" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</Section>
<Section Name="Lockout" Heading="Lockout Settings" ResourceKey="LockoutSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="maximum" HelpText="The Maximum Number Of Sign In Attempts Before A User Is Locked Out" ResourceKey="MaximumFailures">Maximum Failures:</Label>
<div class="col-sm-9">
<input id="maximum" class="form-control" @bind="@_maximumfailures" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lockoutduration" HelpText="The Number Of Minutes A User Should Be Locked Out" ResourceKey="LockoutDuration">Lockout Duration:</Label>
<div class="col-sm-9">
<input id="lockoutduration" class="form-control" @bind="@_lockoutduration" required />
</div>
</div>
</Section>
<Section Name="ExternalLogin" Heading="External Login Settings" ResourceKey="ExternalLoginSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="provider" HelpText="Select the external login provider" ResourceKey="Provider">Provider:</Label>
<div class="col-sm-9">
@ -201,77 +204,77 @@ else
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="providertype" HelpText="Select the external login provider type" ResourceKey="ProviderType">Provider Type:</Label>
<div class="col-sm-9">
<select id="providertype" class="form-select" value="@_providertype" @onchange="(e => ProviderTypeChanged(e))">
<Label Class="col-sm-3" For="providertype" HelpText="Select the external login provider type" ResourceKey="ProviderType">Provider Type:</Label>
<div class="col-sm-9">
<select id="providertype" class="form-select" value="@_providertype" @onchange="(e => ProviderTypeChanged(e))">
<option value="" selected>&lt;@Localizer["Not Specified"]&gt;</option>
<option value="@AuthenticationProviderTypes.OpenIDConnect">@Localizer["OIDC"]</option>
<option value="@AuthenticationProviderTypes.OAuth2">@Localizer["OAuth2"]</option>
</select>
</div>
</div>
@if (_providertype != "")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="providername" HelpText="Specify a friendly name for the external login provider which will be displayed on the Login page" ResourceKey="ProviderName">Provider Name:</Label>
<div class="col-sm-9">
<input id="providername" class="form-control" @bind="@_providername" />
</div>
</div>
}
@if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="authority" HelpText="The Authority Url or Issuer Url associated with the OpenID Connect provider" ResourceKey="Authority">Authority:</Label>
<div class="col-sm-9">
<input id="authority" class="form-control" @bind="@_authority" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="metadataurl" HelpText="The discovery endpoint for obtaining metadata for this provider. Only specify if the OpenID Connect provider does not use the standard approach (ie. /.well-known/openid-configuration)" ResourceKey="MetadataUrl">Metadata Url:</Label>
<div class="col-sm-9">
<input id="metadataurl" class="form-control" @bind="@_metadataurl" />
</div>
</div>
}
@if (_providertype == AuthenticationProviderTypes.OAuth2)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="authorizationurl" HelpText="The endpoint for obtaining an Authorization Code" ResourceKey="AuthorizationUrl">Authorization Url:</Label>
<div class="col-sm-9">
<input id="authorizationurl" class="form-control" @bind="@_authorizationurl" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tokenurl" HelpText="The endpoint for obtaining an Auth Token" ResourceKey="TokenUrl">Token Url:</Label>
<div class="col-sm-9">
<input id="tokenurl" class="form-control" @bind="@_tokenurl" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="userinfourl" HelpText="The endpoint for obtaining user information. This should be an API or Page Url which contains the users email address." ResourceKey="UserInfoUrl">User Info Url:</Label>
<div class="col-sm-9">
<input id="userinfourl" class="form-control" @bind="@_userinfourl" />
</div>
</div>
}
@if (_providertype != "")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clientid" HelpText="The Client ID from the provider" ResourceKey="ClientID">Client ID:</Label>
<div class="col-sm-9">
<input id="clientid" class="form-control" @bind="@_clientid" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clientsecret" HelpText="The Client Secret from the provider" ResourceKey="ClientSecret">Client Secret:</Label>
<div class="col-sm-9">
<div class="input-group">
<input type="@_clientsecrettype" id="clientsecret" class="form-control" @bind="@_clientsecret" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleClientSecret">@_toggleclientsecret</button>
</div>
</div>
</div>
<option value="@AuthenticationProviderTypes.OpenIDConnect">@Localizer["OIDC"]</option>
<option value="@AuthenticationProviderTypes.OAuth2">@Localizer["OAuth2"]</option>
</select>
</div>
</div>
@if (_providertype != "")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="providername" HelpText="Specify a friendly name for the external login provider which will be displayed on the Login page" ResourceKey="ProviderName">Provider Name:</Label>
<div class="col-sm-9">
<input id="providername" class="form-control" @bind="@_providername" />
</div>
</div>
}
@if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="authority" HelpText="The Authority Url or Issuer Url associated with the OpenID Connect provider" ResourceKey="Authority">Authority:</Label>
<div class="col-sm-9">
<input id="authority" class="form-control" @bind="@_authority" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="metadataurl" HelpText="The discovery endpoint for obtaining metadata for this provider. Only specify if the OpenID Connect provider does not use the standard approach (ie. /.well-known/openid-configuration)" ResourceKey="MetadataUrl">Metadata Url:</Label>
<div class="col-sm-9">
<input id="metadataurl" class="form-control" @bind="@_metadataurl" />
</div>
</div>
}
@if (_providertype == AuthenticationProviderTypes.OAuth2)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="authorizationurl" HelpText="The endpoint for obtaining an Authorization Code" ResourceKey="AuthorizationUrl">Authorization Url:</Label>
<div class="col-sm-9">
<input id="authorizationurl" class="form-control" @bind="@_authorizationurl" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tokenurl" HelpText="The endpoint for obtaining an Auth Token" ResourceKey="TokenUrl">Token Url:</Label>
<div class="col-sm-9">
<input id="tokenurl" class="form-control" @bind="@_tokenurl" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="userinfourl" HelpText="The endpoint for obtaining user information. This should be an API or Page Url which contains the users email address." ResourceKey="UserInfoUrl">User Info Url:</Label>
<div class="col-sm-9">
<input id="userinfourl" class="form-control" @bind="@_userinfourl" />
</div>
</div>
}
@if (_providertype != "")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clientid" HelpText="The Client ID from the provider" ResourceKey="ClientID">Client ID:</Label>
<div class="col-sm-9">
<input id="clientid" class="form-control" @bind="@_clientid" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clientsecret" HelpText="The Client Secret from the provider" ResourceKey="ClientSecret">Client Secret:</Label>
<div class="col-sm-9">
<div class="input-group">
<input type="@_clientsecrettype" id="clientsecret" class="form-control" @bind="@_clientsecret" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleClientSecret">@_toggleclientsecret</button>
</div>
</div>
</div>
@if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{
<div class="row mb-1 align-items-center">
@ -291,32 +294,32 @@ else
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="scopes" HelpText="A list of Scopes to request from the provider (separated by commas). If none are specified, standard Scopes will be used by default." ResourceKey="Scopes">Scopes:</Label>
<div class="col-sm-9">
<input id="scopes" class="form-control" @bind="@_scopes" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="parameters" HelpText="Optionally specify any additional parameters as name/value pairs to send to the provider (separated by commas if there are multiple)." ResourceKey="Parameters">Parameters:</Label>
<div class="col-sm-9">
<input id="parameters" class="form-control" @bind="@_parameters" />
</div>
</div>
<Label Class="col-sm-3" For="scopes" HelpText="A list of Scopes to request from the provider (separated by commas). If none are specified, standard Scopes will be used by default." ResourceKey="Scopes">Scopes:</Label>
<div class="col-sm-9">
<input id="scopes" class="form-control" @bind="@_scopes" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pkce" HelpText="Indicate if the provider supports Proof Key for Code Exchange (PKCE)" ResourceKey="PKCE">Use PKCE?</Label>
<div class="col-sm-9">
<select id="pkce" class="form-select" @bind="@_pkce" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="redirecturl" HelpText="The Redirect Url (or Callback Url) which usually needs to be registered with the provider" ResourceKey="RedirectUrl">Redirect Url:</Label>
<div class="col-sm-9">
<input id="redirecturl" class="form-control" @bind="@_redirecturl" readonly />
</div>
</div>
<Label Class="col-sm-3" For="parameters" HelpText="Optionally specify any additional parameters as name/value pairs to send to the provider (separated by commas if there are multiple)." ResourceKey="Parameters">Parameters:</Label>
<div class="col-sm-9">
<input id="parameters" class="form-control" @bind="@_parameters" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pkce" HelpText="Indicate if the provider supports Proof Key for Code Exchange (PKCE)" ResourceKey="PKCE">Use PKCE?</Label>
<div class="col-sm-9">
<select id="pkce" class="form-select" @bind="@_pkce" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="redirecturl" HelpText="The Redirect Url (or Callback Url) which usually needs to be registered with the provider" ResourceKey="RedirectUrl">Redirect Url:</Label>
<div class="col-sm-9">
<input id="redirecturl" class="form-control" @bind="@_redirecturl" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="reviewclaims" HelpText="This option will record the full list of Claims returned by the Provider in the Event Log. It should only be used for testing purposes. External Login will be restricted when this option is enabled." ResourceKey="ReviewClaims">Review Claims?</Label>
<div class="col-sm-9">
@ -334,10 +337,10 @@ else
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="identifierclaimtype" HelpText="Specify the type name of the unique user identifier claim provided by the provider. The default value is 'sub'." ResourceKey="IdentifierClaimType">Identifier Claim:</Label>
<div class="col-sm-9">
<input id="identifierclaimtype" class="form-control" @bind="@_identifierclaimtype" />
</div>
</div>
<div class="col-sm-9">
<input id="identifierclaimtype" class="form-control" @bind="@_identifierclaimtype" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="nameclaimtype" HelpText="Optionally specify the type name of the user's name claim provided by the provider. The typical value is 'name'." ResourceKey="NameClaimType">Name Claim:</Label>
<div class="col-sm-9">
@ -346,16 +349,16 @@ else
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="emailclaimtype" HelpText="Optionally specify the type name of the email address claim provided by the provider. The typical value is 'email'," ResourceKey="EmailClaimType">Email Claim:</Label>
<div class="col-sm-9">
<input id="emailclaimtype" class="form-control" @bind="@_emailclaimtype" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="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="col-sm-9">
<input id="emailclaimtype" class="form-control" @bind="@_emailclaimtype" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="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">
<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">
@ -374,26 +377,35 @@ else
</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" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="domainfilter" HelpText="Provide any email domain filter criteria (separated by commas). Domains to exclude should be prefixed with an exclamation point (!). For example 'microsoft.com,!hotmail.com' would include microsoft.com email addresses but not hotmail.com email addresses." ResourceKey="DomainFilter">Domain Filter:</Label>
<div class="col-sm-9">
<input id="domainfilter" class="form-control" @bind="@_domainfilter" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="createusers" HelpText="Do you want new users to be created automatically? If you disable this option, users must already be registered on the site in order to sign in with their external login." ResourceKey="CreateUsers">Create New Users?</Label>
<div class="col-sm-9">
<select id="createusers" class="form-select" @bind="@_createusers">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<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" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="savetokens" HelpText="Specify whether access and refresh tokens should be saved after a successful login. The default is false to reduce the size of the authentication cookie." ResourceKey="SaveTokens">Save Tokens?</Label>
<div class="col-sm-9">
<select id="savetokens" class="form-select" @bind="@_savetokens" 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="domainfilter" HelpText="Provide any email domain filter criteria (separated by commas). Domains to exclude should be prefixed with an exclamation point (!). For example 'microsoft.com,!hotmail.com' would include microsoft.com email addresses but not hotmail.com email addresses." ResourceKey="DomainFilter">Domain Filter:</Label>
<div class="col-sm-9">
<input id="domainfilter" class="form-control" @bind="@_domainfilter" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="createusers" HelpText="Do you want new users to be created automatically? If you disable this option, users must already be registered on the site in order to sign in with their external login." ResourceKey="CreateUsers">Create New Users?</Label>
<div class="col-sm-9">
<select id="createusers" class="form-select" @bind="@_createusers">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="verifyusers" HelpText="Do you want existing users to perform an additional email verification step to link their external login? If you disable this option, existing users will be linked automatically." ResourceKey="VerifyUsers">Verify Existing Users?</Label>
<div class="col-sm-9">
@ -403,63 +415,86 @@ else
</select>
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowhostrole" HelpText="Indicate if host roles are supported from the identity provider. Please use caution with this option as it allows the host user to administrate every site within your installation." ResourceKey="AllowHostRole">Allow Host Role?</Label>
<div class="col-sm-9">
<select id="allowhostrole" class="form-select" @bind="@_allowhostrole" 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="allowsitelogin" HelpText="Do you want to allow users to sign in using a username and password that is managed locally on this site? Note that you should only disable this option if you have already sucessfully configured an external login provider, or else you may lock yourself out of the site." ResourceKey="AllowSiteLogin">Allow Local Login?</Label>
<div class="col-sm-9">
<select id="allowsitelogin" class="form-select" @bind="@_allowsitelogin">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
}
</Section>
<Section Name="Token" Heading="Token Settings" ResourceKey="TokenSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="jwtsecret" HelpText="If you want to want to provide API access, please specify a secret which will be used to encrypt your tokens. The secret should be 16 characters or more to ensure optimal security. Please note that if you change this secret, all existing tokens will become invalid and will need to be regenerated." ResourceKey="Secret">Secret:</Label>
<div class="col-sm-9">
<div class="input-group">
<input type="@_secrettype" id="jwtsecret" class="form-control" @bind="@_secret" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleSecret">@_togglesecret</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="issuer" HelpText="Optionally provide the issuer of the token" ResourceKey="Issuer">Issuer:</Label>
<div class="col-sm-9">
<input id="issuer" class="form-control" @bind="@_issuer" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="audience" HelpText="Optionally provide the audience for the token" ResourceKey="Audience">Audience:</Label>
<div class="col-sm-9">
<input id="audience" class="form-control" @bind="@_audience" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lifetime" HelpText="The number of minutes for which a token should be valid" ResourceKey="Lifetime">Lifetime:</Label>
<div class="col-sm-9">
<input id="lifetime" class="form-control" @bind="@_lifetime" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="token" HelpText="Select the Create Token button to generate a long-lived access token (valid for 1 year). Be sure to store this token in a safe location as you will not be able to access it in the future." ResourceKey="Token">Access Token:</Label>
<div class="col-sm-9">
<div class="input-group">
<input id="token" class="form-control" @bind="@_token" />
<button type="button" class="btn btn-secondary" @onclick="@CreateToken">@Localizer["CreateToken"]</button>
</div>
</div>
</div>
</Section>
}
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
</Section>
<Section Name="Token" Heading="Token Settings" ResourceKey="TokenSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="jwtsecret" HelpText="If you want to want to provide API access, please specify a secret which will be used to encrypt your tokens. The secret should be 16 characters or more to ensure optimal security. Please note that if you change this secret, all existing tokens will become invalid and will need to be regenerated." ResourceKey="Secret">Secret:</Label>
<div class="col-sm-9">
<div class="input-group">
<input type="@_secrettype" id="jwtsecret" class="form-control" @bind="@_secret" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleSecret">@_togglesecret</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="issuer" HelpText="Optionally provide the issuer of the token" ResourceKey="Issuer">Issuer:</Label>
<div class="col-sm-9">
<input id="issuer" class="form-control" @bind="@_issuer" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="audience" HelpText="Optionally provide the audience for the token" ResourceKey="Audience">Audience:</Label>
<div class="col-sm-9">
<input id="audience" class="form-control" @bind="@_audience" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lifetime" HelpText="The number of minutes for which a token should be valid" ResourceKey="Lifetime">Lifetime:</Label>
<div class="col-sm-9">
<input id="lifetime" class="form-control" @bind="@_lifetime" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="token" HelpText="Select the Create Token button to generate a long-lived access token (valid for 1 year). Be sure to store this token in a safe location as you will not be able to access it in the future." ResourceKey="Token">Access Token:</Label>
<div class="col-sm-9">
<div class="input-group">
<input id="token" class="form-control" @bind="@_token" />
<button type="button" class="btn btn-secondary" @onclick="@CreateToken">@Localizer["CreateToken"]</button>
</div>
</div>
</div>
</Section>
}
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
</TabPanel>
</TabStrip>
</TabStrip>
}
@code {
private List<UserRole> users;
private string _allowregistration;
private string _allowsitelogin;
private string _registerurl;
private string _profileurl;
private string _twofactor;
private string _cookiename;
private string _cookieexpiration;
private string _alwaysremember;
private string _logouteverywhere;
private string _minimumlength;
private string _uniquecharacters;
@ -497,9 +532,12 @@ else
private string _roleclaimmappings;
private string _synchronizeroles;
private string _profileclaimtypes;
private string _savetokens;
private string _domainfilter;
private string _createusers;
private string _verifyusers;
private string _allowhostrole;
private string _allowsitelogin;
private string _secret;
private string _secrettype = "password";
@ -519,8 +557,9 @@ else
await LoadUsersAsync(true);
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_allowregistration = PageState.Site.AllowRegistration.ToString();
_allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true");
_allowregistration = PageState.Site.AllowRegistration.ToString().ToLower();
_registerurl = SettingService.GetSetting(settings, "LoginOptions:RegisterUrl", "");
_profileurl = SettingService.GetSetting(settings, "LoginOptions:ProfileUrl", "");
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
@ -528,6 +567,7 @@ else
_cookiename = SettingService.GetSetting(settings, "LoginOptions:CookieName", ".AspNetCore.Identity.Application");
_cookieexpiration = SettingService.GetSetting(settings, "LoginOptions:CookieExpiration", "");
_alwaysremember = SettingService.GetSetting(settings, "LoginOptions:AlwaysRemember", "false");
_logouteverywhere = SettingService.GetSetting(settings, "LoginOptions:LogoutEverywhere", "false");
_minimumlength = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredLength", "6");
_uniquecharacters = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", "1");
@ -577,9 +617,12 @@ else
_roleclaimmappings = SettingService.GetSetting(settings, "ExternalLogin:RoleClaimMappings", "");
_synchronizeroles = SettingService.GetSetting(settings, "ExternalLogin:SynchronizeRoles", "false");
_profileclaimtypes = SettingService.GetSetting(settings, "ExternalLogin:ProfileClaimTypes", "");
_savetokens = SettingService.GetSetting(settings, "ExternalLogin:SaveTokens", "false");
_domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", "");
_createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true");
_verifyusers = SettingService.GetSetting(settings, "ExternalLogin:VerifyUsers", "true");
_allowhostrole = SettingService.GetSetting(settings, "ExternalLogin:AllowHostRole", "false");
_allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true");
}
private async Task LoadUsersAsync(bool load)
@ -600,19 +643,31 @@ else
{
try
{
var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId);
if (user != null)
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId);
await logger.LogInformation("User Deleted {User}", UserRole.User);
await LoadUsersAsync(true);
StateHasChanged();
var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId);
if (user != null)
{
user.IsDeleted = true;
await UserService.UpdateUserAsync(user);
await logger.LogInformation("User Soft Deleted {User}", user);
}
}
else
{
var userrole = await UserRoleService.GetUserRoleAsync(UserRole.UserRoleId);
userrole.ExpiryDate = DateTime.UtcNow;
await UserRoleService.UpdateUserRoleAsync(userrole);
await logger.LogInformation("User {Username} Expired From Role {Role}", userrole.User.Username, userrole.Role.Name);
}
AddModuleMessage(Localizer["Success.DeleteUser"], MessageType.Success);
await LoadUsersAsync(true);
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting User {User} {Error}", UserRole.User, ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
AddModuleMessage(Localizer["Error.DeleteUser"], MessageType.Error);
}
}
@ -625,14 +680,16 @@ else
await SiteService.UpdateSiteAsync(site);
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
settings = SettingService.SetSetting(settings, "LoginOptions:AllowSiteLogin", _allowsitelogin, false);
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
settings = SettingService.SetSetting(settings, "LoginOptions:RegisterUrl", _registerurl, false);
settings = SettingService.SetSetting(settings, "LoginOptions:ProfileUrl", _profileurl, false);
settings = SettingService.SetSetting(settings, "LoginOptions:TwoFactor", _twofactor, false);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, true);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieExpiration", _cookieexpiration, true);
settings = SettingService.SetSetting(settings, "LoginOptions:AlwaysRemember", _alwaysremember, false);
settings = SettingService.SetSetting(settings, "LoginOptions:LogoutEverywhere", _logouteverywhere, false);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredLength", _minimumlength, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", _uniquecharacters, true);
@ -666,9 +723,12 @@ else
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:SaveTokens", _savetokens, 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);
settings = SettingService.SetSetting(settings, "ExternalLogin:AllowHostRole", _allowhostrole, true);
settings = SettingService.SetSetting(settings, "LoginOptions:AllowSiteLogin", _allowsitelogin, false);
settings = SettingService.SetSetting(settings, "JwtOptions:Secret", _secret, true);
settings = SettingService.SetSetting(settings, "JwtOptions:Issuer", _issuer, true);

View File

@ -53,17 +53,17 @@ else
<p align="center">
<Pager Items="@userroles">
<Header>
<th>@Localizer["Roles"]</th>
<th>@Localizer["Effective"]</th>
<th>@Localizer["Expiry"]</th>
<th>&nbsp;</th>
<th>@Localizer["Roles"]</th>
<th>@Localizer["Effective"]</th>
<th>@Localizer["Expiry"]</th>
<th>&nbsp;</th>
</Header>
<Row>
<td>@context.Role.Name</td>
<td>@Utilities.UtcAsLocalDate(context.EffectiveDate)</td>
<td>@Utilities.UtcAsLocalDate(context.ExpiryDate)</td>
<td>
<ActionDialog Header="Remove Role" Message="@string.Format(Localizer["Confirm.User.RemoveRole"], context.Role.Name)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.IsAutoAssigned || (context.Role.Name == RoleNames.Host && userid == PageState.User.UserId))" ResourceKey="DeleteUserRole" />
<ActionDialog Header="Remove Role" Message="@string.Format(Localizer["Confirm.User.RemoveRole"], context.Role.Name)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.Name == RoleNames.Host && userid == PageState.User.UserId)" ResourceKey="DeleteUserRole" />
</td>
</Row>
</Pager>
@ -171,8 +171,18 @@ else
{
try
{
await UserRoleService.DeleteUserRoleAsync(UserRoleId);
await logger.LogInformation("User Removed From Role {UserRoleId}", UserRoleId);
var userrole = await UserRoleService.GetUserRoleAsync(UserRoleId);
if (userrole.Role.Name == RoleNames.Registered)
{
userrole.ExpiryDate = DateTime.UtcNow;
await UserRoleService.UpdateUserRoleAsync(userrole);
await logger.LogInformation("User {Username} Expired From Role {Role}", userrole.User.Username, userrole.Role.Name);
}
else
{
await UserRoleService.DeleteUserRoleAsync(UserRoleId);
await logger.LogInformation("User {Username} Removed From Role {Role}", userrole.User.Username, userrole.Role.Name);
}
AddModuleMessage(Localizer["Success.User.Remove"], MessageType.Success);
await GetUserRoles();
StateHasChanged();

View File

@ -101,8 +101,8 @@
_url = visitor.Url;
_referrer = visitor.Referrer;
_visits = visitor.Visits.ToString();
_visited = visitor.VisitedOn.ToString(CultureInfo.CurrentCulture);
_created = visitor.CreatedOn.ToString(CultureInfo.CurrentCulture);
_visited = UtcToLocal(visitor.VisitedOn).Value.ToString(CultureInfo.CurrentCulture);
_created = UtcToLocal(visitor.CreatedOn).Value.ToString(CultureInfo.CurrentCulture);
if (visitor.UserId != null)
{

View File

@ -53,8 +53,8 @@ else
</td>
<td>@context.Language</td>
<td>@context.Visits</td>
<td>@context.VisitedOn</td>
<td>@context.CreatedOn</td>
<td>@UtcToLocal(context.VisitedOn)</td>
<td>@UtcToLocal(context.CreatedOn)</td>
</Row>
</Pager>
</TabPanel>
@ -100,6 +100,18 @@ else
<br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
</TabPanel>
<TabPanel Name="Robots" Heading="Robots.txt" ResourceKey="Robots">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="robots" HelpText="Specify your robots.txt instructions to provide bots with guidance on which parts of your site should be indexed" ResourceKey="Robots">Instructions: </Label>
<div class="col-sm-9">
<textarea id="robots" class="form-control" @bind="@_robots" rows="3"></textarea>
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
</TabPanel>
</TabStrip>
}
@ -113,6 +125,7 @@ else
private string _filter = "";
private int _retention = 30;
private string _correlation = "true";
private string _robots = "";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
@ -139,7 +152,8 @@ else
_filter = SettingService.GetSetting(settings, "VisitorFilter", Constants.DefaultVisitorFilter);
_retention = int.Parse(SettingService.GetSetting(settings, "VisitorRetention", "30"));
_correlation = SettingService.GetSetting(settings, "VisitorCorrelation", "true");
}
_robots = SettingService.GetSetting(settings, "Robots", "");
}
private async void TypeChanged(ChangeEventArgs e)
{
@ -191,6 +205,7 @@ else
settings = SettingService.SetSetting(settings, "VisitorFilter", _filter, true);
settings = SettingService.SetSetting(settings, "VisitorRetention", _retention.ToString(), true);
settings = SettingService.SetSetting(settings, "VisitorCorrelation", _correlation, true);
settings = SettingService.SetSetting(settings, "Robots", _robots, true);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);

View File

@ -22,9 +22,9 @@
<div class="modal-footer">
@if (!string.IsNullOrEmpty(Action))
{
<button type="button" class="@Class" @onclick="Confirm">@((MarkupString)_iconSpan) @Text</button>
<button type="button" class="@ConfirmClass" @onclick="Confirm">@((MarkupString)_iconSpan) @Text</button>
}
<button type="button" class="btn btn-secondary" @onclick="DisplayModal">@SharedLocalizer["Cancel"]</button>
<button type="button" class="@CancelClass" @onclick="DisplayModal">@SharedLocalizer["Cancel"]</button>
</div>
</div>
</div>
@ -66,12 +66,12 @@ else
{
<form method="post" @formname="@($"ActionDialogConfirmForm:{ModuleState.PageModuleId}:{Id}")" @onsubmit="Confirm" data-enhance>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<button type="submit" class="@Class">@((MarkupString)_iconSpan) @Text</button>
<button type="submit" class="@ConfirmClass">@((MarkupString)_iconSpan) @Text</button>
</form>
}
<form method="post" @formname="@($"ActionDialogCancelForm:{ModuleState.PageModuleId}:{Id}")" @onsubmit="DisplayModal" data-enhance>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<button type="submit" class="btn btn-secondary">@SharedLocalizer["Cancel"]</button>
<button type="submit" class="@CancelClass">@SharedLocalizer["Cancel"]</button>
</form>
</div>
</div>
@ -128,6 +128,12 @@ else
[Parameter]
public string Class { get; set; } // optional
[Parameter]
public string ConfirmClass { get; set; } // optional - for Confirm modal button
[Parameter]
public string CancelClass { get; set; } // optional - for Cancel modal button
[Parameter]
public bool Disabled { get; set; } // optional
@ -168,6 +174,16 @@ else
Class = "btn btn-success";
}
if (string.IsNullOrEmpty(ConfirmClass))
{
ConfirmClass = Class;
}
if (string.IsNullOrEmpty(CancelClass))
{
CancelClass = "btn btn-secondary";
}
if (!string.IsNullOrEmpty(EditMode))
{
_editmode = bool.Parse(EditMode);
@ -196,7 +212,7 @@ else
_openIconSpan = $"<span class=\"{IconName}\"></span>{(IconOnly ? "" : "&nbsp")}";
_iconSpan = $"<span class=\"{IconName}\"></span>&nbsp";
}
_permissions = (PermissionList == null) ? ModuleState.PermissionList : PermissionList;
_authorized = IsAuthorized();

View File

@ -52,7 +52,7 @@
if (CreatedOn != null)
{
_text += $" {Localizer["On"]} <b>{CreatedOn.Value.ToString(DateTimeFormat)}</b>";
_text += $" {Localizer["On"]} <b>{UtcToLocal(CreatedOn).Value.ToString(DateTimeFormat)}</ b >";
}
_text += "</p>";
@ -69,7 +69,7 @@
if (ModifiedOn != null)
{
_text += $" {Localizer["On"]} <b>{ModifiedOn.Value.ToString(DateTimeFormat)}</b>";
_text += $" {Localizer["On"]} <b>{UtcToLocal(ModifiedOn).Value.ToString(DateTimeFormat)}</ b >";
}
_text += "</p>";
@ -86,7 +86,7 @@
if (DeletedOn != null)
{
_text += $" {Localizer["On"]} <b>{DeletedOn.Value.ToString(DateTimeFormat)}</b>";
_text += $" {Localizer["On"]} <b>{UtcToLocal(DeletedOn).Value.ToString(DateTimeFormat)}</ b >";
}
_text += "</p>";

View File

@ -3,8 +3,8 @@
@inherits ModuleControlBase
@inject IFolderService FolderService
@inject IFileService FileService
@inject ISettingService SettingService
@inject IUserService UserService
@inject ISettingService SettingService
@inject IStringLocalizer<FileManager> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@ -157,9 +157,19 @@
[Parameter]
public bool UploadMultiple { get; set; } = false; // optional - enable multiple file uploads - default false
[Parameter]
public int ChunkSize { get; set; } = 1; // optional - size of file chunks to upload in MB
[Parameter]
public EventCallback<int> OnUpload { get; set; } // optional - executes a method in the calling component when a file is uploaded
[Parameter]
public EventCallback<int> OnSelectFolder { get; set; } // optional - executes a method in the calling component when a folder is selected
[Parameter]
public EventCallback<int> OnSelectFile { get; set; } // optional - executes a method in the calling component when a file is selected
[Obsolete("Use OnSelectFile instead.")]
[Parameter]
public EventCallback<int> OnSelect { get; set; } // optional - executes a method in the calling component when a file is selected
@ -297,6 +307,8 @@
FileId = -1;
_file = null;
_image = string.Empty;
await OnSelectFolder.InvokeAsync(FolderId);
StateHasChanged();
}
catch (Exception ex)
@ -312,7 +324,10 @@
_message = string.Empty;
FileId = int.Parse((string)e.Value);
await SetImage();
#pragma warning disable CS0618
await OnSelect.InvokeAsync(FileId);
#pragma warning restore CS0618
await OnSelectFile.InvokeAsync(FileId);
StateHasChanged();
}
@ -359,6 +374,8 @@
}
if (restricted == "")
{
CancellationTokenSource tokenSource = new CancellationTokenSource();
try
{
// upload the files
@ -377,57 +394,21 @@
}
}
var chunksize = ChunkSize;
if (chunksize == 1)
{
// if ChunkSize parameter is not overridden use the site setting
chunksize = int.Parse(SettingService.GetSetting(PageState.Site.Settings, "MaxChunkSize", "1"));
}
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
var success = true;
int upload = 0;
while (upload < uploads.Length && success)
{
success = false;
var filename = uploads[upload].Split(':')[0];
var size = Int64.Parse(uploads[upload].Split(':')[1]); // bytes
var megabits = (size / 1048576.0) * 8; // binary conversion
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
int attempts = 0;
while (attempts < maxattempts && !success)
{
attempts += 1;
Thread.Sleep(sleep);
if (Folder == Constants.PackagesFolder)
{
var files = await FileService.GetFilesAsync(folder);
if (files != null && files.Any(item => item.Name == filename))
{
success = true;
}
}
else
{
var file = await FileService.GetFileAsync(int.Parse(folder), filename);
if (file != null)
{
success = true;
}
}
}
if (success)
{
upload++;
}
}
// upload files
var success = await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt, chunksize, tokenSource.Token);
// reset progress indicators
if (ShowProgress)
@ -452,7 +433,7 @@
}
else
{
await logger.LogInformation("File Upload Failed Or Is Still In Progress {Files}", uploads);
await logger.LogError("File Upload Failed {Files}", uploads);
_message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error;
}
@ -469,7 +450,10 @@
{
FileId = file.FileId;
await SetImage();
#pragma warning disable CS0618
await OnSelect.InvokeAsync(FileId);
#pragma warning restore CS0618
await OnSelectFile.InvokeAsync(FileId);
await OnUpload.InvokeAsync(FileId);
}
await GetFiles();
@ -482,6 +466,10 @@
_message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error;
_uploading = false;
await tokenSource.CancelAsync();
}
finally {
tokenSource.Dispose();
}
}
@ -516,7 +504,10 @@
await GetFiles();
FileId = -1;
await SetImage();
#pragma warning disable CS0618
await OnSelect.InvokeAsync(FileId);
#pragma warning restore CS0618
await OnSelectFile.InvokeAsync(FileId);
StateHasChanged();
}
catch (Exception ex)

View File

@ -60,7 +60,7 @@
@foreach (User user in _users)
{
<tr>
<td>@user.DisplayName</td>
<td>@user.DisplayName (@user.Username)</td>
@foreach (var permissionname in _permissionnames)
{
<td style="text-align: center; width: 1px;">
@ -270,8 +270,8 @@
private async Task<Dictionary<string, string>> GetUsers(string filter)
{
var users = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Registered);
return users.Where(item => item.User.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase))
.ToDictionary(item => item.UserId.ToString(), item => item.User.DisplayName);
return users.Where(item => item.User.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase) || item.User.Username.Contains(filter, StringComparison.OrdinalIgnoreCase))
.ToDictionary(item => item.UserId.ToString(), item => item.User.DisplayName + " (" + item.User.Username + ")");
}
private async Task AddUser()

View File

@ -30,8 +30,6 @@ else
[Parameter]
public SecurityAccessLevel? Security { get; set; } // optional - can be used to specify SecurityAccessLevel
public bool IsActive { get; set; }
protected override void OnParametersSet()
{
base.OnParametersSet();

View File

@ -8,9 +8,18 @@
@foreach (TabPanel tabPanel in _tabPanels)
{
<li class="nav-item" @key="tabPanel.Name">
<a class="nav-link @(tabPanel.IsActive ? "active" : "")" data-bs-toggle="tab" href="#@(Id + tabPanel.Name)" role="tab" @onclick:preventDefault="true" @onclick="() => SetActiveTab(tabPanel.Name)">
@tabPanel.DisplayHeading()
</a>
@if (tabPanel.Name.ToLower() == ActiveTab.ToLower())
{
<a class="nav-link active" data-bs-toggle="tab" href="#@(Id + tabPanel.Name)" role="tab" @onclick:preventDefault="true">
@tabPanel.DisplayHeading()
</a>
}
else
{
<a class="nav-link" data-bs-toggle="tab" href="#@(Id + tabPanel.Name)" role="tab" @onclick:preventDefault="true">
@tabPanel.DisplayHeading()
</a>
}
</li>
}
</ul>
@ -50,43 +59,16 @@
}
}
protected override void OnParametersSet()
protected override void OnParametersSet()
{
base.OnParametersSet();
if (PageState.QueryString.ContainsKey("tab"))
{
ActiveTab = PageState.QueryString["tab"];
}
if (_tabPanels == null || Refresh)
{
_tabPanels = new List<TabPanel>();
}
// Ensure the active tab is valid and exists
if (!string.IsNullOrEmpty(ActiveTab) && _tabPanels.Any())
{
var activeTabExists = _tabPanels.Any(tp => tp.Name.Equals(ActiveTab, StringComparison.OrdinalIgnoreCase));
if (!activeTabExists)
{
ActiveTab = _tabPanels[0].Name;
}
}
// Update the active tab in the UI
UpdateActiveTab();
}
private void UpdateActiveTab()
{
if (!string.IsNullOrEmpty(ActiveTab) && _tabPanels != null)
{
foreach (var tabPanel in _tabPanels)
{
tabPanel.IsActive = tabPanel.Name.Equals(ActiveTab, StringComparison.OrdinalIgnoreCase);
}
}
}
internal void AddTabPanel(TabPanel tabPanel)
@ -94,20 +76,12 @@
if (!_tabPanels.Exists(item => item.Name == tabPanel.Name) && IsAuthorized(tabPanel))
{
_tabPanels.Add(tabPanel);
if (string.IsNullOrEmpty(ActiveTab))
{
ActiveTab = tabPanel.Name;
}
UpdateActiveTab();
StateHasChanged();
}
}
private void SetActiveTab(string tabName)
{
ActiveTab = tabName;
UpdateActiveTab();
StateHasChanged();
if (string.IsNullOrEmpty(ActiveTab))
{
ActiveTab = tabPanel.Name;
}
}
private bool IsAuthorized(TabPanel tabPanel)

View File

@ -2,6 +2,7 @@
@namespace Oqtane.Modules.HtmlText
@inherits ModuleBase
@inject IHtmlTextService HtmlTextService
@inject ISettingService SettingService
@inject IStringLocalizer<Index> Localizer
@if (PageState.EditMode)
@ -36,6 +37,10 @@
{
content = htmltext.Content;
content = Utilities.FormatContent(content, PageState.Alias, "render");
if (bool.Parse(SettingService.GetSetting(ModuleState.Settings, "DynamicTokens", "false")))
{
content = ReplaceTokens(content);
}
}
else
{

View File

@ -15,7 +15,7 @@ namespace Oqtane.Modules.HtmlText
Version = "1.0.1",
ServerManagerType = "Oqtane.Modules.HtmlText.Manager.HtmlTextManager, Oqtane.Server",
ReleaseVersions = "1.0.0,1.0.1",
SettingsType = string.Empty,
SettingsType = "Oqtane.Modules.HtmlText.Settings, Oqtane.Client",
Resources = new List<Resource>()
{
new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Module.css" }

View File

@ -0,0 +1,55 @@
@namespace Oqtane.Modules.HtmlText
@inherits ModuleBase
@inject ISettingService SettingService
@implements Oqtane.Interfaces.ISettingsControl
@inject IStringLocalizer<Settings> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<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="dynamictokens" ResourceKey="DynamicTokens" ResourceType="@resourceType" HelpText="Do you wish to allow tokens to be dynamically replaced? Please note that this will affect the performance of your site.">Dynamic Tokens? </Label>
<div class="col-sm-9">
<select id="dynamictokens" class="form-select" @bind="@_dynamictokens">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
</form>
@code {
private string resourceType = "Oqtane.Modules.HtmlText.Settings, Oqtane.Client"; // for localization
private ElementReference form;
private bool validated = false;
private string _dynamictokens;
protected override void OnInitialized()
{
try
{
_dynamictokens = SettingService.GetSetting(ModuleState.Settings, "DynamicTokens", "false");
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
public async Task UpdateSettings()
{
try
{
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
settings = SettingService.SetSetting(settings, "DynamicTokens", _dynamictokens);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
}

View File

@ -1,23 +1,25 @@
using Microsoft.AspNetCore.Components;
using Oqtane.Shared;
using Oqtane.Models;
using System.Threading.Tasks;
using Oqtane.Services;
using System;
using Oqtane.Enums;
using Oqtane.UI;
using System.Collections.Generic;
using Microsoft.JSInterop;
using System.Linq;
using System.Dynamic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using Oqtane.Enums;
using Oqtane.Models;
using Oqtane.Services;
using Oqtane.Shared;
using Oqtane.UI;
namespace Oqtane.Modules
{
public abstract class ModuleBase : ComponentBase, IModuleControl
{
private Logger _logger;
private string _urlparametersstate;
private string _urlparametersstate = string.Empty;
private Dictionary<string, string> _urlparameters;
private bool _scriptsloaded = false;
protected Logger logger => _logger ?? (_logger = new Logger(this));
@ -34,7 +36,7 @@ namespace Oqtane.Modules
protected PageState PageState { get; set; }
[CascadingParameter]
protected Module ModuleState { get; set; }
protected Models.Module ModuleState { get; set; }
[Parameter]
public RenderModeBoundary RenderModeBoundary { get; set; }
@ -60,7 +62,7 @@ namespace Oqtane.Modules
public Dictionary<string, string> UrlParameters {
get
{
if (_urlparametersstate == null || _urlparametersstate != PageState.UrlParameters)
if (string.IsNullOrEmpty(_urlparametersstate) || _urlparametersstate != PageState.UrlParameters)
{
_urlparametersstate = PageState.UrlParameters;
_urlparameters = GetUrlParameters(UrlParametersTemplate);
@ -77,18 +79,21 @@ namespace Oqtane.Modules
{
List<Resource> resources = null;
var type = GetType();
if (type.BaseType == typeof(ModuleBase))
if (type.IsSubclassOf(typeof(ModuleBase)))
{
if (PageState.Page.Resources != null)
if (type.IsSubclassOf(typeof(ModuleControlBase)))
{
resources = PageState.Page.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Module && item.Namespace == type.Namespace).ToList();
if (Resources != null)
{
resources = Resources.Where(item => item.ResourceType == ResourceType.Script).ToList();
}
}
}
else // modulecontrolbase
{
if (Resources != null)
else // ModuleBase
{
resources = Resources.Where(item => item.ResourceType == ResourceType.Script).ToList();
if (PageState.Page.Resources != null)
{
resources = PageState.Page.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Module && item.Namespace == type.Namespace).ToList();
}
}
}
if (resources != null && resources.Any())
@ -103,12 +108,12 @@ namespace Oqtane.Modules
if (!string.IsNullOrEmpty(resource.Url))
{
var url = (resource.Url.Contains("://")) ? resource.Url : PageState.Alias.BaseUrl + resource.Url;
scripts.Add(new { href = url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", es6module = resource.ES6Module, location = resource.Location.ToString().ToLower() });
scripts.Add(new { href = url, type = resource.Type ?? "", bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", location = resource.Location.ToString().ToLower(), dataAttributes = resource.DataAttributes });
}
else
{
inline += 1;
await interop.IncludeScript(GetType().Namespace.ToLower() + inline.ToString(), "", "", "", resource.Content, resource.Location.ToString().ToLower());
await interop.IncludeScript(GetType().Namespace.ToLower() + inline.ToString(), "", "", "", resource.Type ?? "", resource.Content, resource.Location.ToString().ToLower());
}
}
}
@ -117,6 +122,7 @@ namespace Oqtane.Modules
await interop.IncludeScripts(scripts.ToArray());
}
}
_scriptsloaded = true;
}
}
@ -125,6 +131,14 @@ namespace Oqtane.Modules
return PageState?.RenderId == ModuleState?.RenderId;
}
public bool ScriptsLoaded
{
get
{
return _scriptsloaded;
}
}
// path method
public string ModulePath()
@ -132,6 +146,15 @@ namespace Oqtane.Modules
return PageState?.Alias.BaseUrl + "/Modules/" + GetType().Namespace + "/";
}
// fingerprint hash code for static assets
public string Fingerprint
{
get
{
return ModuleState.ModuleDefinition.Fingerprint;
}
}
// url methods
// navigate url
@ -394,6 +417,118 @@ namespace Oqtane.Modules
await interop.ScrollTo(0, 0, "smooth");
}
public string ReplaceTokens(string content)
{
return ReplaceTokens(content, null);
}
public string ReplaceTokens(string content, object obj)
{
// Using StringBuilder avoids the performance penalty of repeated string allocations
// that occur with string.Replace or string concatenation inside loops.
var sb = new StringBuilder();
var cache = new Dictionary<string, string>(); // Cache to store resolved tokens
int index = 0;
// Loop through content to find and replace all tokens
while (index < content.Length)
{
int start = content.IndexOf('[', index); // Find start of token
if (start == -1)
{
sb.Append(content, index, content.Length - index); // Append remaining content
break;
}
int end = content.IndexOf(']', start); // Find end of token
if (end == -1)
{
sb.Append(content, index, content.Length - index); // Append unmatched content
break;
}
sb.Append(content, index, start - index); // Append content before token
string token = content.Substring(start + 1, end - start - 1); // Extract token without brackets
string[] parts = token.Split('|', 2); // Separate default fallback if present
string key = parts[0];
string fallback = parts.Length == 2 ? parts[1] : null;
if (!cache.TryGetValue(token, out string replacement)) // Check cache first
{
replacement = "[" + token + "]"; // Default replacement is original token
string[] segments = key.Split(':');
if (segments.Length >= 2)
{
object current = GetTarget(segments[0], obj); // Start from root object
for (int i = 1; i < segments.Length && current != null; i++)
{
var type = current.GetType();
var prop = type.GetProperty(segments[i]);
current = prop?.GetValue(current);
}
if (current != null)
{
replacement = current.ToString();
}
else if (fallback != null)
{
replacement = fallback; // Use fallback if available
}
}
cache[token] = replacement; // Store in cache
}
sb.Append(replacement); // Append replacement value
index = end + 1; // Move index past token
}
return sb.ToString();
}
// Resolve the object instance for a given object name
// Easy to extend with additional object types
private object GetTarget(string name, object obj)
{
return name switch
{
"ModuleState" => ModuleState,
"PageState" => PageState,
_ => (obj != null && obj.GetType().Name == name) ? obj : null // Fallback to obj
};
}
// date methods
public DateTime? UtcToLocal(DateTime? datetime)
{
TimeZoneInfo timezone = null;
if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId))
{
timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.User.TimeZoneId);
}
else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId))
{
timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.Site.TimeZoneId);
}
return Utilities.UtcAsLocalDateTime(datetime, timezone);
}
public DateTime? LocalToUtc(DateTime? datetime)
{
TimeZoneInfo timezone = null;
if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId))
{
timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.User.TimeZoneId);
}
else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId))
{
timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.Site.TimeZoneId);
}
return Utilities.LocalDateAndTimeAsUtc(datetime, timezone);
}
// logging methods
public async Task Log(Alias alias, LogLevel level, string function, Exception exception, string message, params object[] args)
{

View File

@ -4,7 +4,7 @@
<TargetFramework>net9.0</TargetFramework>
<OutputType>Exe</OutputType>
<Configurations>Debug;Release</Configurations>
<Version>6.0.1</Version>
<Version>6.1.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/v6.0.1</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>
@ -22,16 +22,22 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Oqtane.Shared\Oqtane.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="Installer\Controls\AzureSqlConfig.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
</ItemGroup>
<PropertyGroup>
<PublishTrimmed>false</PublishTrimmed>
<BlazorEnableCompression>false</BlazorEnableCompression>

View File

@ -8,20 +8,20 @@
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Oqtane.Client": {
"Oqtane": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:44358/"
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -121,7 +121,7 @@
<value>Server:</value>
</data>
<data name="Server.HelpText" xml:space="preserve">
<value>Enter the database server name. This might include a port number as well if you are using a cloud service (ie. servername.database.windows.net,1433) </value>
<value>Enter the database server name. This might include a port number as well if you are using a cloud service.</value>
</data>
<data name="Database.Text" xml:space="preserve">
<value>Database:</value>

View File

@ -186,4 +186,10 @@
<data name="Message.Username.Invalid" xml:space="preserve">
<value>The Username Provided Does Not Meet The System Requirement, It Can Only Contains Letters Or Digits.</value>
</data>
<data name="Name.Text" xml:space="preserve">
<value>Full Name:</value>
</data>
<data name="Name.HelpText" xml:space="preserve">
<value>Provide the full name of the host user</value>
</data>
</root>

View File

@ -135,9 +135,6 @@
<data name="Error.Folder.Save" xml:space="preserve">
<value>Error Saving Folder</value>
</data>
<data name="Message.Folder.Files.InvalidDelete" xml:space="preserve">
<value>Folder Has Files And Cannot Be Deleted</value>
</data>
<data name="Message.Folder.Subfolders.InvalidDelete" xml:space="preserve">
<value>Folder Has Subfolders And Cannot Be Deleted</value>
</data>
@ -175,7 +172,7 @@
<value>Capacity:</value>
</data>
<data name="ImageSizes.HelpText" xml:space="preserve">
<value>Enter a list of image sizes which can be generated dynamically from uploaded images (ie. 200x200,400x400). Use * to indicate the folder supports all image sizes.</value>
<value>Optionally enter a list of image sizes which can be generated dynamically from uploaded images (ie. 200x200,400x400). Use * to indicate the folder supports all image sizes (not recommended).</value>
</data>
<data name="ImageSizes.Text" xml:space="preserve">
<value>Image Sizes:</value>
@ -198,4 +195,10 @@
<data name="Settings.Heading" xml:space="preserve">
<value>Settings</value>
</data>
<data name="CacheControl.Text" xml:space="preserve">
<value>Caching:</value>
</data>
<data name="CacheControl.HelpText" xml:space="preserve">
<value>Optionally provide a Cache-Control directive for this folder. For example 'public, max-age=60' indicates that files in this folder should be cached for 60 seconds. Please note that when caching is enabled, changes to files will not be immediately reflected in the UI.</value>
</data>
</root>

View File

@ -165,4 +165,31 @@
<data name="UploadFiles.Text" xml:space="preserve">
<value>Upload Files</value>
</data>
<data name="Files.Heading" xml:space="preserve">
<value>Files</value>
</data>
<data name="ImageExtensions.Text" xml:space="preserve">
<value>Image Extensions:</value>
</data>
<data name="ImageExtensions.HelpText" xml:space="preserve">
<value>Enter a comma separated list of image file extensions</value>
</data>
<data name="UploadableFileExtensions.Text" xml:space="preserve">
<value>Uploadable File Extensions:</value>
</data>
<data name="UploadableFileExtensions.HelpText" xml:space="preserve">
<value>Enter a comma separated list of uploadable file extensions</value>
</data>
<data name="MaxChunkSize.Text" xml:space="preserve">
<value>Max Upload Chunk Size (MB):</value>
</data>
<data name="MaxChunkSize.HelpText" xml:space="preserve">
<value>Files are split into chunks to streamline the upload process. Specify the maximum chunk size in MB (note that higher chunk sizes should only be used on faster networks).</value>
</data>
<data name="Success.SaveSiteSettings" xml:space="preserve">
<value>Settings Saved Successfully</value>
</data>
<data name="Error.SaveSiteSettings" xml:space="preserve">
<value>Error Saving Settings</value>
</data>
</root>

View File

@ -180,9 +180,6 @@
<data name="Once" xml:space="preserve">
<value>Execute Once</value>
</data>
<data name="Message.NoJobs" xml:space="preserve">
<value>Please Note That After An Initial Installation You Must &lt;a href={0}&gt;Restart&lt;/a&gt; The Application In Order To Activate The Default Scheduled Jobs.</value>
</data>
<data name="Refresh.Text" xml:space="preserve">
<value>Refresh</value>
</data>

View File

@ -121,10 +121,10 @@
<value>Forgot Password</value>
</data>
<data name="Success.Account.Verified" xml:space="preserve">
<value>User Account Verified Successfully. You Can Now Login With Your Username And Password Below.</value>
<value>User Account Email Address Verified Successfully. You Can Now Login With Your Username And Password.</value>
</data>
<data name="Message.Account.NotVerified" xml:space="preserve">
<value>User Account Could Not Be Verified. Please Contact Your Administrator For Further Instructions.</value>
<value>User Account Email Address Could Not Be Verified. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="Success.Account.Linked" xml:space="preserve">
<value>User Account Linked Successfully. You Can Now Login With Your External Login Below.</value>
@ -133,7 +133,7 @@
<value>External Login Could Not Be Linked. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="Error.Login.Fail" xml:space="preserve">
<value>Login Failed. Please Remember That Passwords Are Case Sensitive. If You Have Attempted To Sign In Multiple Times Unsuccessfully, Your Account Will Be Locked Out For A Period Of Time. Note That User Accounts Require Verification When They Are Initially Created So You May Wish To Check Your Email If You Are A New User.</value>
<value>Login Failed. Please Remember That Passwords Are Case Sensitive. If You Have Attempted To Sign In Multiple Times Unsuccessfully, Your Account Will Be Locked Out For A Period Of Time. Note That User Accounts Often Require Email Address Verification So You May Wish To Check Your Email For A Notification.</value>
</data>
<data name="Message.Required.UserInfo" xml:space="preserve">
<value>Please Provide All Required Fields</value>

View File

@ -147,8 +147,8 @@
<data name="Owner.HelpText" xml:space="preserve">
<value>The owner or creator of the module</value>
</data>
<data name="ReferenceUrl.HelpText" xml:space="preserve">
<value>The reference url of the module</value>
<data name="Url.HelpText" xml:space="preserve">
<value>The url of the module</value>
</data>
<data name="Contact.HelpText" xml:space="preserve">
<value>The contact for the module</value>
@ -171,8 +171,8 @@
<data name="Owner.Text" xml:space="preserve">
<value>Owner: </value>
</data>
<data name="ReferenceUrl.Text" xml:space="preserve">
<value>Reference Url: </value>
<data name="Url.Text" xml:space="preserve">
<value>Url: </value>
</data>
<data name="Contact.Text" xml:space="preserve">
<value>Contact: </value>
@ -228,18 +228,6 @@
<data name="View License" xml:space="preserve">
<value>View License</value>
</data>
<data name="Error.Validate" xml:space="preserve">
<value>Error Validating Package</value>
</data>
<data name="Message.Download" xml:space="preserve">
<value>Package Version Has Been Verified. Please Select The Download Button To Obtain The Package.</value>
</data>
<data name="Message.Validate" xml:space="preserve">
<value>This Package Version Has Not Been Registered In The Oqtane Marketplace Or You Do Not Have The Right To Use It From This Installation</value>
</data>
<data name="Validate" xml:space="preserve">
<value>Validate</value>
</data>
<data name="Browse" xml:space="preserve">
<value>Browse</value>
</data>

View File

@ -159,4 +159,13 @@
<data name="Enabled" xml:space="preserve">
<value>Enabled?</value>
</data>
<data name="Synchronize" xml:space="preserve">
<value>Check For Updates</value>
</data>
<data name="Success.Module.Synchronize" xml:space="preserve">
<value>Module Information Has Been Retrieved From The Marketplace</value>
</data>
<data name="Error.Module.Synchronize" xml:space="preserve">
<value>Error Retrieving Module Information From The Marketplace</value>
</data>
</root>

View File

@ -121,7 +121,7 @@
<value>Export</value>
</data>
<data name="Content.HelpText" xml:space="preserve">
<value>The Exported Module Content</value>
<value>Select the Export option and you will be able to view the module content</value>
</data>
<data name="Content.Text" xml:space="preserve">
<value>Content: </value>
@ -135,4 +135,25 @@
<data name="Export Content" xml:space="preserve">
<value>Export Content</value>
</data>
<data name="Content.Heading" xml:space="preserve">
<value>Content</value>
</data>
<data name="File.Heading" xml:space="preserve">
<value>File</value>
</data>
<data name="Folder.Text" xml:space="preserve">
<value>Folder:</value>
</data>
<data name="Folder.HelpText" xml:space="preserve">
<value>Select a folder where you wish to save the exported content</value>
</data>
<data name="Message.Content.Export" xml:space="preserve">
<value>Please Select A Folder And Provide A Filename Before Choosing Export</value>
</data>
<data name="Filename.Text" xml:space="preserve">
<value>Filename:</value>
</data>
<data name="Filename.HelpText" xml:space="preserve">
<value>Specify a name for the file (without an extension)</value>
</data>
</root>

View File

@ -189,4 +189,19 @@
<data name="ModuleSettings.Title" xml:space="preserve">
<value>Module Settings</value>
</data>
<data name="Header.Text" xml:space="preserve">
<value>Header:</value>
</data>
<data name="Header.HelpText" xml:space="preserve">
<value>Optionally provide content to be injected above the module instance</value>
</data>
<data name="Footer.Text" xml:space="preserve">
<value>Footer:</value>
</data>
<data name="Footer.HelpText" xml:space="preserve">
<value>Optionally provide content to be injected below the module instance</value>
</data>
<data name="ModuleContent.Heading" xml:space="preserve">
<value>Content</value>
</data>
</root>

View File

@ -267,4 +267,4 @@
<data name="ExpiryDate.Text" xml:space="preserve">
<value>Expiry Date: </value>
</data>
</root>
</root>

View File

@ -169,7 +169,7 @@
<value>Select whether the page is part of the site navigation or hidden</value>
</data>
<data name="UrlPath.HelpText" xml:space="preserve">
<value>Optionally enter a url path for this page (ie. home ). If you do not provide a url path, the page name will be used. If the page is intended to be the root path specify '/'.</value>
<value>Optionally enter a url path for this page (ie. home ). If you do not provide a url path, the page name will be used. Please note that spaces and punctuation will be replaced by a dash. If the page is intended to be the root path specify '/'.</value>
</data>
<data name="Redirect.HelpText" xml:space="preserve">
<value>Optionally enter a url which this page should redirect to when a user navigates to it</value>
@ -297,4 +297,16 @@
<data name="ExpiryDate.Text" xml:space="preserve">
<value>Expiry Date: </value>
</data>
<data name="PersonalizedUrlPath.Text" xml:space="preserve">
<value>Url Path:</value>
</data>
<data name="PersonalizedUrlPath.HelpText" xml:space="preserve">
<value>Provide a url path for your personalized page. Please note that spaces and punctuation will be replaced by a dash.</value>
</data>
<data name="UpdateModulePermissions.Text" xml:space="preserve">
<value>Update Module Permissions?</value>
</data>
<data name="UpdateModulePermissions.HelpText" xml:space="preserve">
<value>Specify if changes made to page permissions should be propagated to the modules on this page</value>
</data>
</root>

View File

@ -195,4 +195,25 @@
<data name="Modules.Heading" xml:space="preserve">
<value>Modules</value>
</data>
<data name="Message.Page.Restore" xml:space="preserve">
<value>You Cannot Restore A Page If Its Parent Is Deleted</value>
</data>
<data name="Success.Page.Restore" xml:space="preserve">
<value>Page Restored Successfully</value>
</data>
<data name="Success.Page.Delete" xml:space="preserve">
<value>Page Deleted Successfully</value>
</data>
<data name="Success.Pages.Deleted" xml:space="preserve">
<value>All Pages Deleted Successfully</value>
</data>
<data name="Success.Module.Restore" xml:space="preserve">
<value>Module Restored Successfully</value>
</data>
<data name="Success.Module.Delete" xml:space="preserve">
<value>Module Deleted Successfully</value>
</data>
<data name="Success.Modules.Delete" xml:space="preserve">
<value>All Modules Deleted Successfully</value>
</data>
</root>

View File

@ -180,4 +180,10 @@
<data name="Login" xml:space="preserve">
<value>Already have account? Login now.</value>
</data>
<data name="TimeZone.Text" xml:space="preserve">
<value>Time Zone:</value>
</data>
<data name="TimeZone.HelpText" xml:space="preserve">
<value>Your time zone</value>
</data>
</root>

View File

@ -118,7 +118,7 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="NoCriteria" xml:space="preserve">
<value>You Must Provide Some Search Criteria</value>
<value>Please Enter Some Search Criteria</value>
</data>
<data name="NoResult" xml:space="preserve">
<value>No Content Matches The Criteria Provided</value>

View File

@ -349,7 +349,7 @@
<value>Relay Configured?</value>
</data>
<data name="SiteMap.HelpText" xml:space="preserve">
<value>The site map url for this site which can be submitted to search engines for indexing</value>
<value>The site map url for this site which can be submitted to search engines for indexing. The sitemap is cached for 5 minutes and the cache can be manually cleared.</value>
</data>
<data name="SiteMap.Text" xml:space="preserve">
<value>Site Map:</value>
@ -402,18 +402,6 @@
<data name="Retention.Text" xml:space="preserve">
<value>Retention (Days):</value>
</data>
<data name="ImageExtensions.HelpText" xml:space="preserve">
<value>Enter a comma separated list of image file extensions</value>
</data>
<data name="ImageExtensions.Text" xml:space="preserve">
<value>Image Extensions:</value>
</data>
<data name="UploadableFileExtensions.HelpText" xml:space="preserve">
<value>Enter a comma separated list of uploadable file extensions</value>
</data>
<data name="UploadableFileExtensions.Text" xml:space="preserve">
<value>Uploadable File Extensions:</value>
</data>
<data name="HybridEnabled.HelpText" xml:space="preserve">
<value>Specifies if the site can be integrated with an external .NET MAUI hybrid application</value>
</data>
@ -435,4 +423,34 @@
<data name="Functionality" xml:space="preserve">
<value>Functionality</value>
</data>
<data name="System" xml:space="preserve">
<value>System</value>
</data>
<data name="CookieConsent.HelpText" xml:space="preserve">
<value>Specify if cookie consent is enabled on this site. Please note this option must be used in conjunction with a Theme which supports cookie consent.</value>
</data>
<data name="CookieConsent.Text" xml:space="preserve">
<value>Cookie Consent:</value>
</data>
<data name="OptIn" xml:space="preserve">
<value>Opt-In (GDPR)</value>
</data>
<data name="OptOut" xml:space="preserve">
<value>Opt-Out (CCPA)</value>
</data>
<data name="Theme.Heading" xml:space="preserve">
<value>Theme</value>
</data>
<data name="SiteMap.EvictCache" xml:space="preserve">
<value>Clear Cache</value>
</data>
<data name="Success.SiteMap.CacheEvicted" xml:space="preserve">
<value>Site Map Cache Cleared</value>
</data>
<data name="TimeZone.Text" xml:space="preserve">
<value>Time Zone:</value>
</data>
<data name="TimeZone.HelpText" xml:space="preserve">
<value>The default time zone for the site</value>
</data>
</root>

View File

@ -117,8 +117,8 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Access.ApiFramework" xml:space="preserve">
<value>Access Swagger API</value>
<data name="Swagger" xml:space="preserve">
<value>Swagger UI</value>
</data>
<data name="FrameworkVersion.HelpText" xml:space="preserve">
<value>Framework Version</value>
@ -220,10 +220,10 @@
<value>You Have Been Successfully Registered For Updates</value>
</data>
<data name="PackageManager.HelpText" xml:space="preserve">
<value>Specify The Package Manager Service For Installing Modules, Themes, And Translations. If This Field Is Blank It Means The Package Manager Service Is Disabled For This Installation.</value>
<value>Specify The Url Of The Package Manager Service For Installing Modules, Themes, And Translations. If This Field Is Blank It Means The Package Manager Service Is Disabled For This Installation.</value>
</data>
<data name="PackageManager.Text" xml:space="preserve">
<value>Package Manager:</value>
<value>Package Manager Url:</value>
</data>
<data name="Swagger.HelpText" xml:space="preserve">
<value>Specify If Swagger Is Enabled For Your Server API</value>
@ -294,4 +294,19 @@
<data name="Process.Text" xml:space="preserve">
<value>Process: </value>
</data>
<data name="PackageManagerEmail.Text" xml:space="preserve">
<value>Package Manager Email:</value>
</data>
<data name="PackageManagerEmail.HelpText" xml:space="preserve">
<value>Specify The Email Address Of The User Account Used For Interacting With The Package Manager Service. This Account Is Used For Managing Packages Across Multiple Installations.</value>
</data>
<data name="CacheControl.Text" xml:space="preserve">
<value>Static Asset Caching:</value>
</data>
<data name="CacheControl.HelpText" xml:space="preserve">
<value>Provide a Cache-Control directive for static assets. For example 'public, max-age=60' indicates that static assets should be cached for 60 seconds. A blank value indicates caching is not enabled.</value>
</data>
<data name="Endpoints" xml:space="preserve">
<value>API Endpoints</value>
</data>
</root>

View File

@ -132,8 +132,8 @@
<data name="Owner.Text" xml:space="preserve">
<value>Owner: </value>
</data>
<data name="ReferenceUrl.Text" xml:space="preserve">
<value>Reference Url: </value>
<data name="Url.Text" xml:space="preserve">
<value>Url: </value>
</data>
<data name="Contact.Text" xml:space="preserve">
<value>Contact: </value>
@ -153,8 +153,8 @@
<data name="Owner.HelpText" xml:space="preserve">
<value>The owner or creator of the theme</value>
</data>
<data name="ReferenceUrl.HelpText" xml:space="preserve">
<value>The reference url of the theme</value>
<data name="Url.HelpText" xml:space="preserve">
<value>The url of the theme</value>
</data>
<data name="Contact.HelpText" xml:space="preserve">
<value>The contact for the theme</value>
@ -180,16 +180,4 @@
<data name="View License" xml:space="preserve">
<value>View License</value>
</data>
<data name="Error.Validate" xml:space="preserve">
<value>Error Validating Package</value>
</data>
<data name="Message.Download" xml:space="preserve">
<value>Package Version Has Been Verified. Please Select The Download Button To Obtain The Package.</value>
</data>
<data name="Message.Validate" xml:space="preserve">
<value>This Package Version Has Not Been Registered In The Oqtane Marketplace Or You Do Not Have The Right To Use It From This Installation</value>
</data>
<data name="Validate" xml:space="preserve">
<value>Validate</value>
</data>
</root>

View File

@ -146,7 +146,7 @@
</data>
<data name="InstallTheme.Text" xml:space="preserve">
<value>Install Theme</value>
</data>
</data>
<data name="ViewTheme.Text" xml:space="preserve">
<value>View</value>
</data>
@ -156,4 +156,16 @@
<data name="Enabled" xml:space="preserve">
<value>Enabled?</value>
</data>
<data name="Assign" xml:space="preserve">
<value>Assign</value>
</data>
<data name="Synchronize" xml:space="preserve">
<value>Check For Updates</value>
</data>
<data name="Success.Theme.Synchronize" xml:space="preserve">
<value>Theme Information Has Been Retrieved From The Marketplace</value>
</data>
<data name="Error.Theme.Synchronize" xml:space="preserve">
<value>Error Retrieving Theme Information From The Marketplace</value>
</data>
</root>

View File

@ -124,7 +124,7 @@
<value>Error Downloading Framework Package</value>
</data>
<data name="Framework.HelpText" xml:space="preserve">
<value>Upload a framework package and select Install to complete the installation</value>
<value>Upload A Framework Package (Oqtane.Framework.#.#.#.nupkg) And Then Select Upgrade</value>
</data>
<data name="Framework.Text" xml:space="preserve">
<value>Framework: </value>
@ -144,13 +144,16 @@
<data name="Message.Text" xml:space="preserve">
<value>Framework Is Already Up To Date</value>
</data>
<data name="MessageUpgrade.Text" xml:space="preserve">
<value>Upload A Framework Package (Oqtane.Framework.version.nupkg) And Then Select Upgrade</value>
</data>
<data name="Localhost.Text" xml:space="preserve">
<value>You Cannot Perform A System Update In A Development Environment</value>
</data>
<data name="Disclaimer.Text" xml:space="preserve">
<value>Please Note That The System Update Capability Is A Simplified Upgrade Process Intended For Small To Medium Sized Installations. For Larger Enterprise Installations You Will Want To Use A Manual Upgrade Process. Also Note That The System Update Capability Is Not Recommended When Using Microsoft Azure Due To Environmental Limitations.</value>
<value>Please Note That The System Update Capability Is A Simplified Upgrade Process Intended For Small To Medium Sized Installations. For Larger Enterprise Installations You Will Want To Use A Manual Upgrade Process.</value>
</data>
<data name="Backup.Text" xml:space="preserve">
<value>Backup Files?</value>
</data>
<data name="Backup.HelpText" xml:space="preserve">
<value>Specify if you want to backup files during the upgrade process. Disabling this option will reduce the time required for the upgrade.</value>
</data>
</root>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@ -121,10 +121,10 @@
<value>Redirect To:</value>
</data>
<data name="MappedUrl.HelpText" xml:space="preserve">
<value>A relative or absolute Url where the user will be redirected</value>
<value>A Url where the user will be redirected (absolute or relative). Use '/' for site root path.</value>
</data>
<data name="Url.HelpText" xml:space="preserve">
<value>An absolute Url identifying a path to a specific page in the site</value>
<value>A Url identifying a path to a specific page in the site (absolute or relative)</value>
</data>
<data name="Url.Text" xml:space="preserve">
<value>Url:</value>
@ -141,4 +141,7 @@
<data name="Message.SaveUrlMapping" xml:space="preserve">
<value>The Url must belong to the current site</value>
</data>
<data name="Generate" xml:space="preserve">
<value>Generate</value>
</data>
</root>

View File

@ -121,10 +121,10 @@
<value>Redirect To:</value>
</data>
<data name="MappedUrl.HelpText" xml:space="preserve">
<value>A relative or absolute Url where the user will be redirected</value>
<value>A Url where the user will be redirected (absolute or relative). Use '/' for site root path.</value>
</data>
<data name="Url.HelpText" xml:space="preserve">
<value>A relative Url identifying a path to a specific page in the site</value>
<value>A Url identifying a path to a specific page in the site (absolute or relative)</value>
</data>
<data name="Url.Text" xml:space="preserve">
<value>Url:</value>

View File

@ -162,4 +162,10 @@
<data name="Edit.Text" xml:space="preserve">
<value>Edit</value>
</data>
<data name="Retention.Text" xml:space="preserve">
<value>Retention (Days):</value>
</data>
<data name="Retention.HelpText" xml:space="preserve">
<value>Number of days of broken urls to retain</value>
</data>
</root>

View File

@ -246,4 +246,10 @@
<data name="Logout Everywhere" xml:space="preserve">
<value>Logout Everywhere</value>
</data>
<data name="TimeZone.Text" xml:space="preserve">
<value>Time Zone:</value>
</data>
<data name="TimeZone.HelpText" xml:space="preserve">
<value>Your time zone</value>
</data>
</root>

View File

@ -156,4 +156,10 @@
<data name="Notify.Text" xml:space="preserve">
<value>Notify?</value>
</data>
<data name="TimeZone.Text" xml:space="preserve">
<value>Time Zone:</value>
</data>
<data name="TimeZone.HelpText" xml:space="preserve">
<value>The user's time zone</value>
</data>
</root>

View File

@ -195,4 +195,31 @@
<data name="LastLogin.Text" xml:space="preserve">
<value>Last Login:</value>
</data>
<data name="DeleteUser.Header" xml:space="preserve">
<value>Delete User</value>
</data>
<data name="DeleteUser.Text" xml:space="preserve">
<value>Delete</value>
</data>
<data name="DeleteUser.Message" xml:space="preserve">
<value>Are You Sure You Wish To Permanently Delete This User?</value>
</data>
<data name="Impersonate" xml:space="preserve">
<value>Impersonate</value>
</data>
<data name="Error.User.Impersonate" xml:space="preserve">
<value>Unable To Impersonate User</value>
</data>
<data name="TimeZone.Text" xml:space="preserve">
<value>Time Zone:</value>
</data>
<data name="TimeZone.HelpText" xml:space="preserve">
<value>The user's time zone</value>
</data>
<data name="Confirmed.Text" xml:space="preserve">
<value>Confirmed?</value>
</data>
<data name="Confirmed.HelpText" xml:space="preserve">
<value>Indicates if the user's email is verified</value>
</data>
</root>

View File

@ -132,6 +132,18 @@
<data name="AllowRegistration.Text" xml:space="preserve">
<value>Allow Registration? </value>
</data>
<data name="RegisterUrl.HelpText" xml:space="preserve">
<value>Optionally provide a custom registration url</value>
</data>
<data name="RegisterUrl.Text" xml:space="preserve">
<value>Register Url: </value>
</data>
<data name="ProfileUrl.HelpText" xml:space="preserve">
<value>Optionally provide a custom user profile url</value>
</data>
<data name="ProfileUrl.Text" xml:space="preserve">
<value>Profile Url: </value>
</data>
<data name="Error.SaveSiteSettings" xml:space="preserve">
<value>Error Saving Settings</value>
</data>
@ -208,7 +220,7 @@
<value>Do you want to allow users to sign in using a username and password that is managed locally on this site? Note that you should only disable this option if you have already sucessfully configured an external login provider, or else you may lock yourself out of the site.</value>
</data>
<data name="AllowSiteLogin.Text" xml:space="preserve">
<value>Allow Login?</value>
<value>Allow Local Login?</value>
</data>
<data name="Authority.HelpText" xml:space="preserve">
<value>The authority url or issuer url associated with the identity provider</value>
@ -495,4 +507,28 @@
<data name="OIDC" xml:space="preserve">
<value>OpenID Connect (OIDC)</value>
</data>
<data name="SaveTokens.Text" xml:space="preserve">
<value>Save Tokens?</value>
</data>
<data name="SaveTokens.HelpText" xml:space="preserve">
<value>Specify whether access and refresh tokens should be saved after a successful login. The default is false to reduce the size of the authentication cookie.</value>
</data>
<data name="Success.DeleteUser" xml:space="preserve">
<value>User Deleted Successfully</value>
</data>
<data name="Error.DeleteUser" xml:space="preserve">
<value>Error Deleting User</value>
</data>
<data name="LogoutEverywhere.Text" xml:space="preserve">
<value>Logout Everywhere?</value>
</data>
<data name="LogoutEverywhere.HelpText" xml:space="preserve">
<value>Do you want users to be logged out of every active session on any device, or only their current session?</value>
</data>
<data name="AllowHostRole.Text" xml:space="preserve">
<value>Allow Host Role?</value>
</data>
<data name="AllowHostRole.HelpText" xml:space="preserve">
<value>Indicate if host roles are supported from the identity provider. Please use caution with this option as it allows the host user to administrate every site within your installation.</value>
</data>
</root>

View File

@ -198,4 +198,13 @@
<data name="Duration.Text" xml:space="preserve">
<value>Session Duration:</value>
</data>
<data name="Robots.Heading" xml:space="preserve">
<value>Robots.txt</value>
</data>
<data name="Robots.Text" xml:space="preserve">
<value>Instructions:</value>
</data>
<data name="Robots.HelpText" xml:space="preserve">
<value>Specify your robots.txt instructions to provide bots with guidance on which parts of your site should be indexed</value>
</data>
</root>

View File

@ -127,7 +127,7 @@
<value>Error Loading Files</value>
</data>
<data name="Error.File.Upload" xml:space="preserve">
<value>File Upload Failed Or Is Still In Progress</value>
<value>File Upload Failed</value>
</data>
<data name="Message.File.NotSelected" xml:space="preserve">
<value>You Have Not Selected A File To Upload</value>

View File

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

View File

@ -427,7 +427,7 @@
<value>At Least One Uppercase Letter</value>
</data>
<data name="Password.ValidationCriteria" xml:space="preserve">
<value>Passwords Must Have A Minimum Length Of {0} Characters, Including At Least {1} Unique Character(s), {2}{3}{4}{5} To Satisfy Password Compexity Requirements For This Site.</value>
<value>Passwords Must Have A Minimum Length Of {0} Characters, Including At Least {1} Unique Character(s), {2}{3}{4}{5} To Satisfy Password Complexity Requirements For This Site.</value>
</data>
<data name="ProfileInvalid" xml:space="preserve">
<value>{0} Is Not Valid</value>
@ -474,4 +474,7 @@
<data name="User" xml:space="preserve">
<value>User</value>
</data>
<data name="Path" xml:space="preserve">
<value>Path</value>
</data>
</root>

View File

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

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema

View File

@ -0,0 +1,47 @@
using Oqtane.Models;
using System.Threading.Tasks;
using System.Net.Http;
using System;
using Oqtane.Documentation;
using Oqtane.Shared;
using System.Globalization;
namespace Oqtane.Services
{
/// <inheritdoc cref="ICookieConsentService" />
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class CookieConsentService : ServiceBase, ICookieConsentService
{
public CookieConsentService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string ApiUrl => CreateApiUrl("CookieConsent");
public async Task<bool> IsActionedAsync()
{
return await GetJsonAsync<bool>($"{ApiUrl}/IsActioned");
}
public async Task<bool> CanTrackAsync(bool optOut)
{
return await GetJsonAsync<bool>($"{ApiUrl}/CanTrack?optout=" + optOut);
}
public async Task<string> CreateActionedCookieAsync()
{
var cookie = await GetStringAsync($"{ApiUrl}/CreateActionedCookie");
return cookie ?? string.Empty;
}
public async Task<string> CreateConsentCookieAsync()
{
var cookie = await GetStringAsync($"{ApiUrl}/CreateConsentCookie");
return cookie ?? string.Empty;
}
public async Task<string> WithdrawConsentCookieAsync()
{
var cookie = await GetStringAsync($"{ApiUrl}/WithdrawConsentCookie");
return cookie ?? string.Empty;
}
}
}

View File

@ -42,14 +42,6 @@ namespace Oqtane.Services
return await PutJsonAsync<Folder>($"{ApiUrl}/{folder.FolderId}", folder);
}
public async Task UpdateFolderOrderAsync(int siteId, int folderId, int? parentId)
{
var parent = parentId == null
? string.Empty
: parentId.ToString();
await PutAsync($"{ApiUrl}/?siteid={siteId}&folderid={folderId}&parentid={parent}");
}
public async Task DeleteFolderAsync(int folderId)
{
await DeleteAsync($"{ApiUrl}/{folderId}");

View File

@ -47,19 +47,14 @@ namespace Oqtane.Services
return await PostJsonAsync<InstallConfig,Installation>(ApiUrl, config);
}
public async Task<Installation> Upgrade()
public async Task<Installation> Upgrade(bool backup)
{
return await GetJsonAsync<Installation>($"{ApiUrl}/upgrade");
return await GetJsonAsync<Installation>($"{ApiUrl}/upgrade/?backup={backup}");
}
public async Task RestartAsync()
{
await PostAsync($"{ApiUrl}/restart");
}
public async Task RegisterAsync(string email)
{
await PostJsonAsync($"{ApiUrl}/register?email={WebUtility.UrlEncode(email)}", true);
}
}
}

View File

@ -0,0 +1,42 @@
using Oqtane.Models;
using System;
using System.Threading.Tasks;
namespace Oqtane.Services
{
/// <summary>
/// Service to retrieve cookie consent information.
/// </summary>
public interface ICookieConsentService
{
/// <summary>
/// Get cookie consent bar actioned status
/// </summary>
/// <returns></returns>
Task<bool> IsActionedAsync();
/// <summary>
/// Get cookie consent status
/// </summary>
/// <returns></returns>
Task<bool> CanTrackAsync(bool optOut);
/// <summary>
/// create actioned cookie
/// </summary>
/// <returns></returns>
Task<string> CreateActionedCookieAsync();
/// <summary>
/// create consent cookie
/// </summary>
/// <returns></returns>
Task<string> CreateConsentCookieAsync();
/// <summary>
/// widhdraw consent cookie
/// </summary>
/// <returns></returns>
Task<string> WithdrawConsentCookieAsync();
}
}

View File

@ -39,15 +39,6 @@ namespace Oqtane.Services
/// <returns></returns>
Task<Folder> UpdateFolderAsync(Folder folder);
/// <summary>
/// Update the internal Folder-Order within the list of Folders.
/// </summary>
/// <param name="siteId">Reference to the <see cref="Site"/></param>
/// <param name="folderId">Reference to a <see cref="Folder"/> for the security check</param>
/// <param name="parentId">Reference to the Parent <see cref="Folder"/> or null - this Folders children will be re-sorted.</param>
/// <returns></returns>
Task UpdateFolderOrderAsync(int siteId, int folderId, int? parentId);
/// <summary>
/// Delete a <see cref="Folder"/>
/// </summary>

View File

@ -26,21 +26,14 @@ namespace Oqtane.Services
/// <summary>
/// Starts the upgrade process
/// </summary>
/// <param name="backup">indicates if files should be backed up during upgrade</param>
/// <returns>internal status/message object</returns>
Task<Installation> Upgrade();
Task<Installation> Upgrade(bool backup);
/// <summary>
/// Restarts the installation
/// </summary>
/// <returns>internal status/message object</returns>
Task RestartAsync();
/// <summary>
/// Registers a new <see cref="User"/>
/// </summary>
/// <param name="email">Email of the user to be registered</param>
/// <returns></returns>
Task RegisterAsync(string email);
}
}

View File

@ -10,10 +10,11 @@ namespace Oqtane.Services
public interface IJobLogService
{
/// <summary>
/// Return a list of all <see cref="JobLog"/> entries
/// Return a list of <see cref="JobLog"/> entries
/// </summary>
/// <param name="jobId"></param>
/// <returns></returns>
Task<List<JobLog>> GetJobLogsAsync();
Task<List<JobLog>> GetJobLogsAsync(int jobId);
/// <summary>
/// Return a <see cref="JobLog"/> entry for the given Id

View File

@ -56,7 +56,18 @@ namespace Oqtane.Services
/// Exports a given module
/// </summary>
/// <param name="moduleId"></param>
/// <returns>module in JSON</returns>
/// <param name="pageId"></param>
/// <returns>module content in JSON format</returns>
Task<string> ExportModuleAsync(int moduleId, int pageId);
/// <summary>
/// Exports a given module
/// </summary>
/// <param name="moduleId"></param>
/// <param name="pageId"></param>
/// <param name="folderId"></param>
/// <param name="filename"></param>
/// <returns>file id</returns>
Task<int> ExportModuleAsync(int moduleId, int pageId, int folderId, string filename);
}
}

View File

@ -0,0 +1,18 @@
using System.Threading;
using System.Threading.Tasks;
namespace Oqtane.Services
{
/// <summary>
/// Service to manage cache
/// </summary>
public interface IOutputCacheService
{
/// <summary>
/// Evicts the output cache for a specific tag
/// </summary>
/// <param name="tag"></param>
/// <returns></returns>
Task EvictByTag(string tag);
}
}

View File

@ -0,0 +1,18 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Oqtane.Models;
namespace Oqtane.Services
{
/// <summary>
/// Service to store and retrieve <see cref="TimeZone"/> entries
/// </summary>
public interface ITimeZoneService
{
/// <summary>
/// Get the list of time zones
/// </summary>
/// <returns></returns>
Task<List<TimeZone>> GetTimeZonesAsync();
}
}

View File

@ -15,10 +15,9 @@ namespace Oqtane.Services
private string Apiurl => CreateApiUrl("JobLog");
public async Task<List<JobLog>> GetJobLogsAsync()
public async Task<List<JobLog>> GetJobLogsAsync(int jobId)
{
List<JobLog> joblogs = await GetJsonAsync<List<JobLog>>(Apiurl);
return joblogs.OrderBy(item => item.StartDate).ToList();
return await GetJsonAsync<List<JobLog>>($"{Apiurl}?jobid={jobId}");
}
public async Task<JobLog> GetJobLogAsync(int jobLogId)

View File

@ -47,8 +47,13 @@ namespace Oqtane.Services
}
public async Task<string> ExportModuleAsync(int moduleId, int pageId)
{
{
return await GetStringAsync($"{Apiurl}/export?moduleid={moduleId}&pageid={pageId}");
}
public async Task<int> ExportModuleAsync(int moduleId, int pageId, int folderId, string filename)
{
return await PostJsonAsync<string,int>($"{Apiurl}/export?moduleid={moduleId}&pageid={pageId}&folderid={folderId}&filename={filename}", null);
}
}
}

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