Merge remote-tracking branch 'oqtane/dev' into dev

This commit is contained in:
Leigh Pointer 2023-07-09 21:12:47 +02:00
commit 88ac82a013
21 changed files with 301 additions and 42 deletions

View File

@ -144,6 +144,11 @@ else
<TabPanel Name="Notifications" ResourceKey="Notifications">
@if (notifications != null)
{
<select class="form-select" @onchange="(e => FilterChanged(e))">
<option value="to">@Localizer["Inbox"]</option>
<option value="from">@Localizer["Items.Sent"]</option>
</select>
<br />
<ActionLink Action="Add" Text="Send Notification" Security="SecurityAccessLevel.View" EditMode="false" ResourceKey="SendNotification" />
<br /><br />
@if (filter == "to")
@ -159,22 +164,41 @@ else
<Row>
<td><ActionLink Action="View" Parameters="@($"id=" + context.NotificationId.ToString())" Security="SecurityAccessLevel.View" EditMode="false" ResourceKey="ViewNotification" /></td>
<td><ActionDialog Header="Delete Notification" Message="Are You Sure You Wish To Delete This Notification?" Action="Delete" Security="SecurityAccessLevel.View" Class="btn btn-danger" OnClick="@(async () => await Delete(context))" EditMode="false" ResourceKey="DeleteNotification" /></td>
<td>@context.FromDisplayName</td>
<td>@context.Subject</td>
<td>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", @context.CreatedOn)</td>
@if (context.IsRead)
{
<td>@context.FromDisplayName</td>
<td>@context.Subject</td>
<td>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", @context.CreatedOn)</td>
}
else
{
<td><b>@context.FromDisplayName</b></td>
<td><b>@context.Subject</b></td>
<td><b>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", @context.CreatedOn)</b></td>
}
</Row>
<Detail>
<td colspan="2"></td>
<td colspan="3">
@{
string input = "___";
if (context.Body.Contains(input))
{
context.Body = context.Body.Split(input)[0];
context.Body = context.Body.Replace("\n", "");
context.Body = context.Body.Replace("\r", "");
} }
@(context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body)
string input = "___";
if (context.Body.Contains(input))
{
context.Body = context.Body.Split(input)[0];
context.Body = context.Body.Replace("\n", "");
context.Body = context.Body.Replace("\r", "");
}
notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body;
}
@if (context.IsRead)
{
@notificationSummary
}
else
{
<b>@notificationSummary</b>
}
</td>
</Detail>
</Pager>
@ -192,22 +216,42 @@ else
<Row>
<td><ActionLink Action="View" Parameters="@($"id=" + context.NotificationId.ToString())" Security="SecurityAccessLevel.View" EditMode="false" ResourceKey="ViewNotification" /></td>
<td><ActionDialog Header="Delete Notification" Message="Are You Sure You Wish To Delete This Notification?" Action="Delete" Security="SecurityAccessLevel.View" Class="btn btn-danger" OnClick="@(async () => await Delete(context))" EditMode="false" ResourceKey="DeleteNotification" /></td>
<td>@context.ToDisplayName</td>
<td>@context.Subject</td>
<td>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", @context.CreatedOn)</td>
@if (context.IsRead)
{
<td>@context.ToDisplayName</td>
<td>@context.Subject</td>
<td>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", @context.CreatedOn)</td>
}
else
{
<td><b>@context.ToDisplayName</b></td>
<td><b>@context.Subject</b></td>
<td><b>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", @context.CreatedOn)</b></td>
}
</Row>
<Detail>
<td colspan="2"></td>
<td colspan="3">
@{
string input = "___";
if (context.Body.Contains(input))
{
context.Body = context.Body.Split(input)[0];
context.Body = context.Body.Replace("\n", "");
context.Body = context.Body.Replace("\r", "");
} }
@(context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body)
string input = "___";
if (context.Body.Contains(input))
{
context.Body = context.Body.Split(input)[0];
context.Body = context.Body.Replace("\n", "");
context.Body = context.Body.Replace("\r", "");
}
notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body;
}
@if (context.IsRead)
{
@notificationSummary
}
else
{
<b>@notificationSummary</b>
}
</td>
</Detail>
</Pager>
@ -217,11 +261,6 @@ else
<br />
<ActionDialog Header="Clear Notifications" Message="Are You Sure You Wish To Permanently Delete All Notifications ?" Action="Delete All Notifications" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllNotifications())" ResourceKey="DeleteAllNotifications" />
}
<br /><hr />
<select class="form-select" @onchange="(e => FilterChanged(e))">
<option value="to">@Localizer["Inbox"]</option>
<option value="from">@Localizer["Items.Sent"]</option>
</select>
}
</TabPanel>
</TabStrip>
@ -246,6 +285,7 @@ else
private string category = string.Empty;
private string filter = "to";
private List<Notification> notifications;
private string notificationSummary = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;

View File

@ -118,6 +118,9 @@
Notification notification = await NotificationService.GetNotificationAsync(notificationid);
if (notification != null)
{
notification.IsRead = true;
notification = await NotificationService.UpdateNotificationAsync(notification);
int userid = -1;
if (notification.ToUserId == PageState.User.UserId)
{

View File

@ -17,7 +17,10 @@
<hr class="app-rule" />
</div>
<div class="collapse @_show" id="@Name">
@ChildContent
@if (ChildContent != null)
{
@ChildContent
}
</div>
@code {
@ -26,7 +29,7 @@
private string _show = string.Empty;
[Parameter]
public RenderFragment ChildContent { get; set; }
public RenderFragment ChildContent { get; set; } = null;
[Parameter]
public string Name { get; set; } // required - the name of the section

View File

@ -3,7 +3,6 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<OutputType>Exe</OutputType>
<RazorLangVersion>3.0</RazorLangVersion>
<Configurations>Debug;Release</Configurations>
<Version>4.0.0</Version>
<Product>Oqtane</Product>

View File

@ -18,6 +18,27 @@ namespace Oqtane.Services
/// <returns></returns>
Task<List<Notification>> GetNotificationsAsync(int siteId, string direction, int userId);
/// <summary>
///
/// </summary>
/// <param name="siteId"></param>
/// <param name="direction"></param>
/// <param name="userId"></param>
/// <param name="count"></param>
/// <param name="isRead"></param>
/// <returns></returns>
Task<List<Notification>> GetNotificationsAsync(int siteId, string direction, int userId, int count, bool isRead);
/// <summary>
///
/// </summary>
/// <param name="siteId"></param>
/// <param name="direction"></param>
/// <param name="userId"></param>
/// <param name="isRead"></param>
/// <returns></returns>
Task<int> GetNotificationCountAsync(int siteId, string direction, int userId, bool isRead);
/// <summary>
/// Returns a specific notifications
/// </summary>

View File

@ -27,6 +27,17 @@ namespace Oqtane.Services
/// <returns></returns>
Task<List<Package>> GetPackagesAsync(string type, string search, string price, string package);
/// <summary>
/// Returns a list of packages matching the given parameters
/// </summary>
/// <param name="type"></param>
/// <param name="search"></param>
/// <param name="price"></param>
/// <param name="package"></param>
/// <param name="sort"></param>
/// <returns></returns>
Task<List<Package>> GetPackagesAsync(string type, string search, string price, string package, string sort);
/// <summary>
/// Returns a specific package
/// </summary>

View File

@ -22,6 +22,20 @@ namespace Oqtane.Services
return notifications.OrderByDescending(item => item.CreatedOn).ToList();
}
public async Task<List<Notification>> GetNotificationsAsync(int siteId, string direction, int userId, int count, bool isRead)
{
var notifications = await GetJsonAsync<List<Notification>>($"{Apiurl}/read?siteid={siteId}&direction={direction.ToLower()}&userid={userId}&count={count}&isread={isRead}");
return notifications.OrderByDescending(item => item.CreatedOn).ToList();
}
public async Task<int> GetNotificationCountAsync(int siteId, string direction, int userId, bool isRead)
{
var notificationCount = await GetJsonAsync<int>($"{Apiurl}/read-count?siteid={siteId}&direction={direction.ToLower()}&userid={userId}&isread={isRead}");
return notificationCount;
}
public async Task<Notification> GetNotificationAsync(int notificationId)
{
return await GetJsonAsync<Notification>($"{Apiurl}/{notificationId}");

View File

@ -23,7 +23,12 @@ namespace Oqtane.Services
public async Task<List<Package>> GetPackagesAsync(string type, string search, string price, string package)
{
return await GetJsonAsync<List<Package>>($"{Apiurl}?type={type}&search={WebUtility.UrlEncode(search)}&price={price}&package={package}");
return await GetPackagesAsync(type, search, price, package, "");
}
public async Task<List<Package>> GetPackagesAsync(string type, string search, string price, string package, string sort)
{
return await GetJsonAsync<List<Package>>($"{Apiurl}?type={type}&search={WebUtility.UrlEncode(search)}&price={price}&package={package}&sort={sort}");
}
public async Task<Package> GetPackageAsync(string packageId, string version)

View File

@ -80,7 +80,7 @@ namespace Oqtane.Controllers
public Folder GetByPath(int siteId, string path)
{
var folderPath = WebUtility.UrlDecode(path).Replace("\\", "/");
if (!folderPath.EndsWith("/"))
if (!folderPath.EndsWith("/") && folderPath != "")
{
folderPath += "/";
}

View File

@ -9,6 +9,9 @@ using Oqtane.Repository;
using Oqtane.Security;
using System.Net;
using System.Reflection.Metadata;
using Microsoft.Extensions.Localization;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using System.Linq;
namespace Oqtane.Controllers
{
@ -30,6 +33,72 @@ namespace Oqtane.Controllers
_alias = tenantManager.GetAlias();
}
// GET: api/<controller>/read?siteid=x&direction=to&userid=1&count=5&isread=false
[HttpGet("read")]
[Authorize(Roles = RoleNames.Registered)]
public IEnumerable<Notification> Get(string siteid, string direction, string userid, string count, string isread)
{
IEnumerable<Notification> notifications = null;
int SiteId;
int UserId;
int Count;
bool IsRead;
if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId && int.TryParse(userid, out UserId) && int.TryParse(count, out Count) && bool.TryParse(isread, out IsRead) && IsAuthorized(UserId))
{
if (direction == "to")
{
notifications = _notifications.GetNotifications(SiteId, -1, UserId, Count, IsRead);
}
else
{
notifications = _notifications.GetNotifications(SiteId, UserId, -1, Count, IsRead);
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Notification Get Attempt {SiteId} {Direction} {UserId} {Count} {isRead}", siteid, direction, userid, count, isread);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
notifications = null;
}
return notifications;
}
// GET: api/<controller>/read?siteid=x&direction=to&userid=1&count=5&isread=false
[HttpGet("read-count")]
[Authorize(Roles = RoleNames.Registered)]
public int Get(string siteid, string direction, string userid, string isread)
{
int notificationsCount = 0;
int SiteId;
int UserId;
bool IsRead;
if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId && int.TryParse(userid, out UserId) && bool.TryParse(isread, out IsRead) && IsAuthorized(UserId))
{
if (direction == "to")
{
notificationsCount = _notifications.GetNotificationCount(SiteId, -1, UserId, IsRead);
}
else
{
notificationsCount = _notifications.GetNotificationCount(SiteId, UserId, -1, IsRead);
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Notification Get Attempt {SiteId} {Direction} {UserId} {isRead}", siteid, direction, userid, isread);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
notificationsCount = 0;
}
return notificationsCount;
}
// GET: api/<controller>?siteid=x&type=y&userid=z
[HttpGet]
[Authorize(Roles = RoleNames.Registered)]

View File

@ -34,7 +34,7 @@ namespace Oqtane.Controllers
// GET: api/<controller>?type=x&search=y&price=z&package=a
[HttpGet]
public async Task<IEnumerable<Package>> Get(string type, string search, string price, string package)
public async Task<IEnumerable<Package>> Get(string type, string search, string price, string package, string sort)
{
// get packages
List<Package> packages = new List<Package>();
@ -44,7 +44,7 @@ namespace Oqtane.Controllers
{
client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value);
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version));
packages = await GetJson<List<Package>>(client, Constants.PackageRegistryUrl + $"/api/registry/packages/?id={_configManager.GetInstallationId()}&type={type.ToLower()}&version={Constants.Version}&search={search}&price={price}&package={package}");
packages = await GetJson<List<Package>>(client, Constants.PackageRegistryUrl + $"/api/registry/packages/?id={_configManager.GetInstallationId()}&type={type.ToLower()}&version={Constants.Version}&search={search}&price={price}&package={package}&sort={sort}");
}
}
return packages;

View File

@ -7,14 +7,8 @@ using System.Linq;
using Oqtane.Security;
using System.Net;
using Oqtane.Enums;
using Oqtane.Extensions;
using Oqtane.Infrastructure;
using Oqtane.Repository;
using Oqtane.Modules.Admin.Users;
using System.IO;
using Oqtane.Services;
using Oqtane.UI;
using System;
namespace Oqtane.Controllers
{

View File

@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Oqtane.Databases.Interfaces;
using Oqtane.Migrations.EntityBuilders;
using Oqtane.Repository;
using Oqtane.Shared;
namespace Oqtane.Migrations.Tenant
{
[DbContext(typeof(TenantDBContext))]
[Migration("Tenant.04.00.01.01")]
public class AddNotificationIsRead : MultiDatabaseMigration
{
public AddNotificationIsRead(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var notificationEntityBuilder = new NotificationEntityBuilder(migrationBuilder, ActiveDatabase);
notificationEntityBuilder.AddBooleanColumn("IsRead", false);
notificationEntityBuilder.UpdateColumn("IsRead", "1", "bool", "");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
var notificationEntityBuilder = new NotificationEntityBuilder(migrationBuilder, ActiveDatabase);
notificationEntityBuilder.DropColumn("IsPublic");
}
}
}

View File

@ -6,6 +6,8 @@ namespace Oqtane.Repository
public interface INotificationRepository
{
IEnumerable<Notification> GetNotifications(int siteId, int fromUserId, int toUserId);
IEnumerable<Notification> GetNotifications(int siteId, int fromUserId, int toUserId, int count, bool isRead);
int GetNotificationCount(int siteId, int fromUserId, int toUserId, bool isRead);
Notification AddNotification(Notification notification);
Notification UpdateNotification(Notification notification);
Notification GetNotification(int notificationId);

View File

@ -101,6 +101,7 @@ namespace Oqtane.Repository
ModuleDefinition.IsPortable = moduleDefinition.IsPortable;
ModuleDefinition.Resources = moduleDefinition.Resources;
ModuleDefinition.IsEnabled = moduleDefinition.IsEnabled;
ModuleDefinition.PackageName = moduleDefinition.PackageName;
}
return ModuleDefinition;

View File

@ -33,6 +33,54 @@ namespace Oqtane.Repository
.ToList();
}
public IEnumerable<Notification> GetNotifications(int siteId, int fromUserId, int toUserId, int count, bool isRead)
{
if (toUserId == -1 && fromUserId == -1)
{
return _db.Notification
.Where(item => item.SiteId == siteId)
.Where(item => item.IsDelivered == false && item.IsDeleted == false)
.Where(item => item.SendOn == null || item.SendOn < System.DateTime.UtcNow)
.Where(item => item.IsRead == isRead)
.OrderByDescending(item => item.CreatedOn)
.ToList()
.Take(count);
}
return _db.Notification
.Where(item => item.SiteId == siteId)
.Where(item => item.ToUserId == toUserId || toUserId == -1)
.Where(item => item.FromUserId == fromUserId || fromUserId == -1)
.Where(item => item.IsRead == isRead)
.OrderByDescending(item => item.CreatedOn)
.ToList()
.Take(count);
}
public int GetNotificationCount(int siteId, int fromUserId, int toUserId, bool isRead)
{
if (toUserId == -1 && fromUserId == -1)
{
return _db.Notification
.Where(item => item.SiteId == siteId)
.Where(item => item.IsDelivered == false && item.IsDeleted == false)
.Where(item => item.SendOn == null || item.SendOn < System.DateTime.UtcNow)
.Where(item => item.IsRead == isRead)
.ToList()
.Count();
}
return _db.Notification
.Where(item => item.SiteId == siteId)
.Where(item => item.ToUserId == toUserId || toUserId == -1)
.Where(item => item.FromUserId == fromUserId || fromUserId == -1)
.Where(item => item.IsRead == isRead)
.ToList()
.Count();
}
public Notification AddNotification(Notification notification)
{
_db.Notification.Add(notification);

View File

@ -89,6 +89,7 @@ namespace Oqtane.Repository
Theme.Containers = theme.Containers;
Theme.ThemeSettingsType = theme.ThemeSettingsType;
Theme.ContainerSettingsType = theme.ContainerSettingsType;
Theme.PackageName = theme.PackageName;
Themes.Add(Theme);
}

View File

@ -2,7 +2,6 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<RazorLangVersion>3.0</RazorLangVersion>
<Version>1.0.0</Version>
<Authors>[Owner]</Authors>
<Company>[Owner]</Company>

View File

@ -2,7 +2,6 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<RazorLangVersion>3.0</RazorLangVersion>
<Version>1.0.0</Version>
<Authors>[Owner]</Authors>
<Company>[Owner]</Company>

View File

@ -94,6 +94,10 @@ namespace Oqtane.Models
/// </summary>
public DateTime? SendOn { get; set; }
/// <summary>
/// If it has been read. See also <see cref="IsDelivered" />
/// </summary>
public bool IsRead { get; set; }
// constructors
public Notification() {}
@ -174,6 +178,7 @@ namespace Oqtane.Models
}
IsDelivered = false;
DeliveredOn = null;
IsRead = false;
}
}

View File

@ -32,6 +32,11 @@ namespace Oqtane.Models
/// </summary>
public string Description { get; set; }
/// <summary>
/// logo
/// </summary>
public int? LogoFileId { get; set; }
/// <summary>
/// License for the Package.
/// </summary>
@ -62,6 +67,11 @@ namespace Oqtane.Models
/// </summary>
public string PackageUrl { get; set; }
/// <summary>
/// The direct Url for getting support for the product
/// </summary>
public string SupportUrl { get; set; }
/// <summary>
/// Indicates if any known security vulnerabilities exist
/// </summary>