diff --git a/Oqtane.Client/Modules/Admin/Languages/Add.razor b/Oqtane.Client/Modules/Admin/Languages/Add.razor index 23af68e5..52a958c2 100644 --- a/Oqtane.Client/Modules/Admin/Languages/Add.razor +++ b/Oqtane.Client/Modules/Admin/Languages/Add.razor @@ -94,7 +94,6 @@ else var language = new Language { SiteId = PageState.Page.SiteId, - Name = CultureInfo.GetCultureInfo(_code).DisplayName, Code = _code, IsDefault = (_default == null ? false : Boolean.Parse(_default)) }; @@ -130,7 +129,7 @@ else { var interop = new Interop(JSRuntime); var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)); - await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360); + await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360, true, "Lax"); } } diff --git a/Oqtane.Client/Modules/Admin/Languages/Edit.razor b/Oqtane.Client/Modules/Admin/Languages/Edit.razor index 5929edd9..7aa074c7 100644 --- a/Oqtane.Client/Modules/Admin/Languages/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Languages/Edit.razor @@ -103,7 +103,7 @@ else { var interop = new Interop(JSRuntime); var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)); - await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360); + await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360, true, "Lax"); } } diff --git a/Oqtane.Client/Modules/Admin/RecycleBin/Index.razor b/Oqtane.Client/Modules/Admin/RecycleBin/Index.razor index 91cf3669..4e4e2607 100644 --- a/Oqtane.Client/Modules/Admin/RecycleBin/Index.razor +++ b/Oqtane.Client/Modules/Admin/RecycleBin/Index.razor @@ -22,7 +22,7 @@ else } else { - +
    @@ -50,7 +50,7 @@ else } else { - +
    diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index 405adea4..ff94804a 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -376,7 +376,7 @@
- +
@@ -388,7 +388,7 @@
- +
@@ -571,7 +571,7 @@ if (tenant != null) { _tenant = tenant.Name; - _database = _databases.Find(item => item.DBType == tenant.DBType)?.Name; + _database = _databases.Find(item => item.DBType == tenant.DBType && item.Name != "LocalDB")?.Name; _connectionstring = tenant.DBConnectionString; } } diff --git a/Oqtane.Client/Modules/Admin/Sites/Add.razor b/Oqtane.Client/Modules/Admin/Sites/Add.razor index 300aafcb..47f19b04 100644 --- a/Oqtane.Client/Modules/Admin/Sites/Add.razor +++ b/Oqtane.Client/Modules/Admin/Sites/Add.razor @@ -109,7 +109,7 @@ else
- +
diff --git a/Oqtane.Client/Modules/Admin/Sql/Index.razor b/Oqtane.Client/Modules/Admin/Sql/Index.razor index 6d0dee92..476ebd1e 100644 --- a/Oqtane.Client/Modules/Admin/Sql/Index.razor +++ b/Oqtane.Client/Modules/Admin/Sql/Index.razor @@ -83,24 +83,15 @@ else { @if (_connection != "-") { -
- -
- @if (_databases != null) - { - - } -
-
@if (!string.IsNullOrEmpty(_tenant)) { -
+
+ +
+ +
+
+
@@ -204,12 +195,12 @@ else { _connectionstring = _connections[_connection].ToString(); _tenant = ""; - _databasetype = "-"; + _databasetype = ""; var tenant = _tenants.FirstOrDefault(item => item.DBConnectionString == _connection); if (tenant != null) { _tenant = tenant.Name; - _databasetype = _databases.FirstOrDefault(item => item.DBType == tenant.DBType).Name; + _databasetype = _databases.FirstOrDefault(item => item.DBType == tenant.DBType && item.Name != "LocalDB").Name; } } else diff --git a/Oqtane.Client/Modules/Admin/Upgrade/Index.razor b/Oqtane.Client/Modules/Admin/Upgrade/Index.razor index 2c91688e..c43ae566 100644 --- a/Oqtane.Client/Modules/Admin/Upgrade/Index.razor +++ b/Oqtane.Client/Modules/Admin/Upgrade/Index.razor @@ -54,6 +54,8 @@ } else { + AddModuleMessage(Localizer["Disclaimer.Text"], MessageType.Warning); + List packages = await PackageService.GetPackagesAsync("framework", "", "", ""); if (packages != null) { @@ -97,13 +99,16 @@ { try { + ShowProgressIndicator(); await PackageService.DownloadPackageAsync(packageid, version); await PackageService.DownloadPackageAsync(Constants.UpdaterPackageId, version); + HideProgressIndicator(); AddModuleMessage(Localizer["Success.Framework.Download"], MessageType.Success); } catch (Exception ex) { await logger.LogError(ex, "Error Downloading Framework Package {Error}", ex.Message); + HideProgressIndicator(); AddModuleMessage(Localizer["Error.Framework.Download"], MessageType.Error); } } diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index fa493c36..a59b161d 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -182,13 +182,31 @@ else
-
+
+ +
+
+ + @if (!string.IsNullOrEmpty(_providerurl)) + { + @Localizer["Info"] + } +
+ +
+
+
@@ -452,6 +470,8 @@ else private string _maximumfailures; private string _lockoutduration; + private string _provider; + private string _providerurl; private string _providertype; private string _providername; private string _authority; @@ -519,33 +539,7 @@ else _maximumfailures = SettingService.GetSetting(settings, "IdentityOptions:Lockout:MaxFailedAccessAttempts", "5"); _lockoutduration = TimeSpan.Parse(SettingService.GetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", "00:05:00")).TotalMinutes.ToString(); - _providertype = SettingService.GetSetting(settings, "ExternalLogin:ProviderType", ""); - _providername = SettingService.GetSetting(settings, "ExternalLogin:ProviderName", ""); - _authority = SettingService.GetSetting(settings, "ExternalLogin:Authority", ""); - _metadataurl = SettingService.GetSetting(settings, "ExternalLogin:MetadataUrl", ""); - _authorizationurl = SettingService.GetSetting(settings, "ExternalLogin:AuthorizationUrl", ""); - _tokenurl = SettingService.GetSetting(settings, "ExternalLogin:TokenUrl", ""); - _userinfourl = SettingService.GetSetting(settings, "ExternalLogin:UserInfoUrl", ""); - _clientid = SettingService.GetSetting(settings, "ExternalLogin:ClientId", ""); - _clientsecret = SettingService.GetSetting(settings, "ExternalLogin:ClientSecret", ""); - _toggleclientsecret = SharedLocalizer["ShowPassword"]; - _authresponsetype = SettingService.GetSetting(settings, "ExternalLogin:AuthResponseType", "code"); - _scopes = SettingService.GetSetting(settings, "ExternalLogin:Scopes", ""); - _parameters = SettingService.GetSetting(settings, "ExternalLogin:Parameters", ""); - _pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false"); - _redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype; - _reviewclaims = SettingService.GetSetting(settings, "ExternalLogin:ReviewClaims", "false"); - _externalloginurl = Utilities.TenantUrl(PageState.Alias, "/pages/external"); - _identifierclaimtype = SettingService.GetSetting(settings, "ExternalLogin:IdentifierClaimType", "sub"); - _nameclaimtype = SettingService.GetSetting(settings, "ExternalLogin:NameClaimType", "name"); - _emailclaimtype = SettingService.GetSetting(settings, "ExternalLogin:EmailClaimType", "email"); - _roleclaimtype = SettingService.GetSetting(settings, "ExternalLogin:RoleClaimType", ""); - _roleclaimmappings = SettingService.GetSetting(settings, "ExternalLogin:RoleClaimMappings", ""); - _synchronizeroles = SettingService.GetSetting(settings, "ExternalLogin:SynchronizeRoles", "false"); - _profileclaimtypes = SettingService.GetSetting(settings, "ExternalLogin:ProfileClaimTypes", ""); - _domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", ""); - _createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true"); - _verifyusers = SettingService.GetSetting(settings, "ExternalLogin:VerifyUsers", "true"); + LoadExternalLoginSettings(settings); _secret = SettingService.GetSetting(settings, "JwtOptions:Secret", ""); _togglesecret = SharedLocalizer["ShowPassword"]; @@ -555,6 +549,39 @@ else } } + private void LoadExternalLoginSettings(Dictionary settings) + { + _provider = SettingService.GetSetting(settings, "ExternalLogin:Provider", ""); + _providerurl = SettingService.GetSetting(settings, "ExternalLogin:ProviderUrl", ""); + _providertype = SettingService.GetSetting(settings, "ExternalLogin:ProviderType", ""); + _providername = SettingService.GetSetting(settings, "ExternalLogin:ProviderName", ""); + _authority = SettingService.GetSetting(settings, "ExternalLogin:Authority", ""); + _metadataurl = SettingService.GetSetting(settings, "ExternalLogin:MetadataUrl", ""); + _authorizationurl = SettingService.GetSetting(settings, "ExternalLogin:AuthorizationUrl", ""); + _tokenurl = SettingService.GetSetting(settings, "ExternalLogin:TokenUrl", ""); + _userinfourl = SettingService.GetSetting(settings, "ExternalLogin:UserInfoUrl", ""); + _clientid = SettingService.GetSetting(settings, "ExternalLogin:ClientId", ""); + _clientsecret = SettingService.GetSetting(settings, "ExternalLogin:ClientSecret", ""); + _toggleclientsecret = SharedLocalizer["ShowPassword"]; + _authresponsetype = SettingService.GetSetting(settings, "ExternalLogin:AuthResponseType", "code"); + _scopes = SettingService.GetSetting(settings, "ExternalLogin:Scopes", ""); + _parameters = SettingService.GetSetting(settings, "ExternalLogin:Parameters", ""); + _pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false"); + _redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype; + _reviewclaims = SettingService.GetSetting(settings, "ExternalLogin:ReviewClaims", "false"); + _externalloginurl = Utilities.TenantUrl(PageState.Alias, "/pages/external"); + _identifierclaimtype = SettingService.GetSetting(settings, "ExternalLogin:IdentifierClaimType", "sub"); + _nameclaimtype = SettingService.GetSetting(settings, "ExternalLogin:NameClaimType", "name"); + _emailclaimtype = SettingService.GetSetting(settings, "ExternalLogin:EmailClaimType", "email"); + _roleclaimtype = SettingService.GetSetting(settings, "ExternalLogin:RoleClaimType", ""); + _roleclaimmappings = SettingService.GetSetting(settings, "ExternalLogin:RoleClaimMappings", ""); + _synchronizeroles = SettingService.GetSetting(settings, "ExternalLogin:SynchronizeRoles", "false"); + _profileclaimtypes = SettingService.GetSetting(settings, "ExternalLogin:ProfileClaimTypes", ""); + _domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", ""); + _createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true"); + _verifyusers = SettingService.GetSetting(settings, "ExternalLogin:VerifyUsers", "true"); + } + private async Task LoadUsersAsync(bool load) { if (load) @@ -567,105 +594,117 @@ else users = users.OrderBy(u => u.User.DisplayName).ToList(); } } - } + } - private async Task DeleteUser(UserRole UserRole) - { - try - { - var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId); - if (user != null) - { - await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId); - await logger.LogInformation("User Deleted {User}", UserRole.User); - 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); - } - } + private async Task DeleteUser(UserRole UserRole) + { + try + { + var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId); + if (user != null) + { + await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId); + await logger.LogInformation("User Deleted {User}", UserRole.User); + 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); + } + } - private async Task SaveSiteSettings() - { - try - { - var site = PageState.Site; - site.AllowRegistration = bool.Parse(_allowregistration); - await SiteService.UpdateSiteAsync(site); + private async Task SaveSiteSettings() + { + try + { + var site = PageState.Site; + site.AllowRegistration = bool.Parse(_allowregistration); + await SiteService.UpdateSiteAsync(site); - var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); - settings = SettingService.SetSetting(settings, "LoginOptions:AllowSiteLogin", _allowsitelogin, false); + 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: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); + if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) + { + 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, "IdentityOptions:Password:RequiredLength", _minimumlength, true); - settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", _uniquecharacters, true); - settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireDigit", _requiredigit, true); - settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireUppercase", _requireupper, true); - settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireLowercase", _requirelower, true); - settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireNonAlphanumeric", _requirepunctuation, true); + settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredLength", _minimumlength, true); + settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", _uniquecharacters, true); + settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireDigit", _requiredigit, true); + settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireUppercase", _requireupper, true); + settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireLowercase", _requirelower, true); + settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireNonAlphanumeric", _requirepunctuation, true); - settings = SettingService.SetSetting(settings, "IdentityOptions:Lockout:MaxFailedAccessAttempts", _maximumfailures, true); - settings = SettingService.SetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", TimeSpan.FromMinutes(Convert.ToInt64(_lockoutduration)).ToString(), true); + settings = SettingService.SetSetting(settings, "IdentityOptions:Lockout:MaxFailedAccessAttempts", _maximumfailures, true); + settings = SettingService.SetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", TimeSpan.FromMinutes(Convert.ToInt64(_lockoutduration)).ToString(), true); - settings = SettingService.SetSetting(settings, "ExternalLogin:ProviderType", _providertype, false); - settings = SettingService.SetSetting(settings, "ExternalLogin:ProviderName", _providername, false); - settings = SettingService.SetSetting(settings, "ExternalLogin:Authority", _authority, true); - settings = SettingService.SetSetting(settings, "ExternalLogin:MetadataUrl", _metadataurl, true); - settings = SettingService.SetSetting(settings, "ExternalLogin:AuthorizationUrl", _authorizationurl, true); - settings = SettingService.SetSetting(settings, "ExternalLogin:TokenUrl", _tokenurl, true); - settings = SettingService.SetSetting(settings, "ExternalLogin:UserInfoUrl", _userinfourl, true); - settings = SettingService.SetSetting(settings, "ExternalLogin:ClientId", _clientid, true); - settings = SettingService.SetSetting(settings, "ExternalLogin:ClientSecret", _clientsecret, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:Provider", _provider, false); + settings = SettingService.SetSetting(settings, "ExternalLogin:ProviderType", _providertype, false); + settings = SettingService.SetSetting(settings, "ExternalLogin:ProviderName", _providername, false); + settings = SettingService.SetSetting(settings, "ExternalLogin:Authority", _authority, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:MetadataUrl", _metadataurl, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:AuthorizationUrl", _authorizationurl, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:TokenUrl", _tokenurl, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:UserInfoUrl", _userinfourl, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:ClientId", _clientid, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:ClientSecret", _clientsecret, true); settings = SettingService.SetSetting(settings, "ExternalLogin:AuthResponseType", _authresponsetype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:Scopes", _scopes, true); - settings = SettingService.SetSetting(settings, "ExternalLogin:Parameters", _parameters, true); - settings = SettingService.SetSetting(settings, "ExternalLogin:PKCE", _pkce, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:Parameters", _parameters, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:PKCE", _pkce, true); settings = SettingService.SetSetting(settings, "ExternalLogin:ReviewClaims", _reviewclaims, true); settings = SettingService.SetSetting(settings, "ExternalLogin:IdentifierClaimType", _identifierclaimtype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:NameClaimType", _nameclaimtype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true); - settings = SettingService.SetSetting(settings, "ExternalLogin:RoleClaimType", _roleclaimtype, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:RoleClaimType", _roleclaimtype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:RoleClaimMappings", _roleclaimmappings, true); settings = SettingService.SetSetting(settings, "ExternalLogin:SynchronizeRoles", _synchronizeroles, true); settings = SettingService.SetSetting(settings, "ExternalLogin:ProfileClaimTypes", _profileclaimtypes, true); - settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true); - settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true); settings = SettingService.SetSetting(settings, "ExternalLogin:VerifyUsers", _verifyusers, true); - settings = SettingService.SetSetting(settings, "JwtOptions:Secret", _secret, true); - settings = SettingService.SetSetting(settings, "JwtOptions:Issuer", _issuer, true); - settings = SettingService.SetSetting(settings, "JwtOptions:Audience", _audience, true); - settings = SettingService.SetSetting(settings, "JwtOptions:Lifetime", _lifetime, true); - } + settings = SettingService.SetSetting(settings, "JwtOptions:Secret", _secret, true); + settings = SettingService.SetSetting(settings, "JwtOptions:Issuer", _issuer, true); + settings = SettingService.SetSetting(settings, "JwtOptions:Audience", _audience, true); + settings = SettingService.SetSetting(settings, "JwtOptions:Lifetime", _lifetime, true); + } - await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId); - await SettingService.ClearSiteSettingsCacheAsync(); + await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId); + await SettingService.ClearSiteSettingsCacheAsync(); - if (!string.IsNullOrEmpty(_secret)) - { - SiteState.AuthorizationToken = await UserService.GetTokenAsync(); - } + if (!string.IsNullOrEmpty(_secret)) + { + SiteState.AuthorizationToken = await UserService.GetTokenAsync(); + } - 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); - } + 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); + } + } + + private void ProviderChanged(ChangeEventArgs e) + { + _provider = (string)e.Value; + var provider = Shared.ExternalLoginProviders.Providers.FirstOrDefault(item => item.Name == _provider); + if (provider != null) + { + LoadExternalLoginSettings(provider.Settings); + } + StateHasChanged(); } - - private void ProviderTypeChanged(ChangeEventArgs e) + + private void ProviderTypeChanged(ChangeEventArgs e) { _providertype = (string)e.Value; if (string.IsNullOrEmpty(_providername)) diff --git a/Oqtane.Client/Modules/Controls/ModuleMessage.razor b/Oqtane.Client/Modules/Controls/ModuleMessage.razor index c42b40cb..440feff0 100644 --- a/Oqtane.Client/Modules/Controls/ModuleMessage.razor +++ b/Oqtane.Client/Modules/Controls/ModuleMessage.razor @@ -10,13 +10,16 @@ { View Details } - @if (ModuleState.RenderMode == RenderModes.Static) + @if (ModuleState != null) { - - } - else - { - + @if (ModuleState.RenderMode == RenderModes.Static) + { + + } + else + { + + } }
} diff --git a/Oqtane.Client/Modules/Controls/Pager.razor b/Oqtane.Client/Modules/Controls/Pager.razor index ce14eed6..ab1db740 100644 --- a/Oqtane.Client/Modules/Controls/Pager.razor +++ b/Oqtane.Client/Modules/Controls/Pager.razor @@ -452,9 +452,9 @@ _displayPages = int.Parse(DisplayPages); } - if (PageState.QueryString.ContainsKey("page")) + if (PageState.QueryString.ContainsKey("page") && int.TryParse(PageState.QueryString["page"], out int page)) { - _page = int.Parse(PageState.QueryString["page"]); + _page = page; } else { diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj index ff8b2a1d..72c6447c 100644 --- a/Oqtane.Client/Oqtane.Client.csproj +++ b/Oqtane.Client/Oqtane.Client.csproj @@ -4,7 +4,7 @@ net8.0 Exe Debug;Release - 5.2.3 + 5.2.4 Oqtane Shaun Walker .NET Foundation @@ -12,7 +12,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.4 https://github.com/oqtane/oqtane.framework Git Oqtane @@ -22,10 +22,10 @@ - - - - + + + + diff --git a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx index b232fe61..1f3bf2c6 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx @@ -163,7 +163,7 @@ Enter the site name - The name of the database used for the site + The name of the database used for the site. Note that this is not the physical database name but rather the tenant name which is used within the framework to identify a database. The urls for the site. This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or virtual folders (ie. domain.com/folder). @@ -307,7 +307,7 @@ Type: - The connection information for the database + The name of the connection string in appsettings.json which will be used to connect to the database The type of database diff --git a/Oqtane.Client/Resources/Modules/Admin/Sites/Add.resx b/Oqtane.Client/Resources/Modules/Admin/Sites/Add.resx index e0390b08..d32522e9 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Sites/Add.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Sites/Add.resx @@ -187,7 +187,7 @@ Select the database for the site - Enter the name for the database + Enter the name for the database. Note that this will be the tenant name which is used within the framework to identify the database. Select the database type diff --git a/Oqtane.Client/Resources/Modules/Admin/Upgrade/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Upgrade/Index.resx index 40d8af4c..e9a376e1 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Upgrade/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Upgrade/Index.resx @@ -150,4 +150,7 @@ You Cannot Perform A System Update In A Development Environment + + 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 The Limitations Of That Environment. + \ 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 46c19999..381eff20 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx @@ -471,13 +471,28 @@ Review Claims? - + 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. Optionally specify the type name of the user's name claim provided by the identity provider. The typical value is 'name'. - + Name Claim: + + Select the external login provider + + + Provider: + + + Info + + + OAuth 2.0 + + + OpenID Connect (OIDC) + \ No newline at end of file diff --git a/Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor b/Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor index c02548e0..78a6a99c 100644 --- a/Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor +++ b/Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor @@ -22,7 +22,7 @@ } else { - @culture.DisplayName + @culture.DisplayName } } @@ -45,8 +45,7 @@ { MenuAlignment = DropdownAlignment.ToLower() == "right" ? "dropdown-menu-end" : string.Empty; - var languages = PageState.Languages; - _supportedCultures = languages.Select(l => new Culture { Name = l.Code, DisplayName = l.Name }); + _supportedCultures = PageState.Languages.Select(l => new Culture { Name = l.Code, DisplayName = l.Name }); if (PageState.QueryString.ContainsKey("culture")) { @@ -54,9 +53,18 @@ if (_supportedCultures.Any(item => item.Name == culture)) { var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)); - HttpContext.Response.Cookies.Append(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, new CookieOptions { Path = "/", Expires = DateTimeOffset.UtcNow.AddYears(365) }); + + HttpContext.Response.Cookies.Append(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, new CookieOptions + { + Path = "/", + Expires = DateTimeOffset.UtcNow.AddYears(365), + SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax, // Set SameSite attribute + Secure = true, // Ensure the cookie is only sent over HTTPS + HttpOnly = false // cookie is updated using JS Interop in Interactive render mode + }); + } - NavigationManager.NavigateTo(NavigationManager.Uri.Replace($"?culture={culture}", ""), forceLoad: true); + NavigationManager.NavigateTo(NavigationManager.Uri.Replace($"?culture={culture}", "")); } } @@ -66,8 +74,8 @@ { var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)); var interop = new Interop(JSRuntime); - await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360); - NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true); + await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360, true, "Lax"); + NavigationManager.NavigateTo(NavigationManager.Uri, true); } } } diff --git a/Oqtane.Client/UI/Interop.cs b/Oqtane.Client/UI/Interop.cs index cc379dc6..c5964c96 100644 --- a/Oqtane.Client/UI/Interop.cs +++ b/Oqtane.Client/UI/Interop.cs @@ -16,13 +16,18 @@ namespace Oqtane.UI _jsRuntime = jsRuntime; } - public Task SetCookie(string name, string value, int days) + public async Task SetCookie(string name, string value, int days) + { + await SetCookie(name, value, days, true, "Lax"); + } + + public Task SetCookie(string name, string value, int days, bool secure, string sameSite) { try { _jsRuntime.InvokeVoidAsync( "Oqtane.Interop.setCookie", - name, value, days); + name, value, days, secure, sameSite); return Task.CompletedTask; } catch diff --git a/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj b/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj index da0d7064..d9911acd 100644 --- a/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj +++ b/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj @@ -2,7 +2,7 @@ net8.0 - 5.2.3 + 5.2.4 Oqtane Shaun Walker .NET Foundation @@ -10,7 +10,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.4 https://github.com/oqtane/oqtane.framework Git true @@ -34,7 +34,7 @@ - + diff --git a/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj b/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj index aebd561c..111a1251 100644 --- a/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj +++ b/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj @@ -2,7 +2,7 @@ net8.0 - 5.2.3 + 5.2.4 Oqtane Shaun Walker .NET Foundation @@ -10,7 +10,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.4 https://github.com/oqtane/oqtane.framework Git true @@ -34,8 +34,8 @@ - - + + diff --git a/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj b/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj index c6bc4b37..48f7282f 100644 --- a/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj +++ b/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj @@ -2,7 +2,7 @@ net8.0 - 5.2.3 + 5.2.4 Oqtane Shaun Walker .NET Foundation @@ -10,7 +10,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.4 https://github.com/oqtane/oqtane.framework Git true @@ -33,7 +33,7 @@ - + diff --git a/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj b/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj index 14a44514..1fa10f5b 100644 --- a/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj +++ b/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj @@ -2,7 +2,7 @@ net8.0 - 5.2.3 + 5.2.4 Oqtane Shaun Walker .NET Foundation @@ -10,7 +10,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.4 https://github.com/oqtane/oqtane.framework Git true @@ -33,7 +33,7 @@ - + diff --git a/Oqtane.Maui/Oqtane.Maui.csproj b/Oqtane.Maui/Oqtane.Maui.csproj index 0784cc2f..9697adab 100644 --- a/Oqtane.Maui/Oqtane.Maui.csproj +++ b/Oqtane.Maui/Oqtane.Maui.csproj @@ -6,7 +6,7 @@ Exe - 5.2.3 + 5.2.4 Oqtane Shaun Walker .NET Foundation @@ -14,7 +14,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.4 https://github.com/oqtane/oqtane.framework Git Oqtane.Maui @@ -31,7 +31,7 @@ 0E29FC31-1B83-48ED-B6E0-9F3C67B775D4 - 5.2.3 + 5.2.4 1 14.2 @@ -65,15 +65,15 @@ - - + + - - - - - - + + + + + + diff --git a/Oqtane.Maui/wwwroot/js/interop.js b/Oqtane.Maui/wwwroot/js/interop.js index 9bc74bb8..ef6043f9 100644 --- a/Oqtane.Maui/wwwroot/js/interop.js +++ b/Oqtane.Maui/wwwroot/js/interop.js @@ -1,11 +1,18 @@ var Oqtane = Oqtane || {}; Oqtane.Interop = { - setCookie: function (name, value, days) { + setCookie: function (name, value, days, secure, sameSite) { var d = new Date(); d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000)); var expires = "expires=" + d.toUTCString(); - document.cookie = name + "=" + value + ";" + expires + ";path=/"; + var cookieString = name + "=" + value + ";" + expires + ";path=/"; + if (secure) { + cookieString += "; secure"; + } + if (sameSite === "Lax" || sameSite === "Strict" || sameSite === "None") { + cookieString += "; SameSite=" + sameSite; + } + document.cookie = cookieString; }, getCookie: function (name) { name = name + "="; diff --git a/Oqtane.Package/Oqtane.Client.nuspec b/Oqtane.Package/Oqtane.Client.nuspec index e9e2d38f..79a8088a 100644 --- a/Oqtane.Package/Oqtane.Client.nuspec +++ b/Oqtane.Package/Oqtane.Client.nuspec @@ -2,7 +2,7 @@ Oqtane.Client - 5.2.3 + 5.2.4 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.4 readme.md icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Framework.nuspec b/Oqtane.Package/Oqtane.Framework.nuspec index 196bf08e..5ddceeef 100644 --- a/Oqtane.Package/Oqtane.Framework.nuspec +++ b/Oqtane.Package/Oqtane.Framework.nuspec @@ -2,7 +2,7 @@ Oqtane.Framework - 5.2.3 + 5.2.4 Shaun Walker .NET Foundation Oqtane Framework @@ -11,8 +11,8 @@ .NET Foundation false MIT - https://github.com/oqtane/oqtane.framework/releases/download/v5.2.3/Oqtane.Framework.5.2.3.Upgrade.zip - https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3 + https://github.com/oqtane/oqtane.framework/releases/download/v5.2.4/Oqtane.Framework.5.2.4.Upgrade.zip + https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.4 readme.md icon.png oqtane framework diff --git a/Oqtane.Package/Oqtane.Server.nuspec b/Oqtane.Package/Oqtane.Server.nuspec index 3ff7011d..d43e9a3d 100644 --- a/Oqtane.Package/Oqtane.Server.nuspec +++ b/Oqtane.Package/Oqtane.Server.nuspec @@ -2,7 +2,7 @@ Oqtane.Server - 5.2.3 + 5.2.4 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.4 readme.md icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Shared.nuspec b/Oqtane.Package/Oqtane.Shared.nuspec index b0a2538e..b9f466f8 100644 --- a/Oqtane.Package/Oqtane.Shared.nuspec +++ b/Oqtane.Package/Oqtane.Shared.nuspec @@ -2,7 +2,7 @@ Oqtane.Shared - 5.2.3 + 5.2.4 Shaun Walker .NET Foundation Oqtane Framework @@ -12,14 +12,14 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.4 readme.md icon.png oqtane - - + + diff --git a/Oqtane.Package/Oqtane.Updater.nuspec b/Oqtane.Package/Oqtane.Updater.nuspec index 1b6eddd3..8e3cc368 100644 --- a/Oqtane.Package/Oqtane.Updater.nuspec +++ b/Oqtane.Package/Oqtane.Updater.nuspec @@ -2,7 +2,7 @@ Oqtane.Updater - 5.2.3 + 5.2.4 Shaun Walker .NET Foundation Oqtane Framework @@ -12,13 +12,13 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.4 readme.md icon.png oqtane - + diff --git a/Oqtane.Package/install.ps1 b/Oqtane.Package/install.ps1 index 7ca6ed4a..23cd8498 100644 --- a/Oqtane.Package/install.ps1 +++ b/Oqtane.Package/install.ps1 @@ -1 +1 @@ -Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.2.3.Install.zip" -Force +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.2.4.Install.zip" -Force diff --git a/Oqtane.Package/upgrade.ps1 b/Oqtane.Package/upgrade.ps1 index 1d909b81..19cc36ec 100644 --- a/Oqtane.Package/upgrade.ps1 +++ b/Oqtane.Package/upgrade.ps1 @@ -1 +1 @@ -Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.2.3.Upgrade.zip" -Force +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.2.4.Upgrade.zip" -Force diff --git a/Oqtane.Server/Components/App.razor b/Oqtane.Server/Components/App.razor index c1684977..35d71322 100644 --- a/Oqtane.Server/Components/App.razor +++ b/Oqtane.Server/Components/App.razor @@ -522,7 +522,7 @@ " let currentUrl = window.location.pathname;" + Environment.NewLine + " Blazor.addEventListener('enhancedload', () => {" + Environment.NewLine + " let newUrl = window.location.pathname;" + Environment.NewLine + - " if (currentUrl !== newUrl || window.location.hash === '') {" + Environment.NewLine + + " if (currentUrl !== newUrl || window.location.hash === '#top') {" + Environment.NewLine + " window.scrollTo({ top: 0, left: 0, behavior: 'instant' });" + Environment.NewLine + " }" + Environment.NewLine + " currentUrl = newUrl;" + Environment.NewLine + @@ -609,7 +609,7 @@ Expires = DateTimeOffset.UtcNow.AddYears(1), SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax, // Set SameSite attribute Secure = true, // Ensure the cookie is only sent over HTTPS - HttpOnly = true // Optional: Helps mitigate XSS attacks + HttpOnly = false // cookie is updated using JS Interop in Interactive render mode }; Context.Response.Cookies.Append( diff --git a/Oqtane.Server/Controllers/FileController.cs b/Oqtane.Server/Controllers/FileController.cs index 5aa8a66e..1e8a1740 100644 --- a/Oqtane.Server/Controllers/FileController.cs +++ b/Oqtane.Server/Controllers/FileController.cs @@ -17,11 +17,10 @@ using Oqtane.Infrastructure; using Oqtane.Repository; using Oqtane.Extensions; using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Formats.Png; using System.Net.Http; using Microsoft.AspNetCore.Cors; using System.IO.Compression; +using Oqtane.Services; // ReSharper disable StringIndexOfIsCultureSpecific.1 @@ -38,7 +37,9 @@ namespace Oqtane.Controllers private readonly ILogManager _logger; private readonly Alias _alias; private readonly ISettingRepository _settingRepository; - public FileController(IWebHostEnvironment environment, IFileRepository files, IFolderRepository folders, IUserPermissions userPermissions, ISettingRepository settingRepository, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager) + private readonly IImageService _imageService; + + public FileController(IWebHostEnvironment environment, IFileRepository files, IFolderRepository folders, IUserPermissions userPermissions, ISettingRepository settingRepository, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager, IImageService imageService) { _environment = environment; _files = files; @@ -48,6 +49,7 @@ namespace Oqtane.Controllers _logger = logger; _alias = tenantManager.GetAlias(); _settingRepository = settingRepository; + _imageService = imageService; } // GET: api/?folder=x @@ -681,22 +683,18 @@ namespace Oqtane.Controllers var filepath = _files.GetFilePath(file); if (System.IO.File.Exists(filepath)) { - // validation - if (!Enum.TryParse(mode, true, out ResizeMode _)) mode = "crop"; - if (!Enum.TryParse(position, true, out AnchorPositionMode _)) position = "center"; - if (!Color.TryParseHex("#" + background, out _)) background = "transparent"; - if (!int.TryParse(rotate, out _)) rotate = "0"; - rotate = (int.Parse(rotate) < 0 || int.Parse(rotate) > 360) ? "0" : rotate; if (!bool.TryParse(recreate, out _)) recreate = "false"; - string imagepath = filepath.Replace(Path.GetExtension(filepath), "." + width.ToString() + "x" + height.ToString() + ".png"); + string format = "png"; + + string imagepath = filepath.Replace(Path.GetExtension(filepath), "." + width.ToString() + "x" + height.ToString() + "." + format); if (!System.IO.File.Exists(imagepath) || bool.Parse(recreate)) { // user has edit access to folder or folder supports the image size being created if (_userPermissions.IsAuthorized(User, PermissionNames.Edit, file.Folder.PermissionList) || (!string.IsNullOrEmpty(file.Folder.ImageSizes) && (file.Folder.ImageSizes == "*" || file.Folder.ImageSizes.ToLower().Split(",").Contains(width.ToString() + "x" + height.ToString())))) { - imagepath = CreateImage(filepath, width, height, mode, position, background, rotate, imagepath); + imagepath = _imageService.CreateImage(filepath, width, height, mode, position, background, rotate, format, imagepath); } else { @@ -743,70 +741,6 @@ namespace Oqtane.Controllers return System.IO.File.Exists(errorPath) ? PhysicalFile(errorPath, MimeUtilities.GetMimeType(errorPath)) : null; } - private string CreateImage(string filepath, int width, int height, string mode, string position, string background, string rotate, string imagepath) - { - try - { - using (var stream = new FileStream(filepath, FileMode.Open, FileAccess.Read)) - { - stream.Position = 0; - using (var image = Image.Load(stream)) - { - int.TryParse(rotate, out int angle); - Enum.TryParse(mode, true, out ResizeMode resizemode); - Enum.TryParse(position, true, out AnchorPositionMode anchorpositionmode); - - PngEncoder encoder; - - if (background != "transparent") - { - image.Mutate(x => x - .AutoOrient() // auto orient the image - .Rotate(angle) - .Resize(new ResizeOptions - { - Mode = resizemode, - Position = anchorpositionmode, - Size = new Size(width, height), - PadColor = Color.ParseHex("#" + background) - })); - - encoder = new PngEncoder(); - } - else - { - image.Mutate(x => x - .AutoOrient() // auto orient the image - .Rotate(angle) - .Resize(new ResizeOptions - { - Mode = resizemode, - Position = anchorpositionmode, - Size = new Size(width, height) - })); - - encoder = new PngEncoder - { - ColorType = PngColorType.RgbWithAlpha, - TransparentColorMode = PngTransparentColorMode.Preserve, - BitDepth = PngBitDepth.Bit8, - CompressionLevel = PngCompressionLevel.BestSpeed - }; - } - - image.Save(imagepath, encoder); - } - } - } - catch (Exception ex) - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, ex, "Error Creating Image For File {FilePath} {Width} {Height} {Mode} {Rotate} {Error}", filepath, width, height, mode, rotate, ex.Message); - imagepath = ""; - } - - return imagepath; - } - private string GetFolderPath(string folder) { return Utilities.PathCombine(_environment.ContentRootPath, folder); diff --git a/Oqtane.Server/Controllers/LanguageController.cs b/Oqtane.Server/Controllers/LanguageController.cs index 6ee66cac..36750b83 100644 --- a/Oqtane.Server/Controllers/LanguageController.cs +++ b/Oqtane.Server/Controllers/LanguageController.cs @@ -55,6 +55,10 @@ namespace Oqtane.Controllers else { languages = _languages.GetLanguages(SiteId).ToList(); + foreach (Language language in languages) + { + language.Name = CultureInfo.GetCultureInfo(language.Code).DisplayName; + } if (!string.IsNullOrEmpty(packagename)) { foreach (var file in Directory.EnumerateFiles(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), $"{packagename}*{Constants.SatelliteAssemblyExtension}", SearchOption.AllDirectories)) @@ -85,6 +89,7 @@ namespace Oqtane.Controllers var language = _languages.GetLanguage(id); if (language != null && language.SiteId == _alias.SiteId) { + language.Name = CultureInfo.GetCultureInfo(language.Code).DisplayName; return language; } else diff --git a/Oqtane.Server/Controllers/NotificationController.cs b/Oqtane.Server/Controllers/NotificationController.cs index 44bc7a93..5f7ee353 100644 --- a/Oqtane.Server/Controllers/NotificationController.cs +++ b/Oqtane.Server/Controllers/NotificationController.cs @@ -8,10 +8,6 @@ using Oqtane.Infrastructure; using Oqtane.Repository; using Oqtane.Security; using System.Net; -using System.Reflection.Metadata; -using Microsoft.Extensions.Localization; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using System.Linq; namespace Oqtane.Controllers { diff --git a/Oqtane.Server/Controllers/SettingController.cs b/Oqtane.Server/Controllers/SettingController.cs index 30a01330..d8a95cbe 100644 --- a/Oqtane.Server/Controllers/SettingController.cs +++ b/Oqtane.Server/Controllers/SettingController.cs @@ -189,7 +189,7 @@ namespace Oqtane.Controllers public void Delete(string entityName, int entityId, string settingName) { Setting setting = _settings.GetSetting(entityName, entityId, settingName); - if (IsAuthorized(setting.EntityName, setting.EntityId, PermissionNames.Edit)) + if (setting != null && IsAuthorized(setting.EntityName, setting.EntityId, PermissionNames.Edit)) { _settings.DeleteSetting(setting.EntityName, setting.SettingId); AddSyncEvent(setting.EntityName, setting.EntityId, setting.SettingId, SyncEventActions.Delete); @@ -199,7 +199,7 @@ namespace Oqtane.Controllers { if (entityName != EntityNames.Visitor) { - _logger.Log(LogLevel.Error, this, LogFunction.Delete, "User Not Authorized To Delete Setting {Setting}", setting); + _logger.Log(LogLevel.Error, this, LogFunction.Delete, "Setting Does Not Exist Or User Not Authorized To Delete Setting For Entity {EntityName} Id {EntityId} Name {SettingName}", entityName, entityId, settingName); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } } diff --git a/Oqtane.Server/Databases/Interfaces/IMultiDatabase.cs b/Oqtane.Server/Databases/Interfaces/IMultiDatabase.cs index 01d926f5..033678d1 100644 --- a/Oqtane.Server/Databases/Interfaces/IMultiDatabase.cs +++ b/Oqtane.Server/Databases/Interfaces/IMultiDatabase.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; using Oqtane.Databases.Interfaces; -using Oqtane.Interfaces; namespace Oqtane.Repository.Databases.Interfaces { diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index d9b31fd9..2b03eec6 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -102,6 +102,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // providers services.AddScoped(); @@ -169,6 +170,7 @@ namespace Microsoft.Extensions.DependencyInjection options.Cookie.HttpOnly = true; options.Cookie.SameSite = SameSiteMode.Lax; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; + options.LoginPath = "/login"; // overrides .NET Identity default of /Account/Login options.Events.OnRedirectToLogin = context => { context.Response.StatusCode = (int)HttpStatusCode.Forbidden; diff --git a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs index 1729358b..cb0303e6 100644 --- a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs @@ -592,7 +592,7 @@ namespace Oqtane.Extensions } // create claims identity - identityuser = await _identityUserManager.FindByEmailAsync(user.Username); + identityuser = await _identityUserManager.FindByNameAsync(user.Username); user.SecurityStamp = identityuser.SecurityStamp; identity = UserSecurity.CreateClaimsIdentity(alias, user, userRoles); identity.Label = ExternalLoginStatus.Success; @@ -645,13 +645,13 @@ namespace Oqtane.Extensions } } - _logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} From IP Address {IPAddress} Using Provider {Provider}", user.Username, httpContext.Connection.RemoteIpAddress, providerName); + _logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} From IP Address {IPAddress} Using Provider {Provider}", user.Username, httpContext.Connection.RemoteIpAddress.ToString(), providerName); } } else // claims invalid { identity.Label = ExternalLoginStatus.MissingClaims; - _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return All Of The Claims Types Specified Or Email Address Does Not Saitisfy Domain Filter. The Actual Claims Returned Were {Claims}. Login Was Denied.", claims); + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return All Of The Claims Types Specified Or Email Address Does Not Satisfy Domain Filter. The Actual Claims Returned Were {Claims}. Login Was Denied.", claims); } return identity; diff --git a/Oqtane.Server/Infrastructure/DatabaseManager.cs b/Oqtane.Server/Infrastructure/DatabaseManager.cs index 3d9be5de..30593b3b 100644 --- a/Oqtane.Server/Infrastructure/DatabaseManager.cs +++ b/Oqtane.Server/Infrastructure/DatabaseManager.cs @@ -155,7 +155,7 @@ namespace Oqtane.Infrastructure // add new site if (install.TenantName != TenantNames.Master && install.ConnectionString.Contains("=")) { - _configManager.AddOrUpdateSetting($"{SettingKeys.ConnectionStringsSection}:{install.TenantName}", install.ConnectionString, false); + _configManager.AddOrUpdateSetting($"{SettingKeys.ConnectionStringsSection}:{install.TenantName}", install.ConnectionString, true); } if (install.TenantName == TenantNames.Master && !install.ConnectionString.Contains("=")) { @@ -375,7 +375,6 @@ namespace Oqtane.Infrastructure AddEFMigrationsHistory(sql, _configManager.GetSetting($"{SettingKeys.ConnectionStringsSection}:{tenant.DBConnectionString}", ""), tenant.DBType, tenant.Version, false); // push latest model into database tenantDbContext.Database.Migrate(); - result.Success = true; } } catch (Exception ex) @@ -384,35 +383,35 @@ namespace Oqtane.Infrastructure _filelogger.LogError(Utilities.LogMessage(this, result.Message)); } - // execute any version specific upgrade logic - var version = tenant.Version; - var index = Array.FindIndex(versions, item => item == version); - if (index != (versions.Length - 1)) + if (string.IsNullOrEmpty(result.Message)) { - try + // execute any version specific upgrade logic + var version = tenant.Version; + var index = Array.FindIndex(versions, item => item == version); + if (index != (versions.Length - 1)) { - for (var i = (index + 1); i < versions.Length; i++) + try { - upgrades.Upgrade(tenant, versions[i]); + for (var i = (index + 1); i < versions.Length; i++) + { + upgrades.Upgrade(tenant, versions[i]); + } + tenant.Version = versions[versions.Length - 1]; + db.Entry(tenant).State = EntityState.Modified; + db.SaveChanges(); + } + catch (Exception ex) + { + result.Message = "An Error Occurred Executing Upgrade Logic On Tenant " + tenant.Name + ". " + ex.ToString(); + _filelogger.LogError(Utilities.LogMessage(this, result.Message)); } - tenant.Version = versions[versions.Length - 1]; - db.Entry(tenant).State = EntityState.Modified; - db.SaveChanges(); - } - catch (Exception ex) - { - result.Message = "An Error Occurred Executing Upgrade Logic On Tenant " + tenant.Name + ". " + ex.ToString(); - _filelogger.LogError(Utilities.LogMessage(this, result.Message)); } } } } } - if (string.IsNullOrEmpty(result.Message)) - { - result.Success = true; - } + result.Success = string.IsNullOrEmpty(result.Message); return result; } @@ -588,7 +587,7 @@ namespace Oqtane.Infrastructure // add host role var hostRoleId = roles.GetRoles(user.SiteId, true).FirstOrDefault(item => item.Name == RoleNames.Host)?.RoleId ?? 0; - var userRole = new UserRole { UserId = user.UserId, RoleId = hostRoleId, EffectiveDate = null, ExpiryDate = null }; + var userRole = new UserRole { UserId = user.UserId, RoleId = hostRoleId, EffectiveDate = null, ExpiryDate = null, IgnoreSecurityStamp = true }; userRoles.AddUserRole(userRole); } } diff --git a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs index b47c5732..ab9cc058 100644 --- a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs @@ -89,9 +89,9 @@ namespace Oqtane.Infrastructure } // validate recipient - if (string.IsNullOrEmpty(notification.ToEmail)) + if (string.IsNullOrEmpty(notification.ToEmail) || !MailAddress.TryCreate(notification.ToEmail, out _)) { - log += "Recipient Missing For NotificationId: " + notification.NotificationId + "
"; + log += $"NotificationId: {notification.NotificationId} - Has Missing Or Invalid Recipient {notification.ToEmail}
"; notification.IsDeleted = true; notificationRepository.UpdateNotification(notification); } diff --git a/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs index 8f2cb20a..cfb58b72 100644 --- a/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs +++ b/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs @@ -319,6 +319,19 @@ namespace Oqtane.Migrations.EntityBuilders schema: Schema); } + public virtual void AddForeignKey(string foreignKeyName, string columnName, string principalTable, string principalColumn, string principalSchema, ReferentialAction onDelete) + { + _migrationBuilder.AddForeignKey( + name: RewriteName(foreignKeyName), + table: RewriteName(EntityTableName), + column: RewriteName(columnName), + principalTable: RewriteName(principalTable), + principalColumn: RewriteName(principalColumn), + principalSchema: RewriteName(principalSchema), + onDelete: onDelete, + schema: Schema); + } + /// /// Creates a Migration to add an Index to the Entity (table) /// @@ -368,6 +381,7 @@ namespace Oqtane.Migrations.EntityBuilders column: foreignKey.Column, principalTable: RewriteName(foreignKey.PrincipalTable), principalColumn: RewriteName(foreignKey.PrincipalColumn), + principalSchema: RewriteName(foreignKey.PrincipalSchema), onDelete: foreignKey.OnDeleteAction); } @@ -381,6 +395,7 @@ namespace Oqtane.Migrations.EntityBuilders column: RewriteName(foreignKey.ColumnName), principalTable: RewriteName(foreignKey.PrincipalTable), principalColumn: RewriteName(foreignKey.PrincipalColumn), + principalSchema: RewriteName(foreignKey.PrincipalSchema), onDelete: foreignKey.OnDeleteAction, schema: Schema); } diff --git a/Oqtane.Server/Migrations/Framework/ForeignKey.cs b/Oqtane.Server/Migrations/Framework/ForeignKey.cs index 533518ab..5f1bcd75 100644 --- a/Oqtane.Server/Migrations/Framework/ForeignKey.cs +++ b/Oqtane.Server/Migrations/Framework/ForeignKey.cs @@ -16,6 +16,16 @@ namespace Oqtane.Migrations OnDeleteAction = onDeleteAction; } + public ForeignKey(string name, Expression> column, string principalTable, string principalColumn, string principalSchema, ReferentialAction onDeleteAction) + { + Name = name; + Column = column; + PrincipalTable = principalTable; + PrincipalColumn = principalColumn; + PrincipalSchema = principalSchema; + OnDeleteAction = onDeleteAction; + } + public string Name { get; } public Expression> Column { get;} @@ -34,6 +44,8 @@ namespace Oqtane.Migrations public string PrincipalColumn { get; } + public string PrincipalSchema { get; } + } } diff --git a/Oqtane.Server/Migrations/Tenant/05020401_RemoveLanguageName.cs b/Oqtane.Server/Migrations/Tenant/05020401_RemoveLanguageName.cs new file mode 100644 index 00000000..063b7027 --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/05020401_RemoveLanguageName.cs @@ -0,0 +1,28 @@ +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.05.02.04.01")] + public class RemoveLanguageName : MultiDatabaseMigration + { + public RemoveLanguageName(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var languageEntityBuilder = new LanguageEntityBuilder(migrationBuilder, ActiveDatabase); + languageEntityBuilder.DropColumn("Name"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // not implemented + } + } +} diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index 84a04860..d5c51ea8 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -3,7 +3,7 @@ net8.0 Debug;Release - 5.2.3 + 5.2.4 Oqtane Shaun Walker .NET Foundation @@ -11,7 +11,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.4 https://github.com/oqtane/oqtane.framework Git Oqtane @@ -33,21 +33,21 @@
- - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - - - + + + + diff --git a/Oqtane.Server/Pages/Files.cshtml.cs b/Oqtane.Server/Pages/Files.cshtml.cs index d8cee171..463534fc 100644 --- a/Oqtane.Server/Pages/Files.cshtml.cs +++ b/Oqtane.Server/Pages/Files.cshtml.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; using System.Net; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Hosting; @@ -14,6 +15,7 @@ using Oqtane.Infrastructure; using Oqtane.Models; using Oqtane.Repository; using Oqtane.Security; +using Oqtane.Services; using Oqtane.Shared; namespace Oqtane.Pages @@ -28,8 +30,10 @@ namespace Oqtane.Pages private readonly ISyncManager _syncManager; private readonly ILogManager _logger; private readonly Alias _alias; + private readonly IImageService _imageService; + private readonly ISettingRepository _settingRepository; - public FilesModel(IWebHostEnvironment environment, IFileRepository files, IUserPermissions userPermissions, IUrlMappingRepository urlMappings, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager) + public FilesModel(IWebHostEnvironment environment, IFileRepository files, IUserPermissions userPermissions, IUrlMappingRepository urlMappings, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager, IImageService imageService, ISettingRepository settingRepository) { _environment = environment; _files = files; @@ -38,111 +42,228 @@ namespace Oqtane.Pages _syncManager = syncManager; _logger = logger; _alias = tenantManager.GetAlias(); + _imageService = imageService; + _settingRepository = settingRepository; } public IActionResult OnGet(string path) { - if (!string.IsNullOrEmpty(path)) - { - path = path.Replace("\\", "/"); - var folderpath = ""; - var filename = ""; - - bool download = false; - if (Request.Query.ContainsKey("download")) - { - download = true; - } - - var segments = path.Split('/'); - if (segments.Length > 0) - { - filename = segments[segments.Length - 1].ToLower(); - if (segments.Length > 1) - { - folderpath = string.Join("/", segments, 0, segments.Length - 1).ToLower() + "/"; - } - } - - Models.File file; - if (folderpath == "id/" && int.TryParse(filename, out int fileid)) - { - file = _files.GetFile(fileid, false); - } - else - { - file = _files.GetFile(_alias.SiteId, folderpath, filename); - } - - if (file != null) - { - if (file.Folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.PermissionList)) - { - // calculate ETag using last modified date and file size - var etag = Convert.ToString(file.ModifiedOn.Ticks ^ file.Size, 16); - - var header = ""; - if (HttpContext.Request.Headers.ContainsKey(HeaderNames.IfNoneMatch)) - { - header = HttpContext.Request.Headers[HeaderNames.IfNoneMatch].ToString(); - } - - if (!header.Equals(etag)) - { - var filepath = _files.GetFilePath(file); - if (System.IO.File.Exists(filepath)) - { - if (download) - { - _syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, "Download"); - return PhysicalFile(filepath, file.GetMimeType(), file.Name); - } - else - { - HttpContext.Response.Headers.Append(HeaderNames.ETag, etag); - return PhysicalFile(filepath, file.GetMimeType()); - } - } - else - { - _logger.Log(LogLevel.Error, this, LogFunction.Read, "File Does Not Exist {FilePath}", filepath); - HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; - } - } - else - { - HttpContext.Response.StatusCode = (int)HttpStatusCode.NotModified; - return Content(String.Empty); - } - } - else - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt For Site {SiteId} And Path {Path}", _alias.SiteId, path); - HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; - } - } - else - { - // look for url mapping - var urlMapping = _urlMappings.GetUrlMapping(_alias.SiteId, "files/" + folderpath + filename); - if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl)) - { - var url = urlMapping.MappedUrl; - if (!url.StartsWith("http")) - { - var uri = new Uri(HttpContext.Request.GetEncodedUrl()); - url = uri.Scheme + "://" + uri.Authority + ((!string.IsNullOrEmpty(_alias.Path)) ? "/" + _alias.Path : "") + "/" + url; - } - return RedirectPermanent(url); - } - } - } - else + if (string.IsNullOrWhiteSpace(path)) { _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt - Path Not Specified For Site {SiteId}", _alias.SiteId); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return BrokenFile(); } + path = path.Replace("\\", "/"); + var folderpath = ""; + var filename = ""; + + bool download = false; + if (Request.Query.ContainsKey("download")) + { + download = true; + } + + var segments = path.Split('/'); + if (segments.Length > 0) + { + filename = segments[segments.Length - 1].ToLower(); + if (segments.Length > 1) + { + folderpath = string.Join("/", segments, 0, segments.Length - 1).ToLower() + "/"; + } + } + + Models.File file; + if (folderpath == "id/" && int.TryParse(filename, out int fileid)) + { + file = _files.GetFile(fileid, false); + } + else + { + file = _files.GetFile(_alias.SiteId, folderpath, filename); + } + + if (file == null) + { + // look for url mapping + + var urlMapping = _urlMappings.GetUrlMapping(_alias.SiteId, "files/" + folderpath + filename); + if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl)) + { + var url = urlMapping.MappedUrl; + if (!url.StartsWith("http")) + { + var uri = new Uri(HttpContext.Request.GetEncodedUrl()); + url = uri.Scheme + "://" + uri.Authority + ((!string.IsNullOrEmpty(_alias.Path)) ? "/" + _alias.Path : "") + "/" + url; + } + + // appends the query string to the redirect url + if (Request.QueryString.HasValue && !string.IsNullOrWhiteSpace(Request.QueryString.Value)) + { + if (url.Contains('?')) + { + url += "&"; + } + else + { + url += "?"; + } + + url += Request.QueryString.Value.Substring(1); + } + + return RedirectPermanent(url); + } + + return BrokenFile(); + } + + if (file.Folder.SiteId != _alias.SiteId || !_userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.PermissionList)) + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt For Site {SiteId} And Path {Path}", _alias.SiteId, path); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return BrokenFile(); + } + + string etag; + string downloadName = file.Name; + string filepath = _files.GetFilePath(file); + + var etagValue = file.ModifiedOn.Ticks ^ file.Size; + + bool isRequestingImageManipulation = false; + + int width = 0; + int height = 0; + if (Request.Query.TryGetValue("width", out var widthStr) && int.TryParse(widthStr, out width) && width > 0) + { + isRequestingImageManipulation = true; + etagValue ^= (width * 31); + } + if (Request.Query.TryGetValue("height", out var heightStr) && int.TryParse(heightStr, out height) && height > 0) + { + isRequestingImageManipulation = true; + etagValue ^= (height * 17); + } + + Request.Query.TryGetValue("mode", out var mode); + Request.Query.TryGetValue("position", out var position); + Request.Query.TryGetValue("background", out var background); + + if (width > 0 || height > 0) + { + if (!string.IsNullOrWhiteSpace(mode)) etagValue ^= mode.ToString().GetHashCode(); + if (!string.IsNullOrWhiteSpace(position)) etagValue ^= position.ToString().GetHashCode(); + if (!string.IsNullOrWhiteSpace(background)) etagValue ^= background.ToString().GetHashCode(); + } + + int rotate; + if (Request.Query.TryGetValue("rotate", out var rotateStr) && int.TryParse(rotateStr, out rotate) && 360 > rotate && rotate > 0) + { + isRequestingImageManipulation = true; + etagValue ^= (rotate * 13); + } + + if (Request.Query.TryGetValue("format", out var format) && _imageService.GetAvailableFormats().Contains(format.ToString())) + { + isRequestingImageManipulation = true; + etagValue ^= format.ToString().GetHashCode(); + } + + etag = Convert.ToString(etagValue, 16); + + var header = ""; + if (HttpContext.Request.Headers.TryGetValue(HeaderNames.IfNoneMatch, out var ifNoneMatch)) + { + header = ifNoneMatch.ToString(); + } + + if (header.Equals(etag)) + { + HttpContext.Response.StatusCode = (int)HttpStatusCode.NotModified; + return Content(String.Empty); + } + + if (!System.IO.File.Exists(filepath)) + { + _logger.Log(LogLevel.Error, this, LogFunction.Read, "File Does Not Exist {FilePath}", filepath); + HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; + return BrokenFile(); + } + + if (isRequestingImageManipulation) + { + var _ImageFiles = _settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "ImageFiles")?.SettingValue; + _ImageFiles = (string.IsNullOrEmpty(_ImageFiles)) ? Constants.ImageFiles : _ImageFiles; + + if (!_ImageFiles.Split(',').Contains(file.Extension.ToLower())) + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "File Is Not An Image {File}", file); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return BrokenFile(); + } + + Request.Query.TryGetValue("recreate", out var recreate); + + if (!bool.TryParse(recreate, out _)) recreate = "false"; + if (!_imageService.GetAvailableFormats().Contains(format.ToString())) format = "png"; + if (width == 0 && height == 0) + { + width = file.ImageWidth; + height = file.ImageHeight; + } + + string imagepath = filepath.Replace(Path.GetExtension(filepath), "." + width.ToString() + "x" + height.ToString() + "." + format); + if (!System.IO.File.Exists(imagepath) || bool.Parse(recreate)) + { + // user has edit access to folder or folder supports the image size being created + if (_userPermissions.IsAuthorized(User, PermissionNames.Edit, file.Folder.PermissionList) || + (!string.IsNullOrEmpty(file.Folder.ImageSizes) && (file.Folder.ImageSizes == "*" || file.Folder.ImageSizes.ToLower().Split(",").Contains(width.ToString() + "x" + height.ToString())))) + { + imagepath = _imageService.CreateImage(filepath, width, height, mode, position, background, rotateStr, format, imagepath); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Invalid Image Size For Folder {Folder} {Width} {Height}", file.Folder, width, height); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return BrokenFile(); + } + } + + if (string.IsNullOrWhiteSpace(imagepath)) + { + _logger.Log(LogLevel.Error, this, LogFunction.Create, "Error Displaying Image For File {File} {Width} {Height}", file, widthStr, heightStr); + HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; + return BrokenFile(); + } + + downloadName = file.Name.Replace(Path.GetExtension(filepath), "." + width.ToString() + "x" + height.ToString() + "." + format); + filepath = imagepath; + } + + if (!System.IO.File.Exists(filepath)) + { + _logger.Log(LogLevel.Error, this, LogFunction.Read, "File Does Not Exist {FilePath}", filepath); + HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; + return BrokenFile(); + } + + if (download) + { + _syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, "Download"); + return PhysicalFile(filepath, file.GetMimeType(), downloadName); + } + else + { + HttpContext.Response.Headers.Append(HeaderNames.ETag, etag); + return PhysicalFile(filepath, file.GetMimeType()); + } + } + + private PhysicalFileResult BrokenFile() + { // broken link string errorPath = Path.Combine(Utilities.PathCombine(_environment.ContentRootPath, "wwwroot/images"), "error.png"); return PhysicalFile(errorPath, MimeUtilities.GetMimeType(errorPath)); diff --git a/Oqtane.Server/Repository/PageRepository.cs b/Oqtane.Server/Repository/PageRepository.cs index 4bad39e3..e3bc27d0 100644 --- a/Oqtane.Server/Repository/PageRepository.cs +++ b/Oqtane.Server/Repository/PageRepository.cs @@ -91,18 +91,29 @@ namespace Oqtane.Repository public void DeletePage(int pageId) { using var db = _dbContextFactory.CreateDbContext(); - var page = db.Page.Find(pageId); - _permissions.DeletePermissions(page.SiteId, EntityNames.Page, pageId); - _settings.DeleteSettings(EntityNames.Page, pageId); - // remove page modules for page - var pageModules = db.PageModule.Where(item => item.PageId == pageId).ToList(); - foreach (var pageModule in pageModules) { - _pageModules.DeletePageModule(pageModule.PageModuleId); + var page = db.Page.Find(pageId); + _permissions.DeletePermissions(page.SiteId, EntityNames.Page, pageId); + _settings.DeleteSettings(EntityNames.Page, pageId); + // remove page modules for page + var pageModules = db.PageModule.Where(item => item.PageId == pageId).ToList(); + foreach (var pageModule in pageModules) + { + _pageModules.DeletePageModule(pageModule.PageModuleId); + } + + // At this point the page item is unaware of changes happened in other + // contexts (i.e.: the contex opened and closed in each DeletePageModule). + // Workin on page item may result in unxpected behaviour: + // better close and reopen context to work on a fresh page item. + } + + using var dbContext = _dbContextFactory.CreateDbContext(); + { + var page = dbContext.Page.Find(pageId); + dbContext.Page.Remove(page); + dbContext.SaveChanges(); } - // must occur after page modules are deleted because of cascading delete relationship - db.Page.Remove(page); - db.SaveChanges(); } } } diff --git a/Oqtane.Server/Repository/UserRepository.cs b/Oqtane.Server/Repository/UserRepository.cs index ffbf7412..3c0a40ad 100644 --- a/Oqtane.Server/Repository/UserRepository.cs +++ b/Oqtane.Server/Repository/UserRepository.cs @@ -75,6 +75,7 @@ namespace Oqtane.Repository userrole.RoleId = role.RoleId; userrole.EffectiveDate = null; userrole.ExpiryDate = null; + userrole.IgnoreSecurityStamp = true; _userroles.AddUserRole(userrole); } diff --git a/Oqtane.Server/Repository/UserRoleRepository.cs b/Oqtane.Server/Repository/UserRoleRepository.cs index c438bdb4..8af62274 100644 --- a/Oqtane.Server/Repository/UserRoleRepository.cs +++ b/Oqtane.Server/Repository/UserRoleRepository.cs @@ -72,8 +72,13 @@ namespace Oqtane.Repository DeleteUserRoles(userRole.UserId); } - UpdateSecurityStamp(userRole.UserId); - + if (!userRole.IgnoreSecurityStamp) + { + UpdateSecurityStamp(userRole.UserId); + } + + RefreshCache(userRole.UserId); + return userRole; } @@ -83,7 +88,12 @@ namespace Oqtane.Repository db.Entry(userRole).State = EntityState.Modified; db.SaveChanges(); - UpdateSecurityStamp(userRole.UserId); + if (!userRole.IgnoreSecurityStamp) + { + UpdateSecurityStamp(userRole.UserId); + } + + RefreshCache(userRole.UserId); return userRole; } @@ -144,6 +154,7 @@ namespace Oqtane.Repository db.SaveChanges(); UpdateSecurityStamp(userRole.UserId); + RefreshCache(userRole.UserId); } public void DeleteUserRoles(int userId) @@ -156,11 +167,11 @@ namespace Oqtane.Repository db.SaveChanges(); UpdateSecurityStamp(userId); + RefreshCache(userId); } private void UpdateSecurityStamp(int userId) { - // update user security stamp using var db = _dbContextFactory.CreateDbContext(); var user = db.User.Find(userId); if (user != null) @@ -168,11 +179,13 @@ namespace Oqtane.Repository var identityuser = _identityUserManager.FindByNameAsync(user.Username).GetAwaiter().GetResult(); if (identityuser != null) { - _identityUserManager.UpdateSecurityStampAsync(identityuser); + _identityUserManager.UpdateSecurityStampAsync(identityuser).GetAwaiter().GetResult(); } } + } - // refresh cache + private void RefreshCache(int userId) + { var alias = _tenantManager.GetAlias(); if (alias != null) { diff --git a/Oqtane.Server/Security/PrincipalValidator.cs b/Oqtane.Server/Security/PrincipalValidator.cs index dc6d7256..45e99c34 100644 --- a/Oqtane.Server/Security/PrincipalValidator.cs +++ b/Oqtane.Server/Security/PrincipalValidator.cs @@ -7,13 +7,13 @@ using Oqtane.Models; using Oqtane.Extensions; using Oqtane.Shared; using Oqtane.Managers; - +using Microsoft.AspNetCore.Authentication; namespace Oqtane.Security { public static class PrincipalValidator { - public static Task ValidateAsync(CookieValidatePrincipalContext context) + public static async Task ValidateAsync(CookieValidatePrincipalContext context) { if (context != null && context.Principal.Identity.IsAuthenticated && context.Principal.Identity.Name != null) { @@ -49,6 +49,7 @@ namespace Oqtane.Security // remove principal (ie. log user out) Log(_logger, alias, "Permissions Removed For User {Username} Accessing {Url}", context.Principal.Identity.Name, path); context.RejectPrincipal(); + await context.HttpContext.SignOutAsync(Constants.AuthenticationScheme); } } else @@ -58,7 +59,6 @@ namespace Oqtane.Security } } } - return Task.CompletedTask; } private static void Log (ILogManager logger, Alias alias, string message, string username, string path) diff --git a/Oqtane.Server/Services/ImageService.cs b/Oqtane.Server/Services/ImageService.cs new file mode 100644 index 00000000..e22f3b8e --- /dev/null +++ b/Oqtane.Server/Services/ImageService.cs @@ -0,0 +1,124 @@ +using Oqtane.Enums; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Processing; +using System.IO; +using System; +using SixLabors.ImageSharp; +using Oqtane.Infrastructure; +using Oqtane.Shared; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Webp; +using System.Linq; + +namespace Oqtane.Services +{ + public class ImageService : IImageService + { + private readonly ILogManager _logger; + private static readonly string[] _formats = ["png", "webp"]; + + public ImageService(ILogManager logger) + { + _logger = logger; + } + + public string[] GetAvailableFormats() + { + return _formats; + } + + public string CreateImage(string filepath, int width, int height, string mode, string position, string background, string rotate, string format, string imagepath) + { + try + { + // params validation + if (!Enum.TryParse(mode, true, out ResizeMode _)) mode = "crop"; + if (!Enum.TryParse(position, true, out AnchorPositionMode _)) position = "center"; + if (!Color.TryParseHex("#" + background, out _)) background = "transparent"; + if (!int.TryParse(rotate, out _)) rotate = "0"; + rotate = (int.Parse(rotate) < 0 || int.Parse(rotate) > 360) ? "0" : rotate; + if (!_formats.Contains(format)) format = "png"; + + using (var stream = new FileStream(filepath, FileMode.Open, FileAccess.Read)) + { + stream.Position = 0; + using (var image = Image.Load(stream)) + { + int.TryParse(rotate, out int angle); + Enum.TryParse(mode, true, out ResizeMode resizemode); + Enum.TryParse(position, true, out AnchorPositionMode anchorpositionmode); + + if (width == 0 && height == 0) + { + width = image.Width; + height = image.Height; + } + + IImageEncoder encoder; + var resizeOptions = new ResizeOptions + { + Mode = resizemode, + Position = anchorpositionmode, + Size = new Size(width, height) + }; + + if (background != "transparent") + { + resizeOptions.PadColor = Color.ParseHex("#" + background); + encoder = GetEncoder(format, transparent: false); + } + else + { + encoder = GetEncoder(format, transparent: true); + } + + image.Mutate(x => x + .AutoOrient() // auto orient the image + .Rotate(angle) + .Resize(resizeOptions)); + + image.Save(imagepath, encoder); + } + } + } + catch (Exception ex) + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, ex, "Error Creating Image For File {FilePath} {Width} {Height} {Mode} {Rotate} {Error}", filepath, width, height, mode, rotate, ex.Message); + imagepath = ""; + } + + return imagepath; + } + + private static IImageEncoder GetEncoder(string format, bool transparent) + { + return format switch + { + "png" => GetPngEncoder(transparent), + "webp" => GetWebpEncoder(transparent), + _ => GetPngEncoder(transparent), + }; + } + + private static PngEncoder GetPngEncoder(bool transparent) + { + return new PngEncoder() + { + ColorType = transparent ? PngColorType.RgbWithAlpha : PngColorType.Rgb, + TransparentColorMode = transparent ? PngTransparentColorMode.Preserve : PngTransparentColorMode.Clear, + BitDepth = PngBitDepth.Bit8, + CompressionLevel = PngCompressionLevel.BestSpeed + }; + } + + private static WebpEncoder GetWebpEncoder(bool transparent) + { + return new WebpEncoder() + { + FileFormat = WebpFileFormatType.Lossy, + Quality = 60, + TransparentColorMode = transparent ? WebpTransparentColorMode.Preserve : WebpTransparentColorMode.Clear, + }; + } + } +} diff --git a/Oqtane.Server/Services/SiteService.cs b/Oqtane.Server/Services/SiteService.cs index 08e64147..84d752c2 100644 --- a/Oqtane.Server/Services/SiteService.cs +++ b/Oqtane.Server/Services/SiteService.cs @@ -92,6 +92,13 @@ namespace Oqtane.Services } site.Pages = pages; + // get language display name for user + foreach (Language language in site.Languages) + { + language.Name = CultureInfo.GetCultureInfo(language.Code).DisplayName; + } + site.Languages = site.Languages.OrderBy(item => item.Name).ToList(); + return Task.FromResult(site); } @@ -130,7 +137,10 @@ namespace Oqtane.Services // languages site.Languages = _languages.GetLanguages(site.SiteId).ToList(); var defaultCulture = CultureInfo.GetCultureInfo(Constants.DefaultCulture); - site.Languages.Add(new Language { Code = defaultCulture.Name, Name = defaultCulture.DisplayName, Version = Constants.Version, IsDefault = !site.Languages.Any(l => l.IsDefault) }); + if (!site.Languages.Exists(item => item.Code == defaultCulture.Name)) + { + site.Languages.Add(new Language { Code = defaultCulture.Name, Name = "", Version = Constants.Version, IsDefault = !site.Languages.Any(l => l.IsDefault) }); + } // themes site.Themes = _themes.FilterThemes(_themes.GetThemes().ToList()); diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index 164d8661..d873bd24 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -159,7 +159,7 @@ namespace Oqtane } }).AddHubOptions(options => { - options.MaximumReceiveMessageSize = null; // no limit (for large amnounts of data ie. textarea components) + options.MaximumReceiveMessageSize = null; // no limit (for large amounts of data ie. textarea components) }) .AddInteractiveWebAssemblyComponents(); diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj index 9d3a0460..21ef0312 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj @@ -13,11 +13,11 @@
- - - - - + + + + + diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj index e8540965..8a9cfe06 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj @@ -19,10 +19,10 @@
- - - - + + + + diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj index 2dca8a8e..48056a8a 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj @@ -13,9 +13,9 @@
- - - + + + diff --git a/Oqtane.Server/wwwroot/js/interop.js b/Oqtane.Server/wwwroot/js/interop.js index 9bc74bb8..ef6043f9 100644 --- a/Oqtane.Server/wwwroot/js/interop.js +++ b/Oqtane.Server/wwwroot/js/interop.js @@ -1,11 +1,18 @@ var Oqtane = Oqtane || {}; Oqtane.Interop = { - setCookie: function (name, value, days) { + setCookie: function (name, value, days, secure, sameSite) { var d = new Date(); d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000)); var expires = "expires=" + d.toUTCString(); - document.cookie = name + "=" + value + ";" + expires + ";path=/"; + var cookieString = name + "=" + value + ";" + expires + ";path=/"; + if (secure) { + cookieString += "; secure"; + } + if (sameSite === "Lax" || sameSite === "Strict" || sameSite === "None") { + cookieString += "; SameSite=" + sameSite; + } + document.cookie = cookieString; }, getCookie: function (name) { name = name + "="; diff --git a/Oqtane.Shared/Interfaces/IImageService.cs b/Oqtane.Shared/Interfaces/IImageService.cs new file mode 100644 index 00000000..f872b72d --- /dev/null +++ b/Oqtane.Shared/Interfaces/IImageService.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Oqtane.Services +{ + public interface IImageService + { + public string[] GetAvailableFormats(); + + public string CreateImage(string filepath, int width, int height, string mode, string position, string background, string rotate, string format, string imagepath); + } +} diff --git a/Oqtane.Shared/Models/ExternalLoginProvider.cs b/Oqtane.Shared/Models/ExternalLoginProvider.cs new file mode 100644 index 00000000..8cda01d4 --- /dev/null +++ b/Oqtane.Shared/Models/ExternalLoginProvider.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Oqtane.Models +{ + public class ExternalLoginProvider + { + public string Name { get; set; } + + public Dictionary Settings { get; set; } + } +} diff --git a/Oqtane.Shared/Models/Language.cs b/Oqtane.Shared/Models/Language.cs index 82a837ce..b64a9381 100644 --- a/Oqtane.Shared/Models/Language.cs +++ b/Oqtane.Shared/Models/Language.cs @@ -19,11 +19,6 @@ namespace Oqtane.Models /// public int? SiteId { get; set; } - /// - /// Language Name - corresponds to , _not_ - /// - public string Name { get; set; } - /// /// Language / Culture code, like 'en-US' - corresponds to /// @@ -34,6 +29,12 @@ namespace Oqtane.Models /// public bool IsDefault { get; set; } + [NotMapped] + /// + /// Language Name - corresponds to , _not_ + /// + public string Name { get; set; } + [NotMapped] /// /// Version of the satellite assembly diff --git a/Oqtane.Shared/Models/UserRole.cs b/Oqtane.Shared/Models/UserRole.cs index b3597ae2..2c891126 100644 --- a/Oqtane.Shared/Models/UserRole.cs +++ b/Oqtane.Shared/Models/UserRole.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel.DataAnnotations.Schema; namespace Oqtane.Models { @@ -26,11 +27,18 @@ namespace Oqtane.Models /// Start of when this assignment is valid. See also /// public DateTime? EffectiveDate { get; set; } + /// /// End of when this assignment is valid. See also /// public DateTime? ExpiryDate { get; set; } + /// + /// Indicates that the User Security Stamp should not be updated when this user role is added or updated + /// + [NotMapped] + public bool IgnoreSecurityStamp { get; set; } + /// /// Direct reference to the object. /// TODO: todoc - is this always populated? diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj index acff7959..ab34bf21 100644 --- a/Oqtane.Shared/Oqtane.Shared.csproj +++ b/Oqtane.Shared/Oqtane.Shared.csproj @@ -3,7 +3,7 @@ net8.0 Debug;Release - 5.2.3 + 5.2.4 Oqtane Shaun Walker .NET Foundation @@ -11,7 +11,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.4 https://github.com/oqtane/oqtane.framework Git Oqtane @@ -19,11 +19,11 @@ - - - + + + - + diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index bdf195ea..12edb80b 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -4,8 +4,8 @@ namespace Oqtane.Shared { public class Constants { - public static readonly string Version = "5.2.3"; - public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3"; + public static readonly string Version = "5.2.4"; + public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3,5.2.4"; public const string PackageId = "Oqtane.Framework"; public const string ClientId = "Oqtane.Client"; public const string UpdaterPackageId = "Oqtane.Updater"; diff --git a/Oqtane.Shared/Shared/ExternalLoginProviders.cs b/Oqtane.Shared/Shared/ExternalLoginProviders.cs new file mode 100644 index 00000000..8b62bad9 --- /dev/null +++ b/Oqtane.Shared/Shared/ExternalLoginProviders.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.Linq; +using Oqtane.Models; + +namespace Oqtane.Shared +{ + public class ExternalLoginProviders + { + public static List Providers + { + get + { + var providers = new List + { + new ExternalLoginProvider + { + Name = "", + Settings = new Dictionary() + }, + // OIDC + new ExternalLoginProvider + { + Name = "Microsoft Entra", + Settings = new Dictionary() + { + { "ExternalLogin:ProviderUrl", "https://entra.microsoft.com" }, + { "ExternalLogin:ProviderType", "oidc" }, + { "ExternalLogin:ProviderName", "Microsoft Entra" }, + { "ExternalLogin:Authority", "https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0" }, + { "ExternalLogin:ClientId", "YOUR CLIENT ID" }, + { "ExternalLogin:ClientSecret", "YOUR CLIENT SECRET" } + } + }, + new ExternalLoginProvider + { + Name = "Auth0 (by Okta)", + Settings = new Dictionary() + { + { "ExternalLogin:ProviderUrl", "https://auth0.com/docs/get-started" }, + { "ExternalLogin:ProviderType", "oidc" }, + { "ExternalLogin:ProviderName", "Auth0" }, + { "ExternalLogin:Authority", "YOUR DOMAIN" }, + { "ExternalLogin:ClientId", "YOUR CLIENT ID" }, + { "ExternalLogin:ClientSecret", "YOUR CLIENT SECRET" } + } + }, + // OAuth2 + new ExternalLoginProvider + { + Name = "GitHub", + Settings = new Dictionary() + { + { "ExternalLogin:ProviderUrl", "https://github.com/settings/developers#oauth-apps" }, + { "ExternalLogin:ProviderType", "oauth2" }, + { "ExternalLogin:ProviderName", "GitHub" }, + { "ExternalLogin:AuthorizationUrl", "https://github.com/login/oauth/authorize" }, + { "ExternalLogin:TokenUrl", "https://github.com/login/oauth/access_token" }, + { "ExternalLogin:UserInfoUrl", "https://api.github.com/user/emails" }, + { "ExternalLogin:ClientId", "YOUR CLIENT ID" }, + { "ExternalLogin:ClientSecret", "YOUR CLIENT SECRET" }, + { "ExternalLogin:Scopes", "user:email" }, + { "ExternalLogin:IdentifierClaimType", "email" }, + { "ExternalLogin:DomainFilter", "!users.noreply.github.com" } + } + }, + new ExternalLoginProvider + { + Name = "Facebook", + Settings = new Dictionary() + { + { "ExternalLogin:ProviderUrl", "https://developers.facebook.com/apps/" }, + { "ExternalLogin:ProviderType", "oauth2" }, + { "ExternalLogin:ProviderName", "Facebook" }, + { "ExternalLogin:AuthorizationUrl", "https://www.facebook.com/v18.0/dialog/oauth" }, + { "ExternalLogin:TokenUrl", "https://graph.facebook.com/v18.0/oauth/access_token" }, + { "ExternalLogin:UserInfoUrl", "https://graph.facebook.com/v18.0/me" }, + { "ExternalLogin:ClientId", "YOUR CLIENT ID" }, + { "ExternalLogin:ClientSecret", "YOUR CLIENT SECRET" }, + { "ExternalLogin:Scopes", "public_profile" }, + { "ExternalLogin:IdentifierClaimType", "id" } + } + } + }; + + return providers.OrderBy(item => item.Name).ToList(); + } + } + } +} diff --git a/Oqtane.Updater/Oqtane.Updater.csproj b/Oqtane.Updater/Oqtane.Updater.csproj index 5b50daf5..aab4be52 100644 --- a/Oqtane.Updater/Oqtane.Updater.csproj +++ b/Oqtane.Updater/Oqtane.Updater.csproj @@ -3,7 +3,7 @@ net8.0 Exe - 5.2.3 + 5.2.4 Oqtane Shaun Walker .NET Foundation @@ -11,7 +11,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.4 https://github.com/oqtane/oqtane.framework Git Oqtane diff --git a/README.md b/README.md index 088f1c71..8e9d7884 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Latest Release -[5.2.2](https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.2) was released on September 23, 2024 and is a maintenance release including 55 pull requests by 8 different contributors, pushing the total number of project commits all-time to over 5800. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. +[5.2.3](https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3) was released on September 23, 2024 and is a maintenance release including 55 pull requests by 8 different contributors, pushing the total number of project commits all-time to over 5800. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Foqtane%2Foqtane.framework%2Fmaster%2Fazuredeploy.json) @@ -75,6 +75,12 @@ Explore and enhance your Oqtane experience by visiting the Oqtane Marketplace. D # Documentation There is a separate [Documentation repository](https://github.com/oqtane/oqtane.docs) which contains a variety of types of documentation for Oqtane, including API documentation that is auto generated using Docfx. The contents of the repository is published to Githib Pages and is available at [https://docs.oqtane.org](https://docs.oqtane.org/) +# Join the Community + +Connect with other developers, get support, and share ideas by joining the Oqtane community on Discord! + +[![Join our Discord](https://img.shields.io/badge/Join%20Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/BnPny88avK) + # Roadmap This project is open source, and therefore is a work in progress... @@ -83,6 +89,9 @@ Backlog (TBD) - [ ] Folder Providers - [ ] Generative AI Integration +[5.2.3](https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.3) (Sep 23, 2024) +- [x] Stabilization improvements + [5.2.2](https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.2) (Sep 23, 2024) - [x] Stabilization improvements - [x] Support for Security Stamp to faciliate Logout Everywhere