From 9c32937c83bcceba3fc030d4c30ea07a0370b007 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Thu, 9 Dec 2021 08:48:56 -0500 Subject: [PATCH] added support for url mapping and viitors --- Oqtane.Client/App.razor | 5 +- .../OqtaneServiceCollectionExtensions.cs | 2 + Oqtane.Client/Modules/Admin/Site/Index.razor | 12 -- .../Modules/Admin/UrlMappings/Add.razor | 71 ++++++++ .../Modules/Admin/UrlMappings/Edit.razor | 83 +++++++++ .../Modules/Admin/UrlMappings/Index.razor | 134 ++++++++++++++ Oqtane.Client/Modules/Admin/Users/Index.razor | 104 +++++++---- .../Modules/Admin/Visitors/Index.razor | 140 +++++++++++++++ Oqtane.Client/Oqtane.Client.csproj | 1 - .../Resources/Modules/Admin/Site/Index.resx | 19 +- .../Modules/Admin/UrlMappings/Add.resx | 138 +++++++++++++++ .../Modules/Admin/UrlMappings/Edit.resx | 141 +++++++++++++++ .../Modules/Admin/UrlMappings/Index.resx | 165 ++++++++++++++++++ .../Resources/Modules/Admin/Users/Index.resx | 18 ++ .../Modules/Admin/Visitors/Index.resx | 165 ++++++++++++++++++ .../Services/Interfaces/IUrlMappingService.cs | 48 +++++ .../Services/Interfaces/IVisitorService.cs | 21 +++ Oqtane.Client/Services/UrlMappingService.cs | 51 ++++++ Oqtane.Client/Services/VisitorService.cs | 32 ++++ Oqtane.Client/UI/PageState.cs | 1 + Oqtane.Client/UI/SiteRouter.razor | 6 +- .../Controllers/SettingController.cs | 8 + .../Controllers/UrlMappingController.cs | 119 +++++++++++++ .../Controllers/VisitorController.cs | 46 +++++ .../OqtaneServiceCollectionExtensions.cs | 2 + .../Infrastructure/DatabaseManager.cs | 9 +- .../Infrastructure/UpgradeManager.cs | 127 ++++++++------ .../EntityBuilders/UrlMappingEntityBuilder.cs | 51 ++++++ .../EntityBuilders/VisitorEntityBuilder.cs | 57 ++++++ .../Tenant/03000102_AddVisitorTable.cs | 29 +++ .../Tenant/03000103_AddUrlMappingTable.cs | 30 ++++ .../Tenant/03000104_AddSiteVisitorTracking.cs | 35 ++++ Oqtane.Server/Pages/_Host.cshtml | 2 +- Oqtane.Server/Pages/_Host.cshtml.cs | 107 +++++++++++- .../Repository/Context/TenantDBContext.cs | 2 + .../Interfaces/IUrlMappingRepository.cs | 17 ++ .../Interfaces/IVisitorRepository.cs | 15 ++ Oqtane.Server/Repository/SiteRepository.cs | 75 +++++++- .../Repository/UrlMappingRepository.cs | 73 ++++++++ Oqtane.Server/Repository/VisitorRepository.cs | 51 ++++++ Oqtane.Shared/Models/Site.cs | 12 +- Oqtane.Shared/Models/UrlMapping.cs | 47 +++++ Oqtane.Shared/Models/Visitor.cs | 61 +++++++ Oqtane.Shared/Shared/Constants.cs | 4 +- Oqtane.Shared/Shared/EntityNames.cs | 3 +- 45 files changed, 2212 insertions(+), 127 deletions(-) create mode 100644 Oqtane.Client/Modules/Admin/UrlMappings/Add.razor create mode 100644 Oqtane.Client/Modules/Admin/UrlMappings/Edit.razor create mode 100644 Oqtane.Client/Modules/Admin/UrlMappings/Index.razor create mode 100644 Oqtane.Client/Modules/Admin/Visitors/Index.razor create mode 100644 Oqtane.Client/Resources/Modules/Admin/UrlMappings/Add.resx create mode 100644 Oqtane.Client/Resources/Modules/Admin/UrlMappings/Edit.resx create mode 100644 Oqtane.Client/Resources/Modules/Admin/UrlMappings/Index.resx create mode 100644 Oqtane.Client/Resources/Modules/Admin/Visitors/Index.resx create mode 100644 Oqtane.Client/Services/Interfaces/IUrlMappingService.cs create mode 100644 Oqtane.Client/Services/Interfaces/IVisitorService.cs create mode 100644 Oqtane.Client/Services/UrlMappingService.cs create mode 100644 Oqtane.Client/Services/VisitorService.cs create mode 100644 Oqtane.Server/Controllers/UrlMappingController.cs create mode 100644 Oqtane.Server/Controllers/VisitorController.cs create mode 100644 Oqtane.Server/Migrations/EntityBuilders/UrlMappingEntityBuilder.cs create mode 100644 Oqtane.Server/Migrations/EntityBuilders/VisitorEntityBuilder.cs create mode 100644 Oqtane.Server/Migrations/Tenant/03000102_AddVisitorTable.cs create mode 100644 Oqtane.Server/Migrations/Tenant/03000103_AddUrlMappingTable.cs create mode 100644 Oqtane.Server/Migrations/Tenant/03000104_AddSiteVisitorTracking.cs create mode 100644 Oqtane.Server/Repository/Interfaces/IUrlMappingRepository.cs create mode 100644 Oqtane.Server/Repository/Interfaces/IVisitorRepository.cs create mode 100644 Oqtane.Server/Repository/UrlMappingRepository.cs create mode 100644 Oqtane.Server/Repository/VisitorRepository.cs create mode 100644 Oqtane.Shared/Models/UrlMapping.cs create mode 100644 Oqtane.Shared/Models/Visitor.cs diff --git a/Oqtane.Client/App.razor b/Oqtane.Client/App.razor index cef437d1..14f64da5 100644 --- a/Oqtane.Client/App.razor +++ b/Oqtane.Client/App.razor @@ -15,7 +15,7 @@
- +
@@ -39,6 +39,9 @@ [Parameter] public string RenderMode { get; set; } + [Parameter] + public int VisitorId { get; set; } + private bool _initialized = false; private string _display = "display: none;"; private Installation _installation = new Installation { Success = false, Message = "" }; diff --git a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs index 40db5da5..a994ff33 100644 --- a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs @@ -46,6 +46,8 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); return services; diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index b5db984a..ebe7548f 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -34,15 +34,6 @@ } -
- -
- -
-
@@ -258,7 +249,6 @@ private string _themetype = "-"; private string _containertype = "-"; private string _admincontainertype = "-"; - private string _allowregistration; private string _smtphost = string.Empty; private string _smtpport = string.Empty; private string _smtpssl = "False"; @@ -294,7 +284,6 @@ _name = site.Name; _runtime = site.Runtime; _prerender = site.RenderMode.Replace(_runtime, ""); - _allowregistration = site.AllowRegistration.ToString(); _isdeleted = site.IsDeleted.ToString(); if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) @@ -437,7 +426,6 @@ reload = true; // needs to be reloaded on server } } - site.AllowRegistration = (_allowregistration == null ? true : Boolean.Parse(_allowregistration)); site.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted)); site.LogoFileId = null; diff --git a/Oqtane.Client/Modules/Admin/UrlMappings/Add.razor b/Oqtane.Client/Modules/Admin/UrlMappings/Add.razor new file mode 100644 index 00000000..efde8b66 --- /dev/null +++ b/Oqtane.Client/Modules/Admin/UrlMappings/Add.razor @@ -0,0 +1,71 @@ +@namespace Oqtane.Modules.Admin.UrlMappings +@inherits ModuleBase +@inject NavigationManager NavigationManager +@inject IUrlMappingService UrlMappingService +@inject IStringLocalizer Localizer +@inject IStringLocalizer SharedLocalizer + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+

+ + @SharedLocalizer["Cancel"] +
+
+ +@code { + private ElementReference form; + private bool validated = false; + + private string _url = string.Empty; + private string _mappedurl = string.Empty; + + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; + + private async Task SaveUrlMapping() + { + validated = true; + var interop = new Interop(JSRuntime); + if (await interop.FormValid(form)) + { + var route = new Route(_url, PageState.Alias.Path); + var url = route.SiteUrl + "/" + route.PagePath; + + var urlmapping = new UrlMapping(); + urlmapping.SiteId = PageState.Site.SiteId; + urlmapping.Url = url; + 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(SharedLocalizer["Message.InfoRequired"], MessageType.Warning); + } + } +} diff --git a/Oqtane.Client/Modules/Admin/UrlMappings/Edit.razor b/Oqtane.Client/Modules/Admin/UrlMappings/Edit.razor new file mode 100644 index 00000000..1ef1b79a --- /dev/null +++ b/Oqtane.Client/Modules/Admin/UrlMappings/Edit.razor @@ -0,0 +1,83 @@ +@namespace Oqtane.Modules.Admin.UrlMappings +@inherits ModuleBase +@inject NavigationManager NavigationManager +@inject IUrlMappingService UrlMappingService +@inject IStringLocalizer Localizer +@inject IStringLocalizer SharedLocalizer + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+

+ + @SharedLocalizer["Cancel"] +
+
+ +@code { + private ElementReference form; + private bool validated = false; + + private int _urlmappingid; + private string _url = string.Empty; + private string _mappedurl = string.Empty; + + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; + + protected override async Task OnInitializedAsync() + { + try + { + _urlmappingid = Int32.Parse(PageState.QueryString["id"]); + var urlmapping = await UrlMappingService.GetUrlMappingAsync(_urlmappingid); + if (urlmapping != null) + { + _url = urlmapping.Url; + _mappedurl = urlmapping.MappedUrl; + } + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Loading UrlMapping {UrlMappingId} {Error}", _urlmappingid, ex.Message); + AddModuleMessage(Localizer["Error.LoadUrlMapping"], MessageType.Error); + } + } + + private async Task SaveUrlMapping() + { + validated = true; + var interop = new Interop(JSRuntime); + if (await interop.FormValid(form)) + { + var urlmapping = await UrlMappingService.GetUrlMappingAsync(_urlmappingid); + urlmapping.MappedUrl = _mappedurl; + + try + { + urlmapping = await UrlMappingService.UpdateUrlMappingAsync(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(SharedLocalizer["Message.InfoRequired"], MessageType.Warning); + } + } +} diff --git a/Oqtane.Client/Modules/Admin/UrlMappings/Index.razor b/Oqtane.Client/Modules/Admin/UrlMappings/Index.razor new file mode 100644 index 00000000..b748604c --- /dev/null +++ b/Oqtane.Client/Modules/Admin/UrlMappings/Index.razor @@ -0,0 +1,134 @@ +@namespace Oqtane.Modules.Admin.UrlMappings +@inherits ModuleBase +@inject IUrlMappingService UrlMappingService +@inject ISiteService SiteService +@inject IStringLocalizer Localizer +@inject IStringLocalizer SharedLocalizer + +@if (_urlMappings == null) +{ +

@SharedLocalizer["Loading"]

+} +else +{ + + +
+
+
+ +
+
+ +
+
+
+
+ +
+   +   + @Localizer["Url"] + @Localizer["Requests"] + @Localizer["Requested"] +
+ + + + + @context.Url + @if (_mapped) + { + @((MarkupString)"
>> ")@context.MappedUrl + } + + @context.Requests + @context.RequestedOn +
+
+
+ +
+
+ +
+ +
+
+
+
+ +
+
+} + +@code { + private bool _mapped = true; + private List _urlMappings; + private string _capturebrokenurls; + + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; + + protected override async Task OnParametersSetAsync() + { + await GetUrlMappings(); + _capturebrokenurls = PageState.Site.CaptureBrokenUrls.ToString(); + } + + private async void MappedChanged(ChangeEventArgs e) + { + try + { + _mapped = bool.Parse(e.Value.ToString()); + await GetUrlMappings(); + StateHasChanged(); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error On TypeChanged"); + } + } + + private async Task DeleteUrlMapping(UrlMapping urlMapping) + { + try + { + await UrlMappingService.DeleteUrlMappingAsync(urlMapping.UrlMappingId); + await logger.LogInformation("UrlMapping Deleted {UrlMapping}", urlMapping); + await GetUrlMappings(); + StateHasChanged(); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Deleting UrlMapping {UrlMapping} {Error}", urlMapping, ex.Message); + AddModuleMessage(Localizer["Error.DeleteUrlMapping"], MessageType.Error); + } + } + + private async Task GetUrlMappings() + { + _urlMappings = await UrlMappingService.GetUrlMappingsAsync(PageState.Site.SiteId, _mapped); + } + + private async Task SaveSiteSettings() + { + try + { + var site = PageState.Site; + site.CaptureBrokenUrls = bool.Parse(_capturebrokenurls); + await SiteService.UpdateSiteAsync(site); + 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); + } + } +} diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index c56c440a..3f138c34 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -3,6 +3,7 @@ @inject IUserRoleService UserRoleService @inject IUserService UserService @inject ISettingService SettingService +@inject ISiteService SiteService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -14,45 +15,65 @@ } else { -
-
-
- -
-
- -
-
- -
-
-
- -
-   -   -   - @SharedLocalizer["Name"] -
- - - - - - - - - - - @context.User.DisplayName - -
+ + +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+   +   +   + @SharedLocalizer["Name"] +
+ + + + + + + + + + + @context.User.DisplayName + +
+
+ +
+
+ +
+ +
+
+
+
+ +
+
} @code { private List allroles; private List userroles; private string _search; + private string _allowregistration; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; @@ -61,6 +82,7 @@ else allroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId); await LoadSettingsAsync(); userroles = Search(_search); + _allowregistration = PageState.Site.AllowRegistration.ToString(); } private List Search(string search) @@ -122,4 +144,20 @@ else await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId); } + private async Task SaveSiteSettings() + { + try + { + var site = PageState.Site; + site.AllowRegistration = bool.Parse(_allowregistration); + await SiteService.UpdateSiteAsync(site); + 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); + } + } + } diff --git a/Oqtane.Client/Modules/Admin/Visitors/Index.razor b/Oqtane.Client/Modules/Admin/Visitors/Index.razor new file mode 100644 index 00000000..87b13296 --- /dev/null +++ b/Oqtane.Client/Modules/Admin/Visitors/Index.razor @@ -0,0 +1,140 @@ +@namespace Oqtane.Modules.Admin.Visitors +@inherits ModuleBase +@inject IVisitorService VisitorService +@inject ISiteService SiteService +@inject IStringLocalizer Localizer +@inject IStringLocalizer SharedLocalizer + +@if (_visitors == null) +{ +

@SharedLocalizer["Loading"]

+} +else +{ + + +
+
+
+ +
+
+ +
+
+
+
+ +
+ @Localizer["IP"] + @Localizer["User"] + @Localizer["Language"] + @Localizer["Visits"] + @Localizer["Visited"] +
+ + @context.IPAddress + + @if (context.UserId != null) + { + @context.User.DisplayName + } + + @context.Language + @context.Visits + @context.VisitedOn + +
+
+ +
+
+ +
+ +
+
+
+
+ +
+
+} + +@code { + private bool _users = false; + private int _days = 1; + private List _visitors; + private string _visitortracking; + + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; + + protected override async Task OnParametersSetAsync() + { + await GetVisitors(); + _visitortracking = PageState.Site.VisitorTracking.ToString(); + } + + private async void TypeChanged(ChangeEventArgs e) + { + try + { + _users = bool.Parse(e.Value.ToString()); + await GetVisitors(); + StateHasChanged(); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error On TypeChanged"); + } + } + + private async void DateChanged(ChangeEventArgs e) + { + try + { + _days = int.Parse(e.Value.ToString()); + await GetVisitors(); + StateHasChanged(); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error On DateChanged"); + } + } + + private async Task GetVisitors() + { + _visitors = await VisitorService.GetVisitorsAsync(PageState.Site.SiteId, DateTime.UtcNow.AddDays(-_days)); + if (_users) + { + _visitors = _visitors.Where(item => item.UserId != null).ToList(); + } + } + + private async Task SaveSiteSettings() + { + try + { + var site = PageState.Site; + site.VisitorTracking = bool.Parse(_visitortracking); + await SiteService.UpdateSiteAsync(site); + 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); + } + } +} diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj index 21f56ced..f054b12f 100644 --- a/Oqtane.Client/Oqtane.Client.csproj +++ b/Oqtane.Client/Oqtane.Client.csproj @@ -36,7 +36,6 @@ - diff --git a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx index 3404baa2..f3aa6a9c 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx @@ -129,7 +129,7 @@ Default Container: - + Appearance @@ -171,9 +171,6 @@ The aliases for the site. An alias can be a domain name (www.site.com) or a virtual folder (ie. www.site.com/folder). If a site has multiple aliases they should be separated by commas. - - Do you want the users to be able to register for an account on the site - Is this site deleted? @@ -225,9 +222,6 @@ Aliases: - - Allow User Registration? - Is Deleted? @@ -282,7 +276,7 @@ Select Theme - + Hosting Model @@ -300,4 +294,13 @@ Browse + + Tenant Information + + + PWA Settings + + + SMTP Settings + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Add.resx b/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Add.resx new file mode 100644 index 00000000..6f983495 --- /dev/null +++ b/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Add.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Redirect To: + + + A fully qualified Url where the user will be redirected + + + A fully qualified Url for this site + + + Url: + + + Error Saving Url Mapping + + + Please Provide All Required Information + + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Edit.resx new file mode 100644 index 00000000..400ba0d5 --- /dev/null +++ b/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Edit.resx @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Redirect To: + + + A fully qualified Url where the user will be redirected + + + A fully qualified Url for this site + + + Url: + + + Error Loading Url Mapping + + + Error Saving Url Mapping + + + Please Provide All Required Information + + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Index.resx b/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Index.resx new file mode 100644 index 00000000..a2eed5e6 --- /dev/null +++ b/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Index.resx @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Are You Sure You Wish To Delete {0}? + + + Add Url Mapping + + + Delete Url Mapping + + + IP + + + User + + + Visited + + + Visits + + + Mapped Urls + + + Broken Urls + + + Specify if broken Urls should be captured automatically and saved in Url Mappings + + + Capture Broken Urls? + + + Error Saving Settings + + + Settings + + + Settings Saved Successfully + + + Urls + + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx index bb13df9e..65cd38e6 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx @@ -126,4 +126,22 @@ Delete User + + Do you want the users to be able to register for an account on the site + + + Allow User Registration? + + + Error Saving Settings + + + Settings + + + Settings Saved Successfully + + + Users + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Visitors/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Visitors/Index.resx new file mode 100644 index 00000000..096b6d2a --- /dev/null +++ b/Oqtane.Client/Resources/Modules/Admin/Visitors/Index.resx @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Url + + + Requested + + + Requests + + + All Visitors + + + Past Day + + + Past Month + + + Past Week + + + Users Only + + + Language + + + Error Saving Settings + + + Settings + + + Settings Saved Successfully + + + Visitors + + + Specify if visitor tracking is enabled + + + Visitor Tracking Enabled? + + \ No newline at end of file diff --git a/Oqtane.Client/Services/Interfaces/IUrlMappingService.cs b/Oqtane.Client/Services/Interfaces/IUrlMappingService.cs new file mode 100644 index 00000000..0a8ef940 --- /dev/null +++ b/Oqtane.Client/Services/Interfaces/IUrlMappingService.cs @@ -0,0 +1,48 @@ +using Oqtane.Models; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Oqtane.Services +{ + /// + /// Service to manage s on a + /// + public interface IUrlMappingService + { + /// + /// Get all s of this . + /// + /// + /// ID-reference of a + /// + Task> GetUrlMappingsAsync(int siteId, bool isMapped); + + /// + /// Get one specific + /// + /// ID-reference of a + /// + Task GetUrlMappingAsync(int urlMappingId); + + /// + /// Add / save a new to the database. + /// + /// + /// + Task AddUrlMappingAsync(UrlMapping urlMapping); + + /// + /// Update a in the database. + /// + /// + /// + Task UpdateUrlMappingAsync(UrlMapping urlMapping); + + /// + /// Delete a in the database. + /// + /// ID-reference of a + /// + Task DeleteUrlMappingAsync(int urlMappingId); + } +} diff --git a/Oqtane.Client/Services/Interfaces/IVisitorService.cs b/Oqtane.Client/Services/Interfaces/IVisitorService.cs new file mode 100644 index 00000000..b25779a5 --- /dev/null +++ b/Oqtane.Client/Services/Interfaces/IVisitorService.cs @@ -0,0 +1,21 @@ +using Oqtane.Models; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Oqtane.Services +{ + /// + /// Service to manage s on a + /// + public interface IVisitorService + { + /// + /// Get all s of this . + /// + /// + /// ID-reference of a + /// + Task> GetVisitorsAsync(int siteId, DateTime fromDate); + } +} diff --git a/Oqtane.Client/Services/UrlMappingService.cs b/Oqtane.Client/Services/UrlMappingService.cs new file mode 100644 index 00000000..725149dc --- /dev/null +++ b/Oqtane.Client/Services/UrlMappingService.cs @@ -0,0 +1,51 @@ +using Oqtane.Models; +using System.Threading.Tasks; +using System.Net.Http; +using System.Linq; +using System.Collections.Generic; +using Oqtane.Documentation; +using Oqtane.Shared; + +namespace Oqtane.Services +{ + [PrivateApi("Don't show in the documentation, as everything should use the Interface")] + public class UrlMappingService : ServiceBase, IUrlMappingService + { + + private readonly SiteState _siteState; + + public UrlMappingService(HttpClient http, SiteState siteState) : base(http) + { + + _siteState = siteState; + } + + private string Apiurl => CreateApiUrl("UrlMapping", _siteState.Alias); + + public async Task> GetUrlMappingsAsync(int siteId, bool isMapped) + { + List urlMappings = await GetJsonAsync>($"{Apiurl}?siteid={siteId}&ismapped={isMapped}"); + return urlMappings.OrderByDescending(item => item.RequestedOn).ToList(); + } + + public async Task GetUrlMappingAsync(int urlMappingId) + { + return await GetJsonAsync($"{Apiurl}/{urlMappingId}"); + } + + public async Task AddUrlMappingAsync(UrlMapping role) + { + return await PostJsonAsync(Apiurl, role); + } + + public async Task UpdateUrlMappingAsync(UrlMapping role) + { + return await PutJsonAsync($"{Apiurl}/{role.UrlMappingId}", role); + } + + public async Task DeleteUrlMappingAsync(int urlMappingId) + { + await DeleteAsync($"{Apiurl}/{urlMappingId}"); + } + } +} diff --git a/Oqtane.Client/Services/VisitorService.cs b/Oqtane.Client/Services/VisitorService.cs new file mode 100644 index 00000000..ca36ee3a --- /dev/null +++ b/Oqtane.Client/Services/VisitorService.cs @@ -0,0 +1,32 @@ +using Oqtane.Models; +using System.Threading.Tasks; +using System.Net.Http; +using System.Linq; +using System.Collections.Generic; +using Oqtane.Documentation; +using Oqtane.Shared; +using System; + +namespace Oqtane.Services +{ + [PrivateApi("Don't show in the documentation, as everything should use the Interface")] + public class VisitorService : ServiceBase, IVisitorService + { + + private readonly SiteState _siteState; + + public VisitorService(HttpClient http, SiteState siteState) : base(http) + { + + _siteState = siteState; + } + + private string Apiurl => CreateApiUrl("Visitor", _siteState.Alias); + + public async Task> GetVisitorsAsync(int siteId, DateTime fromDate) + { + List visitors = await GetJsonAsync>($"{Apiurl}?siteid={siteId}&fromdate={fromDate.ToString("dd-MMM-yyyy")}"); + return visitors.OrderByDescending(item => item.VisitedOn).ToList(); + } + } +} diff --git a/Oqtane.Client/UI/PageState.cs b/Oqtane.Client/UI/PageState.cs index 325f15d2..bc0fdacf 100644 --- a/Oqtane.Client/UI/PageState.cs +++ b/Oqtane.Client/UI/PageState.cs @@ -20,5 +20,6 @@ namespace Oqtane.UI public bool EditMode { get; set; } public DateTime LastSyncDate { get; set; } public Oqtane.Shared.Runtime Runtime { get; set; } + public int VisitorId { get; set; } } } diff --git a/Oqtane.Client/UI/SiteRouter.razor b/Oqtane.Client/UI/SiteRouter.razor index 876f1ead..a6a222db 100644 --- a/Oqtane.Client/UI/SiteRouter.razor +++ b/Oqtane.Client/UI/SiteRouter.razor @@ -25,6 +25,9 @@ [Parameter] public string RenderMode { get; set; } + [Parameter] + public int VisitorId { get; set; } + [CascadingParameter] PageState PageState { get; set; } @@ -221,7 +224,8 @@ Action = action, EditMode = editmode, LastSyncDate = lastsyncdate, - Runtime = runtime + Runtime = runtime, + VisitorId = VisitorId }; OnStateChange?.Invoke(_pagestate); diff --git a/Oqtane.Server/Controllers/SettingController.cs b/Oqtane.Server/Controllers/SettingController.cs index b305686e..9e93dbf5 100644 --- a/Oqtane.Server/Controllers/SettingController.cs +++ b/Oqtane.Server/Controllers/SettingController.cs @@ -164,6 +164,14 @@ namespace Oqtane.Controllers authorized = User.IsInRole(RoleNames.Admin) || (_userPermissions.GetUser(User).UserId == entityId); } break; + case EntityNames.Visitor: + authorized = false; + var visitorCookie = "APP_VISITOR_" + _alias.SiteId.ToString(); + if (int.TryParse(Request.Cookies[visitorCookie], out int visitorId)) + { + authorized = (visitorId == entityId); + } + break; } return authorized; } diff --git a/Oqtane.Server/Controllers/UrlMappingController.cs b/Oqtane.Server/Controllers/UrlMappingController.cs new file mode 100644 index 00000000..d3a0ea53 --- /dev/null +++ b/Oqtane.Server/Controllers/UrlMappingController.cs @@ -0,0 +1,119 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Oqtane.Enums; +using Oqtane.Models; +using Oqtane.Shared; +using Oqtane.Infrastructure; +using Oqtane.Repository; +using System.Net; + +namespace Oqtane.Controllers +{ + [Route(ControllerRoutes.ApiRoute)] + public class UrlMappingController : Controller + { + private readonly IUrlMappingRepository _urlMappings; + private readonly ILogManager _logger; + private readonly Alias _alias; + + public UrlMappingController(IUrlMappingRepository urlMappings, ILogManager logger, ITenantManager tenantManager) + { + _urlMappings = urlMappings; + _logger = logger; + _alias = tenantManager.GetAlias(); + } + + // GET: api/?siteid=x&ismapped=y + [HttpGet] + [Authorize(Roles = RoleNames.Admin)] + public IEnumerable Get(string siteid, string ismapped) + { + int SiteId; + if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId) + { + return _urlMappings.GetUrlMappings(SiteId, bool.Parse(ismapped)); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized UrlMapping Get Attempt {SiteId}", siteid); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return null; + } + } + + // GET api//5 + [HttpGet("{id}")] + [Authorize(Roles = RoleNames.Admin)] + public UrlMapping Get(int id) + { + var urlMapping = _urlMappings.GetUrlMapping(id); + if (urlMapping != null && (urlMapping.SiteId == _alias.SiteId)) + { + return urlMapping; + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized UrlMapping Get Attempt {UrlMappingId}", id); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return null; + } + } + + // POST api/ + [HttpPost] + [Authorize(Roles = RoleNames.Admin)] + public UrlMapping Post([FromBody] UrlMapping urlMapping) + { + if (ModelState.IsValid && urlMapping.SiteId == _alias.SiteId) + { + urlMapping = _urlMappings.AddUrlMapping(urlMapping); + _logger.Log(LogLevel.Information, this, LogFunction.Create, "UrlMapping Added {UrlMapping}", urlMapping); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized UrlMapping Post Attempt {Role}", urlMapping); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + urlMapping = null; + } + return urlMapping; + } + + // PUT api//5 + [HttpPut("{id}")] + [Authorize(Roles = RoleNames.Admin)] + public UrlMapping Put(int id, [FromBody] UrlMapping urlMapping) + { + if (ModelState.IsValid && urlMapping.SiteId == _alias.SiteId && _urlMappings.GetUrlMapping(urlMapping.UrlMappingId, false) != null) + { + urlMapping = _urlMappings.UpdateUrlMapping(urlMapping); + _logger.Log(LogLevel.Information, this, LogFunction.Update, "UrlMapping Updated {UrlMapping}", urlMapping); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized UrlMapping Put Attempt {UrlMapping}", urlMapping); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + urlMapping = null; + } + return urlMapping; + } + + // DELETE api//5 + [HttpDelete("{id}")] + [Authorize(Roles = RoleNames.Admin)] + public void Delete(int id) + { + var urlMapping = _urlMappings.GetUrlMapping(id); + if (urlMapping != null && urlMapping.SiteId == _alias.SiteId) + { + _urlMappings.DeleteUrlMapping(id); + _logger.Log(LogLevel.Information, this, LogFunction.Delete, "UrlMapping Deleted {UrlMappingId}", id); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized UrlMapping Delete Attempt {UrlMappingId}", id); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } + } + } +} diff --git a/Oqtane.Server/Controllers/VisitorController.cs b/Oqtane.Server/Controllers/VisitorController.cs new file mode 100644 index 00000000..901c5ea5 --- /dev/null +++ b/Oqtane.Server/Controllers/VisitorController.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Oqtane.Enums; +using Oqtane.Models; +using Oqtane.Shared; +using Oqtane.Infrastructure; +using Oqtane.Repository; +using System.Net; +using System; + +namespace Oqtane.Controllers +{ + [Route(ControllerRoutes.ApiRoute)] + public class VisitorController : Controller + { + private readonly IVisitorRepository _visitors; + private readonly ILogManager _logger; + private readonly Alias _alias; + + public VisitorController(IVisitorRepository visitors, ILogManager logger, ITenantManager tenantManager) + { + _visitors = visitors; + _logger = logger; + _alias = tenantManager.GetAlias(); + } + + // GET: api/?siteid=x&fromdate=y + [HttpGet] + [Authorize(Roles = RoleNames.Admin)] + public IEnumerable Get(string siteid, string fromdate) + { + int SiteId; + if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId) + { + return _visitors.GetVisitors(SiteId, DateTime.Parse(fromdate)); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Visitor Get Attempt {SiteId}", siteid); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return null; + } + } + } +} diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index adf8c326..cd5bb63a 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -98,6 +98,8 @@ namespace Microsoft.Extensions.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); // obsolete - replaced by ITenantManager services.AddTransient(); diff --git a/Oqtane.Server/Infrastructure/DatabaseManager.cs b/Oqtane.Server/Infrastructure/DatabaseManager.cs index 403ad93b..fa5a9029 100644 --- a/Oqtane.Server/Infrastructure/DatabaseManager.cs +++ b/Oqtane.Server/Infrastructure/DatabaseManager.cs @@ -582,12 +582,19 @@ namespace Oqtane.Infrastructure TenantId = tenant.TenantId, Name = install.SiteName, LogoFileId = null, + FaviconFileId = null, + PwaIsEnabled = false, + PwaAppIconFileId = null, + PwaSplashIconFileId = null, + AllowRegistration = false, + CaptureBrokenUrls = true, + VisitorTracking = true, DefaultThemeType = (!string.IsNullOrEmpty(install.DefaultTheme)) ? install.DefaultTheme : Constants.DefaultTheme, DefaultContainerType = (!string.IsNullOrEmpty(install.DefaultContainer)) ? install.DefaultContainer : Constants.DefaultContainer, AdminContainerType = (!string.IsNullOrEmpty(install.DefaultAdminContainer)) ? install.DefaultAdminContainer : Constants.DefaultAdminContainer, SiteTemplateType = install.SiteTemplate, Runtime = (!string.IsNullOrEmpty(install.Runtime)) ? install.Runtime : _configManager.GetSection("Runtime").Value, - RenderMode = (!string.IsNullOrEmpty(install.RenderMode)) ? install.RenderMode : _configManager.GetSection("RenderMode").Value + RenderMode = (!string.IsNullOrEmpty(install.RenderMode)) ? install.RenderMode : _configManager.GetSection("RenderMode").Value, }; site = sites.AddSite(site); diff --git a/Oqtane.Server/Infrastructure/UpgradeManager.cs b/Oqtane.Server/Infrastructure/UpgradeManager.cs index 51da8971..124652c9 100644 --- a/Oqtane.Server/Infrastructure/UpgradeManager.cs +++ b/Oqtane.Server/Infrastructure/UpgradeManager.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; +using Oqtane.Extensions; using Oqtane.Models; using Oqtane.Repository; using Oqtane.Shared; @@ -37,9 +38,6 @@ namespace Oqtane.Infrastructure switch (version) { - case "1.0.0": - Upgrade_1_0_0(tenant, scope); - break; case "2.0.2": Upgrade_2_0_2(tenant, scope); break; @@ -49,61 +47,13 @@ namespace Oqtane.Infrastructure case "2.2.0": Upgrade_2_2_0(tenant, scope); break; + case "3.0.1": + Upgrade_3_0_1(tenant, scope); + break; } } } - /// - /// **Note: this code is commented out on purpose - it provides an example of how to programmatically add a page to all existing sites on upgrade - /// - /// - /// - private void Upgrade_1_0_0(Tenant tenant, IServiceScope scope) - { - //var pageTemplates = new List(); - // - //pageTemplates.Add(new PageTemplate - //{ - // Name = "Test", - // Parent = "", - // Order = 1, - // Path = "test", - // Icon = Icons.Badge, - // IsNavigation = true, - // IsPersonalizable = false, - // IsClickable = true, - // PagePermissions = new List - // { - // new Permission(PermissionNames.View, RoleNames.Admin, true), - // new Permission(PermissionNames.View, RoleNames.Everyone, true), - // new Permission(PermissionNames.Edit, RoleNames.Admin, true) - // }.EncodePermissions(), - // PageTemplateModules = new List - // { - // new PageTemplateModule - // { - // ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Login.Index).ToModuleDefinitionName(), Title = "Test", Pane = "Content", - // ModulePermissions = new List - // { - // new Permission(PermissionNames.View, RoleNames.Admin, true), - // new Permission(PermissionNames.View, RoleNames.Everyone, true), - // new Permission(PermissionNames.Edit, RoleNames.Admin, true) - // }.EncodePermissions(), - // Content = "" - // } - // } - //}); - // - //if (pageTemplates.Count != 0) - //{ - // var sites = scope.ServiceProvider.GetRequiredService(); - // foreach (Site site in sites.GetSites().ToList()) - // { - // sites.CreatePages(site, pageTemplates); - // } - //} - } - private void Upgrade_2_0_2(Tenant tenant, IServiceScope scope) { if (tenant.Name == TenantNames.Master) @@ -163,5 +113,74 @@ namespace Oqtane.Infrastructure } } } + + private void Upgrade_3_0_1(Tenant tenant, IServiceScope scope) + { + var pageTemplates = new List(); + + pageTemplates.Add(new PageTemplate + { + Name = "Url Mappings", + Parent = "Admin", + Order = 33, + Path = "admin/urlmappings", + Icon = Icons.LinkBroken, + IsNavigation = true, + IsPersonalizable = false, + PagePermissions = new List + { + new Permission(PermissionNames.View, RoleNames.Admin, true), + new Permission(PermissionNames.Edit, RoleNames.Admin, true) + }.EncodePermissions(), + PageTemplateModules = new List + { + new PageTemplateModule + { + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.UrlMappings.Index).ToModuleDefinitionName(), Title = "Url Mappings", Pane = PaneNames.Admin, + ModulePermissions = new List + { + new Permission(PermissionNames.View, RoleNames.Admin, true), + new Permission(PermissionNames.Edit, RoleNames.Admin, true) + }.EncodePermissions(), + Content = "" + } + } + }); + + pageTemplates.Add(new PageTemplate + { + Name = "Visitor Management", + Parent = "Admin", + Order = 35, + Path = "admin/visitors", + Icon = Icons.Eye, + IsNavigation = true, + IsPersonalizable = false, + PagePermissions = new List + { + new Permission(PermissionNames.View, RoleNames.Admin, true), + new Permission(PermissionNames.Edit, RoleNames.Admin, true) + }.EncodePermissions(), + PageTemplateModules = new List + { + new PageTemplateModule + { + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Visitors.Index).ToModuleDefinitionName(), Title = "Visitor Management", Pane = PaneNames.Admin, + ModulePermissions = new List + { + new Permission(PermissionNames.View, RoleNames.Admin, true), + new Permission(PermissionNames.Edit, RoleNames.Admin, true) + }.EncodePermissions(), + Content = "" + } + } + }); + + var sites = scope.ServiceProvider.GetRequiredService(); + foreach (Site site in sites.GetSites().ToList()) + { + sites.CreatePages(site, pageTemplates); + } + } } } diff --git a/Oqtane.Server/Migrations/EntityBuilders/UrlMappingEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/UrlMappingEntityBuilder.cs new file mode 100644 index 00000000..7879fdd3 --- /dev/null +++ b/Oqtane.Server/Migrations/EntityBuilders/UrlMappingEntityBuilder.cs @@ -0,0 +1,51 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations.Operations; +using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; +using Oqtane.Databases.Interfaces; + +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global + +namespace Oqtane.Migrations.EntityBuilders +{ + public class UrlMappingEntityBuilder : BaseEntityBuilder + { + private const string _entityTableName = "UrlMapping"; + private readonly PrimaryKey _primaryKey = new("PK_UrlMapping", x => x.UrlMappingId); + private readonly ForeignKey _urlMappingForeignKey = new("FK_UrlMapping_Site", x => x.SiteId, "Site", "SiteId", ReferentialAction.Cascade); + + public UrlMappingEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) + { + EntityTableName = _entityTableName; + PrimaryKey = _primaryKey; + ForeignKeys.Add(_urlMappingForeignKey); + } + + protected override UrlMappingEntityBuilder BuildTable(ColumnsBuilder table) + { + UrlMappingId = AddAutoIncrementColumn(table, "UrlMappingId"); + SiteId = AddIntegerColumn(table, "SiteId"); + Url = AddStringColumn(table, "Url", 500); + MappedUrl = AddStringColumn(table, "MappedUrl", 500); + Requests = AddIntegerColumn(table, "Requests"); + CreatedOn = AddDateTimeColumn(table, "CreatedOn"); + RequestedOn = AddDateTimeColumn(table, "RequestedOn"); + + return this; + } + + public OperationBuilder UrlMappingId { get; private set; } + + public OperationBuilder SiteId { get; private set; } + + public OperationBuilder Url { get; private set; } + + public OperationBuilder MappedUrl { get; private set; } + + public OperationBuilder Requests { get; private set; } + + public OperationBuilder CreatedOn { get; private set; } + + public OperationBuilder RequestedOn { get; private set; } + } +} diff --git a/Oqtane.Server/Migrations/EntityBuilders/VisitorEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/VisitorEntityBuilder.cs new file mode 100644 index 00000000..2aaf59e7 --- /dev/null +++ b/Oqtane.Server/Migrations/EntityBuilders/VisitorEntityBuilder.cs @@ -0,0 +1,57 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations.Operations; +using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; +using Oqtane.Databases.Interfaces; + +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global + +namespace Oqtane.Migrations.EntityBuilders +{ + public class VisitorEntityBuilder : BaseEntityBuilder + { + private const string _entityTableName = "Visitor"; + private readonly PrimaryKey _primaryKey = new("PK_Visitor", x => x.VisitorId); + private readonly ForeignKey _visitorForeignKey = new("FK_Visitor_Site", x => x.SiteId, "Site", "SiteId", ReferentialAction.Cascade); + + public VisitorEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) + { + EntityTableName = _entityTableName; + PrimaryKey = _primaryKey; + ForeignKeys.Add(_visitorForeignKey); + } + + protected override VisitorEntityBuilder BuildTable(ColumnsBuilder table) + { + VisitorId = AddAutoIncrementColumn(table, "VisitorId"); + SiteId = AddIntegerColumn(table, "SiteId"); + UserId = AddIntegerColumn(table, "UserId", true); + Visits = AddIntegerColumn(table, "Visits"); + IPAddress = AddStringColumn(table,"IPAddress", 50); + UserAgent = AddStringColumn(table, "UserAgent", 256); + Language = AddStringColumn(table, "Language", 50); + CreatedOn = AddDateTimeColumn(table, "CreatedOn"); + VisitedOn = AddDateTimeColumn(table, "VisitedOn"); + + return this; + } + + public OperationBuilder VisitorId { get; private set; } + + public OperationBuilder SiteId { get; private set; } + + public OperationBuilder UserId { get; private set; } + + public OperationBuilder Visits { get; private set; } + + public OperationBuilder IPAddress { get; private set; } + + public OperationBuilder UserAgent { get; private set; } + + public OperationBuilder Language { get; private set; } + + public OperationBuilder CreatedOn { get; private set; } + + public OperationBuilder VisitedOn { get; private set; } + } +} diff --git a/Oqtane.Server/Migrations/Tenant/03000102_AddVisitorTable.cs b/Oqtane.Server/Migrations/Tenant/03000102_AddVisitorTable.cs new file mode 100644 index 00000000..cef6841d --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/03000102_AddVisitorTable.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Oqtane.Databases.Interfaces; +using Oqtane.Migrations.EntityBuilders; +using Oqtane.Repository; + +namespace Oqtane.Migrations.Tenant +{ + [DbContext(typeof(TenantDBContext))] + [Migration("Tenant.03.00.01.02")] + public class AddVisitorTable : MultiDatabaseMigration + { + public AddVisitorTable(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var visitorEntityBuilder = new VisitorEntityBuilder(migrationBuilder, ActiveDatabase); + visitorEntityBuilder.Create(); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + var visitorEntityBuilder = new VisitorEntityBuilder(migrationBuilder, ActiveDatabase); + visitorEntityBuilder.Drop(); + } + } +} diff --git a/Oqtane.Server/Migrations/Tenant/03000103_AddUrlMappingTable.cs b/Oqtane.Server/Migrations/Tenant/03000103_AddUrlMappingTable.cs new file mode 100644 index 00000000..56583b5d --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/03000103_AddUrlMappingTable.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Oqtane.Databases.Interfaces; +using Oqtane.Migrations.EntityBuilders; +using Oqtane.Repository; + +namespace Oqtane.Migrations.Tenant +{ + [DbContext(typeof(TenantDBContext))] + [Migration("Tenant.03.00.01.03")] + public class AddUrlMappingTable : MultiDatabaseMigration + { + public AddUrlMappingTable(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var urlMappingEntityBuilder = new UrlMappingEntityBuilder(migrationBuilder, ActiveDatabase); + urlMappingEntityBuilder.Create(); + urlMappingEntityBuilder.AddIndex("IX_UrlMapping", new[] { "SiteId", "Url" }, true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + var urlMappingEntityBuilder = new UrlMappingEntityBuilder(migrationBuilder, ActiveDatabase); + urlMappingEntityBuilder.Drop(); + } + } +} diff --git a/Oqtane.Server/Migrations/Tenant/03000104_AddSiteVisitorTracking.cs b/Oqtane.Server/Migrations/Tenant/03000104_AddSiteVisitorTracking.cs new file mode 100644 index 00000000..9c5c8e5d --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/03000104_AddSiteVisitorTracking.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Oqtane.Databases.Interfaces; +using Oqtane.Migrations.EntityBuilders; +using Oqtane.Repository; + +namespace Oqtane.Migrations.Tenant +{ + [DbContext(typeof(TenantDBContext))] + [Migration("Tenant.03.00.01.04")] + public class AddSiteVisitorTracking : MultiDatabaseMigration + { + public AddSiteVisitorTracking(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase); + + siteEntityBuilder.AddBooleanColumn("VisitorTracking", true); + siteEntityBuilder.UpdateColumn("VisitorTracking", "1", "bool", ""); + siteEntityBuilder.AddBooleanColumn("CaptureBrokenUrls", true); + siteEntityBuilder.UpdateColumn("CaptureBrokenUrls", "1", "bool", ""); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase); + + siteEntityBuilder.DropColumn("VisitorTracking"); + siteEntityBuilder.DropColumn("CaptureBrokenUrls"); + } + } +} diff --git a/Oqtane.Server/Pages/_Host.cshtml b/Oqtane.Server/Pages/_Host.cshtml index 8e0980ec..b939f77a 100644 --- a/Oqtane.Server/Pages/_Host.cshtml +++ b/Oqtane.Server/Pages/_Host.cshtml @@ -22,7 +22,7 @@ @(Html.AntiForgeryToken()) - +
diff --git a/Oqtane.Server/Pages/_Host.cshtml.cs b/Oqtane.Server/Pages/_Host.cshtml.cs index e03ef2bd..7882eeb9 100644 --- a/Oqtane.Server/Pages/_Host.cshtml.cs +++ b/Oqtane.Server/Pages/_Host.cshtml.cs @@ -14,6 +14,10 @@ using Microsoft.Extensions.Configuration; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; +using Microsoft.AspNetCore.Http; +using System.Security.Claims; namespace Oqtane.Pages { @@ -26,8 +30,10 @@ namespace Oqtane.Pages private readonly IAntiforgery _antiforgery; private readonly ISiteRepository _sites; private readonly IPageRepository _pages; + private readonly IUrlMappingRepository _urlMappings; + private readonly IVisitorRepository _visitors; - public HostModel(IConfiguration configuration, ITenantManager tenantManager, ILocalizationManager localizationManager, ILanguageRepository languages, IAntiforgery antiforgery, ISiteRepository sites, IPageRepository pages) + public HostModel(IConfiguration configuration, ITenantManager tenantManager, ILocalizationManager localizationManager, ILanguageRepository languages, IAntiforgery antiforgery, ISiteRepository sites, IPageRepository pages, IUrlMappingRepository urlMappings, IVisitorRepository visitors) { _configuration = configuration; _tenantManager = tenantManager; @@ -36,11 +42,14 @@ namespace Oqtane.Pages _antiforgery = antiforgery; _sites = sites; _pages = pages; + _urlMappings = urlMappings; + _visitors = visitors; } public string AntiForgeryToken = ""; public string Runtime = "Server"; public RenderMode RenderMode = RenderMode.Server; + public int VisitorId = -1; public string HeadResources = ""; public string BodyResources = ""; public string Title = ""; @@ -48,7 +57,7 @@ namespace Oqtane.Pages public string PWAScript = ""; public string ThemeType = ""; - public void OnGet() + public IActionResult OnGet() { AntiForgeryToken = _antiforgery.GetAndStoreTokens(HttpContext).RequestToken; @@ -92,6 +101,11 @@ namespace Oqtane.Pages Title = site.Name; ThemeType = site.DefaultThemeType; + if (site.VisitorTracking) + { + TrackVisitor(site.SiteId); + } + var page = _pages.GetPage(route.PagePath, site.SiteId); if (page != null) { @@ -111,6 +125,37 @@ namespace Oqtane.Pages ThemeType = page.ThemeType; } } + else + { + // page does not exist + var url = route.SiteUrl + "/" + route.PagePath; + var urlMapping = _urlMappings.GetUrlMapping(site.SiteId, url); + if (urlMapping == null) + { + if (site.CaptureBrokenUrls) + { + urlMapping = new UrlMapping(); + urlMapping.SiteId = site.SiteId; + urlMapping.Url = url; + urlMapping.MappedUrl = ""; + urlMapping.Requests = 1; + urlMapping.CreatedOn = DateTime.UtcNow; + urlMapping.RequestedOn = DateTime.UtcNow; + _urlMappings.AddUrlMapping(urlMapping); + } + } + else + { + urlMapping.Requests += 1; + urlMapping.RequestedOn = DateTime.UtcNow; + _urlMappings.UpdateUrlMapping(urlMapping); + + if (!string.IsNullOrEmpty(urlMapping.MappedUrl)) + { + return RedirectPermanent(urlMapping.MappedUrl); + } + } + } } // include global resources @@ -139,6 +184,64 @@ namespace Oqtane.Pages } } } + return Page(); + } + + private void TrackVisitor(int SiteId) + { + var VisitorCookie = "APP_VISITOR_" + SiteId.ToString(); + if (!int.TryParse(Request.Cookies[VisitorCookie], out VisitorId)) + { + var visitor = new Visitor(); + visitor.SiteId = SiteId; + visitor.IPAddress = HttpContext.Connection.RemoteIpAddress.ToString(); + visitor.UserAgent = Request.Headers[HeaderNames.UserAgent]; + visitor.Language = Request.Headers[HeaderNames.AcceptLanguage]; + if (visitor.Language.Contains(",")) + { + visitor.Language = visitor.Language.Substring(0, visitor.Language.IndexOf(",")); + } + visitor.UserId = null; + visitor.Visits = 1; + visitor.CreatedOn = DateTime.UtcNow; + visitor.VisitedOn = DateTime.UtcNow; + visitor = _visitors.AddVisitor(visitor); + + Response.Cookies.Append( + VisitorCookie, + visitor.VisitorId.ToString(), + new CookieOptions() + { + Expires = DateTimeOffset.UtcNow.AddYears(1), + IsEssential = true + } + ); + } + else + { + var visitor = _visitors.GetVisitor(VisitorId); + if (visitor != null) + { + visitor.IPAddress = HttpContext.Connection.RemoteIpAddress.ToString(); + visitor.UserAgent = Request.Headers[HeaderNames.UserAgent]; + visitor.Language = Request.Headers[HeaderNames.AcceptLanguage]; + if (visitor.Language.Contains(",")) + { + visitor.Language = visitor.Language.Substring(0, visitor.Language.IndexOf(",")); + } + if (User.HasClaim(item => item.Type == ClaimTypes.PrimarySid)) + { + visitor.UserId = int.Parse(User.Claims.First(item => item.Type == ClaimTypes.PrimarySid).Value); + } + visitor.Visits += 1; + visitor.VisitedOn = DateTime.UtcNow; + _visitors.UpdateVisitor(visitor); + } + else + { + Response.Cookies.Delete(VisitorCookie); + } + } } private string CreatePWAScript(Alias alias, Site site, Route route) diff --git a/Oqtane.Server/Repository/Context/TenantDBContext.cs b/Oqtane.Server/Repository/Context/TenantDBContext.cs index ca8c822d..dc2c219b 100644 --- a/Oqtane.Server/Repository/Context/TenantDBContext.cs +++ b/Oqtane.Server/Repository/Context/TenantDBContext.cs @@ -29,5 +29,7 @@ namespace Oqtane.Repository public virtual DbSet Folder { get; set; } public virtual DbSet File { get; set; } public virtual DbSet Language { get; set; } + public virtual DbSet Visitor { get; set; } + public virtual DbSet UrlMapping { get; set; } } } diff --git a/Oqtane.Server/Repository/Interfaces/IUrlMappingRepository.cs b/Oqtane.Server/Repository/Interfaces/IUrlMappingRepository.cs new file mode 100644 index 00000000..f954d87b --- /dev/null +++ b/Oqtane.Server/Repository/Interfaces/IUrlMappingRepository.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using Oqtane.Models; + +namespace Oqtane.Repository +{ + public interface IUrlMappingRepository + { + IEnumerable GetUrlMappings(int siteId, bool isMapped); + UrlMapping AddUrlMapping(UrlMapping urlMapping); + UrlMapping UpdateUrlMapping(UrlMapping urlMapping); + UrlMapping GetUrlMapping(int urlMappingId); + UrlMapping GetUrlMapping(int urlMappingId, bool tracking); + UrlMapping GetUrlMapping(int siteId, string url); + void DeleteUrlMapping(int urlMappingId); + } +} diff --git a/Oqtane.Server/Repository/Interfaces/IVisitorRepository.cs b/Oqtane.Server/Repository/Interfaces/IVisitorRepository.cs new file mode 100644 index 00000000..d50ceec2 --- /dev/null +++ b/Oqtane.Server/Repository/Interfaces/IVisitorRepository.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using Oqtane.Models; + +namespace Oqtane.Repository +{ + public interface IVisitorRepository + { + IEnumerable GetVisitors(int siteId, DateTime fromDate); + Visitor AddVisitor(Visitor visitor); + Visitor UpdateVisitor(Visitor visitor); + Visitor GetVisitor(int visitorId); + void DeleteVisitor(int visitorId); + } +} diff --git a/Oqtane.Server/Repository/SiteRepository.cs b/Oqtane.Server/Repository/SiteRepository.cs index bbf8c856..0fcf9cef 100644 --- a/Oqtane.Server/Repository/SiteRepository.cs +++ b/Oqtane.Server/Repository/SiteRepository.cs @@ -615,13 +615,70 @@ namespace Oqtane.Repository } } }); + pageTemplates.Add(new PageTemplate + { + Name = "Url Mappings", + Parent = "Admin", + Order = 15, + Path = "admin/urlmappings", + Icon = Icons.LinkBroken, + IsNavigation = true, + IsPersonalizable = false, + PagePermissions = new List + { + new Permission(PermissionNames.View, RoleNames.Admin, true), + new Permission(PermissionNames.Edit, RoleNames.Admin, true) + }.EncodePermissions(), + PageTemplateModules = new List + { + new PageTemplateModule + { + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.UrlMappings.Index).ToModuleDefinitionName(), Title = "Url Mappings", Pane = PaneNames.Admin, + ModulePermissions = new List + { + new Permission(PermissionNames.View, RoleNames.Admin, true), + new Permission(PermissionNames.Edit, RoleNames.Admin, true) + }.EncodePermissions(), + Content = "" + } + } + }); + + pageTemplates.Add(new PageTemplate + { + Name = "Visitor Management", + Parent = "Admin", + Order = 17, + Path = "admin/visitors", + Icon = Icons.Eye, + IsNavigation = true, + IsPersonalizable = false, + PagePermissions = new List + { + new Permission(PermissionNames.View, RoleNames.Admin, true), + new Permission(PermissionNames.Edit, RoleNames.Admin, true) + }.EncodePermissions(), + PageTemplateModules = new List + { + new PageTemplateModule + { + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Visitors.Index).ToModuleDefinitionName(), Title = "Visitor Management", Pane = PaneNames.Admin, + ModulePermissions = new List + { + new Permission(PermissionNames.View, RoleNames.Admin, true), + new Permission(PermissionNames.Edit, RoleNames.Admin, true) + }.EncodePermissions(), + Content = "" + } + } + }); // host pages pageTemplates.Add(new PageTemplate { Name = "Event Log", Parent = "Admin", - Order = 15, + Order = 19, Path = "admin/log", Icon = Icons.MagnifyingGlass, IsNavigation = false, @@ -649,7 +706,7 @@ namespace Oqtane.Repository { Name = "Site Management", Parent = "Admin", - Order = 17, + Order = 21, Path = "admin/sites", Icon = Icons.Globe, IsNavigation = false, @@ -677,7 +734,7 @@ namespace Oqtane.Repository { Name = "Module Management", Parent = "Admin", - Order = 19, + Order = 23, Path = "admin/modules", Icon = Icons.Browser, IsNavigation = false, @@ -705,7 +762,7 @@ namespace Oqtane.Repository { Name = "Theme Management", Parent = "Admin", - Order = 21, + Order = 25, Path = "admin/themes", Icon = Icons.Brush, IsNavigation = false, @@ -733,7 +790,7 @@ namespace Oqtane.Repository { Name = "Language Management", Parent = "Admin", - Order = 23, + Order = 27, Path = "admin/languages", Icon = Icons.Text, IsNavigation = false, @@ -765,7 +822,7 @@ namespace Oqtane.Repository { Name = "Scheduled Jobs", Parent = "Admin", - Order = 25, + Order = 29, Path = "admin/jobs", Icon = Icons.Timer, IsNavigation = false, @@ -793,7 +850,7 @@ namespace Oqtane.Repository { Name = "Sql Management", Parent = "Admin", - Order = 27, + Order = 31, Path = "admin/sql", Icon = Icons.Spreadsheet, IsNavigation = false, @@ -821,7 +878,7 @@ namespace Oqtane.Repository { Name = "System Info", Parent = "Admin", - Order = 29, + Order = 33, Path = "admin/system", Icon = Icons.MedicalCross, IsNavigation = false, @@ -849,7 +906,7 @@ namespace Oqtane.Repository { Name = "System Update", Parent = "Admin", - Order = 31, + Order = 35, Path = "admin/update", Icon = Icons.Aperture, IsNavigation = false, diff --git a/Oqtane.Server/Repository/UrlMappingRepository.cs b/Oqtane.Server/Repository/UrlMappingRepository.cs new file mode 100644 index 00000000..306d8f69 --- /dev/null +++ b/Oqtane.Server/Repository/UrlMappingRepository.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Oqtane.Models; + +namespace Oqtane.Repository +{ + public class UrlMappingRepository : IUrlMappingRepository + { + private TenantDBContext _db; + + public UrlMappingRepository(TenantDBContext context) + { + _db = context; + } + + public IEnumerable GetUrlMappings(int siteId, bool isMapped) + { + if (isMapped) + { + return _db.UrlMapping.Where(item => item.SiteId == siteId && !string.IsNullOrEmpty(item.MappedUrl)).Take(200); + } + else + { + return _db.UrlMapping.Where(item => item.SiteId == siteId && string.IsNullOrEmpty(item.MappedUrl)).Take(200); + } + } + + public UrlMapping AddUrlMapping(UrlMapping urlMapping) + { + _db.UrlMapping.Add(urlMapping); + _db.SaveChanges(); + return urlMapping; + } + + public UrlMapping UpdateUrlMapping(UrlMapping urlMapping) + { + _db.Entry(urlMapping).State = EntityState.Modified; + _db.SaveChanges(); + return urlMapping; + } + + public UrlMapping GetUrlMapping(int urlMappingId) + { + return GetUrlMapping(urlMappingId, true); + } + + public UrlMapping GetUrlMapping(int urlMappingId, bool tracking) + { + if (tracking) + { + return _db.UrlMapping.Find(urlMappingId); + } + else + { + return _db.UrlMapping.AsNoTracking().FirstOrDefault(item => item.UrlMappingId == urlMappingId); + } + } + + public UrlMapping GetUrlMapping(int siteId, string url) + { + return _db.UrlMapping.Where(item => item.SiteId == siteId && item.Url == url).FirstOrDefault(); + } + + public void DeleteUrlMapping(int urlMappingId) + { + UrlMapping urlMapping = _db.UrlMapping.Find(urlMappingId); + _db.UrlMapping.Remove(urlMapping); + _db.SaveChanges(); + } + } +} diff --git a/Oqtane.Server/Repository/VisitorRepository.cs b/Oqtane.Server/Repository/VisitorRepository.cs new file mode 100644 index 00000000..254c48e7 --- /dev/null +++ b/Oqtane.Server/Repository/VisitorRepository.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Oqtane.Models; + +namespace Oqtane.Repository +{ + public class VisitorRepository : IVisitorRepository + { + private TenantDBContext _db; + + public VisitorRepository(TenantDBContext context) + { + _db = context; + } + + public IEnumerable GetVisitors(int siteId, DateTime fromDate) + { + return _db.Visitor.AsNoTracking() + .Include(item => item.User) // eager load users + .Where(item => item.SiteId == siteId && item.VisitedOn >= fromDate); + } + + public Visitor AddVisitor(Visitor visitor) + { + _db.Visitor.Add(visitor); + _db.SaveChanges(); + return visitor; + } + + public Visitor UpdateVisitor(Visitor visitor) + { + _db.Entry(visitor).State = EntityState.Modified; + _db.SaveChanges(); + return visitor; + } + + public Visitor GetVisitor(int visitorId) + { + return _db.Visitor.Find(visitorId); + } + + public void DeleteVisitor(int visitorId) + { + Visitor visitor = _db.Visitor.Find(visitorId); + _db.Visitor.Remove(visitor); + _db.SaveChanges(); + } + } +} diff --git a/Oqtane.Shared/Models/Site.cs b/Oqtane.Shared/Models/Site.cs index 5ccf11d0..eb7682a2 100644 --- a/Oqtane.Shared/Models/Site.cs +++ b/Oqtane.Shared/Models/Site.cs @@ -49,10 +49,20 @@ namespace Oqtane.Models public int? PwaSplashIconFileId { get; set; } /// - /// Determines if users may register / create accounts + /// Determines if visitors may register / create user accounts /// public bool AllowRegistration { get; set; } + /// + /// Determines if visitors will be tracked + /// + public bool VisitorTracking { get; set; } + + /// + /// Determines if broken urls (404s) will be captured automatically + /// + public bool CaptureBrokenUrls { get; set; } + /// /// Unique GUID to identify the Site. /// diff --git a/Oqtane.Shared/Models/UrlMapping.cs b/Oqtane.Shared/Models/UrlMapping.cs new file mode 100644 index 00000000..b5a15384 --- /dev/null +++ b/Oqtane.Shared/Models/UrlMapping.cs @@ -0,0 +1,47 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Oqtane.Models +{ + /// + /// Describes a UrlMapping in Oqtane. + /// + public class UrlMapping + { + /// + /// ID of this UrlMapping. + /// + public int UrlMappingId { get; set; } + + /// + /// Reference to a + /// + public int SiteId { get; set; } + + /// + /// A fully quaified Url + /// + public string Url { get; set; } + + /// + /// A Url the visitor will be redirected to + /// + public string MappedUrl { get; set; } + + /// + /// Number of requests all time for the url + /// + public int Requests { get; set; } + + /// + /// Date when the url was first requested for the site + /// + public DateTime CreatedOn { get; set; } + + /// + /// Date when the url was last requested for the site + /// + public DateTime RequestedOn { get; set; } + + } +} diff --git a/Oqtane.Shared/Models/Visitor.cs b/Oqtane.Shared/Models/Visitor.cs new file mode 100644 index 00000000..b27fdca5 --- /dev/null +++ b/Oqtane.Shared/Models/Visitor.cs @@ -0,0 +1,61 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Oqtane.Models +{ + /// + /// Describes a Visitor in Oqtane. + /// + public class Visitor + { + /// + /// ID of this Visitor. + /// + public int VisitorId { get; set; } + + /// + /// Reference to a + /// + public int SiteId { get; set; } + + /// + /// Reference to a if applicable + /// + public int? UserId { get; set; } + + /// + /// Number of times a visitor has visited a site + /// + public int Visits { get; set; } + + /// + /// IP Address of visitor + /// + public string IPAddress { get; set; } + + /// + /// User agent of visitor + /// + public string UserAgent { get; set; } + + /// + /// Language of visitor + /// + public string Language { get; set; } + + /// + /// Date the visitor first visited the site + /// + public DateTime CreatedOn { get; set; } + + /// + /// Date the visitor last visited the site + /// + public DateTime VisitedOn { get; set; } + + /// + /// Direct reference to the object (if applicable) + /// + public User User { get; set; } + } +} diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 9a2c19cc..eef3e4e1 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -3,8 +3,8 @@ using System; namespace Oqtane.Shared { public class Constants { - public static readonly string Version = "3.0.0"; - public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0"; + public static readonly string Version = "3.0.1"; + public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1"; public const string PackageId = "Oqtane.Framework"; public const string UpdaterPackageId = "Oqtane.Updater"; public const string PackageRegistryUrl = "https://www.oqtane.net"; diff --git a/Oqtane.Shared/Shared/EntityNames.cs b/Oqtane.Shared/Shared/EntityNames.cs index 345267f3..5efbd716 100644 --- a/Oqtane.Shared/Shared/EntityNames.cs +++ b/Oqtane.Shared/Shared/EntityNames.cs @@ -1,4 +1,4 @@ -namespace Oqtane.Shared +namespace Oqtane.Shared { public class EntityNames { @@ -10,5 +10,6 @@ public const string Page = "Page"; public const string Folder = "Folder"; public const string User = "User"; + public const string Visitor = "Visitor"; } }