Merge pull request #3131 from oqtane/dev

4.0.2 Release
This commit is contained in:
Shaun Walker
2023-08-09 08:22:24 -04:00
committed by GitHub
115 changed files with 1247 additions and 732 deletions

View File

@ -66,6 +66,12 @@
public override string Title => "File Management";
protected override void OnParametersSet()
{
base.OnParametersSet();
base.SetModuleTitle(Localizer["ModuleTitle.Text"]);
}
protected override async Task OnInitializedAsync()
{
try

View File

@ -110,8 +110,13 @@
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
public override string Title => "Folder Management";
public override string Title => "Folder Management";
protected override void OnParametersSet()
{
base.OnParametersSet();
base.SetModuleTitle(Localizer["ModuleTitle.Text"]);
}
protected override async Task OnInitializedAsync()
{
try

View File

@ -11,7 +11,7 @@
else
{
<ActionLink Action="Log" Class="btn btn-secondary" Text="View Logs" ResourceKey="ViewJobs" />
<button type="button" class="btn btn-secondary" @onclick="(async () => await Refresh())">Refresh</button>
<button type="button" class="btn btn-secondary" @onclick="(async () => await Refresh())">@Localizer["Refresh.Text"]</button>
<br />
<br />

View File

@ -16,7 +16,7 @@
else
{
<TabStrip>
<TabPanel Name="Manage" ResourceKey="Manage">
<TabPanel Name="Manage" ResourceKey="Manage" Heading="Manage">
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
@ -45,7 +45,7 @@ else
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
</form>
</TabPanel>
<TabPanel Name="Upload" ResourceKey="Upload" Security="SecurityAccessLevel.Host">
<TabPanel Name="Upload" ResourceKey="Upload" Security="SecurityAccessLevel.Host" Heading="Upload">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" HelpText="Upload one or more translations. Once they are uploaded click Install." ResourceKey="LanguageUpload">Translation: </Label>

View File

@ -11,9 +11,6 @@
<Authorizing>
<text>...</text>
</Authorizing>
<Authorized>
<div>@Localizer["Info.SignedIn"]</div>
</Authorized>
<NotAuthorized>
@if (!twofactor)
{
@ -69,259 +66,265 @@
</AuthorizeView>
@code {
private bool _allowsitelogin = true;
private bool _allowexternallogin = false;
private ElementReference login;
private bool validated = false;
private bool twofactor = false;
private string _username = string.Empty;
private ElementReference username;
private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private bool _remember = false;
private string _code = string.Empty;
private bool _allowsitelogin = true;
private bool _allowexternallogin = false;
private ElementReference login;
private bool validated = false;
private bool twofactor = false;
private string _username = string.Empty;
private ElementReference username;
private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private bool _remember = false;
private string _code = string.Empty;
private string _returnUrl = string.Empty;
private string _returnUrl = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
public override List<Resource> Resources => new List<Resource>()
public override List<Resource> Resources => new List<Resource>()
{
new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" }
};
protected override async Task OnInitializedAsync()
{
try
{
_togglepassword = SharedLocalizer["ShowPassword"];
protected override async Task OnInitializedAsync()
{
try
{
_togglepassword = SharedLocalizer["ShowPassword"];
if (PageState.Site.Settings.ContainsKey("LoginOptions:AllowSiteLogin") && !string.IsNullOrEmpty(PageState.Site.Settings["LoginOptions:AllowSiteLogin"]))
{
_allowsitelogin = bool.Parse(PageState.Site.Settings["LoginOptions:AllowSiteLogin"]);
}
if (PageState.Site.Settings.ContainsKey("LoginOptions:AllowSiteLogin") && !string.IsNullOrEmpty(PageState.Site.Settings["LoginOptions:AllowSiteLogin"]))
{
_allowsitelogin = bool.Parse(PageState.Site.Settings["LoginOptions:AllowSiteLogin"]);
}
if (PageState.Site.Settings.ContainsKey("ExternalLogin:ProviderType") && !string.IsNullOrEmpty(PageState.Site.Settings["ExternalLogin:ProviderType"]))
{
_allowexternallogin = true;
}
if (PageState.Site.Settings.ContainsKey("ExternalLogin:ProviderType") && !string.IsNullOrEmpty(PageState.Site.Settings["ExternalLogin:ProviderType"]))
{
_allowexternallogin = true;
}
if (PageState.QueryString.ContainsKey("returnurl"))
{
_returnUrl = PageState.QueryString["returnurl"];
}
if (PageState.QueryString.ContainsKey("returnurl"))
{
_returnUrl = PageState.QueryString["returnurl"];
}
if (PageState.QueryString.ContainsKey("name"))
{
_username = PageState.QueryString["name"];
}
if (PageState.QueryString.ContainsKey("name"))
{
_username = PageState.QueryString["name"];
}
if (PageState.QueryString.ContainsKey("token") && !string.IsNullOrEmpty(_username))
{
var user = new User();
user.SiteId = PageState.Site.SiteId;
user.Username = _username;
if (PageState.QueryString.ContainsKey("token") && !string.IsNullOrEmpty(_username))
{
var user = new User();
user.SiteId = PageState.Site.SiteId;
user.Username = _username;
if (PageState.QueryString.ContainsKey("key"))
{
user = await UserService.LinkUserAsync(user, PageState.QueryString["token"], PageState.Site.Settings["ExternalLogin:ProviderType"], PageState.QueryString["key"], PageState.Site.Settings["ExternalLogin:ProviderName"]);
if (user != null)
{
await logger.LogInformation(LogFunction.Security, "External Login Linkage Successful For Username {Username}", _username);
AddModuleMessage(Localizer["Success.Account.Linked"], MessageType.Info);
}
else
{
await logger.LogError(LogFunction.Security, "External Login Linkage Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Message.Account.NotLinked"], MessageType.Warning);
}
_username = "";
}
else
{
user = await UserService.VerifyEmailAsync(user, PageState.QueryString["token"]);
if (user != null)
{
await logger.LogInformation(LogFunction.Security, "Email Verified For For Username {Username}", _username);
AddModuleMessage(Localizer["Success.Account.Verified"], MessageType.Info);
}
else
{
await logger.LogError(LogFunction.Security, "Email Verification Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Message.Account.NotVerified"], MessageType.Warning);
}
}
}
else
{
if (PageState.QueryString.ContainsKey("status"))
{
AddModuleMessage(Localizer["ExternalLoginStatus." + PageState.QueryString["status"]], MessageType.Info);
}
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Login {Error}", ex.Message);
AddModuleMessage(Localizer["Error.LoadLogin"], MessageType.Error);
}
}
if (PageState.QueryString.ContainsKey("key"))
{
user = await UserService.LinkUserAsync(user, PageState.QueryString["token"], PageState.Site.Settings["ExternalLogin:ProviderType"], PageState.QueryString["key"], PageState.Site.Settings["ExternalLogin:ProviderName"]);
if (user != null)
{
await logger.LogInformation(LogFunction.Security, "External Login Linkage Successful For Username {Username}", _username);
AddModuleMessage(Localizer["Success.Account.Linked"], MessageType.Info);
}
else
{
await logger.LogError(LogFunction.Security, "External Login Linkage Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Message.Account.NotLinked"], MessageType.Warning);
}
_username = "";
}
else
{
user = await UserService.VerifyEmailAsync(user, PageState.QueryString["token"]);
if (user != null)
{
await logger.LogInformation(LogFunction.Security, "Email Verified For For Username {Username}", _username);
AddModuleMessage(Localizer["Success.Account.Verified"], MessageType.Info);
}
else
{
await logger.LogError(LogFunction.Security, "Email Verification Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Message.Account.NotVerified"], MessageType.Warning);
}
}
}
else
{
if (PageState.QueryString.ContainsKey("status"))
{
AddModuleMessage(Localizer["ExternalLoginStatus." + PageState.QueryString["status"]], MessageType.Info);
}
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Login {Error}", ex.Message);
AddModuleMessage(Localizer["Error.LoadLogin"], MessageType.Error);
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && PageState.User == null)
{
await username.FocusAsync();
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && PageState.User == null)
{
await username.FocusAsync();
}
private async Task Login()
{
try
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(login))
{
var hybrid = (PageState.Runtime == Shared.Runtime.Hybrid);
var user = new User { SiteId = PageState.Site.SiteId, Username = _username, Password = _password, LastIPAddress = SiteState.RemoteIPAddress};
if (!twofactor)
{
user = await UserService.LoginUserAsync(user, hybrid, _remember);
}
else
{
user = await UserService.VerifyTwoFactorAsync(user, _code);
}
// redirect logged in user to specified page
if (PageState.User != null)
{
NavigationManager.NavigateTo(PageState.ReturnUrl);
}
}
if (user.IsAuthenticated)
{
await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username);
private async Task Login()
{
try
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(login))
{
var hybrid = (PageState.Runtime == Shared.Runtime.Hybrid);
var user = new User { SiteId = PageState.Site.SiteId, Username = _username, Password = _password, LastIPAddress = SiteState.RemoteIPAddress};
if (hybrid)
{
// hybrid apps utilize an interactive login
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider
.GetService(typeof(IdentityAuthenticationStateProvider));
authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(NavigateUrl(WebUtility.UrlDecode(_returnUrl), true));
}
else
{
// post back to the Login page so that the cookies are set correctly
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = _returnUrl };
string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/");
await interop.SubmitForm(url, fields);
}
}
else
{
if ((PageState.Site.Settings.ContainsKey("LoginOptions:TwoFactor") && PageState.Site.Settings["LoginOptions:TwoFactor"] == "required") || user.TwoFactorRequired)
{
twofactor = true;
validated = false;
AddModuleMessage(Localizer["Message.TwoFactor"], MessageType.Info);
}
else
{
if (!twofactor)
{
await logger.LogInformation(LogFunction.Security, "Login Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Error.Login.Fail"], MessageType.Error);
}
else
{
await logger.LogInformation(LogFunction.Security, "Two Factor Verification Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Error.TwoFactor.Fail"], MessageType.Error);
}
}
}
}
else
{
AddModuleMessage(Localizer["Message.Required.UserInfo"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Performing Login {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Login"], MessageType.Error);
}
}
if (!twofactor)
{
user = await UserService.LoginUserAsync(user, hybrid, _remember);
}
else
{
user = await UserService.VerifyTwoFactorAsync(user, _code);
}
private void Cancel()
{
NavigationManager.NavigateTo(_returnUrl);
}
if (user.IsAuthenticated)
{
await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username);
private async Task Forgot()
{
try
{
if (_username != string.Empty)
{
var user = await UserService.GetUserAsync(_username, PageState.Site.SiteId);
if (user != null)
{
await UserService.ForgotPasswordAsync(user);
await logger.LogInformation(LogFunction.Security, "Password Reset Notification Sent For Username {Username}", _username);
AddModuleMessage(Localizer["Message.ForgotUser"], MessageType.Info);
}
else
{
AddModuleMessage(Localizer["Message.UserDoesNotExist"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["Message.ForgotPassword"], MessageType.Info);
}
if (hybrid)
{
// hybrid apps utilize an interactive login
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider
.GetService(typeof(IdentityAuthenticationStateProvider));
authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(NavigateUrl(WebUtility.UrlDecode(_returnUrl), true));
}
else
{
// post back to the Login page so that the cookies are set correctly
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = _returnUrl };
string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/");
await interop.SubmitForm(url, fields);
}
}
else
{
if ((PageState.Site.Settings.ContainsKey("LoginOptions:TwoFactor") && PageState.Site.Settings["LoginOptions:TwoFactor"] == "required") || user.TwoFactorRequired)
{
twofactor = true;
validated = false;
AddModuleMessage(Localizer["Message.TwoFactor"], MessageType.Info);
}
else
{
if (!twofactor)
{
await logger.LogInformation(LogFunction.Security, "Login Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Error.Login.Fail"], MessageType.Error);
}
else
{
await logger.LogInformation(LogFunction.Security, "Two Factor Verification Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Error.TwoFactor.Fail"], MessageType.Error);
}
}
}
}
else
{
AddModuleMessage(Localizer["Message.Required.UserInfo"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Performing Login {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Login"], MessageType.Error);
}
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Resetting Password {Error}", ex.Message);
AddModuleMessage(Localizer["Error.ResetPassword"], MessageType.Error);
}
}
private void Cancel()
{
NavigationManager.NavigateTo(_returnUrl);
}
private void Reset()
{
twofactor = false;
_username = "";
_password = "";
ClearModuleMessage();
StateHasChanged();
}
private async Task Forgot()
{
try
{
if (_username != string.Empty)
{
var user = await UserService.GetUserAsync(_username, PageState.Site.SiteId);
if (user != null)
{
await UserService.ForgotPasswordAsync(user);
await logger.LogInformation(LogFunction.Security, "Password Reset Notification Sent For Username {Username}", _username);
AddModuleMessage(Localizer["Message.ForgotUser"], MessageType.Info);
}
else
{
AddModuleMessage(Localizer["Message.UserDoesNotExist"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["Message.ForgotPassword"], MessageType.Info);
}
private async Task KeyPressed(KeyboardEventArgs e)
{
if (e.Code == "Enter" || e.Code == "NumpadEnter")
{
await Login();
}
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Resetting Password {Error}", ex.Message);
AddModuleMessage(Localizer["Error.ResetPassword"], MessageType.Error);
}
}
private void TogglePassword()
{
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = SharedLocalizer["ShowPassword"];
}
}
private void Reset()
{
twofactor = false;
_username = "";
_password = "";
ClearModuleMessage();
StateHasChanged();
}
private void ExternalLogin()
{
private async Task KeyPressed(KeyboardEventArgs e)
{
if (e.Code == "Enter" || e.Code == "NumpadEnter")
{
await Login();
}
}
private void TogglePassword()
{
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = SharedLocalizer["ShowPassword"];
}
}
private void ExternalLogin()
{
NavigationManager.NavigateTo(Utilities.TenantUrl(PageState.Alias, "/pages/external?returnurl=" + _returnUrl), true);
}
}
}

View File

@ -10,65 +10,110 @@
<TabStrip>
<TabPanel Name="Download" ResourceKey="Download">
<div class="row justify-content-center mb-3">
<div class="col-sm-6">
<div class="text-center">
<div class="form-check form-check-inline">
<input id="free" class="form-check-input" type="radio" checked="@(_price == "free")" name="Price" @onchange="@(() => PriceChanged("free"))" />
<label class="form-check-label" for="free">@SharedLocalizer["Free"]</label>
</div>
<div class="form-check form-check-inline">
<input id="paid" class="form-check-input" type="radio" checked="@(_price == "paid")" name="Price" @onchange="@(() => PriceChanged("paid"))" />
<label class="form-check-label" for="paid">@SharedLocalizer["Paid"]</label>
</div>
</div>
</div>
<div class="row justify-content-center mb-3">
<div class="col">
<div class="input-group">
<select id="price" class="form-select custom-select" @onchange="(e => PriceChanged(e))">
<option value="free">@SharedLocalizer["Free"]</option>
<option value="paid">@SharedLocalizer["Paid"]</option>
</select>
<span class="input-group-text">@Localizer["Product"]</span>
<input id="search" class="form-control" placeholder="@SharedLocalizer["Search.Hint"]" @bind="@_search" />
<button type="button" class="btn btn-primary" @onclick="Search">@SharedLocalizer["Search"]</button>
<button type="button" class="btn btn-secondary" @onclick="Reset">@SharedLocalizer["Reset"]</button>
</div>
</div>
</div>
@if (_packages != null)
{
if (_packages.Count > 0)
{
<Pager Items="@_packages">
<Row>
<td>
<h3 style="display: inline;"><a href="@context.ProductUrl" target="_new">@context.Name</a></h3>&nbsp;&nbsp;by:&nbsp;&nbsp;<strong><a href="@context.OwnerUrl" target="new">@context.Owner</a></strong><br />
@(context.Description.Length > 400 ? (context.Description.Substring(0, 400) + "...") : context.Description)<br />
<strong>@(String.Format("{0:n0}", context.Downloads))</strong> @SharedLocalizer["Search.Downloads"]&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Released"]: <strong>@context.ReleaseDate.ToString("MMM dd, yyyy")</strong>&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Version"]: <strong>@context.Version</strong>
@((MarkupString)(!string.IsNullOrEmpty(context.PackageUrl) ? "&nbsp;&nbsp;|&nbsp;&nbsp;" + SharedLocalizer["Search.Source"] + ": <strong>" + new Uri(context.PackageUrl).Host + "</strong>" : ""))
@((MarkupString)(context.TrialPeriod > 0 ? "&nbsp;&nbsp;|&nbsp;&nbsp;<strong>" + context.TrialPeriod + " " + @SharedLocalizer["Trial"] + "</strong>" : ""))
</td>
<td style="width: 1px; vertical-align: middle;">
@if (context.Price != null && !string.IsNullOrEmpty(context.PackageUrl))
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(context.PackageId, context.Version))>@SharedLocalizer["Download"]</button>
}
</td>
<td style="width: 1px; vertical-align: middle;">
@if (context.Price != null && !string.IsNullOrEmpty(context.PaymentUrl))
{
<a class="btn btn-primary" style="text-decoration: none !important" href="@context.PaymentUrl" target="_new">@context.Price.Value.ToString("$#,##0.00")</a>
}
else
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(context.PackageId, context.Version))>@SharedLocalizer["Download"]</button>
}
</td>
</Row>
</Pager>
}
else
{
<br />
<div class="mx-auto text-center">
@Localizer["Search.NoResults"]
</div>
}
}
<div class="row mb-3">
<div class="col">
@if (_initialized)
{
<br />
<div class="row mb-3">
<div class="col-sm-4">
<h3>@((_packages != null) ? _packages.Count : 0) @SharedLocalizer["Search.Results"]</h3>
</div>
<div class="col-sm-4">
&nbsp;
</div>
<div class="col-sm-4">
<select class="form-select" value="@_sort" @onchange="(e => SortChanged(e))">
<option value="popularity">@SharedLocalizer["Search.Popularity"]</option>
<option value="alphabetical">@SharedLocalizer["Search.Alphabetical"]</option>
<option value="downloads">@SharedLocalizer["Search.Downloads"]</option>
<option value="recent">@SharedLocalizer["Search.RecentlyReleased"]</option>
@if (_price == "paid")
{
<option value="price">@SharedLocalizer["Search.Price"]</option>
}
</select>
</div>
</div>
<Pager Format="Grid" Items="@_packages" DisplayPages="1" PageSize="9" Toolbar="Both" Class="container-fluid px-0" RowClass="row g-0" ColumnClass="col-lg-4 col-md-6">
<Row>
<div class="m-2 p-2 d-flex justify-content-center">
<div class="container-fluid px-0">
<div class="row g-0">
<div class="col-6">
@if (context.LogoFileId != null)
{
<img src="@GetLogo(context.LogoFileId.Value)" class="img-fluid" alt="@context.Name" />
}
else
{
<img src="/package.png" class="img-fluid" alt="@context.Name" />
}
</div>
<div class="col-6 text-end">
<small>@SharedLocalizer["Search.Version"]:</small> <strong>@context.Version</strong>
<br /><small>@SharedLocalizer["Search.Downloads"]:</small> <strong>@(String.Format("{0:n0}", context.Downloads))</strong>
<br /><small>@SharedLocalizer["Search.Released"]:</small> <strong>@context.ReleaseDate.ToString("MM/dd/yyyy")</strong>
@if (!string.IsNullOrEmpty(context.PackageUrl))
{
<br /><small>@SharedLocalizer["Search.Source"]:</small> <strong>@(new Uri(context.PackageUrl).Host)</strong>
}
</div>
</div>
<div class="row g-0">
<div class="col">
<h3 style="display: inline;"><a href="@context.ProductUrl" target="_blank">@context.Name</a></h3><br />
<small>@SharedLocalizer["Search.By"]:</small> <strong><a href="@context.OwnerUrl" target="new">@context.Owner</a></strong><br />
@(context.Description.Length > 400 ? (context.Description.Substring(0, 400) + "...") : context.Description)<br />
@if (context.Price != null && !string.IsNullOrEmpty(context.PaymentUrl))
{
<small>@SharedLocalizer["Search.Price"]:</small> <strong>@context.Price.Value.ToString("$#,##0.00")</strong>
@((MarkupString)(context.TrialPeriod > 0 ? " <strong>(" + context.TrialPeriod + " Day Trial)</strong>" : ""))
}
<br />
@if (!string.IsNullOrEmpty(context.PackageUrl))
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(context.PackageId, context.Version))>@SharedLocalizer["Download"]</button>
}
@if (context.Price != null && !string.IsNullOrEmpty(context.PaymentUrl))
{
<a class="btn btn-success ms-2" style="text-decoration: none !important" href="@context.PaymentUrl" target="_new">@SharedLocalizer["Buy"]</a>
}
<br />
</div>
</div>
</div>
</div>
</Row>
</Pager>
}
</div>
</div>
<br />
<ModuleMessage Type="MessageType.Info" Message="@SharedLocalizer["Oqtane.Marketplace"]" />
</TabPanel>
<TabPanel Name="Upload" ResourceKey="Upload">
<TabPanel Name="Upload" ResourceKey="Upload" Heading="Upload">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" HelpText="Upload one or more module packages. Once they are uploaded click Install to complete the installation." ResourceKey="Module">Module: </Label>
@ -116,8 +161,10 @@
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@code {
private bool _initialized = false;
private List<Package> _packages;
private string _price = "free";
private string _sort = "popularity";
private string _search = "";
private string _productname = "";
private string _packageid = "";
@ -131,6 +178,7 @@
try
{
await LoadModuleDefinitions();
_initialized = true;
}
catch (Exception ex)
{
@ -141,8 +189,10 @@
private async Task LoadModuleDefinitions()
{
ShowProgressIndicator();
var moduledefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId);
_packages = await PackageService.GetPackagesAsync("module", _search, _price, "");
_packages = await PackageService.GetPackagesAsync("module", _search, _price, "", _sort);
if (_packages != null)
{
@ -154,21 +204,22 @@
}
}
}
HideProgressIndicator();
}
private async void PriceChanged(ChangeEventArgs e)
private string GetLogo(int fileid)
{
try
{
_price = (string)e.Value;
_search = "";
await LoadModuleDefinitions();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error On PriceChanged");
}
var url = ImageUrl(fileid, 100, 100);
url = (!string.IsNullOrEmpty(PageState.Alias.Path)) ? url.Substring(PageState.Alias.Path.Length + 1) : url;
return Constants.PackageRegistryUrl + url;
}
private async void PriceChanged(string price)
{
_price = price;
await LoadModuleDefinitions();
StateHasChanged();
}
private async Task Search()
@ -196,6 +247,12 @@
}
}
private async void SortChanged(ChangeEventArgs e)
{
_sort = (string)e.Value;
await LoadModuleDefinitions();
}
private void HideModal()
{
_productname = "";

View File

@ -80,7 +80,7 @@ else
}
</td>
<td>
@((MarkupString)SupportLink(context.PackageName))
@((MarkupString)SupportLink(context.PackageName, context.Version))
</td>
<td>
@((MarkupString)PurchaseLink(context.PackageName))
@ -145,7 +145,7 @@ else
return link;
}
private string SupportLink(string packagename)
private string SupportLink(string packagename, string version)
{
string link = "";
if (!string.IsNullOrEmpty(packagename) && _packages != null)
@ -153,7 +153,7 @@ else
var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault();
if (package != null && !string.IsNullOrEmpty(package.SupportUrl))
{
link += "<a class=\"btn btn-success\" style=\"text-decoration: none !important\" href=\"" + package.SupportUrl + "\" target=\"_new\">" + SharedLocalizer["Help"] + "</a>";
link += "<a class=\"btn btn-info\" style=\"text-decoration: none !important\" href=\"" + package.SupportUrl.Replace("{Version}", version) + "\" target=\"_new\">" + SharedLocalizer["Help"] + "</a>";
}
}
return link;

View File

@ -22,6 +22,11 @@
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Title => "Export Content";
protected override void OnParametersSet()
{
base.OnParametersSet();
base.SetModuleTitle(Localizer["ModuleTitle.Text"]);
}
private async Task ExportModule()
{

View File

@ -28,6 +28,11 @@
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Title => "Import Content";
protected override void OnParametersSet()
{
base.OnParametersSet();
base.SetModuleTitle(Localizer["ModuleTitle.Text"]);
}
private async Task ImportModule()
{
validated = true;

View File

@ -130,6 +130,11 @@
private string modifiedby;
private DateTime modifiedon;
protected override void OnParametersSet()
{
base.OnParametersSet();
base.SetModuleTitle(Localizer["ModuleTitle.Text"]);
}
protected override void OnInitialized()
{
_module = ModuleState.ModuleDefinition.Name;

View File

@ -10,7 +10,7 @@
{
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<TabStrip Refresh="@_refresh">
<TabPanel Name="Settings" ResourceKey="Settings">
<TabPanel Name="Settings" ResourceKey="Settings" Heading="Settings">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the page name" ResourceKey="Name">Name: </Label>
@ -126,7 +126,7 @@
</div>
</div>
<Section Name="Appearance" Heading="Appearance" ResourceKey="Appearance">
<Section Name="Appearance" ResourceKey="Appearance" Heading=@Localizer["Appearance.Name"]>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="title" HelpText="Optionally enter the page title. If you do not provide a page title, the page name will be used." ResourceKey="Title">Title: </Label>
@ -158,7 +158,7 @@
</div>
</div>
</Section>
<Section Name="PageContent" Heading="Page Content" ResourceKey="PageContent">
<Section Name="PageContent" ResourceKey="PageContent" Heading=@Localizer["PageContent.Heading"]>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="headcontent" HelpText="Optionally enter content to be included in the page head (ie. meta, link, or script tags)" ResourceKey="HeadContent">Head Content: </Label>
@ -175,7 +175,7 @@
</div>
</Section>
</TabPanel>
<TabPanel Name="Permissions" ResourceKey="Permissions">
<TabPanel Name="Permissions" ResourceKey="Permissions" Heading=@Localizer["Permissions.Heading"]>
<div class="container">
<div class="row mb-1 align-items-center">
<PermissionGrid EntityName="@EntityNames.Page" Permissions="@_permissions" @ref="_permissionGrid" />
@ -184,7 +184,7 @@
</TabPanel>
@if (_themeSettingsType != null)
{
<TabPanel Name="ThemeSettings" Heading="Theme Settings" ResourceKey="ThemeSettings">
<TabPanel Name="ThemeSettings" Heading=@Localizer["Theme.Heading"] ResourceKey="ThemeSettings">
@ThemeSettingsComponent
</TabPanel>
}

View File

@ -14,7 +14,7 @@
@if (_page.UserId == null)
{
<TabStrip Refresh="@_refresh">
<TabPanel Name="Settings" ResourceKey="Settings" Heading=@Localizer["Settings.Heading"]>
<TabPanel Name="Settings" ResourceKey="Settings" Heading="Settings">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the page name" ResourceKey="Name">Name: </Label>
@ -137,7 +137,7 @@
</div>
</div>
</div>
<Section Name="Appearance" ResourceKey="Appearance">
<Section Name="Appearance" ResourceKey="Appearance" Heading="Appearance">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="title" HelpText="Optionally enter the page title. If you do not provide a page title, the page name will be used." ResourceKey="Title">Title: </Label>
@ -189,7 +189,7 @@
<br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo>
</TabPanel>
<TabPanel Name="Permissions" ResourceKey="Permissions">
<TabPanel Name="Permissions" ResourceKey="Permissions" Heading="Permissions">
<div class="container">
<div class="row mb-1 align-items-center">
<PermissionGrid EntityName="@EntityNames.Page" PermissionList="@_permissions" @ref="_permissionGrid" />
@ -224,7 +224,7 @@
else
{
<TabStrip Refresh="@_refresh">
<TabPanel Name="Settings" ResourceKey="Settings" Heading=@Localizer["Settings.Heading"]>
<TabPanel Name="Settings" ResourceKey="Settings" Heading="Settings">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="title" HelpText="Optionally enter the page title. If you do not provide a page title, the page name will be used." ResourceKey="Title">Title: </Label>

View File

@ -14,7 +14,7 @@
else
{
<TabStrip>
<TabPanel Name="Pages" ResourceKey="Pages">
<TabPanel Name="Pages" ResourceKey="Pages" Heading="Pages">
@if (!_pages.Where(item => item.IsDeleted).Any())
{
<br />
@ -31,7 +31,7 @@ else
<th>@Localizer["DeletedOn"]</th>
</Header>
<Row>
<td><button type="button" @onclick="@(() => RestorePage(context))" class="btn btn-success" title="Restore">Restore</button></td>
<td><button type="button" @onclick="@(() => RestorePage(context))" class="btn btn-success" title="Restore">@Localizer["Restore"]</button></td>
<td><ActionDialog Header="Delete Page" Message="@string.Format(Localizer["Confirm.Page.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeletePage(context))" ResourceKey="DeletePage" /></td>
<td>@context.Name</td>
<td>@context.DeletedBy</td>
@ -42,7 +42,7 @@ else
<ActionDialog Header="Remove All Deleted Pages" Message="Are You Sure You Wish To Permanently Remove All Deleted Pages?" Action="Remove All Deleted Pages" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllPages())" ResourceKey="DeleteAllPages" />
}
</TabPanel>
<TabPanel Name="Modules" ResourceKey="Modules">
<TabPanel Name="Modules" ResourceKey="Modules" Heading="Modules">
@if (!_modules.Where(item => item.IsDeleted).Any())
{
<br />

View File

@ -26,7 +26,7 @@
<Label Class="col-sm-3" For="homepage" HelpText="Select the home page for the site (to be used if there is no page with a path of '/')" ResourceKey="HomePage">Home Page: </Label>
<div class="col-sm-9">
<select id="homepage" class="form-select" @bind="@_homepageid" required>
<option value="-">&lt;@Localizer["Not Specified"]&gt;</option>
<option value="-">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (Page page in PageState.Pages)
{
if (UserSecurity.ContainsRole(page.PermissionList, PermissionNames.View, RoleNames.Everyone))
@ -60,7 +60,7 @@
</div>
</div>
<br />
<Section Name="Appearance" ResourceKey="Appearance">
<Section Name="Appearance" Heading="Appearance" ResourceKey="Appearance">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="logo" HelpText="Specify a logo for the site" ResourceKey="Logo">Logo: </Label>

View File

@ -8,7 +8,7 @@
@if (_sites == null)
{
<p><em>Loading...</em></p>
<p><em>@SharedLocalizer["Loading"]</em></p>
}
else
{

View File

@ -10,61 +10,108 @@
<TabStrip>
<TabPanel Name="Download" ResourceKey="Download">
<div class="row justify-content-center mb-3">
<div class="col-sm-6">
<div class="text-center">
<div class="form-check form-check-inline">
<input id="free" class="form-check-input" type="radio" checked="@(_price == "free")" name="Price" @onchange="@(() => PriceChanged("free"))" />
<label class="form-check-label" for="free">@SharedLocalizer["Free"]</label>
</div>
<div class="form-check form-check-inline">
<input id="paid" class="form-check-input" type="radio" checked="@(_price == "paid")" name="Price" @onchange="@(() => PriceChanged("paid"))" />
<label class="form-check-label" for="paid">@SharedLocalizer["Paid"]</label>
</div>
</div>
</div>
<div class="row justify-content-center mb-3">
<div class="col">
<div class="input-group">
<select id="price" class="form-select custom-select" @onchange="(e => PriceChanged(e))">
<option value="free">@SharedLocalizer["Free"]</option>
<option value="paid">@SharedLocalizer["Paid"]</option>
</select>
<span class="input-group-text">Product</span>
<input id="search" class="form-control" placeholder="@SharedLocalizer["Search.Hint"]" @bind="@_search" />
<button type="button" class="btn btn-primary" @onclick="Search">@SharedLocalizer["Search"]</button>
<button type="button" class="btn btn-secondary" @onclick="Reset">@SharedLocalizer["Reset"]</button>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col">
@if (_initialized)
{
<br />
<div class="row mb-3">
<div class="col-sm-4">
<h3>@((_packages != null) ? _packages.Count : 0) @SharedLocalizer["Search.Results"]</h3>
</div>
<div class="col-sm-4">
&nbsp;
</div>
<div class="col-sm-4">
<select class="form-select" value="@_sort" @onchange="(e => SortChanged(e))">
<option value="popularity">@SharedLocalizer["Search.Popularity"]</option>
<option value="alphabetical">@SharedLocalizer["Search.Alphabetical"]</option>
<option value="downloads">@SharedLocalizer["Search.Downloads"]</option>
<option value="recent">@SharedLocalizer["Search.RecentlyReleased"]</option>
@if (_price == "paid")
{
<option value="price">@SharedLocalizer["Search.Price"]</option>
}
</select>
</div>
</div>
<Pager Format="Grid" Items="@_packages" DisplayPages="1" PageSize="9" Toolbar="Both" Class="container-fluid px-0" RowClass="row g-0" ColumnClass="col-lg-4 col-md-6">
<Row>
<div class="m-2 p-2 d-flex justify-content-center">
<div class="container-fluid px-0">
<div class="row g-0">
<div class="col-6">
@if (context.LogoFileId != null)
{
<img src="@GetLogo(context.LogoFileId.Value)" class="img-fluid" alt="@context.Name" />
}
else
{
<img src="/package.png" class="img-fluid" alt="@context.Name" />
}
</div>
<div class="col-6 text-end">
<small>@SharedLocalizer["Search.Version"]:</small> <strong>@context.Version</strong>
<br /><small>@SharedLocalizer["Search.Downloads"]:</small> <strong>@(String.Format("{0:n0}", context.Downloads))</strong>
<br /><small>@SharedLocalizer["Search.Released"]:</small> <strong>@context.ReleaseDate.ToString("MM/dd/yyyy")</strong>
@if (!string.IsNullOrEmpty(context.PackageUrl))
{
<br />
@if (_packages != null)
{
if (_packages.Count > 0)
{
<Pager Items="@_packages">
<Row>
<td>
<h3 style="display: inline;"><a href="@context.ProductUrl" target="_new">@context.Name</a></h3>&nbsp;&nbsp;@SharedLocalizer["Search.By"]:&nbsp;&nbsp;<strong><a href="@context.OwnerUrl" target="new">@context.Owner</a></strong><br />
@(context.Description.Length > 400 ? (context.Description.Substring(0, 400) + "...") : context.Description)<br />
<strong>@(String.Format("{0:n0}", context.Downloads))</strong> @SharedLocalizer["Search.Downloads"]&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Released"]: <strong>@context.ReleaseDate.ToString("MMM dd, yyyy")</strong>&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Version"]: <strong>@context.Version</strong>
@((MarkupString)(!string.IsNullOrEmpty(context.PackageUrl) ? "&nbsp;&nbsp;|&nbsp;&nbsp;" + SharedLocalizer["Search.Source"] + ": <strong>" + new Uri(context.PackageUrl).Host + "</strong>" : ""))
@((MarkupString)(context.TrialPeriod > 0 ? "&nbsp;&nbsp;|&nbsp;&nbsp;<strong>" + context.TrialPeriod + " " + @SharedLocalizer["Trial"] + "</strong>" : ""))
</td>
<td style="width: 1px; vertical-align: middle;">
@if (context.Price != null && !string.IsNullOrEmpty(context.PackageUrl))
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(context.PackageId, context.Version))>@SharedLocalizer["Download"]</button>
}
</td>
<td style="width: 1px; vertical-align: middle;">
@if (context.Price != null && !string.IsNullOrEmpty(context.PaymentUrl))
{
<a class="btn btn-primary" style="text-decoration: none !important" href="@context.PaymentUrl" target="_new">@context.Price.Value.ToString("$#,##0.00")</a>
}
else
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(context.PackageId, context.Version))>@SharedLocalizer["Download"]</button>
}
</td>
</Row>
</Pager>
}
else
{
<br />
<div class="mx-auto text-center">
@Localizer["Search.NoResults"]
</div>
}
}
<small>@SharedLocalizer["Search.Source"]:</small> <strong>@(new Uri(context.PackageUrl).Host)</strong>
}
</div>
</div>
<div class="row g-0">
<div class="col">
<h3 style="display: inline;"><a href="@context.ProductUrl" target="_blank">@context.Name</a></h3><br />
<small>@SharedLocalizer["Search.By"]:</small> <strong><a href="@context.OwnerUrl" target="new">@context.Owner</a></strong><br />
@(context.Description.Length > 400 ? (context.Description.Substring(0, 400) + "...") : context.Description)<br />
@if (context.Price != null && !string.IsNullOrEmpty(context.PaymentUrl))
{
<small>@SharedLocalizer["Search.Price"]:</small> <strong>@context.Price.Value.ToString("$#,##0.00")</strong>
@((MarkupString)(context.TrialPeriod > 0 ? " <strong>(" + context.TrialPeriod + " Day Trial)</strong>" : ""))
}
<br />
@if (!string.IsNullOrEmpty(context.PackageUrl))
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(context.PackageId, context.Version))>@SharedLocalizer["Download"]</button>
}
@if (context.Price != null && !string.IsNullOrEmpty(context.PaymentUrl))
{
<a class="btn btn-success ms-2" style="text-decoration: none !important" href="@context.PaymentUrl" target="_new">@SharedLocalizer["Buy"]</a>
}
<br />
</div>
</div>
</div>
</div>
</Row>
</Pager>
}
</div>
</div>
<br />
<ModuleMessage Type="MessageType.Info" Message="@SharedLocalizer["Oqtane.Marketplace"]" />
</TabPanel>
@ -116,8 +163,10 @@
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@code {
private bool _initialized = false;
private List<Package> _packages;
private string _price = "free";
private string _sort = "popularity";
private string _search = "";
private string _productname = "";
private string _license = "";
@ -131,6 +180,7 @@
try
{
await LoadThemes();
_initialized = true;
}
catch (Exception ex)
{
@ -141,8 +191,10 @@
private async Task LoadThemes()
{
ShowProgressIndicator();
var themes = await ThemeService.GetThemesAsync();
_packages = await PackageService.GetPackagesAsync("theme", _search, _price, "");
_packages = await PackageService.GetPackagesAsync("theme", _search, _price, "", _sort);
if (_packages != null)
{
@ -154,21 +206,22 @@
}
}
}
HideProgressIndicator();
}
private async void PriceChanged(ChangeEventArgs e)
private string GetLogo(int fileid)
{
try
{
_price = (string)e.Value;
_search = "";
await LoadThemes();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error On PriceChanged");
}
var url = ImageUrl(fileid, 100, 100);
url = (!string.IsNullOrEmpty(PageState.Alias.Path)) ? url.Substring(PageState.Alias.Path.Length + 1) : url;
return Constants.PackageRegistryUrl + url;
}
private async void PriceChanged(string price)
{
_price = price;
await LoadThemes();
StateHasChanged();
}
private async Task Search()
@ -196,6 +249,12 @@
}
}
private async void SortChanged(ChangeEventArgs e)
{
_sort = (string)e.Value;
await LoadThemes();
}
private void HideModal()
{
_productname = "";

View File

@ -27,7 +27,7 @@
</div>
</div>
</form>
<Section Name="Information" ResourceKey="Information">
<Section Name="Information" ResourceKey="Information" Heading="Information">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="themename" HelpText="The internal name of the module" ResourceKey="InternalName">Internal Name: </Label>

View File

@ -13,7 +13,7 @@
}
else
{
<ActionLink Action="Add" Text="Install Theme" />
<ActionLink Action="Add" Text="Install Theme" ResourceKey="InstallTheme" />
@((MarkupString)"&nbsp;")
<ActionLink Action="Create" Text="Create Theme" ResourceKey="CreateTheme" Class="btn btn-secondary" />
@ -49,7 +49,7 @@ else
}
</td>
<td>
@((MarkupString)SupportLink(context.PackageName))
@((MarkupString)SupportLink(context.PackageName, context.Version))
</td>
<td>
@((MarkupString)PurchaseLink(context.PackageName))
@ -112,7 +112,7 @@ else
return link;
}
private string SupportLink(string packagename)
private string SupportLink(string packagename, string version)
{
string link = "";
if (!string.IsNullOrEmpty(packagename) && _packages != null)
@ -120,7 +120,7 @@ else
var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault();
if (package != null && !string.IsNullOrEmpty(package.SupportUrl))
{
link += "<a class=\"btn btn-success\" style=\"text-decoration: none !important\" href=\"" + package.SupportUrl + "\" target=\"_new\">" + SharedLocalizer["Help"] + "</a>";
link += "<a class=\"btn btn-info\" style=\"text-decoration: none !important\" href=\"" + package.SupportUrl.Replace("{Version}", version) + "\" target=\"_new\">" + SharedLocalizer["Help"] + "</a>";
}
}
return link;

View File

@ -17,11 +17,11 @@
}
else
{
<ModuleMessage Type="MessageType.Info" Message="Framework Is Already Up To Date"></ModuleMessage>
<ModuleMessage Type="MessageType.Info" Message=@Localizer["Message.Text"]></ModuleMessage>
}
</TabPanel>
<TabPanel Name="Upload" ResourceKey="Upload">
<ModuleMessage Type="MessageType.Info" Message="Upload A Framework Package (Oqtane.Framework.version.nupkg) And Then Select Upgrade"></ModuleMessage>
<ModuleMessage Type="MessageType.Info" Message=@Localizer["MessgeUpgrade.Text"]></ModuleMessage>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" HelpText="Upload A Framework Package And Then Select Upgrade" ResourceKey="Framework">Framework: </Label>

View File

@ -42,6 +42,12 @@
public override string Title => "Send Notification";
protected override void OnParametersSet()
{
base.OnParametersSet();
base.SetModuleTitle(Localizer["ModuleTitle.Text"]);
}
private async Task Send()
{
try

View File

@ -110,6 +110,11 @@
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
public override string Title => "View Notification";
protected override void OnParametersSet()
{
base.OnParametersSet();
base.SetModuleTitle(Localizer["ModuleTitle.Text"]);
}
protected override async Task OnInitializedAsync()
{
try

View File

@ -140,34 +140,26 @@
{
if (_password == confirm)
{
var user = await UserService.GetUserAsync(username, PageState.Site.SiteId);
if (user == null)
var user = new User();
user.SiteId = PageState.Site.SiteId;
user.Username = username;
user.Password = _password;
user.Email = email;
user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname;
user.PhotoFileId = null;
user = await UserService.AddUserAsync(user);
if (user != null)
{
user = new User();
user.SiteId = PageState.Site.SiteId;
user.Username = username;
user.Password = _password;
user.Email = email;
user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname;
user.PhotoFileId = null;
user = await UserService.AddUserAsync(user);
if (user != null)
{
await SettingService.UpdateUserSettingsAsync(settings, user.UserId);
await logger.LogInformation("User Created {User}", user);
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
await logger.LogError("Error Adding User {Username} {Email}", username, email);
AddModuleMessage(Localizer["Error.User.AddCheckPass"], MessageType.Error);
}
await SettingService.UpdateUserSettingsAsync(settings, user.UserId);
await logger.LogInformation("User Created {User}", user);
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
AddModuleMessage(Localizer["Message.Username.Exists"], MessageType.Warning);
await logger.LogError("Error Adding User {Username} {Email}", username, email);
AddModuleMessage(Localizer["Error.User.AddCheckPass"], MessageType.Error);
}
}
else

View File

@ -2,7 +2,7 @@
@inherits LocalizableComponent
<div class="app-autocomplete">
<input class="form-control" value="@Value" @oninput="OnInput" @onkeyup="OnKeyUp" placeholder="@Placeholder" autocomplete="off" />
<input class="form-control" value="@Value" @oninput="OnInput" @onkeyup="OnKeyUp" placeholder="@Placeholder" autocomplete="off" @attributes="InputAttributes" />
@if (_results != null)
{
<select class="form-select" style="position: relative;" value="@Value" size="@Rows" @onkeyup="OnKeyUp" @onchange="(e => OnChange(e))">
@ -29,27 +29,48 @@
</div>
@code {
Dictionary<string, string> _results;
Dictionary<string, string> _results;
Dictionary<string, object> InputAttributes { get; set; } = new();
[Parameter]
public Func<string, Task<Dictionary<string, string>>> OnSearch { get; set; } // required - an async delegate method which accepts a filter string parameter and returns a dictionary
[Parameter]
public Func<string, Task<Dictionary<string, string>>> OnSearch { get; set; } // required - an async delegate method which accepts a filter string parameter and returns a dictionary
[Parameter]
public int Characters { get; set; } = 3; // optional - number of characters before search is initiated
[Parameter]
public int Characters { get; set; } = 3; // optional - number of characters before search is initiated
[Parameter]
public int Rows { get; set; } = 3; // optional - number of result rows to display
[Parameter]
public int Rows { get; set; } = 3; // optional - number of result rows to display
[Parameter]
public string Placeholder { get; set; } // optional - placeholder input text
[Parameter]
public string Placeholder { get; set; } // optional - placeholder input text
[Parameter]
public string Value { get; set; } // value of item selected
[Parameter]
public string Value { get; set; } // value of item selected
[Parameter]
public string Key { get; set; } // key of item selected
[Parameter]
public string Key { get; set; } // key of item selected
private async Task OnInput(ChangeEventArgs e)
[Parameter]
public bool Required { get; set; } // optional - if the item is required
protected override void OnParametersSet()
{
if (Required)
{
if (!InputAttributes.ContainsKey(nameof(Required)))
{
InputAttributes.Add(nameof(Required), true);
}
}
else
{
if (InputAttributes.ContainsKey(nameof(Required)))
{
InputAttributes.Remove(nameof(Required));
}
}
}
private async Task OnInput(ChangeEventArgs e)
{
Value = e.Value?.ToString();
if (Value?.Length >= Characters)

View File

@ -168,8 +168,6 @@
ShowSuccess = true;
}
_folders = await FolderService.GetFoldersAsync(ModuleState.SiteId);
if (!string.IsNullOrEmpty(Folder) && Folder != Constants.PackagesFolder)
{
Folder folder = await FolderService.GetFolderAsync(ModuleState.SiteId, Folder);
@ -185,6 +183,22 @@
}
}
if (ShowFolders)
{
_folders = await FolderService.GetFoldersAsync(ModuleState.SiteId);
}
else
{
if (FolderId != -1)
{
var folder = await FolderService.GetFolderAsync(FolderId);
if (folder != null)
{
_folders = new List<Folder> { folder };
}
}
}
if (FileId != -1)
{
File file = await FileService.GetFileAsync(FileId);
@ -358,10 +372,21 @@
attempts += 1;
Thread.Sleep(1000 * attempts); // progressive retry
var file = await FileService.GetFileAsync(int.Parse(folder), uploads[upload]);
if (file != null)
if (Folder == Constants.PackagesFolder)
{
success = true;
var files = await FileService.GetFilesAsync(folder);
if (files != null && files.Any(item => item.Name == uploads[upload]))
{
success = true;
}
}
else
{
var file = await FileService.GetFileAsync(int.Parse(folder), uploads[upload]);
if (file != null)
{
success = true;
}
}
}
if (success)

View File

@ -42,6 +42,8 @@
protected override void OnParametersSet()
{
base.OnParametersSet(); // must be included to call method in LocalizableComponent
_heading = !string.IsNullOrEmpty(Heading) ? Localize(nameof(Heading), Heading) : Localize(nameof(Name), Name);
_expanded = (!string.IsNullOrEmpty(Expanded)) ? Expanded.ToLower() : "false";
if (_expanded == "true") { _show = "show"; }

View File

@ -68,6 +68,12 @@
private List<Models.HtmlText> _htmltexts;
private string _view = "";
protected override void OnParametersSet()
{
base.OnParametersSet();
base.SetModuleTitle(Localizer["ModuleTitle.Text"]);
}
protected override async Task OnInitializedAsync()
{
try

View File

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

View File

@ -1,19 +1,23 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Runtime.Loader;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop;
using Oqtane.Documentation;
using Oqtane.Models;
using Oqtane.Modules;
using Oqtane.Services;
using Oqtane.UI;
@ -65,6 +69,14 @@ namespace Oqtane.Client
private static async Task LoadClientAssemblies(HttpClient http, IServiceProvider serviceProvider)
{
// get alias
var navigationManager = serviceProvider.GetRequiredService<NavigationManager>();
var urlpath = GetUrlPath(navigationManager.Uri);
var json = await http.GetStringAsync($"api/Installation/installed/?path={WebUtility.UrlEncode(urlpath)}");
var installation = JsonSerializer.Deserialize<Installation>(json, new JsonSerializerOptions(JsonSerializerDefaults.Web));
urlpath = installation.Alias.Path;
urlpath = (!string.IsNullOrEmpty(urlpath)) ? urlpath + "/" : urlpath;
var dlls = new Dictionary<string, byte[]>();
var pdbs = new Dictionary<string, byte[]>();
var list = new List<string>();
@ -76,7 +88,7 @@ namespace Oqtane.Client
if (files.Count() != 0)
{
// get list of assemblies from server
var json = await http.GetStringAsync("/api/Installation/list");
json = await http.GetStringAsync($"{urlpath}api/Installation/list");
var assemblies = JsonSerializer.Deserialize<List<string>>(json);
// determine which assemblies need to be downloaded
@ -138,7 +150,7 @@ namespace Oqtane.Client
if (list.Count != 0)
{
// get assemblies from server and load into client app domain
var zip = await http.GetByteArrayAsync($"/api/Installation/load?list=" + string.Join(",", list));
var zip = await http.GetByteArrayAsync($"{urlpath}api/Installation/load?list=" + string.Join(",", list));
// asemblies and debug symbols are packaged in a zip file
using (ZipArchive archive = new ZipArchive(new MemoryStream(zip)))
@ -254,5 +266,10 @@ namespace Oqtane.Client
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
}
private static string GetUrlPath(string url)
{
return new Uri(url).AbsolutePath.Substring(1);
}
}
}

View File

@ -162,4 +162,7 @@
<data name="Name.Text" xml:space="preserve">
<value>Name:</value>
</data>
<data name="DownloadFiles.Heading" xml:space="preserve">
<value>Download Files</value>
</data>
</root>

View File

@ -150,4 +150,7 @@
<data name="Description.Text" xml:space="preserve">
<value>Description:</value>
</data>
<data name="ModuleTitle.Text" xml:space="preserve">
<value>File Management</value>
</data>
</root>

View File

@ -183,4 +183,16 @@
<data name="ImageSizes.Text" xml:space="preserve">
<value>Image Sizes:</value>
</data>
<data name="FolderManagement.Title" xml:space="preserve">
<value>Folder Management!</value>
</data>
<data name="Private" xml:space="preserve">
<value>Private</value>
</data>
<data name="Public" xml:space="preserve">
<value>Public</value>
</data>
<data name="ModuleTitle.Text" xml:space="preserve">
<value>Folder Management</value>
</data>
</root>

View File

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

View File

@ -136,9 +136,12 @@
<value>No Modules Match The Criteria Provided Or Package Service Is Disabled</value>
</data>
<data name="Download.Heading" xml:space="preserve">
<value>Download</value>
<value>Marketplace</value>
</data>
<data name="Upload.Heading" xml:space="preserve">
<value>Upload</value>
</data>
<data name="Product.Text" xml:space="preserve">
<value>Product</value>
</data>
</root>

View File

@ -144,6 +144,9 @@
<data name="DeleteModule.Header" xml:space="preserve">
<value>Delete Module</value>
</data>
<data name="DeleteModule.Text" xml:space="preserve">
<value>Delete</value>
</data>
<data name="InUse" xml:space="preserve">
<value>In Use?</value>
</data>

View File

@ -132,4 +132,7 @@
<data name="Success.Content.Export" xml:space="preserve">
<value>Content Exported Successfully</value>
</data>
<data name="ModuleTitle.Text" xml:space="preserve">
<value>Export Content</value>
</data>
</root>

View File

@ -138,4 +138,7 @@
<data name="Message.Required.ImportContent" xml:space="preserve">
<value>You Must Enter Some Content To Import</value>
</data>
<data name="ModuleTitle.Text" xml:space="preserve">
<value>Import Content</value>
</data>
</root>

View File

@ -156,4 +156,7 @@
<data name="Module.Text" xml:space="preserve">
<value>Module:</value>
</data>
<data name="ModuleTitle.Text" xml:space="preserve">
<value>Module Settings</value>
</data>
</root>

View File

@ -249,4 +249,10 @@
<data name="ThemeChanged.Message" xml:space="preserve">
<value>Please Note That Overriding The Default Site Theme With An Unrelated Page Theme May Result In Compatibility Issues For Your Site</value>
</data>
<data name="Permissions.Heading" xml:space="preserve">
<value>Permissions</value>
</data>
<data name="Theme.Heading" xml:space="preserve">
<value>Theme Settings</value>
</data>
</root>

View File

@ -120,9 +120,15 @@
<data name="DeleteModule.Header" xml:space="preserve">
<value>Delete Module</value>
</data>
<data name="DeleteModule.Text" xml:space="preserve">
<value>Delete</value>
</data>
<data name="DeletePage.Header" xml:space="preserve">
<value>Delete Page</value>
</data>
<data name="DeletePage.Text" xml:space="preserve">
<value>Delete</value>
</data>
<data name="NoPage.Deleted" xml:space="preserve">
<value>No Deleted Pages</value>
</data>

View File

@ -135,4 +135,10 @@
<data name="Search.NoResults" xml:space="preserve">
<value>No Themes Match The Criteria Provided Or Package Service Is Disabled</value>
</data>
<data name="Download.Heading" xml:space="preserve">
<value>Marketplace</value>
</data>
<data name="Upload.Heading" xml:space="preserve">
<value>Upload</value>
</data>
</root>

View File

@ -141,6 +141,9 @@
<data name="CreateTheme.Text" xml:space="preserve">
<value>Create Theme</value>
</data>
<data name="InstallTheme.Text" xml:space="preserve">
<value>Install Theme</value>
</data>
<data name="ViewTheme.Text" xml:space="preserve">
<value>View</value>
</data>

View File

@ -141,4 +141,10 @@
<data name="Upload.Heading" xml:space="preserve">
<value>Upload</value>
</data>
<data name="Message.Text" xml:space="preserve">
<value>Framework Is Already Up To Date</value>
</data>
<data name="MessgeUpgrade.Text" xml:space="preserve">
<value>Upload A Framework Package (Oqtane.Framework.version.nupkg) And Then Select Upgrade</value>
</data>
</root>

View File

@ -141,4 +141,7 @@
<data name="Subject.Text" xml:space="preserve">
<value>Subject: </value>
</data>
<data name="ModuleTitle.Text" xml:space="preserve">
<value>Send Notification</value>
</data>
</root>

View File

@ -144,4 +144,7 @@
<data name="OriginalMessage" xml:space="preserve">
<value>Original Message</value>
</data>
<data name="ModuleTitle.Text" xml:space="preserve">
<value>View Notification</value>
</data>
</root>

View File

@ -156,6 +156,9 @@
<data name="Message.Content.Restored" xml:space="preserve">
<value>Version Restored</value>
</data>
<data name="ModuleTitle.Text" xml:space="preserve">
<value>Edit Html/Text</value>
</data>
<data name="Restore.Header" xml:space="preserve">
<value>Restore Version</value>
</data>

View File

@ -223,13 +223,13 @@
<value>by</value>
</data>
<data name="Search.Downloads" xml:space="preserve">
<value>downloads</value>
<value>Downloads</value>
</data>
<data name="Search.Released" xml:space="preserve">
<value>released</value>
<value>Released</value>
</data>
<data name="Search.Version" xml:space="preserve">
<value>version</value>
<value>Version</value>
</data>
<data name="Edit" xml:space="preserve">
<value>Edit</value>
@ -277,19 +277,19 @@
<value>Installed Version</value>
</data>
<data name="Search.Source" xml:space="preserve">
<value>source</value>
<value>Source</value>
</data>
<data name="Message.InfoRequired" xml:space="preserve">
<value>Please Provide All Required Information</value>
</data>
<data name="Free" xml:space="preserve">
<value>Free</value>
<value>Open Source</value>
</data>
<data name="Paid" xml:space="preserve">
<value>Paid</value>
<value>Commercial</value>
</data>
<data name="Search.Price" xml:space="preserve">
<value>price</value>
<value>Price</value>
</data>
<data name="Accept" xml:space="preserve">
<value>Accept</value>
@ -390,4 +390,19 @@
<data name="Support" xml:space="preserve">
<value>Support</value>
</data>
<data name="Search.Alphabetical" xml:space="preserve">
<value>Alphabetical</value>
</data>
<data name="Buy" xml:space="preserve">
<value>Buy Now</value>
</data>
<data name="Search.Popularity" xml:space="preserve">
<value>Popularity</value>
</data>
<data name="Search.Results" xml:space="preserve">
<value>Results</value>
</data>
<data name="Search.RecentlyReleased" xml:space="preserve">
<value>Recently Released</value>
</data>
</root>

View File

@ -22,7 +22,7 @@ namespace Oqtane.Services
/// <inheritdoc />
public async Task<List<Alias>> GetAliasesAsync()
{
List<Alias> aliases = await GetJsonAsync<List<Alias>>(ApiUrl);
List<Alias> aliases = await GetJsonAsync<List<Alias>>(ApiUrl, Enumerable.Empty<Alias>().ToList());
return aliases.OrderBy(item => item.Name).ToList();
}

View File

@ -6,6 +6,7 @@ using Oqtane.Shared;
using Microsoft.AspNetCore.Components;
using System;
using System.Net;
using System.Linq;
namespace Oqtane.Services
{
@ -14,11 +15,13 @@ namespace Oqtane.Services
{
private readonly NavigationManager _navigationManager;
private readonly SiteState _siteState;
private readonly HttpClient _http;
public InstallationService(HttpClient http, SiteState siteState, NavigationManager navigationManager) : base(http, siteState)
{
_navigationManager = navigationManager;
_siteState = siteState;
_http = http;
}
private string ApiUrl => (_siteState.Alias == null)
@ -27,7 +30,15 @@ namespace Oqtane.Services
public async Task<Installation> IsInstalled()
{
var path = new Uri(_navigationManager.Uri).LocalPath.Substring(1);
var path = "";
if (_http.DefaultRequestHeaders.UserAgent.ToString().Contains(Constants.MauiUserAgent))
{
path = _http.DefaultRequestHeaders.GetValues(Constants.MauiAliasPath).First();
}
else
{
path = new Uri(_navigationManager.Uri).LocalPath.Substring(1);
}
return await GetJsonAsync<Installation>($"{ApiUrl}/installed/?path={WebUtility.UrlEncode(path)}");
}

View File

@ -14,6 +14,6 @@ namespace Oqtane.Services
/// </summary>
/// <param name="lastSyncDate"></param>
/// <returns></returns>
Task<Sync> GetSyncAsync(DateTime lastSyncDate);
Task<Sync> GetSyncEventsAsync(DateTime lastSyncDate);
}
}

View File

@ -145,10 +145,19 @@ namespace Oqtane.Services
{
return await response.Content.ReadFromJsonAsync<T>();
}
return default;
}
protected async Task<T> GetJsonAsync<T>(string uri, T defaultResult)
{
var response = await GetHttpClient().GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None);
if (await CheckResponse(response, uri) && ValidateJsonContent(response.Content))
{
return await response.Content.ReadFromJsonAsync<T>();
}
return defaultResult;
}
protected async Task PutAsync(string uri)
{
var response = await GetHttpClient().PutAsync(uri, null);
@ -202,17 +211,27 @@ namespace Oqtane.Services
private async Task<bool> CheckResponse(HttpResponseMessage response, string uri)
{
if (response.IsSuccessStatusCode && uri.Contains("/api/") && !response.RequestMessage.RequestUri.AbsolutePath.Contains("/api/"))
if (response.IsSuccessStatusCode)
{
await Log(uri, response.RequestMessage.Method.ToString(), response.StatusCode.ToString(), "Request {Uri} Not Mapped To An API Controller Method", uri);
// if response from api call is not from an api url then the route was not mapped correctly
if (uri.Contains("/api/") && !response.RequestMessage.RequestUri.AbsolutePath.Contains("/api/"))
{
await Log(uri, response.RequestMessage.Method.ToString(), response.StatusCode.ToString(), "Request {Uri} Not Mapped To An API Controller Method", uri);
return false;
}
else
{
return true;
}
}
else
{
if (response.StatusCode != HttpStatusCode.NoContent && response.StatusCode != HttpStatusCode.NotFound)
{
await Log(uri, response.RequestMessage.Method.ToString(), response.StatusCode.ToString(), "Request {Uri} Failed With Status {StatusCode} - {ReasonPhrase}", uri, response.StatusCode, response.ReasonPhrase);
}
return false;
}
if (response.IsSuccessStatusCode) return true;
if (response.StatusCode != HttpStatusCode.NoContent && response.StatusCode != HttpStatusCode.NotFound)
{
await Log(uri, response.RequestMessage.Method.ToString(), response.StatusCode.ToString(), "Request {Uri} Failed With Status {StatusCode} - {ReasonPhrase}", uri, response.StatusCode, response.ReasonPhrase);
}
return false;
}
private static bool ValidateJsonContent(HttpContent content)

View File

@ -16,7 +16,7 @@ namespace Oqtane.Services
private string ApiUrl => CreateApiUrl("Sync");
/// <inheritdoc />
public async Task<Sync> GetSyncAsync(DateTime lastSyncDate)
public async Task<Sync> GetSyncEventsAsync(DateTime lastSyncDate)
{
return await GetJsonAsync<Sync>($"{ApiUrl}/{lastSyncDate.ToString("yyyyMMddHHmmssfff")}");
}

View File

@ -2,6 +2,7 @@
@namespace Oqtane.Themes.Controls
@inherits ContainerBase
@attribute [OqtaneIgnore]
@inject IStringLocalizer<SharedResources> SharedLocalizer
<span class="app-moduletitle">
@((MarkupString)title)
@ -23,7 +24,7 @@
}
else
{
title = ModuleState.Title;
title = SharedLocalizer[ModuleState.Title];
}
}

View File

@ -93,12 +93,12 @@
[SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")]
private async Task Refresh()
{
Site site;
Page page;
Site site = null;
Page page = null;
User user = null;
var editmode = false;
var refresh = false;
var lastsyncdate = DateTime.UtcNow.AddHours(-1);
var lastsyncdate = DateTime.MinValue;
var runtime = (Shared.Runtime)Enum.Parse(typeof(Shared.Runtime), Runtime);
_error = "";
@ -112,8 +112,8 @@
returnurl = WebUtility.UrlDecode(querystring["returnurl"]);
}
// reload the client application from the server if there is a forced reload or the user navigated to a site with a different alias
if (querystring.ContainsKey("reload") || (!NavigationManager.ToBaseRelativePath(_absoluteUri).ToLower().StartsWith(SiteState.Alias.Path.ToLower()) && !string.IsNullOrEmpty(SiteState.Alias.Path)))
// reload the client application from the server if there is a forced reload
if (querystring.ContainsKey("reload"))
{
if (querystring.ContainsKey("reload") && querystring["reload"] == "post")
{
@ -126,7 +126,7 @@
}
else
{
NavigationManager.NavigateTo(_absoluteUri.Replace("?reload", ""), true);
NavigationManager.NavigateTo(_absoluteUri.Replace("?reload", "").Replace("&reload", ""), true);
return;
}
}
@ -163,31 +163,39 @@
else
{
user = PageState.User;
}
}
// process any sync events
var sync = await SyncService.GetSyncAsync(lastsyncdate);
var sync = await SyncService.GetSyncEventsAsync(lastsyncdate);
lastsyncdate = sync.SyncDate;
if (sync.SyncEvents.Any())
{
// reload client application if server was restarted or site runtime/rendermode was modified
if (PageState != null && sync.SyncEvents.Exists(item => (item.Action == SyncEventActions.Reload)))
// reload client application if server was restarted
if (sync.SyncEvents.Exists(item => item.Action == SyncEventActions.Reload && item.EntityName == EntityNames.Host))
{
NavigationManager.NavigateTo(_absoluteUri, true);
return;
}
// when site information has changed the PageState needs to be refreshed
if (sync.SyncEvents.Exists(item => item.EntityName == EntityNames.Site && item.EntityId == SiteState.Alias.SiteId))
// reload client application if site runtime/rendermode was modified
if (sync.SyncEvents.Exists(item => item.Action == SyncEventActions.Reload && item.EntityName == EntityNames.Site && item.EntityId == SiteState.Alias.SiteId))
{
refresh = true;
NavigationManager.NavigateTo(_absoluteUri, true);
return;
}
// when user information has changed the PageState needs to be refreshed as the list of pages/modules may have changed
if (user != null && sync.SyncEvents.Exists(item => item.EntityName == EntityNames.User && item.EntityId == user.UserId))
// reload client application if current user auth information has changed
if (user != null && sync.SyncEvents.Exists(item => item.Action == SyncEventActions.Reload && item.EntityName == EntityNames.User && item.EntityId == user.UserId))
{
NavigationManager.NavigateTo(_absoluteUri, true);
return;
}
// refresh PageState when site information has changed
if (sync.SyncEvents.Exists(item => item.Action == SyncEventActions.Refresh && item.EntityName == EntityNames.Site && item.EntityId == SiteState.Alias.SiteId))
{
refresh = true;
}
}
// get site
if (PageState == null || refresh || PageState.Alias.SiteId != SiteState.Alias.SiteId)
{
site = await SiteService.GetSiteAsync(SiteState.Alias.SiteId);

View File

@ -70,7 +70,7 @@
var elements = (">" + content.Replace("\n", "") + "<").Split("><");
foreach (var element in elements)
{
if (!string.IsNullOrEmpty(element) && !element.Contains("script"))
if (!string.IsNullOrEmpty(element) && !element.ToLower().StartsWith("script"))
{
if (!headcontent.Contains("<" + element + ">"))
{

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Version>4.0.1</Version>
<Version>4.0.2</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.2</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Database.MySQL</id>
<version>4.0.1</version>
<version>4.0.2</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane MySQL Provider</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.2</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Version>4.0.1</Version>
<Version>4.0.2</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.2</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Database.PostgreSQL</id>
<version>4.0.1</version>
<version>4.0.2</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane PostgreSQL Provider</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.2</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Version>4.0.1</Version>
<Version>4.0.2</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.2</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Database.SqlServer</id>
<version>4.0.1</version>
<version>4.0.2</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane SQL Server Provider</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.2</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Version>4.0.1</Version>
<Version>4.0.2</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.2</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Database.Sqlite</id>
<version>4.0.1</version>
<version>4.0.2</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane SQLite Provider</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.2</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -1,18 +1,52 @@
<DynamicComponent Type="@ComponentType" Parameters="@Parameters"></DynamicComponent>
@using System.Text.Json;
@using System.Text.Json.Nodes;
@code {
Type ComponentType = Type.GetType("Oqtane.App, Oqtane.Client");
private IDictionary<string, object> Parameters { get; set; }
protected override void OnInitialized()
{
Parameters = new Dictionary<string, object>();
Parameters.Add(new KeyValuePair<string, object>("AntiForgeryToken", ""));
Parameters.Add(new KeyValuePair<string, object>("Runtime", "Hybrid"));
Parameters.Add(new KeyValuePair<string, object>("RenderMode", "Hybrid"));
Parameters.Add(new KeyValuePair<string, object>("VisitorId", -1));
Parameters.Add(new KeyValuePair<string, object>("RemoteIPAddress", ""));
Parameters.Add(new KeyValuePair<string, object>("AuthorizationToken", ""));
}
@if (string.IsNullOrEmpty(message))
{
<DynamicComponent Type="@ComponentType" Parameters="@Parameters"></DynamicComponent>
}
else
{
<br /><br /><center>@message</center>
}
@code {
Type ComponentType = Type.GetType("Oqtane.App, Oqtane.Client");
private IDictionary<string, object> Parameters { get; set; }
private string message = "";
protected override void OnInitialized()
{
Parameters = new Dictionary<string, object>();
Parameters.Add(new KeyValuePair<string, object>("AntiForgeryToken", ""));
Parameters.Add(new KeyValuePair<string, object>("Runtime", "Hybrid"));
Parameters.Add(new KeyValuePair<string, object>("RenderMode", "Hybrid"));
Parameters.Add(new KeyValuePair<string, object>("VisitorId", -1));
Parameters.Add(new KeyValuePair<string, object>("RemoteIPAddress", ""));
Parameters.Add(new KeyValuePair<string, object>("AuthorizationToken", ""));
if (MauiConstants.UseAppSettings)
{
string file = Path.Combine(FileSystem.Current.AppDataDirectory, "appsettings.json");
if (File.Exists(file))
{
using FileStream stream = File.OpenRead(file);
using StreamReader reader = new StreamReader(stream);
var content = reader.ReadToEnd();
var obj = JsonSerializer.Deserialize<JsonObject>(content)!;
if (string.IsNullOrEmpty((string)obj["Url"]) && string.IsNullOrEmpty(MauiConstants.ApiUrl))
{
message = "You Must Set The Url In Either MauiConstants.cs Or " + file;
}
}
}
else
{
if (string.IsNullOrEmpty(MauiConstants.ApiUrl))
{
message = "You Must Set The Url In MauiConstants.cs";
}
}
}
}

View File

@ -0,0 +1,13 @@
namespace Oqtane.Maui;
public static class MauiConstants
{
// the API service url (used as fallback if not set in appsettings.json)
public static string ApiUrl = "";
//public static string ApiUrl = "http://localhost:44357/"; // for local development (Oqtane.Server must be already running for MAUI client to connect)
//public static string apiurl = "http://localhost:44357/sitename/"; // local microsite example
//public static string apiurl = "https://www.dnfprojects.com/"; // for testing remote site
// specify if you wish to allow users to override the url via appsettings.json in the AppDataDirectory
public static bool UseAppSettings = true;
}

View File

@ -6,20 +6,16 @@ using Oqtane.Modules;
using Oqtane.Services;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace Oqtane.Maui;
public static class MauiProgram
{
// the API service url
//static string apiurl = "https://www.dnfprojects.com"; // for testing
static string apiurl = "http://localhost:44357"; // for local development (Oqtane.Server must be already running for MAUI client to connect)
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
builder.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
@ -28,15 +24,21 @@ public static class MauiProgram
builder.Services.AddMauiBlazorWebView();
#if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools();
#endif
#endif
var httpClient = new HttpClient { BaseAddress = new Uri(apiurl) };
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(Shared.Constants.MauiUserAgent);
builder.Services.AddSingleton(httpClient);
builder.Services.AddHttpClient(); // IHttpClientFactory for calling remote services via RemoteServiceBase
var apiurl = LoadAppSettings();
// dynamically load client assemblies
LoadClientAssemblies(httpClient);
if (!string.IsNullOrEmpty(apiurl))
{
var httpClient = new HttpClient { BaseAddress = new Uri(GetBaseUrl(apiurl)) };
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(Shared.Constants.MauiUserAgent);
httpClient.DefaultRequestHeaders.Add(Shared.Constants.MauiAliasPath, GetUrlPath(apiurl).Replace("/", ""));
builder.Services.AddSingleton(httpClient);
builder.Services.AddHttpClient(); // IHttpClientFactory for calling remote services via RemoteServiceBase
// dynamically load client assemblies
LoadClientAssemblies(httpClient, apiurl);
}
// register localization services
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
@ -60,7 +62,37 @@ public static class MauiProgram
return builder.Build();
}
private static void LoadClientAssemblies(HttpClient http)
private static string LoadAppSettings()
{
var url = MauiConstants.ApiUrl;
if (MauiConstants.UseAppSettings)
{
string file = Path.Combine(FileSystem.Current.AppDataDirectory, "appsettings.json");
if (File.Exists(file))
{
using FileStream stream = File.OpenRead(file);
using StreamReader reader = new StreamReader(stream);
var content = reader.ReadToEnd();
var obj = JsonSerializer.Deserialize<JsonObject>(content)!;
if (!string.IsNullOrEmpty((string)obj["Url"]))
{
url = (string)obj["Url"];
}
}
else
{
// create template appsettings.json file
using (StreamWriter writer = File.CreateText(file))
{
writer.WriteLine("{ \"Url\": \"\" }");
}
}
}
return url;
}
private static void LoadClientAssemblies(HttpClient http, string apiurl)
{
try
{
@ -84,7 +116,7 @@ public static class MauiProgram
if (files.Count() != 0)
{
// get list of assemblies from server
var json = Task.Run(() => http.GetStringAsync("/api/Installation/list")).GetAwaiter().GetResult();
var json = Task.Run(() => http.GetStringAsync($"{GetUrlPath(apiurl)}api/Installation/list")).GetAwaiter().GetResult();
var assemblies = JsonSerializer.Deserialize<List<string>>(json);
// determine which assemblies need to be downloaded
@ -148,7 +180,7 @@ public static class MauiProgram
if (list.Count != 0)
{
// get assemblies from server
var zip = Task.Run(() => http.GetByteArrayAsync("/api/Installation/load?list=" + string.Join(",", list))).GetAwaiter().GetResult();
var zip = Task.Run(() => http.GetByteArrayAsync($"{GetUrlPath(apiurl)}api/Installation/load?list=" + string.Join(",", list))).GetAwaiter().GetResult();
// asemblies and debug symbols are packaged in a zip file
using (ZipArchive archive = new ZipArchive(new MemoryStream(zip)))
@ -199,7 +231,7 @@ public static class MauiProgram
}
catch (Exception ex)
{
Debug.WriteLine($"Oqtane Error: Loading Client Assemblies {ex}");
Debug.WriteLine($"Error Loading Client Assemblies From {apiurl} - {ex}");
}
}
@ -245,4 +277,17 @@ public static class MauiProgram
// could not interrogate assembly - likely missing dependencies
}
}
private static string GetBaseUrl(string url)
{
var uri = new Uri(url);
return uri.Scheme + "://"+ uri.Authority + "/";
}
private static string GetUrlPath(string url)
{
var path = new Uri(url).AbsolutePath.Substring(1);
path = (!string.IsNullOrEmpty(path) && !path.EndsWith("/")) ? path + "/" : path;
return path;
}
}

View File

@ -6,7 +6,7 @@
<!-- <TargetFrameworks>net7.0-android;net7.0-ios;net7.0-maccatalyst</TargetFrameworks> -->
<!-- <TargetFrameworks>$(TargetFrameworks);net7.0-tizen</TargetFrameworks> -->
<OutputType>Exe</OutputType>
<Version>4.0.1</Version>
<Version>4.0.2</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -14,7 +14,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.2</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane.Maui</RootNamespace>
@ -31,7 +31,7 @@
<ApplicationIdGuid>0E29FC31-1B83-48ED-B6E0-9F3C67B775D4</ApplicationIdGuid>
<!-- Versions -->
<ApplicationDisplayVersion>4.0.1</ApplicationDisplayVersion>
<ApplicationDisplayVersion>4.0.2</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">14.2</SupportedOSPlatformVersion>
@ -44,7 +44,7 @@
<ItemGroup>
<!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
<MauiIcon Include="Resources\AppIcon\appicon.svg" />
<!-- Splash Screen -->
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />

View File

@ -1,4 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="456" height="456" fill="#512BD4" />
</svg>
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="200.000000pt" height="200.000000pt" viewBox="0 0 200.000000 200.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,200.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M994 1922 c-6 -4 -25 -50 -44 -102 -80 -218 -210 -455 -391 -712 -49
-70 -100 -151 -114 -180 -105 -226 -53 -507 126 -686 190 -188 468 -234 709
-116 104 51 223 170 274 274 84 172 86 367 7 532 -16 35 -72 122 -123 193 -52
72 -111 161 -133 198 -21 37 -42 67 -46 67 -3 0 -46 -61 -94 -136 l-88 -136
106 -156 c57 -87 110 -172 116 -191 16 -50 13 -150 -6 -207 -23 -69 -110 -155
-182 -179 -68 -22 -144 -22 -212 0 -74 25 -159 110 -183 183 -19 57 -22 148
-7 200 6 19 116 193 246 387 129 195 235 358 235 363 0 5 -22 56 -49 113 -27
57 -64 145 -81 196 -31 88 -46 109 -66 95z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 228 B

After

Width:  |  Height:  |  Size: 1008 B

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Client</id>
<version>4.0.1</version>
<version>4.0.2</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.2</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Framework</id>
<version>4.0.1</version>
<version>4.0.2</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -11,8 +11,8 @@
<copyright>.NET Foundation</copyright>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v4.0.1/Oqtane.Framework.4.0.1.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</releaseNotes>
<projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v4.0.2/Oqtane.Framework.4.0.2.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.2</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane framework</tags>
</metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Server</id>
<version>4.0.1</version>
<version>4.0.2</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.2</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Shared</id>
<version>4.0.1</version>
<version>4.0.2</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.2</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Updater</id>
<version>4.0.1</version>
<version>4.0.2</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.2</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net7.0\publish\*" -DestinationPath "Oqtane.Framework.4.0.1.Install.zip" -Force
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net7.0\publish\*" -DestinationPath "Oqtane.Framework.4.0.2.Install.zip" -Force

View File

@ -14,7 +14,7 @@ dotnet publish ..\Oqtane.Server\Oqtane.Server.csproj /p:Configuration=Release
del /F/Q/S "..\Oqtane.Server\bin\Release\net7.0\publish\wwwroot\Content" > NUL
rmdir /Q/S "..\Oqtane.Server\bin\Release\net7.0\publish\wwwroot\Content"
setlocal ENABLEDELAYEDEXPANSION
set retain=Oqtane.Modules.Admin.Login,Oqtane.Modules.HtmlText,Templates
set retain=Oqtane.Modules.Admin.Login,Oqtane.Modules.HtmlText
for /D %%i in ("..\Oqtane.Server\bin\Release\net7.0\publish\wwwroot\Modules\*") do (
set /A found=0
for %%j in (%retain%) do (
@ -22,7 +22,7 @@ if "%%~nxi" == "%%j" set /A found=1
)
if not !found! == 1 rmdir /Q/S "%%i"
)
set retain=Oqtane.Themes.BlazorTheme,Oqtane.Themes.OqtaneTheme,Templates
set retain=Oqtane.Themes.BlazorTheme,Oqtane.Themes.OqtaneTheme
for /D %%i in ("..\Oqtane.Server\bin\Release\net7.0\publish\wwwroot\Themes\*") do (
set /A found=0
for %%j in (%retain%) do (

View File

@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net7.0\publish\*" -DestinationPath "Oqtane.Framework.4.0.1.Upgrade.zip" -Force
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net7.0\publish\*" -DestinationPath "Oqtane.Framework.4.0.2.Upgrade.zip" -Force

View File

@ -572,7 +572,7 @@ namespace Oqtane.Controllers
// 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 = "000000";
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";
@ -644,10 +644,23 @@ namespace Oqtane.Controllers
Mode = resizemode,
Position = anchorpositionmode,
Size = new Size(width, height)
})
.BackgroundColor(Color.ParseHex("#" + background)));
}));
image.Save(imagepath, new PngEncoder());
if (background != "transparent")
{
image.Mutate(x => x
.BackgroundColor(Color.ParseHex("#" + background)));
}
PngEncoder encoder = new PngEncoder
{
ColorType = PngColorType.RgbWithAlpha,
TransparentColorMode = PngTransparentColorMode.Preserve,
BitDepth = PngBitDepth.Bit8,
CompressionLevel = PngCompressionLevel.BestSpeed
};
image.Save(imagepath, encoder);
}
}
}
@ -677,7 +690,14 @@ namespace Oqtane.Controllers
path = Utilities.PathCombine(path, folder, Path.DirectorySeparatorChar.ToString());
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
try
{
Directory.CreateDirectory(path);
}
catch (Exception ex)
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, ex, "Unable To Create Folder {Folder}", path);
}
}
}
}

View File

@ -43,7 +43,8 @@ namespace Oqtane.Controllers
{
foreach (Folder folder in _folders.GetFolders(SiteId))
{
if (_userPermissions.IsAuthorized(User, PermissionNames.View, folder.PermissionList))
// note that Browse permission is used for this method
if (_userPermissions.IsAuthorized(User, PermissionNames.Browse, folder.PermissionList))
{
folders.Add(folder);
}
@ -87,6 +88,7 @@ namespace Oqtane.Controllers
public Folder GetByPath(int siteId, string path)
{
var folderPath = WebUtility.UrlDecode(path).Replace("\\", "/");
folderPath = (folderPath == "/") ? "" : folderPath;
if (!folderPath.EndsWith("/") && folderPath != "")
{
folderPath += "/";

View File

@ -34,9 +34,9 @@ namespace Oqtane.Controllers
private readonly IAliasRepository _aliases;
private readonly ILogger<InstallationController> _filelogger;
private readonly ITenantManager _tenantManager;
private readonly ServerStateManager _serverState;
private readonly IServerStateManager _serverState;
public InstallationController(IConfigManager configManager, IInstallationManager installationManager, IDatabaseManager databaseManager, ILocalizationManager localizationManager, IMemoryCache cache, IHttpContextAccessor accessor, IAliasRepository aliases, ILogger<InstallationController> filelogger, ITenantManager tenantManager, ServerStateManager serverState)
public InstallationController(IConfigManager configManager, IInstallationManager installationManager, IDatabaseManager databaseManager, ILocalizationManager localizationManager, IMemoryCache cache, IHttpContextAccessor accessor, IAliasRepository aliases, ILogger<InstallationController> filelogger, ITenantManager tenantManager, IServerStateManager serverState)
{
_configManager = configManager;
_installationManager = installationManager;
@ -119,9 +119,9 @@ namespace Oqtane.Controllers
private List<ClientAssembly> GetAssemblyList()
{
int siteId = _tenantManager.GetAlias().SiteId;
var siteKey = _tenantManager.GetAlias().SiteKey;
return _cache.GetOrCreate($"assemblieslist:{siteId}", entry =>
return _cache.GetOrCreate($"assemblieslist:{siteKey}", entry =>
{
var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
var assemblyList = new List<ClientAssembly>();
@ -134,7 +134,7 @@ namespace Oqtane.Controllers
}
// get site assemblies which should be downloaded to client
var assemblies = _serverState.GetServerState(siteId).Assemblies;
var assemblies = _serverState.GetServerState(siteKey).Assemblies;
// populate assembly list
foreach (var assembly in assemblies)
@ -179,9 +179,11 @@ namespace Oqtane.Controllers
private byte[] GetAssemblies(string list)
{
var siteKey = _tenantManager.GetAlias().SiteKey;
if (list == "*")
{
return _cache.GetOrCreate("assemblies", entry =>
return _cache.GetOrCreate($"assemblies:{siteKey}", entry =>
{
return GetZIP(list);
});

View File

@ -283,23 +283,26 @@ namespace Oqtane.Controllers
var templates = new List<Template>();
var root = Directory.GetParent(_environment.ContentRootPath);
string templatePath = Utilities.PathCombine(_environment.WebRootPath, "Modules", "Templates", Path.DirectorySeparatorChar.ToString());
foreach (string directory in Directory.GetDirectories(templatePath))
if (Directory.Exists(templatePath))
{
string name = directory.Replace(templatePath, "");
if (System.IO.File.Exists(Path.Combine(directory, "template.json")))
foreach (string directory in Directory.GetDirectories(templatePath))
{
var template = JsonSerializer.Deserialize<Template>(System.IO.File.ReadAllText(Path.Combine(directory, "template.json")));
template.Name = name;
template.Location = "";
if (template.Type.ToLower() != "internal")
string name = directory.Replace(templatePath, "");
if (System.IO.File.Exists(Path.Combine(directory, "template.json")))
{
template.Location = Utilities.PathCombine(root.Parent.ToString(), Path.DirectorySeparatorChar.ToString());
var template = JsonSerializer.Deserialize<Template>(System.IO.File.ReadAllText(Path.Combine(directory, "template.json")));
template.Name = name;
template.Location = "";
if (template.Type.ToLower() != "internal")
{
template.Location = Utilities.PathCombine(root.Parent.ToString(), Path.DirectorySeparatorChar.ToString());
}
templates.Add(template);
}
else
{
templates.Add(new Template { Name = name, Title = name, Type = "External", Version = "", Location = Utilities.PathCombine(root.Parent.ToString(), Path.DirectorySeparatorChar.ToString()) });
}
templates.Add(template);
}
else
{
templates.Add(new Template { Name = name, Title = name, Type = "External", Version = "", Location = Utilities.PathCombine(root.Parent.ToString(), Path.DirectorySeparatorChar.ToString()) });
}
}
return templates;

View File

@ -23,10 +23,16 @@ namespace Oqtane.Controllers
[HttpGet("{lastSyncDate}")]
public Sync Get(string lastSyncDate)
{
DateTime currentdate = DateTime.UtcNow;
DateTime lastdate = DateTime.ParseExact(lastSyncDate, "yyyyMMddHHmmssfff", CultureInfo.InvariantCulture);
if (lastdate == DateTime.MinValue)
{
lastdate = currentdate;
}
Sync sync = new Sync
{
SyncDate = DateTime.UtcNow,
SyncEvents = _syncManager.GetSyncEvents(_alias.TenantId, DateTime.ParseExact(lastSyncDate, "yyyyMMddHHmmssfff", CultureInfo.InvariantCulture))
SyncDate = currentdate,
SyncEvents = _syncManager.GetSyncEvents(_alias.TenantId, lastdate)
};
return sync;
}

View File

@ -137,23 +137,26 @@ namespace Oqtane.Controllers
var templates = new List<Template>();
var root = Directory.GetParent(_environment.ContentRootPath);
string templatePath = Utilities.PathCombine(_environment.WebRootPath, "Themes", "Templates", Path.DirectorySeparatorChar.ToString());
foreach (string directory in Directory.GetDirectories(templatePath))
if (Directory.Exists(templatePath))
{
string name = directory.Replace(templatePath, "");
if (System.IO.File.Exists(Path.Combine(directory, "template.json")))
foreach (string directory in Directory.GetDirectories(templatePath))
{
var template = JsonSerializer.Deserialize<Template>(System.IO.File.ReadAllText(Path.Combine(directory, "template.json")));
template.Name = name;
template.Location = "";
if (template.Type.ToLower() != "internal")
string name = directory.Replace(templatePath, "");
if (System.IO.File.Exists(Path.Combine(directory, "template.json")))
{
template.Location = Utilities.PathCombine(root.Parent.ToString(), Path.DirectorySeparatorChar.ToString());
var template = JsonSerializer.Deserialize<Template>(System.IO.File.ReadAllText(Path.Combine(directory, "template.json")));
template.Name = name;
template.Location = "";
if (template.Type.ToLower() != "internal")
{
template.Location = Utilities.PathCombine(root.Parent.ToString(), Path.DirectorySeparatorChar.ToString());
}
templates.Add(template);
}
else
{
templates.Add(new Template { Name = name, Title = name, Type = "External", Version = "", Location = Utilities.PathCombine(root.Parent.ToString(), Path.DirectorySeparatorChar.ToString()) });
}
templates.Add(template);
}
else
{
templates.Add(new Template { Name = name, Title = name, Type = "External", Version = "", Location = Utilities.PathCombine(root.Parent.ToString(), Path.DirectorySeparatorChar.ToString()) });
}
}
return templates;

View File

@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Oqtane.Models;
using Microsoft.AspNetCore.Identity;
using System.Threading.Tasks;
using System.Linq;
using System.Security.Claims;
@ -22,23 +21,17 @@ namespace Oqtane.Controllers
public class UserController : Controller
{
private readonly IUserRepository _users;
private readonly UserManager<IdentityUser> _identityUserManager;
private readonly SignInManager<IdentityUser> _identitySignInManager;
private readonly ITenantManager _tenantManager;
private readonly INotificationRepository _notifications;
private readonly IUserManager _userManager;
private readonly ISiteRepository _sites;
private readonly IUserPermissions _userPermissions;
private readonly IJwtManager _jwtManager;
private readonly ILogManager _logger;
public UserController(IUserRepository users, UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager, ITenantManager tenantManager, INotificationRepository notifications, IUserManager userManager, ISiteRepository sites, IUserPermissions userPermissions, IJwtManager jwtManager, ILogManager logger)
public UserController(IUserRepository users, ITenantManager tenantManager, IUserManager userManager, ISiteRepository sites, IUserPermissions userPermissions, IJwtManager jwtManager, ILogManager logger)
{
_users = users;
_identityUserManager = identityUserManager;
_identitySignInManager = identitySignInManager;
_tenantManager = tenantManager;
_notifications = notifications;
_userManager = userManager;
_sites = sites;
_userPermissions = userPermissions;

View File

@ -131,9 +131,8 @@ namespace Oqtane.Controllers
{
userRole = _userRoles.AddUserRole(userRole);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.UserRole, userRole.UserRoleId, SyncEventActions.Create);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.User, userRole.UserId, SyncEventActions.Reload);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "User Role Added {UserRole}", userRole);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.User, userRole.UserId, SyncEventActions.Refresh);
}
else
{
@ -154,7 +153,7 @@ namespace Oqtane.Controllers
{
userRole = _userRoles.UpdateUserRole(userRole);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.UserRole, userRole.UserRoleId, SyncEventActions.Update);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.User, userRole.UserId, SyncEventActions.Refresh);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.User, userRole.UserId, SyncEventActions.Reload);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "User Role Updated {UserRole}", userRole);
}
else
@ -171,25 +170,24 @@ namespace Oqtane.Controllers
[Authorize(Policy = $"{EntityNames.UserRole}:{PermissionNames.Write}:{RoleNames.Admin}")]
public void Delete(int id)
{
UserRole userrole = _userRoles.GetUserRole(id);
if (userrole != null && SiteValid(userrole.Role.SiteId) && RoleValid(userrole.Role.Name))
UserRole userRole = _userRoles.GetUserRole(id);
if (userRole != null && SiteValid(userRole.Role.SiteId) && RoleValid(userRole.Role.Name))
{
_userRoles.DeleteUserRole(id);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.UserRole, userrole.UserRoleId, SyncEventActions.Delete);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "User Role Deleted {UserRole}", userrole);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.UserRole, userRole.UserRoleId, SyncEventActions.Delete);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.User, userRole.UserId, SyncEventActions.Reload);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "User Role Deleted {UserRole}", userRole);
if (userrole.Role.Name == RoleNames.Host)
if (userRole.Role.Name == RoleNames.Host)
{
// add site specific user roles to preserve user access
var role = _roles.GetRoles(_alias.SiteId).FirstOrDefault(item => item.Name == RoleNames.Registered);
userrole = _userRoles.AddUserRole(new UserRole { UserId = userrole.UserId, RoleId = role.RoleId, EffectiveDate = null, ExpiryDate = null });
_logger.Log(LogLevel.Information, this, LogFunction.Create, "User Role Added {UserRole}", userrole);
userRole = _userRoles.AddUserRole(new UserRole { UserId = userRole.UserId, RoleId = role.RoleId, EffectiveDate = null, ExpiryDate = null });
_logger.Log(LogLevel.Information, this, LogFunction.Create, "User Role Added {UserRole}", userRole);
role = _roles.GetRoles(_alias.SiteId).FirstOrDefault(item => item.Name == RoleNames.Admin);
userrole = _userRoles.AddUserRole(new UserRole { UserId = userrole.UserId, RoleId = role.RoleId, EffectiveDate = null, ExpiryDate = null });
_logger.Log(LogLevel.Information, this, LogFunction.Create, "User Role Added {UserRole}", userrole);
userRole = _userRoles.AddUserRole(new UserRole { UserId = userRole.UserId, RoleId = role.RoleId, EffectiveDate = null, ExpiryDate = null });
_logger.Log(LogLevel.Information, this, LogFunction.Create, "User Role Added {UserRole}", userRole);
}
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.User, userrole.UserId, SyncEventActions.Refresh);
}
else
{

View File

@ -62,7 +62,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddSingleton<ILoggerProvider, FileLoggerProvider>();
services.AddSingleton<AutoValidateAntiforgeryTokenFilter>();
services.AddSingleton<IAuthorizationPolicyProvider, AuthorizationPolicyProvider>();
services.AddSingleton<ServerStateManager>();
services.AddSingleton<IServerStateManager, ServerStateManager>();
return services;
}

View File

@ -0,0 +1,7 @@
namespace Oqtane.Infrastructure
{
public interface IServerStateManager
{
ServerState GetServerState(string siteKey);
}
}

View File

@ -40,9 +40,13 @@ namespace Oqtane.Infrastructure
public string[] GetInstalledCultures()
{
var cultures = new List<string>();
foreach (var file in Directory.EnumerateFiles(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), $"{Constants.ClientId}{Constants.SatelliteAssemblyExtension}", SearchOption.AllDirectories))
foreach (var file in Directory.EnumerateFiles(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), $"*{Constants.SatelliteAssemblyExtension}", SearchOption.AllDirectories))
{
cultures.Add(Path.GetFileName(Path.GetDirectoryName(file)));
var culture = Path.GetFileName(Path.GetDirectoryName(file));
if (!cultures.Contains(culture))
{
cultures.Add(culture);
}
}
return cultures.OrderBy(c => c).ToArray();
}

View File

@ -37,20 +37,46 @@ namespace Oqtane.Infrastructure
var identity = jwtManager.ValidateToken(token, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""));
if (identity != null && identity.Claims.Any())
{
// create user identity using jwt claims (note the difference in claimtype names)
var user = new User
var idclaim = "nameid";
var nameclaim = "unique_name";
var legacynameclaim = "name"; // this was a breaking change in System.IdentityModel.Tokens.Jwt in .NET 7
// get jwt claims for userid and username
var userid = identity.Claims.FirstOrDefault(item => item.Type == idclaim)?.Value;
if (userid != null)
{
UserId = int.Parse(identity.Claims.FirstOrDefault(item => item.Type == "nameid")?.Value),
Username = identity.Claims.FirstOrDefault(item => item.Type == "name")?.Value
};
// jwt already contains the roles - we are reloading to ensure most accurate permissions
var _userRoles = context.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository;
if (!int.TryParse(userid, out _))
{
userid = null;
}
}
var username = identity.Claims.FirstOrDefault(item => item.Type == nameclaim)?.Value;
if (username == null)
{
// fallback for legacy clients
username = identity.Claims.FirstOrDefault(item => item.Type == legacynameclaim)?.Value;
}
// set claims identity
var claimsidentity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, alias.SiteId).ToList());
context.User = new ClaimsPrincipal(claimsidentity);
if (userid != null && username != null)
{
// create user identity
var user = new User
{
UserId = int.Parse(userid),
Username = username
};
logger.Log(alias.SiteId, LogLevel.Information, "TokenValidation", Enums.LogFunction.Security, "Token Validated For User {Username}", user.Username);
// set claims identity (note jwt already contains the roles - we are reloading to ensure most accurate permissions)
var _userRoles = context.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository;
var claimsidentity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, alias.SiteId).ToList());
context.User = new ClaimsPrincipal(claimsidentity);
logger.Log(alias.SiteId, LogLevel.Information, "TokenValidation", Enums.LogFunction.Security, "Token Validated For UserId {UserId} And Username {Username}", user.UserId, user.Username);
}
else
{
logger.Log(alias.SiteId, LogLevel.Error, "TokenValidation", Enums.LogFunction.Security, "Token Validated But Could Not Locate UserId Or Username In Claims {Claims}", identity.Claims.ToString());
}
}
else
{

View File

@ -22,6 +22,7 @@ namespace Oqtane.Infrastructure
var config = context.RequestServices.GetService(typeof(IConfigManager)) as IConfigManager;
string path = context.Request.Path.ToString();
if (config.IsInstalled() && !path.StartsWith("/_blazor"))
{
// get alias (note that this also sets SiteState.Alias)
@ -43,6 +44,14 @@ namespace Oqtane.Infrastructure
});
context.Items.Add(Constants.HttpContextSiteSettingsKey, sitesettings);
// handle first request to site
var serverState = context.RequestServices.GetService(typeof(IServerStateManager)) as IServerStateManager;
if (!serverState.GetServerState(alias.SiteKey).IsInitialized)
{
var sites = context.RequestServices.GetService(typeof(ISiteRepository)) as ISiteRepository;
sites.InitializeSite(alias);
}
// rewrite path by removing alias path prefix from reserved route (api,pages,files) requests for consistent routes
if (!string.IsNullOrEmpty(alias.Path))
{

View File

@ -5,9 +5,9 @@ namespace Oqtane.Infrastructure
{
public class ServerState
{
public int SiteId { get; set; }
public string SiteKey { get; set; }
public List<string> Assemblies { get; set; } = new List<string>();
public List<Resource>Scripts { get; set; } = new List<Resource>();
public bool IsMigrated { get; set; } = false;
public bool IsInitialized { get; set; } = false;
}
}

View File

@ -5,7 +5,7 @@ using Oqtane.Models;
namespace Oqtane.Infrastructure
{
// singleton
public class ServerStateManager
public class ServerStateManager : IServerStateManager
{
private List<ServerState> _serverStates { get; set; }
@ -14,36 +14,19 @@ namespace Oqtane.Infrastructure
_serverStates = new List<ServerState>();
}
public ServerState GetServerState(int siteId)
public ServerState GetServerState(string siteKey)
{
var serverState = _serverStates.FirstOrDefault(item => item.SiteId == siteId);
var serverState = _serverStates.FirstOrDefault(item => item.SiteKey == siteKey);
if (serverState == null)
{
serverState = new ServerState();
serverState.SiteId = siteId;
serverState.SiteKey = siteKey;
serverState.Assemblies = new List<string>();
serverState.Scripts = new List<Resource>();
return serverState;
}
else
{
return serverState;
}
}
public void SetServerState(int siteId, ServerState serverState)
{
var serverstate = _serverStates.FirstOrDefault(item => item.SiteId == siteId);
if (serverstate == null)
{
serverState.SiteId = siteId;
serverState.IsInitialized = false;
_serverStates.Add(serverState);
}
else
{
serverstate.Assemblies = serverState.Assemblies;
serverstate.Scripts = serverState.Scripts;
}
return serverState;
}
}
}

View File

@ -133,35 +133,6 @@ namespace Oqtane.SiteTemplates
}
}
});
_pageTemplates.Add(new PageTemplate
{
Name = "Develop",
Parent = "",
Order = 7,
Path = "develop",
Icon = "oi oi-wrench",
IsNavigation = true,
IsPersonalizable = false,
PermissionList = new List<Permission> {
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true)
},
PageTemplateModules = new List<PageTemplateModule> {
new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Software Development", Pane = PaneNames.Default,
PermissionList = new List<Permission> {
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true)
},
Content = "<p>Oqtane offers a Module Creator which allows you to create new modules to extend the framework with additional capabilities. Simply provide some basic information and the system will scaffold a completely functional module which includes all of the necessary code files and assets to get you up and running as quickly as possible.</p>"
},
new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.Admin.ModuleCreator, Oqtane.Client", Title = "Module Creator", Pane = PaneNames.Default,
PermissionList = new List<Permission> {
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true)
}
}
}
});
if (System.IO.File.Exists(Path.Combine(_environment.WebRootPath, "images", "logo-white.png")))
{

View File

@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Oqtane.Models;
@ -57,7 +58,7 @@ namespace Oqtane.Infrastructure
alias.BaseUrl = "";
if (httpcontext.Request.Headers.ContainsKey("User-Agent") && httpcontext.Request.Headers["User-Agent"] == Shared.Constants.MauiUserAgent)
{
alias.BaseUrl = alias.Protocol + alias.Name;
alias.BaseUrl = alias.Protocol + alias.Name.Replace("/" + alias.Path, "");
}
_siteState.Alias = alias;
}

View File

@ -231,7 +231,7 @@ namespace Oqtane.Infrastructure
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Content = "<p>The page you requested does not exist.</p>"
Content = "<p>The page you requested does not exist or you do not have sufficient rights to view it.</p>"
}
}
});

View File

@ -130,14 +130,14 @@ namespace Oqtane.Managers
if (!user.EmailConfirmed)
{
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = alias.Protocol + "://" + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nIn Order To Complete The Registration Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, User, "User Account Verification", body);
_notifications.AddNotification(notification);
}
else
{
string url = alias.Protocol + "://" + alias.Name;
string url = alias.Protocol + alias.Name;
string body = "Dear " + user.DisplayName + ",\n\nA User Account Has Been Successfully Created For You. Please Use The Following Link To Access The Site:\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, User, "User Account Notification", body);
_notifications.AddNotification(notification);
@ -178,7 +178,7 @@ namespace Oqtane.Managers
user = _users.UpdateUser(user);
_syncManager.AddSyncEvent(_tenantManager.GetAlias().TenantId, EntityNames.User, user.UserId, SyncEventActions.Update);
_syncManager.AddSyncEvent(_tenantManager.GetAlias().TenantId, EntityNames.User, user.UserId, SyncEventActions.Refresh);
_syncManager.AddSyncEvent(_tenantManager.GetAlias().TenantId, EntityNames.User, user.UserId, SyncEventActions.Reload);
user.Password = ""; // remove sensitive information
_logger.Log(LogLevel.Information, this, LogFunction.Update, "User Updated {User}", user);
}
@ -228,6 +228,7 @@ namespace Oqtane.Managers
// delete user
_users.DeleteUser(userid);
_syncManager.AddSyncEvent(_tenantManager.GetAlias().TenantId, EntityNames.User, userid, SyncEventActions.Delete);
_syncManager.AddSyncEvent(_tenantManager.GetAlias().TenantId, EntityNames.User, userid, SyncEventActions.Reload);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "User Deleted {UserId}", userid, result.ToString());
}
else
@ -299,7 +300,7 @@ namespace Oqtane.Managers
var alias = _tenantManager.GetAlias();
user = _users.GetUser(user.Username);
string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser);
string url = alias.Protocol + "://" + alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string url = alias.Protocol + alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nYou attempted multiple times unsuccessfully to log in to your account and it is now locked out. Please wait a few minutes and then try again... or use the link below to reset your password:\n\n" + url +
"\n\nPlease note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site." +
"\n\nThank You!";
@ -348,7 +349,7 @@ namespace Oqtane.Managers
var alias = _tenantManager.GetAlias();
user = _users.GetUser(user.Username);
string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser);
string url = alias.Protocol + "://" + alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string url = alias.Protocol + alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nYou recently requested to reset your password. Please use the link below to complete the process:\n\n" + url +
"\n\nPlease note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site." +
"\n\nIf you did not request to reset your password you can safely ignore this message." +

View File

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

View File

@ -3,6 +3,9 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Oqtane.Extensions;
using Oqtane.Infrastructure;
using Oqtane.Managers;
using Oqtane.Shared;
namespace Oqtane.Pages
@ -10,9 +13,28 @@ namespace Oqtane.Pages
[Authorize]
public class LogoutModel : PageModel
{
private readonly IUserManager _userManager;
private readonly ISyncManager _syncManager;
public LogoutModel(IUserManager userManager, ISyncManager syncManager)
{
_userManager = userManager;
_syncManager = syncManager;
}
public async Task<IActionResult> OnPostAsync(string returnurl)
{
await HttpContext.SignOutAsync(Constants.AuthenticationScheme);
if (HttpContext.User != null)
{
var alias = HttpContext.GetAlias();
var user = _userManager.GetUser(HttpContext.User.Identity.Name, alias.SiteId);
if (user != null)
{
_syncManager.AddSyncEvent(alias.TenantId, EntityNames.User, user.UserId, SyncEventActions.Reload);
}
await HttpContext.SignOutAsync(Constants.AuthenticationScheme);
}
returnurl = (returnurl == null) ? "/" : returnurl;
returnurl = (!returnurl.StartsWith("/")) ? "/" + returnurl : returnurl;

View File

@ -4,7 +4,6 @@ using Oqtane.Shared;
using Oqtane.Models;
using System;
using System.Linq;
using System.Reflection;
using Oqtane.Repository;
using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.Configuration;
@ -38,10 +37,11 @@ namespace Oqtane.Pages
private readonly IVisitorRepository _visitors;
private readonly IAliasRepository _aliases;
private readonly ISettingRepository _settings;
private readonly ServerStateManager _serverState;
private readonly IThemeRepository _themes;
private readonly IServerStateManager _serverState;
private readonly ILogManager _logger;
public HostModel(IConfigManager configuration, ITenantManager tenantManager, ILocalizationManager localizationManager, ILanguageRepository languages, IAntiforgery antiforgery, IJwtManager jwtManager, ISiteRepository sites, IPageRepository pages, IUrlMappingRepository urlMappings, IVisitorRepository visitors, IAliasRepository aliases, ISettingRepository settings, ServerStateManager serverState, ILogManager logger)
public HostModel(IConfigManager configuration, ITenantManager tenantManager, ILocalizationManager localizationManager, ILanguageRepository languages, IAntiforgery antiforgery, IJwtManager jwtManager, ISiteRepository sites, IPageRepository pages, IUrlMappingRepository urlMappings, IVisitorRepository visitors, IAliasRepository aliases, ISettingRepository settings, IThemeRepository themes, IServerStateManager serverState, ILogManager logger)
{
_configuration = configuration;
_tenantManager = tenantManager;
@ -55,6 +55,7 @@ namespace Oqtane.Pages
_visitors = visitors;
_aliases = aliases;
_settings = settings;
_themes = themes;
_serverState = serverState;
_logger = logger;
}
@ -113,7 +114,7 @@ namespace Oqtane.Pages
}
}
var site = _sites.InitializeSite(alias);
var site = _sites.GetSite(alias.SiteId);
if (site != null && (!site.IsDeleted || url.Contains("admin/site")) && site.Runtime != "Hybrid")
{
Route route = new Route(url, alias.Path);
@ -167,12 +168,13 @@ namespace Oqtane.Pages
}
// stylesheets
var themes = _themes.GetThemes().ToList();
var resources = new List<Resource>();
if (string.IsNullOrEmpty(page.ThemeType))
{
page.ThemeType = site.DefaultThemeType;
}
var theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == page.ThemeType));
var theme = themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == page.ThemeType));
if (theme?.Resources != null)
{
resources.AddRange(theme.Resources.Where(item => item.ResourceType == ResourceType.Stylesheet).ToList());
@ -199,7 +201,7 @@ namespace Oqtane.Pages
}
HeadResources += ParseScripts(site.HeadContent);
BodyResources += ParseScripts(site.BodyContent);
var scripts = _serverState.GetServerState(site.SiteId).Scripts;
var scripts = _serverState.GetServerState(alias.SiteKey).Scripts;
foreach (var script in scripts)
{
AddScript(script, alias);

View File

@ -11,7 +11,7 @@ namespace Oqtane.Repository
Site GetSite(int siteId);
Site GetSite(int siteId, bool tracking);
void DeleteSite(int siteId);
Site InitializeSite(Alias alias);
void InitializeSite(Alias alias);
void CreatePages(Site site, List<PageTemplate> pageTemplates, Alias alias);
}
}

View File

@ -21,10 +21,10 @@ namespace Oqtane.Repository
private readonly IPermissionRepository _permissions;
private readonly ITenantManager _tenants;
private readonly ISettingRepository _settings;
private readonly ServerStateManager _serverState;
private readonly IServerStateManager _serverState;
private readonly string settingprefix = "SiteEnabled:";
public ModuleDefinitionRepository(MasterDBContext context, IMemoryCache cache, IPermissionRepository permissions, ITenantManager tenants, ISettingRepository settings, ServerStateManager serverState)
public ModuleDefinitionRepository(MasterDBContext context, IMemoryCache cache, IPermissionRepository permissions, ITenantManager tenants, ISettingRepository settings, IServerStateManager serverState)
{
_db = context;
_cache = cache;
@ -179,6 +179,8 @@ namespace Oqtane.Repository
if (siteId != -1)
{
var siteKey = _tenants.GetAlias().SiteKey;
// get all module definition permissions for site
List<Permission> permissions = _permissions.GetPermissions(siteId, EntityNames.ModuleDefinition).ToList();
@ -186,7 +188,7 @@ namespace Oqtane.Repository
var settings = _settings.GetSettings(EntityNames.ModuleDefinition).ToList();
// populate module definition site settings and permissions
var serverState = _serverState.GetServerState(siteId);
var serverState = _serverState.GetServerState(siteKey);
foreach (ModuleDefinition moduledefinition in ModuleDefinitions)
{
moduledefinition.SiteId = siteId;
@ -212,9 +214,9 @@ namespace Oqtane.Repository
{
foreach (var assembly in moduledefinition.Dependencies.Replace(".dll", "").Split(',', StringSplitOptions.RemoveEmptyEntries).Reverse())
{
if (!serverState.Assemblies.Contains(assembly))
if (!serverState.Assemblies.Contains(assembly.Trim()))
{
serverState.Assemblies.Insert(0, assembly);
serverState.Assemblies.Insert(0, assembly.Trim());
}
}
}
@ -251,7 +253,6 @@ namespace Oqtane.Repository
}
}
}
_serverState.SetServerState(siteId, serverState);
// clean up any orphaned permissions
var ids = new HashSet<int>(ModuleDefinitions.Select(item => item.ModuleDefinitionId));

View File

@ -27,13 +27,13 @@ namespace Oqtane.Repository
private readonly IThemeRepository _themeRepository;
private readonly IServiceProvider _serviceProvider;
private readonly IConfigurationRoot _config;
private readonly ServerStateManager _serverState;
private readonly IServerStateManager _serverState;
private readonly ILogManager _logger;
private static readonly object _lock = new object();
public SiteRepository(TenantDBContext context, IRoleRepository roleRepository, IProfileRepository profileRepository, IFolderRepository folderRepository, IPageRepository pageRepository,
IModuleRepository moduleRepository, IPageModuleRepository pageModuleRepository, IModuleDefinitionRepository moduleDefinitionRepository, IThemeRepository themeRepository, IServiceProvider serviceProvider,
IConfigurationRoot config, ServerStateManager serverState, ILogManager logger)
IConfigurationRoot config, IServerStateManager serverState, ILogManager logger)
{
_db = context;
_roleRepository = roleRepository;
@ -95,23 +95,25 @@ namespace Oqtane.Repository
_db.SaveChanges();
}
public Site InitializeSite(Alias alias)
public void InitializeSite(Alias alias)
{
var site = GetSite(alias.SiteId);
// load themes and module definitions
site.Themes = _themeRepository.GetThemes().ToList();
var moduleDefinitions = _moduleDefinitionRepository.GetModuleDefinitions(alias.SiteId);
// site migrations
var serverstate = _serverState.GetServerState(alias.SiteId);
if (!serverstate.IsMigrated)
var serverstate = _serverState.GetServerState(alias.SiteKey);
if (!serverstate.IsInitialized)
{
// ensure migrations are only executed once
// ensure site initialization is only executed once
lock (_lock)
{
if (!serverstate.IsMigrated)
if (!serverstate.IsInitialized)
{
var site = GetSite(alias.SiteId);
// initialize theme Assemblies and Scripts
site.Themes = _themeRepository.GetThemes().ToList();
// initialize module Assemblies and Scripts
var moduleDefinitions = _moduleDefinitionRepository.GetModuleDefinitions(alias.SiteId);
// execute migrations
var version = ProcessSiteMigrations(alias, site);
version = ProcessPageTemplates(alias, site, moduleDefinitions, version);
if (site.Version != version)
@ -119,13 +121,11 @@ namespace Oqtane.Repository
site.Version = version;
UpdateSite(site);
}
serverstate.IsMigrated = true;
_serverState.SetServerState(alias.SiteId, serverstate);
serverstate.IsInitialized = true;
}
}
}
return site;
}
}
private string ProcessSiteMigrations(Alias alias, Site site)
@ -640,7 +640,7 @@ namespace Oqtane.Repository
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Content = "<p>The page you requested does not exist.</p>"
Content = "<p>The page you requested does not exist or you do not have sufficient rights to view it.</p>"
}
}
});

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