diff --git a/common/ASC.Api.Core/ASC.Api.Core.csproj b/common/ASC.Api.Core/ASC.Api.Core.csproj
index b9bace788a..43e65541d4 100644
--- a/common/ASC.Api.Core/ASC.Api.Core.csproj
+++ b/common/ASC.Api.Core/ASC.Api.Core.csproj
@@ -26,6 +26,8 @@
+
+
diff --git a/common/ASC.Api.Core/Auth/AuthHandler.cs b/common/ASC.Api.Core/Auth/AuthHandler.cs
index ca6dcc5856..9029d65e2a 100644
--- a/common/ASC.Api.Core/Auth/AuthHandler.cs
+++ b/common/ASC.Api.Core/Auth/AuthHandler.cs
@@ -24,6 +24,8 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
+using Role = ASC.Common.Security.Authorizing.Role;
+
namespace ASC.Api.Core.Auth;
[Scope]
diff --git a/common/ASC.Api.Core/Core/BaseStartup.cs b/common/ASC.Api.Core/Core/BaseStartup.cs
index 6ab1c9a96c..5ac04c34ad 100644
--- a/common/ASC.Api.Core/Core/BaseStartup.cs
+++ b/common/ASC.Api.Core/Core/BaseStartup.cs
@@ -100,6 +100,88 @@ public abstract class BaseStartup
}
});
+ var redisOptions = _configuration.GetSection("Redis").Get().ConfigurationOptions;
+ var connectionMultiplexer = ConnectionMultiplexer.Connect(redisOptions);
+
+ services.AddRateLimiter(options =>
+ {
+ options.GlobalLimiter = PartitionedRateLimiter.CreateChained(
+ PartitionedRateLimiter.Create(httpContext =>
+ {
+ var userId = httpContext?.User?.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Sid)?.Value;
+
+ if (userId == null)
+ {
+ return RateLimitPartition.GetNoLimiter("no_limiter");
+ }
+
+ var permitLimit = 1500;
+
+ string partitionKey;
+
+ partitionKey = $"sw_{userId}";
+
+ return RedisRateLimitPartition.GetSlidingWindowRateLimiter(partitionKey, key => new RedisSlidingWindowRateLimiterOptions
+ {
+ PermitLimit = permitLimit,
+ Window = TimeSpan.FromMinutes(1),
+ ConnectionMultiplexerFactory = () => connectionMultiplexer
+ });
+ }),
+ PartitionedRateLimiter.Create(httpContext =>
+ {
+ var userId = httpContext?.User?.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Sid)?.Value;
+ string partitionKey;
+ int permitLimit;
+
+ if (userId == null)
+ {
+ return RateLimitPartition.GetNoLimiter("no_limiter");
+ }
+
+ if (String.Compare(httpContext.Request.Method, "GET", true) == 0)
+ {
+ permitLimit = 50;
+ partitionKey = $"cr_read_{userId}";
+
+ }
+ else
+ {
+ permitLimit = 15;
+ partitionKey = $"cr_write_{userId}";
+ }
+
+ return RedisRateLimitPartition.GetConcurrencyRateLimiter(partitionKey, key => new RedisConcurrencyRateLimiterOptions
+ {
+ PermitLimit = permitLimit,
+ QueueLimit = 0,
+ ConnectionMultiplexerFactory = () => connectionMultiplexer
+ });
+ }
+ ));
+
+ options.AddPolicy("sensitive_api", httpContext => {
+ var userId = httpContext?.User?.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Sid)?.Value;
+
+ if (userId == null)
+ {
+ return RateLimitPartition.GetNoLimiter("no_limiter");
+ }
+
+ var permitLimit = 5;
+ var partitionKey = $"sensitive_api_{userId}";
+
+ return RedisRateLimitPartition.GetSlidingWindowRateLimiter(partitionKey, key => new RedisSlidingWindowRateLimiterOptions
+ {
+ PermitLimit = permitLimit,
+ Window = TimeSpan.FromMinutes(1),
+ ConnectionMultiplexerFactory = () => connectionMultiplexer
+ });
+ });
+
+ options.OnRejected = (context, ct) => RateLimitMetadata.OnRejected(context.HttpContext, context.Lease, ct);
+ });
+
services.AddScoped();
services.AddBaseDbContextPool()
@@ -311,6 +393,8 @@ public abstract class BaseStartup
app.UseAuthentication();
+ app.UseRateLimiter();
+
app.UseAuthorization();
app.UseCultureMiddleware();
@@ -346,6 +430,8 @@ public abstract class BaseStartup
await context.Response.WriteAsync($"{Environment.MachineName} running {CustomHealthCheck.Running}");
});
});
+
+
}
public void ConfigureContainer(ContainerBuilder builder)
diff --git a/common/ASC.Api.Core/GlobalUsings.cs b/common/ASC.Api.Core/GlobalUsings.cs
index 8fa7cc301f..dd9f820332 100644
--- a/common/ASC.Api.Core/GlobalUsings.cs
+++ b/common/ASC.Api.Core/GlobalUsings.cs
@@ -41,6 +41,7 @@ global using System.Text.Encodings.Web;
global using System.Text.Json;
global using System.Text.Json.Serialization;
global using System.Text.RegularExpressions;
+global using System.Threading.RateLimiting;
global using System.Web;
global using System.Xml.Linq;
@@ -150,6 +151,10 @@ global using NLog.Web;
global using RabbitMQ.Client;
+global using RedisRateLimiting;
+global using RedisRateLimiting.AspNetCore;
+
+global using StackExchange.Redis;
global using StackExchange.Redis.Extensions.Core.Configuration;
global using StackExchange.Redis.Extensions.Newtonsoft;
diff --git a/common/ASC.Api.Core/Security/InvitationLinkHelper.cs b/common/ASC.Api.Core/Security/InvitationLinkHelper.cs
index fd28365277..1d79feafab 100644
--- a/common/ASC.Api.Core/Security/InvitationLinkHelper.cs
+++ b/common/ASC.Api.Core/Security/InvitationLinkHelper.cs
@@ -151,7 +151,7 @@ public class InvitationLinkHelper
return linkId == default ? (ValidationResult.Invalid, default) : (ValidationResult.Ok, linkId);
}
- private async Task GetLinkVisitMessageAsync(string email, string key)
+ private async Task GetLinkVisitMessageAsync(string email, string key)
{
await using var context = _dbContextFactory.CreateDbContext();
@@ -188,7 +188,7 @@ public class LinkValidationResult
static file class Queries
{
- public static readonly Func> AuditEventsAsync =
+ public static readonly Func> AuditEventsAsync =
EF.CompileAsyncQuery(
(MessagesContext ctx, string target, string description) =>
ctx.AuditEvents.FirstOrDefault(a => a.Target == target && a.DescriptionRaw == description));
diff --git a/common/ASC.Core.Common/Data/DbLoginEventsManager.cs b/common/ASC.Core.Common/Data/DbLoginEventsManager.cs
index 810d2b5f64..924d031da3 100644
--- a/common/ASC.Core.Common/Data/DbLoginEventsManager.cs
+++ b/common/ASC.Core.Common/Data/DbLoginEventsManager.cs
@@ -61,7 +61,7 @@ public class DbLoginEventsManager
_mapper = mapper;
}
- public async Task GetByIdAsync(int id)
+ public async Task GetByIdAsync(int id)
{
if (id < 0) return null;
@@ -78,7 +78,7 @@ public class DbLoginEventsManager
var loginInfo = await Queries.LoginEventsAsync(loginEventContext, tenantId, userId, _loginActions, date).ToListAsync();
- return _mapper.Map, List>(loginInfo);
+ return _mapper.Map, List>(loginInfo);
}
public async Task LogOutEventAsync(int loginEventId)
@@ -135,7 +135,7 @@ public class DbLoginEventsManager
static file class Queries
{
- public static readonly Func, DateTime, IAsyncEnumerable>
+ public static readonly Func, DateTime, IAsyncEnumerable>
LoginEventsAsync = EF.CompileAsyncQuery(
(MessagesContext ctx, int tenantId, Guid userId, IEnumerable loginActions, DateTime date) =>
ctx.LoginEvents
diff --git a/common/ASC.Core.Common/Data/DbUserService.cs b/common/ASC.Core.Common/Data/DbUserService.cs
index 24c6f82967..24b09cc79b 100644
--- a/common/ASC.Core.Common/Data/DbUserService.cs
+++ b/common/ASC.Core.Common/Data/DbUserService.cs
@@ -564,6 +564,16 @@ public class EFUserService : IUserService
}
await userDbContext.AddOrUpdateAsync(q => q.Photos, userPhoto);
+
+ var userEntity = new User
+ {
+ Id = id,
+ LastModified = DateTime.UtcNow,
+ TenantId = tenant
+ };
+
+ userDbContext.Users.Attach(userEntity);
+ userDbContext.Entry(userEntity).Property(x => x.LastModified).IsModified = true;
}
else if (userPhoto != null)
{
diff --git a/common/ASC.Core.Common/Messaging/MessagesContext.cs b/common/ASC.Core.Common/EF/Context/MessagesContext.cs
similarity index 95%
rename from common/ASC.Core.Common/Messaging/MessagesContext.cs
rename to common/ASC.Core.Common/EF/Context/MessagesContext.cs
index a3a21f713f..8c753d81ed 100644
--- a/common/ASC.Core.Common/Messaging/MessagesContext.cs
+++ b/common/ASC.Core.Common/EF/Context/MessagesContext.cs
@@ -28,8 +28,8 @@ namespace ASC.MessagingSystem.EF.Context;
public class MessagesContext : DbContext
{
- public DbSet AuditEvents { get; set; }
- public DbSet LoginEvents { get; set; }
+ public DbSet AuditEvents { get; set; }
+ public DbSet LoginEvents { get; set; }
public DbSet WebstudioSettings { get; set; }
public DbSet Tenants { get; set; }
public DbSet Users { get; set; }
diff --git a/common/ASC.Core.Common/Messaging/AuditEvent.cs b/common/ASC.Core.Common/EF/Model/DbAuditEvent.cs
similarity index 94%
rename from common/ASC.Core.Common/Messaging/AuditEvent.cs
rename to common/ASC.Core.Common/EF/Model/DbAuditEvent.cs
index 8de1133b11..80d7c7f02b 100644
--- a/common/ASC.Core.Common/Messaging/AuditEvent.cs
+++ b/common/ASC.Core.Common/EF/Model/DbAuditEvent.cs
@@ -28,7 +28,7 @@ using Profile = AutoMapper.Profile;
namespace ASC.MessagingSystem.EF.Model;
-public class AuditEvent : MessageEvent, IMapFrom
+public class DbAuditEvent : MessageEvent, IMapFrom
{
public string Initiator { get; set; }
public string Target { get; set; }
@@ -37,8 +37,8 @@ public class AuditEvent : MessageEvent, IMapFrom
public void Mapping(Profile profile)
{
- profile.CreateMap();
- profile.CreateMap()
+ profile.CreateMap();
+ profile.CreateMap()
.ConvertUsing();
}
}
@@ -47,7 +47,7 @@ public static class AuditEventExtension
{
public static ModelBuilderWrapper AddAuditEvent(this ModelBuilderWrapper modelBuilder)
{
- modelBuilder.Entity().Navigation(e => e.Tenant).AutoInclude(false);
+ modelBuilder.Entity().Navigation(e => e.Tenant).AutoInclude(false);
modelBuilder
.Add(MySqlAddAuditEvent, Provider.MySql)
@@ -58,7 +58,7 @@ public static class AuditEventExtension
public static void MySqlAddAuditEvent(this ModelBuilder modelBuilder)
{
- modelBuilder.Entity(entity =>
+ modelBuilder.Entity(entity =>
{
entity.ToTable("audit_events")
.HasCharSet("utf8");
@@ -133,7 +133,7 @@ public static class AuditEventExtension
}
public static void PgSqlAddAuditEvent(this ModelBuilder modelBuilder)
{
- modelBuilder.Entity(entity =>
+ modelBuilder.Entity(entity =>
{
entity.ToTable("audit_events", "onlyoffice");
diff --git a/common/ASC.Core.Common/Messaging/LoginEvent.cs b/common/ASC.Core.Common/EF/Model/DbLoginEvent.cs
similarity index 94%
rename from common/ASC.Core.Common/Messaging/LoginEvent.cs
rename to common/ASC.Core.Common/EF/Model/DbLoginEvent.cs
index ba1f1b06bd..e0b4a53393 100644
--- a/common/ASC.Core.Common/Messaging/LoginEvent.cs
+++ b/common/ASC.Core.Common/EF/Model/DbLoginEvent.cs
@@ -28,7 +28,7 @@ using Profile = AutoMapper.Profile;
namespace ASC.MessagingSystem.EF.Model;
-public class LoginEvent : MessageEvent, IMapFrom
+public class DbLoginEvent : MessageEvent, IMapFrom
{
public string Login { get; set; }
public bool Active { get; set; }
@@ -37,8 +37,8 @@ public class LoginEvent : MessageEvent, IMapFrom
public void Mapping(Profile profile)
{
- profile.CreateMap();
- profile.CreateMap()
+ profile.CreateMap();
+ profile.CreateMap()
.ConvertUsing();
}
}
@@ -47,7 +47,7 @@ public static class LoginEventsExtension
{
public static ModelBuilderWrapper AddLoginEvents(this ModelBuilderWrapper modelBuilder)
{
- modelBuilder.Entity().Navigation(e => e.Tenant).AutoInclude(false);
+ modelBuilder.Entity().Navigation(e => e.Tenant).AutoInclude(false);
modelBuilder
.Add(MySqlAddLoginEvents, Provider.MySql)
@@ -58,7 +58,7 @@ public static class LoginEventsExtension
public static void MySqlAddLoginEvents(this ModelBuilder modelBuilder)
{
- modelBuilder.Entity(entity =>
+ modelBuilder.Entity(entity =>
{
entity.ToTable("login_events")
.HasCharSet("utf8");
@@ -131,7 +131,7 @@ public static class LoginEventsExtension
}
public static void PgSqlAddLoginEvents(this ModelBuilder modelBuilder)
{
- modelBuilder.Entity(entity =>
+ modelBuilder.Entity(entity =>
{
entity.ToTable("login_events", "onlyoffice");
diff --git a/common/ASC.Core.Common/GeolocationHelper.cs b/common/ASC.Core.Common/GeolocationHelper.cs
index e15886d7aa..afe0c7e84f 100644
--- a/common/ASC.Core.Common/GeolocationHelper.cs
+++ b/common/ASC.Core.Common/GeolocationHelper.cs
@@ -55,6 +55,33 @@ public class GeolocationHelper
_cache = cache;
}
+ public async Task AddGeolocationAsync(BaseEvent baseEvent)
+ {
+ var location = await GetGeolocationAsync(baseEvent.IP);
+ baseEvent.Country = location[0];
+ baseEvent.City = location[1];
+ return baseEvent;
+ }
+
+ public async Task GetGeolocationAsync(string ip)
+ {
+ try
+ {
+ var location = await GetIPGeolocationAsync(IPAddress.Parse(ip));
+ if (string.IsNullOrEmpty(location.Key))
+ {
+ return new string[] { string.Empty, string.Empty };
+ }
+ var regionInfo = new RegionInfo(location.Key).EnglishName;
+ return new string[] { regionInfo, location.City };
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorWithException(ex);
+ return new string[] { string.Empty, string.Empty };
+ }
+ }
+
public async Task GetIPGeolocationAsync(IPAddress address)
{
try
diff --git a/common/ASC.Core.Common/Messaging/BaseEvent.cs b/common/ASC.Core.Common/Messaging/BaseEvent.cs
index 6493c12767..4c3cb16281 100644
--- a/common/ASC.Core.Common/Messaging/BaseEvent.cs
+++ b/common/ASC.Core.Common/Messaging/BaseEvent.cs
@@ -28,7 +28,7 @@ using Profile = AutoMapper.Profile;
namespace ASC.AuditTrail.Models;
-public class BaseEvent : IMapFrom
+public class BaseEvent : IMapFrom
{
public int Id { get; set; }
public int TenantId { get; set; }
@@ -37,7 +37,13 @@ public class BaseEvent : IMapFrom
public IList Description { get; set; }
[Event("IpCol")]
- public string IP { get; set; }
+ public string IP { get; set; }
+
+ [Event("CountryCol")]
+ public string Country { get; set; }
+
+ [Event("CityCol")]
+ public string City { get; set; }
[Event("BrowserCol")]
public string Browser { get; set; }
@@ -59,7 +65,7 @@ public class BaseEvent : IMapFrom
public virtual void Mapping(Profile profile)
{
- profile.CreateMap()
+ profile.CreateMap()
.ForMember(r => r.IP, opt => opt.MapFrom())
.ForMember(r => r.Date, opt => opt.MapFrom())
;
diff --git a/common/ASC.Core.Common/Messaging/BaseEventTypeResolver.cs b/common/ASC.Core.Common/Messaging/BaseEventTypeResolver.cs
index 235b5e7485..de54d32ae5 100644
--- a/common/ASC.Core.Common/Messaging/BaseEventTypeResolver.cs
+++ b/common/ASC.Core.Common/Messaging/BaseEventTypeResolver.cs
@@ -27,9 +27,9 @@
namespace ASC.MessagingSystem.Mapping;
[Scope]
-public class BaseEventTypeIpResolver : IValueResolver
+public class BaseEventTypeIpResolver : IValueResolver
{
- public string Resolve(LoginEvent source, BaseEvent destination, string destMember, ResolutionContext context)
+ public string Resolve(DbLoginEvent source, BaseEvent destination, string destMember, ResolutionContext context)
{
if (!string.IsNullOrEmpty(source.Ip))
{
@@ -45,7 +45,7 @@ public class BaseEventTypeIpResolver : IValueResolver
+public class BaseEventTypeDateResolver : IValueResolver
{
private readonly TenantUtil _tenantUtil;
@@ -54,7 +54,7 @@ public class BaseEventTypeDateResolver : IValueResolver, ITypeConverter
+public class EventTypeConverter : ITypeConverter, ITypeConverter
{
- public LoginEvent Convert(EventMessage source, LoginEvent destination, ResolutionContext context)
+ public DbLoginEvent Convert(EventMessage source, DbLoginEvent destination, ResolutionContext context)
{
var messageEvent = context.Mapper.Map(source);
- var loginEvent = context.Mapper.Map(messageEvent);
+ var loginEvent = context.Mapper.Map(messageEvent);
loginEvent.Login = source.Initiator;
loginEvent.Active = source.Active;
@@ -50,10 +50,10 @@ public class EventTypeConverter : ITypeConverter, ITyp
return loginEvent;
}
- public AuditEvent Convert(EventMessage source, AuditEvent destination, ResolutionContext context)
+ public DbAuditEvent Convert(EventMessage source, DbAuditEvent destination, ResolutionContext context)
{
var messageEvent = context.Mapper.Map(source);
- var auditEvent = context.Mapper.Map(messageEvent);
+ var auditEvent = context.Mapper.Map(messageEvent);
auditEvent.Initiator = source.Initiator;
auditEvent.Target = source.Target?.ToString();
diff --git a/common/ASC.MessagingSystem/Data/MessagesRepository.cs b/common/ASC.MessagingSystem/Data/MessagesRepository.cs
index 5463e03844..f94f34c24b 100644
--- a/common/ASC.MessagingSystem/Data/MessagesRepository.cs
+++ b/common/ASC.MessagingSystem/Data/MessagesRepository.cs
@@ -174,12 +174,12 @@ public class MessagesRepository : IDisposable
// messages with action code < 2000 are related to login-history
if ((int)message.Action < 2000)
{
- var loginEvent = _mapper.Map(message);
+ var loginEvent = _mapper.Map(message);
await ef.LoginEvents.AddAsync(loginEvent);
}
else
{
- var auditEvent = _mapper.Map(message);
+ var auditEvent = _mapper.Map(message);
await ef.AuditEvents.AddAsync(auditEvent);
}
}
@@ -230,12 +230,12 @@ public class MessagesRepository : IDisposable
// messages with action code < 2000 are related to login-history
if ((int)message.Action < 2000)
{
- var loginEvent = _mapper.Map(message);
+ var loginEvent = _mapper.Map(message);
ef.LoginEvents.Add(loginEvent);
}
else
{
- var auditEvent = _mapper.Map(message);
+ var auditEvent = _mapper.Map(message);
ef.AuditEvents.Add(auditEvent);
}
}
@@ -245,7 +245,7 @@ public class MessagesRepository : IDisposable
private async Task AddLoginEventAsync(EventMessage message, MessagesContext dbContext)
{
- var loginEvent = _mapper.Map(message);
+ var loginEvent = _mapper.Map(message);
await dbContext.LoginEvents.AddAsync(loginEvent);
await dbContext.SaveChangesAsync();
@@ -255,7 +255,7 @@ public class MessagesRepository : IDisposable
private async Task AddAuditEventAsync(EventMessage message, MessagesContext dbContext)
{
- var auditEvent = _mapper.Map(message);
+ var auditEvent = _mapper.Map(message);
await dbContext.AuditEvents.AddAsync(auditEvent);
await dbContext.SaveChangesAsync();
diff --git a/common/Tools/ASC.Migrations.Core/EF/MigrationContext.cs b/common/Tools/ASC.Migrations.Core/EF/MigrationContext.cs
index 64f7cacd20..7e87696763 100644
--- a/common/Tools/ASC.Migrations.Core/EF/MigrationContext.cs
+++ b/common/Tools/ASC.Migrations.Core/EF/MigrationContext.cs
@@ -68,8 +68,8 @@ public class MigrationContext : DbContext
public DbSet InstanceRegistrations { get; set; }
- public DbSet AuditEvents { get; set; }
- public DbSet LoginEvents { get; set; }
+ public DbSet AuditEvents { get; set; }
+ public DbSet LoginEvents { get; set; }
public DbSet Backups { get; set; }
public DbSet Schedules { get; set; }
diff --git a/common/services/ASC.AuditTrail/AuditReportResource.Designer.cs b/common/services/ASC.AuditTrail/AuditReportResource.Designer.cs
index eb999cf92a..ec7abab6cc 100644
--- a/common/services/ASC.AuditTrail/AuditReportResource.Designer.cs
+++ b/common/services/ASC.AuditTrail/AuditReportResource.Designer.cs
@@ -186,6 +186,15 @@ namespace ASC.AuditTrail {
}
}
+ ///
+ /// Looks up a localized string similar to City.
+ ///
+ public static string CityCol {
+ get {
+ return ResourceManager.GetString("CityCol", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Color Theme Changed.
///
@@ -213,6 +222,15 @@ namespace ASC.AuditTrail {
}
}
+ ///
+ /// Looks up a localized string similar to Country.
+ ///
+ public static string CountryCol {
+ get {
+ return ResourceManager.GetString("CountryCol", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Create.
///
diff --git a/common/services/ASC.AuditTrail/AuditReportResource.resx b/common/services/ASC.AuditTrail/AuditReportResource.resx
index 8e0a06e29a..04dd12f34a 100644
--- a/common/services/ASC.AuditTrail/AuditReportResource.resx
+++ b/common/services/ASC.AuditTrail/AuditReportResource.resx
@@ -777,4 +777,10 @@
Trash emptied
+
+ City
+
+
+ Country
+
\ No newline at end of file
diff --git a/common/services/ASC.AuditTrail/GlobalUsings.cs b/common/services/ASC.AuditTrail/GlobalUsings.cs
index acf8b3cfdf..6a230c86ac 100644
--- a/common/services/ASC.AuditTrail/GlobalUsings.cs
+++ b/common/services/ASC.AuditTrail/GlobalUsings.cs
@@ -35,7 +35,9 @@ global using ASC.AuditTrail.Types;
global using ASC.Common;
global using ASC.Common.Mapping;
global using ASC.Core;
+global using ASC.Core.Common.EF;
global using ASC.Core.Users;
+global using ASC.Geolocation;
global using ASC.MessagingSystem.Core;
global using ASC.MessagingSystem.EF.Context;
global using ASC.MessagingSystem.EF.Model;
diff --git a/common/services/ASC.AuditTrail/Mappers/AuditActionMapper.cs b/common/services/ASC.AuditTrail/Mappers/AuditActionMapper.cs
index c8d24164e5..20ef0ffbfc 100644
--- a/common/services/ASC.AuditTrail/Mappers/AuditActionMapper.cs
+++ b/common/services/ASC.AuditTrail/Mappers/AuditActionMapper.cs
@@ -46,7 +46,7 @@ public class AuditActionMapper
};
}
- public string GetActionText(MessageMaps action, AuditEventDto evt)
+ public string GetActionText(MessageMaps action, AuditEvent evt)
{
if (action == null)
{
@@ -78,7 +78,7 @@ public class AuditActionMapper
}
}
- public string GetActionText(MessageMaps action, LoginEventDto evt)
+ public string GetActionText(MessageMaps action, LoginEvent evt)
{
if (action == null)
{
diff --git a/common/services/ASC.AuditTrail/Models/AuditEventDto.cs b/common/services/ASC.AuditTrail/Models/AuditEvent.cs
similarity index 92%
rename from common/services/ASC.AuditTrail/Models/AuditEventDto.cs
rename to common/services/ASC.AuditTrail/Models/AuditEvent.cs
index 3636d412f9..af2038f560 100644
--- a/common/services/ASC.AuditTrail/Models/AuditEventDto.cs
+++ b/common/services/ASC.AuditTrail/Models/AuditEvent.cs
@@ -26,7 +26,7 @@
namespace ASC.AuditTrail.Models;
-public class AuditEventDto : BaseEvent, IMapFrom
+public class AuditEvent : BaseEvent, IMapFrom
{
public string Initiator { get; set; }
@@ -48,9 +48,9 @@ public class AuditEventDto : BaseEvent, IMapFrom
public override void Mapping(Profile profile)
{
- profile.CreateMap();
+ profile.CreateMap();
- profile.CreateMap()
+ profile.CreateMap()
.ConvertUsing();
}
}
\ No newline at end of file
diff --git a/common/services/ASC.AuditTrail/Models/AuditEventQuery.cs b/common/services/ASC.AuditTrail/Models/AuditEventQuery.cs
index c6b44ce0b9..378f986dc4 100644
--- a/common/services/ASC.AuditTrail/Models/AuditEventQuery.cs
+++ b/common/services/ASC.AuditTrail/Models/AuditEventQuery.cs
@@ -28,7 +28,7 @@ namespace ASC.AuditTrail.Models;
public class AuditEventQuery
{
- public AuditEvent Event { get; set; }
+ public DbAuditEvent Event { get; set; }
public string UserName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
diff --git a/common/services/ASC.AuditTrail/Models/LoginEventDto.cs b/common/services/ASC.AuditTrail/Models/LoginEvent.cs
similarity index 90%
rename from common/services/ASC.AuditTrail/Models/LoginEventDto.cs
rename to common/services/ASC.AuditTrail/Models/LoginEvent.cs
index 95f1dbf096..46fcffe219 100644
--- a/common/services/ASC.AuditTrail/Models/LoginEventDto.cs
+++ b/common/services/ASC.AuditTrail/Models/LoginEvent.cs
@@ -26,16 +26,16 @@
namespace ASC.AuditTrail.Models;
-public class LoginEventDto : BaseEvent, IMapFrom
+public class LoginEvent : BaseEvent, IMapFrom
{
public string Login { get; set; }
public int Action { get; set; }
public override void Mapping(Profile profile)
{
- profile.CreateMap();
+ profile.CreateMap();
- profile.CreateMap()
+ profile.CreateMap()
.ConvertUsing();
}
}
\ No newline at end of file
diff --git a/common/services/ASC.AuditTrail/Models/LoginEventQuery.cs b/common/services/ASC.AuditTrail/Models/LoginEventQuery.cs
index ae6aa29eeb..d90a5fb73e 100644
--- a/common/services/ASC.AuditTrail/Models/LoginEventQuery.cs
+++ b/common/services/ASC.AuditTrail/Models/LoginEventQuery.cs
@@ -28,7 +28,7 @@ namespace ASC.AuditTrail.Models;
public class LoginEventQuery
{
- public LoginEvent Event { get; set; }
+ public DbLoginEvent Event { get; set; }
public string UserName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
diff --git a/common/services/ASC.AuditTrail/Models/Mappings/EventTypeConverter.cs b/common/services/ASC.AuditTrail/Models/Mappings/EventTypeConverter.cs
index ea3b9f9a36..41f6a90604 100644
--- a/common/services/ASC.AuditTrail/Models/Mappings/EventTypeConverter.cs
+++ b/common/services/ASC.AuditTrail/Models/Mappings/EventTypeConverter.cs
@@ -29,8 +29,8 @@ using ASC.Core.Tenants;
namespace ASC.AuditTrail.Models.Mappings;
[Scope]
-internal class EventTypeConverter : ITypeConverter,
- ITypeConverter
+internal class EventTypeConverter : ITypeConverter,
+ ITypeConverter
{
private readonly UserFormatter _userFormatter;
private readonly AuditActionMapper _auditActionMapper;
@@ -49,9 +49,9 @@ internal class EventTypeConverter : ITypeConverter(source.Event);
+ var result = context.Mapper.Map(source.Event);
if (source.Event.DescriptionRaw != null)
{
@@ -95,11 +95,11 @@ internal class EventTypeConverter : ITypeConverter(source.Event);
+ var result = context.Mapper.Map(source.Event);
result.Target = _messageTarget.Parse(target);
diff --git a/common/services/ASC.AuditTrail/Repositories/AuditEventsRepository.cs b/common/services/ASC.AuditTrail/Repositories/AuditEventsRepository.cs
index 4c740c70dd..fe4bf184da 100644
--- a/common/services/ASC.AuditTrail/Repositories/AuditEventsRepository.cs
+++ b/common/services/ASC.AuditTrail/Repositories/AuditEventsRepository.cs
@@ -24,8 +24,6 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
-using ASC.Core.Common.EF;
-
namespace ASC.AuditTrail.Repositories;
[Scope(Additional = typeof(AuditEventsRepositoryExtensions))]
@@ -35,20 +33,23 @@ public class AuditEventsRepository
private readonly TenantManager _tenantManager;
private readonly IDbContextFactory _dbContextFactory;
private readonly IMapper _mapper;
+ private readonly GeolocationHelper _geolocationHelper;
public AuditEventsRepository(
AuditActionMapper auditActionMapper,
TenantManager tenantManager,
IDbContextFactory dbContextFactory,
- IMapper mapper)
+ IMapper mapper,
+ GeolocationHelper geolocationHelper)
{
_auditActionMapper = auditActionMapper;
_tenantManager = tenantManager;
_dbContextFactory = dbContextFactory;
_mapper = mapper;
+ _geolocationHelper = geolocationHelper;
}
- public async Task> GetByFilterAsync(
+ public async Task> GetByFilterAsync(
Guid? userId = null,
ProductType? productType = null,
ModuleType? moduleType = null,
@@ -77,7 +78,7 @@ public class AuditEventsRepository
withoutUserId);
}
- public async Task> GetByFilterWithActionsAsync(
+ public async Task> GetByFilterWithActionsAsync(
Guid? userId = null,
ProductType? productType = null,
ModuleType? moduleType = null,
@@ -204,7 +205,13 @@ public class AuditEventsRepository
{
query = query.Take(limit);
}
- return _mapper.Map, IEnumerable>(await query.ToListAsync());
+ var events = _mapper.Map, IEnumerable>(await query.ToListAsync());
+
+ foreach(var e in events)
+ {
+ await _geolocationHelper.AddGeolocationAsync(e);
+ }
+ return events;
}
private static void FindByEntry(IQueryable q, EntryType entry, string target, IEnumerable> actions)
diff --git a/common/services/ASC.AuditTrail/Repositories/LoginEventsRepository.cs b/common/services/ASC.AuditTrail/Repositories/LoginEventsRepository.cs
index 49ae4bf07a..3dbc736f32 100644
--- a/common/services/ASC.AuditTrail/Repositories/LoginEventsRepository.cs
+++ b/common/services/ASC.AuditTrail/Repositories/LoginEventsRepository.cs
@@ -32,18 +32,21 @@ public class LoginEventsRepository
private readonly TenantManager _tenantManager;
private readonly IDbContextFactory _dbContextFactory;
private readonly IMapper _mapper;
+ private readonly GeolocationHelper _geolocationHelper;
public LoginEventsRepository(
TenantManager tenantManager,
IDbContextFactory dbContextFactory,
- IMapper mapper)
+ IMapper mapper,
+ GeolocationHelper geolocationHelper)
{
_tenantManager = tenantManager;
_dbContextFactory = dbContextFactory;
_mapper = mapper;
+ _geolocationHelper = geolocationHelper;
}
- public async Task> GetByFilterAsync(
+ public async Task> GetByFilterAsync(
Guid? login = null,
MessageAction? action = null,
DateTime? fromDate = null,
@@ -108,7 +111,13 @@ public class LoginEventsRepository
}
}
- return _mapper.Map, IEnumerable>(await query.ToListAsync());
+ var events = _mapper.Map, IEnumerable>(await query.ToListAsync());
+
+ foreach (var e in events)
+ {
+ await _geolocationHelper.AddGeolocationAsync(e);
+ }
+ return events;
}
}
diff --git a/packages/client/src/components/FilesSelector/FilesSelector.types.ts b/packages/client/src/components/FilesSelector/FilesSelector.types.ts
index cdf23ebde7..b63db08782 100644
--- a/packages/client/src/components/FilesSelector/FilesSelector.types.ts
+++ b/packages/client/src/components/FilesSelector/FilesSelector.types.ts
@@ -42,6 +42,22 @@ export type useLoadersHelperProps = {
items: Item[] | null;
};
+export type setItemsCallback = (value: Item[] | null) => Item[] | null;
+export type setBreadCrumbsCallback = (
+ value: BreadCrumb[] | []
+) => BreadCrumb[] | [];
+export type setTotalCallback = (value: number) => number;
+
+export type useSocketHelperProps = {
+ socketHelper: any;
+ socketSubscribersId: Set;
+ setItems: (callback: setItemsCallback) => void;
+ setBreadCrumbs: (callback: setBreadCrumbsCallback) => void;
+ setTotal: (callback: setTotalCallback) => void;
+ disabledItems: string[] | number[];
+ filterParam?: string;
+};
+
export type useRootHelperProps = {
setBreadCrumbs: (items: BreadCrumb[]) => void;
setIsBreadCrumbsLoading: (value: boolean) => void;
@@ -81,6 +97,7 @@ export type useFilesHelpersProps = {
onSelectTreeNode?: (treeNode: any) => void;
setSelectedTreeNode: (treeNode: any) => void;
filterParam?: string;
+ getRootData?: () => Promise;
};
export type FilesSelectorProps = {
@@ -152,4 +169,9 @@ export type FilesSelectorProps = {
descriptionText?: string;
setSelectedItems: () => void;
+
+ includeFolder?: boolean;
+
+ socketHelper: any;
+ socketSubscribersId: Set;
};
diff --git a/packages/client/src/components/FilesSelector/helpers/useFilesHelper.ts b/packages/client/src/components/FilesSelector/helpers/useFilesHelper.ts
index 602a8d6b59..b1acca8d22 100644
--- a/packages/client/src/components/FilesSelector/helpers/useFilesHelper.ts
+++ b/packages/client/src/components/FilesSelector/helpers/useFilesHelper.ts
@@ -171,7 +171,7 @@ const getIconUrl = (extension: string, isImage: boolean, isMedia: boolean) => {
return iconSize32.get(path);
};
-const convertFoldersToItems = (
+export const convertFoldersToItems = (
folders: any,
disabledItems: any[],
filterParam?: string
@@ -215,9 +215,17 @@ const convertFoldersToItems = (
return items;
};
-const convertFilesToItems = (files: any, filterParam?: string) => {
+export const convertFilesToItems = (files: any, filterParam?: string) => {
const items = files.map((file: any) => {
- const { id, title, security, parentId, rootFolderType, fileExst } = file;
+ const {
+ id,
+ title,
+ security,
+ parentId,
+ folderId,
+ rootFolderType,
+ fileExst,
+ } = file;
const isImage = file.viewAccessability.ImageView;
const isMedia = file.viewAccessability.MediaView;
@@ -231,9 +239,8 @@ const convertFilesToItems = (files: any, filterParam?: string) => {
label: title.replace(fileExst, ""),
title,
icon,
-
security,
- parentId,
+ parentId: parentId || folderId,
rootFolderType,
isFolder: false,
isDisabled: !filterParam,
@@ -259,6 +266,7 @@ export const useFilesHelper = ({
onSelectTreeNode,
setSelectedTreeNode,
filterParam,
+ getRootData,
}: useFilesHelpersProps) => {
const getFileList = React.useCallback(
async (
@@ -305,70 +313,88 @@ export const useFilesHelper = ({
filter.folder = id.toString();
- const currentFolder = await getFolder(id, filter);
+ try {
+ if (isInit && getRootData) {
+ const folder = await getFolderInfo(id);
- const { folders, files, total, count, pathParts, current } =
- currentFolder;
+ if (
+ folder.rootFolderType === FolderType.TRASH ||
+ folder.rootFolderType === FolderType.Archive
+ ) {
+ await getRootData();
- setSelectedItemSecurity(current.security);
-
- const foldersList: Item[] = convertFoldersToItems(
- folders,
- disabledItems,
- filterParam
- );
-
- const filesList: Item[] = convertFilesToItems(files, filterParam);
-
- const itemList = [...foldersList, ...filesList];
-
- setHasNextPage(count === PAGE_COUNT);
-
- onSelectTreeNode && setSelectedTreeNode({ ...current, path: pathParts });
-
- if (isInit) {
- if (isThirdParty) {
- const breadCrumbs: BreadCrumb[] = [
- { label: current.title, isRoom: false, id: current.id },
- ];
-
- setBreadCrumbs(breadCrumbs);
- setIsBreadCrumbsLoading(false);
- } else {
- const breadCrumbs: BreadCrumb[] = await Promise.all(
- pathParts.map(async (folderId: number | string) => {
- const folderInfo: any = await getFolderInfo(folderId);
-
- const { title, id, parentId, rootFolderType, roomType } =
- folderInfo;
-
- return {
- label: title,
- id: id,
- isRoom: parentId === 0 && rootFolderType === FolderType.Rooms,
- roomType,
- };
- })
- );
-
- breadCrumbs.unshift({ ...defaultBreadCrumb });
-
- setBreadCrumbs(breadCrumbs);
- setIsBreadCrumbsLoading(false);
+ return;
+ }
}
- }
- if (isFirstLoad || startIndex === 0) {
- setTotal(total);
- setItems(itemList);
- } else {
- setItems((prevState: Item[] | null) => {
- if (prevState) return [...prevState, ...itemList];
- return [...itemList];
- });
+ const currentFolder = await getFolder(id, filter);
+
+ const { folders, files, total, count, pathParts, current } =
+ currentFolder;
+
+ setSelectedItemSecurity(current.security);
+
+ const foldersList: Item[] = convertFoldersToItems(
+ folders,
+ disabledItems,
+ filterParam
+ );
+
+ const filesList: Item[] = convertFilesToItems(files, filterParam);
+
+ const itemList = [...foldersList, ...filesList];
+
+ setHasNextPage(count === PAGE_COUNT);
+
+ onSelectTreeNode &&
+ setSelectedTreeNode({ ...current, path: pathParts });
+
+ if (isInit) {
+ if (isThirdParty) {
+ const breadCrumbs: BreadCrumb[] = [
+ { label: current.title, isRoom: false, id: current.id },
+ ];
+
+ setBreadCrumbs(breadCrumbs);
+ setIsBreadCrumbsLoading(false);
+ } else {
+ const breadCrumbs: BreadCrumb[] = await Promise.all(
+ pathParts.map(async (folderId: number | string) => {
+ const folderInfo: any = await getFolderInfo(folderId);
+
+ const { title, id, parentId, rootFolderType, roomType } =
+ folderInfo;
+
+ return {
+ label: title,
+ id: id,
+ isRoom: parentId === 0 && rootFolderType === FolderType.Rooms,
+ roomType,
+ };
+ })
+ );
+
+ breadCrumbs.unshift({ ...defaultBreadCrumb });
+
+ setBreadCrumbs(breadCrumbs);
+ setIsBreadCrumbsLoading(false);
+ }
+ }
+
+ if (isFirstLoad || startIndex === 0) {
+ setTotal(total);
+ setItems(itemList);
+ } else {
+ setItems((prevState: Item[] | null) => {
+ if (prevState) return [...prevState, ...itemList];
+ return [...itemList];
+ });
+ }
+ setIsRoot(false);
+ setIsNextPageLoading(false);
+ } catch (e) {
+ getRootData && getRootData();
}
- setIsRoot(false);
- setIsNextPageLoading(false);
},
[selectedItemId, searchValue, isFirstLoad, disabledItems]
);
diff --git a/packages/client/src/components/FilesSelector/helpers/useRoomsHelper.ts b/packages/client/src/components/FilesSelector/helpers/useRoomsHelper.ts
index 8312347509..12b7125dfd 100644
--- a/packages/client/src/components/FilesSelector/helpers/useRoomsHelper.ts
+++ b/packages/client/src/components/FilesSelector/helpers/useRoomsHelper.ts
@@ -32,7 +32,7 @@ const getRoomLogo = (roomType: number) => {
return iconSize32.get(path);
};
-const convertRoomsToItems = (rooms: any) => {
+export const convertRoomsToItems = (rooms: any) => {
const items = rooms.map((room: any) => {
const {
id,
diff --git a/packages/client/src/components/FilesSelector/helpers/useSocketHelper.ts b/packages/client/src/components/FilesSelector/helpers/useSocketHelper.ts
new file mode 100644
index 0000000000..e7a80dc115
--- /dev/null
+++ b/packages/client/src/components/FilesSelector/helpers/useSocketHelper.ts
@@ -0,0 +1,235 @@
+import React from "react";
+
+import { convertFilesToItems, convertFoldersToItems } from "./useFilesHelper";
+
+import {
+ Item,
+ setItemsCallback,
+ useSocketHelperProps,
+} from "../FilesSelector.types";
+import { convertRoomsToItems } from "./useRoomsHelper";
+
+const useSocketHelper = ({
+ socketHelper,
+ socketSubscribersId,
+ setItems,
+ setBreadCrumbs,
+ setTotal,
+ disabledItems,
+ filterParam,
+}: useSocketHelperProps) => {
+ const subscribedId = React.useRef(null);
+
+ const subscribe = (id: number) => {
+ const roomParts = `DIR-${id}`;
+
+ if (socketSubscribersId.has(roomParts)) return (subscribedId.current = id);
+
+ if (subscribedId.current && !socketSubscribersId.has(roomParts)) {
+ unsubscribe(subscribedId.current, false);
+ }
+
+ socketHelper.emit({
+ command: "subscribe",
+ data: {
+ roomParts: `DIR-${id}`,
+ individual: true,
+ },
+ });
+
+ subscribedId.current = id;
+ };
+
+ const unsubscribe = (id: number, clear = true) => {
+ if (clear) {
+ subscribedId.current = null;
+ }
+
+ if (id && !socketSubscribersId.has(`DIR-${id}`)) {
+ socketHelper.emit({
+ command: "unsubscribe",
+ data: {
+ roomParts: `DIR-${id}`,
+ individual: true,
+ },
+ });
+ }
+ };
+
+ const addItem = React.useCallback((opt: any) => {
+ if (!opt?.data) return;
+
+ const data = JSON.parse(opt.data);
+
+ if (
+ data.folderId
+ ? data.folderId !== subscribedId.current
+ : data.parentId !== subscribedId.current
+ )
+ return;
+
+ let item: null | Item = null;
+
+ if (opt?.type === "file") {
+ item = convertFilesToItems([data], filterParam)[0];
+ } else if (opt?.type === "folder") {
+ item = !!data.roomType
+ ? convertRoomsToItems([data])[0]
+ : convertFoldersToItems([data], disabledItems, filterParam)[0];
+ }
+
+ const callback: setItemsCallback = (value: Item[] | null) => {
+ if (!item || !value) return value;
+
+ if (opt.type === "folder") {
+ setTotal((value) => value + 1);
+
+ return [item, ...value];
+ }
+
+ if (opt.type === "file") {
+ let idx = 0;
+
+ for (let i = 0; i < value.length - 1; i++) {
+ if (!value[i].isFolder) break;
+
+ idx = i + 1;
+ }
+
+ const newValue = [...value];
+
+ newValue.splice(idx, 0, item);
+
+ setTotal((value) => value + 1);
+
+ return newValue;
+ }
+
+ return value;
+ };
+
+ setItems(callback);
+ }, []);
+
+ const updateItem = React.useCallback((opt: any) => {
+ if (!opt?.data) return;
+
+ const data = JSON.parse(opt.data);
+
+ if (
+ ((data.folderId && data.folderId !== subscribedId.current) ||
+ (data.parentId && data.parentId !== subscribedId.current)) &&
+ data.id !== subscribedId.current
+ )
+ return;
+
+ let item: null | Item = null;
+
+ if (opt?.type === "file") {
+ item = convertFilesToItems([data], filterParam)[0];
+ } else if (opt?.type === "folder") {
+ item = !!data.roomType
+ ? convertRoomsToItems([data])[0]
+ : convertFoldersToItems([data], disabledItems, filterParam)[0];
+ }
+
+ if (item?.id === subscribedId.current) {
+ return setBreadCrumbs((value) => {
+ if (!value) return value;
+
+ const newValue = [...value];
+
+ if (newValue[newValue.length - 1].id === item?.id) {
+ newValue[newValue.length - 1].label = item.label;
+ }
+
+ return newValue;
+ });
+ }
+
+ const callback: setItemsCallback = (value: Item[] | null) => {
+ if (!item || !value) return value;
+
+ if (opt.type === "folder") {
+ const idx = value.findIndex((v) => v.id === item?.id && v.isFolder);
+
+ if (idx > -1) {
+ const newValue = [...value];
+
+ newValue.splice(idx, 1, item);
+
+ return newValue;
+ }
+
+ setBreadCrumbs((breadCrumbsValue) => {
+ return breadCrumbsValue;
+ });
+ }
+
+ if (opt.type === "file") {
+ const idx = value.findIndex((v) => v.id === item?.id && !v.isFolder);
+
+ if (idx > -1) {
+ const newValue = [...value];
+
+ newValue.splice(idx, 1, item);
+
+ return [...newValue];
+ }
+ }
+
+ return value;
+ };
+
+ setItems(callback);
+ }, []);
+
+ const deleteItem = React.useCallback((opt: any) => {
+ const callback: setItemsCallback = (value: Item[] | null) => {
+ if (!value) return value;
+
+ if (opt.type === "folder") {
+ const newValue = value.filter((v) => +v.id !== +opt?.id || !v.isFolder);
+
+ if (newValue.length !== value.length) {
+ setTotal((value) => value - 1);
+ }
+
+ return newValue;
+ }
+ if (opt.type === "file") {
+ const newValue = value.filter((v) => +v.id !== +opt?.id || v.isFolder);
+
+ if (newValue.length !== value.length) {
+ setTotal((value) => value - 1);
+ }
+
+ return newValue;
+ }
+
+ return value;
+ };
+
+ setItems(callback);
+ }, []);
+
+ React.useEffect(() => {
+ socketHelper.on("s:modify-folder", async (opt: any) => {
+ switch (opt?.cmd) {
+ case "create":
+ addItem(opt);
+ break;
+ case "update":
+ updateItem(opt);
+ break;
+ case "delete":
+ deleteItem(opt);
+ break;
+ }
+ });
+ }, [addItem, updateItem, deleteItem]);
+
+ return { subscribe, unsubscribe };
+};
+
+export default useSocketHelper;
diff --git a/packages/client/src/components/FilesSelector/index.tsx b/packages/client/src/components/FilesSelector/index.tsx
index 6ca943780a..be1c343bb4 100644
--- a/packages/client/src/components/FilesSelector/index.tsx
+++ b/packages/client/src/components/FilesSelector/index.tsx
@@ -29,6 +29,7 @@ import useRoomsHelper from "./helpers/useRoomsHelper";
import useLoadersHelper from "./helpers/useLoadersHelper";
import useFilesHelper from "./helpers/useFilesHelper";
import { getAcceptButtonLabel, getHeaderLabel, getIsDisabled } from "./utils";
+import useSocketHelper from "./helpers/useSocketHelper";
const FilesSelector = ({
isPanelVisible = false,
@@ -82,6 +83,11 @@ const FilesSelector = ({
descriptionText,
setSelectedItems,
+
+ includeFolder,
+
+ socketHelper,
+ socketSubscribersId,
setMoveToPublicRoomVisible,
}: FilesSelectorProps) => {
const { t } = useTranslation(["Files", "Common", "Translations"]);
@@ -113,6 +119,16 @@ const FilesSelector = ({
const [isRequestRunning, setIsRequestRunning] =
React.useState(false);
+ const { subscribe, unsubscribe } = useSocketHelper({
+ socketHelper,
+ socketSubscribersId,
+ setItems,
+ setBreadCrumbs,
+ setTotal,
+ disabledItems,
+ filterParam,
+ });
+
const {
setIsBreadCrumbsLoading,
isNextPageLoading,
@@ -162,6 +178,7 @@ const FilesSelector = ({
onSelectTreeNode,
setSelectedTreeNode,
filterParam,
+ getRootData,
});
const onSelectAction = (item: Item) => {
@@ -193,6 +210,13 @@ const FilesSelector = ({
}
};
+ React.useEffect(() => {
+ if (!selectedItemId) return;
+ if (selectedItemId && isRoot) return unsubscribe(+selectedItemId);
+
+ subscribe(+selectedItemId);
+ }, [selectedItemId, isRoot]);
+
React.useEffect(() => {
if (!withoutBasicSelection) {
onSelectFolder && onSelectFolder(currentFolderId);
@@ -419,7 +443,8 @@ const FilesSelector = ({
isRequestRunning,
selectedItemSecurity,
filterParam,
- !!selectedFileInfo
+ !!selectedFileInfo,
+ includeFolder
);
return (
@@ -521,7 +546,12 @@ export default inject(
}: any,
{ isCopy, isRestoreAll, isMove, isPanelVisible, id, passedFoldersTree }: any
) => {
- const { id: selectedId, parentId, rootFolderType } = selectedFolderStore;
+ const {
+ id: selectedId,
+ parentId,
+ rootFolderType,
+ socketSubscribersId,
+ } = selectedFolderStore;
const { setConflictDialogData, checkFileConflicts, setSelectedItems } =
filesActionsStore;
@@ -558,10 +588,15 @@ export default inject(
setMoveToPublicRoomVisible,
} = dialogsStore;
- const { theme } = auth.settingsStore;
+ const { theme, socketHelper } = auth.settingsStore;
- const { selection, bufferSelection, filesList, setMovingInProgress } =
- filesStore;
+ const {
+ selection,
+ bufferSelection,
+ filesList,
+
+ setMovingInProgress,
+ } = filesStore;
const selections =
isMove || isCopy || isRestoreAll
@@ -588,6 +623,9 @@ export default inject(
}
});
+ const includeFolder =
+ selectionsWithoutEditing.filter((i: any) => i.isFolder).length > 0;
+
return {
currentFolderId,
fromFolderId,
@@ -612,6 +650,9 @@ export default inject(
setRestoreAllPanelVisible,
setIsFolderActions,
setSelectedItems,
+ includeFolder,
+ socketHelper,
+ socketSubscribersId,
setMoveToPublicRoomVisible,
};
}
diff --git a/packages/client/src/components/FilesSelector/utils.ts b/packages/client/src/components/FilesSelector/utils.ts
index 3d79b4f38f..296b6cc05c 100644
--- a/packages/client/src/components/FilesSelector/utils.ts
+++ b/packages/client/src/components/FilesSelector/utils.ts
@@ -56,12 +56,14 @@ export const getIsDisabled = (
isRequestRunning?: boolean,
security?: Security,
filterParam?: string,
- isFileSelected?: boolean
+ isFileSelected?: boolean,
+ includeFolder?: boolean
) => {
if (isFirstLoad) return true;
if (isRequestRunning) return true;
if (!!filterParam) return !isFileSelected;
if (sameId && !isCopy) return true;
+ if (sameId && isCopy && includeFolder) return true;
if (isRooms) return true;
if (isRoot) return true;
if (isCopy) return !security?.CopyTo;
diff --git a/packages/client/src/store/FilesStore.js b/packages/client/src/store/FilesStore.js
index c24ef51603..7414f3ea1e 100644
--- a/packages/client/src/store/FilesStore.js
+++ b/packages/client/src/store/FilesStore.js
@@ -165,6 +165,21 @@ class FilesStore {
const { socketHelper } = authStore.settingsStore;
socketHelper.on("s:modify-folder", async (opt) => {
+ const { socketSubscribersId } = this.selectedFolderStore;
+ if (opt && opt.data) {
+ const data = JSON.parse(opt.data);
+
+ const pathParts = data.folderId
+ ? `DIR-${data.folderId}`
+ : `DIR-${data.parentId}`;
+
+ if (
+ !socketSubscribersId.has(pathParts) &&
+ !socketSubscribersId.has(`DIR-${data.id}`)
+ )
+ return;
+ }
+
console.log("[WS] s:modify-folder", opt);
if (!(this.clientLoadingStore.isLoading || this.operationAction))
@@ -200,6 +215,11 @@ class FilesStore {
});
socketHelper.on("refresh-folder", (id) => {
+ const { socketSubscribersId } = this.selectedFolderStore;
+ const pathParts = `DIR-${id}`;
+
+ if (!socketSubscribersId.has(pathParts)) return;
+
if (!id || this.clientLoadingStore.isLoading) return;
//console.log(
@@ -216,6 +236,11 @@ class FilesStore {
});
socketHelper.on("s:markasnew-folder", ({ folderId, count }) => {
+ const { socketSubscribersId } = this.selectedFolderStore;
+ const pathParts = `DIR-${folderId}`;
+
+ if (!socketSubscribersId.has(pathParts)) return;
+
console.log(`[WS] markasnew-folder ${folderId}:${count}`);
const foundIndex =
@@ -229,6 +254,11 @@ class FilesStore {
});
socketHelper.on("s:markasnew-file", ({ fileId, count }) => {
+ const { socketSubscribersId } = this.selectedFolderStore;
+ const pathParts = `FILE-${fileId}`;
+
+ if (!socketSubscribersId.has(pathParts)) return;
+
console.log(`[WS] markasnew-file ${fileId}:${count}`);
const foundIndex = fileId && this.files.findIndex((x) => x.id === fileId);
@@ -246,6 +276,11 @@ class FilesStore {
//WAIT FOR RESPONSES OF EDITING FILE
socketHelper.on("s:start-edit-file", (id) => {
+ const { socketSubscribersId } = this.selectedFolderStore;
+ const pathParts = `FILE-${id}`;
+
+ if (!socketSubscribersId.has(pathParts)) return;
+
const foundIndex = this.files.findIndex((x) => x.id === id);
if (foundIndex == -1) return;
@@ -264,6 +299,11 @@ class FilesStore {
});
socketHelper.on("s:stop-edit-file", (id) => {
+ const { socketSubscribersId } = this.selectedFolderStore;
+ const pathParts = `FILE-${id}`;
+
+ if (!socketSubscribersId.has(pathParts)) return;
+
const foundIndex = this.files.findIndex((x) => x.id === id);
if (foundIndex == -1) return;
@@ -811,9 +851,14 @@ class FilesStore {
setFiles = (files) => {
const { socketHelper } = this.authStore.settingsStore;
+ const { addSocketSubscribersId, deleteSocketSubscribersId } =
+ this.selectedFolderStore;
if (files.length === 0 && this.files.length === 0) return;
if (this.files?.length > 0) {
+ this.files.forEach((f) => {
+ deleteSocketSubscribersId(`FILE-${f.id}`);
+ });
socketHelper.emit({
command: "unsubscribe",
data: {
@@ -826,6 +871,10 @@ class FilesStore {
this.files = files;
if (this.files?.length > 0) {
+ this.files.forEach((f) => {
+ addSocketSubscribersId(`FILE-${f.id}`);
+ });
+
socketHelper.emit({
command: "subscribe",
data: {
@@ -843,33 +892,38 @@ class FilesStore {
};
setFolders = (folders) => {
+ const { addSocketSubscribersId, deleteSocketSubscribersId } =
+ this.selectedFolderStore;
const { socketHelper } = this.authStore.settingsStore;
if (folders.length === 0 && this.folders.length === 0) return;
if (this.folders?.length > 0) {
- this.folders.forEach((f) =>
- socketHelper.emit({
- command: "unsubscribe",
- data: {
- roomParts: `DIR-${f.id}`,
- individual: true,
- },
- })
- );
+ this.folders.forEach((f) => {
+ deleteSocketSubscribersId(`DIR-${f.id}`);
+ });
+
+ socketHelper.emit({
+ command: "unsubscribe",
+ data: {
+ roomParts: this.folders.map((f) => `DIR-${f.id}`),
+ individual: true,
+ },
+ });
}
this.folders = folders;
if (this.folders?.length > 0) {
- this.folders.forEach((f) =>
- socketHelper.emit({
- command: "subscribe",
- data: {
- roomParts: `DIR-${f.id}`,
- individual: true,
- },
- })
- );
+ this.folders.forEach((f) => {
+ addSocketSubscribersId(`DIR-${f.id}`);
+ });
+ socketHelper.emit({
+ command: "subscribe",
+ data: {
+ roomParts: this.folders.map((f) => `DIR-${f.id}`),
+ individual: true,
+ },
+ });
}
};
diff --git a/packages/client/src/store/SelectedFolderStore.js b/packages/client/src/store/SelectedFolderStore.js
index e6b5493718..68b9b06da5 100644
--- a/packages/client/src/store/SelectedFolderStore.js
+++ b/packages/client/src/store/SelectedFolderStore.js
@@ -32,6 +32,8 @@ class SelectedFolderStore {
settingsStore = null;
security = null;
+ socketSubscribersId = new Set();
+
constructor(settingsStore) {
makeAutoObservable(this);
this.settingsStore = settingsStore;
@@ -69,6 +71,7 @@ class SelectedFolderStore {
this.tags = null;
this.rootFolderId = null;
this.security = null;
+ this.socketSubscribersId = new Set();
};
setParentId = (parentId) => {
@@ -112,6 +115,14 @@ class SelectedFolderStore {
};
};
+ addSocketSubscribersId = (path) => {
+ this.socketSubscribersId.add(path);
+ };
+
+ deleteSocketSubscribersId = (path) => {
+ this.socketSubscribersId.delete(path);
+ };
+
setSelectedFolder = (selectedFolder) => {
const { socketHelper } = this.settingsStore;
@@ -120,6 +131,8 @@ class SelectedFolderStore {
command: "unsubscribe",
data: { roomParts: `DIR-${this.id}`, individual: true },
});
+
+ this.deleteSocketSubscribersId(`DIR-${this.id}`);
}
if (selectedFolder) {
@@ -127,6 +140,8 @@ class SelectedFolderStore {
command: "subscribe",
data: { roomParts: `DIR-${selectedFolder.id}`, individual: true },
});
+
+ this.addSocketSubscribersId(`DIR-${selectedFolder.id}`);
}
if (!selectedFolder) {
diff --git a/packages/client/src/store/TreeFoldersStore.js b/packages/client/src/store/TreeFoldersStore.js
index d27879402f..788adc28d4 100644
--- a/packages/client/src/store/TreeFoldersStore.js
+++ b/packages/client/src/store/TreeFoldersStore.js
@@ -29,8 +29,14 @@ class TreeFoldersStore {
listenTreeFolders = (treeFolders) => {
const { socketHelper } = this.authStore.settingsStore;
+ const { addSocketSubscribersId, deleteSocketSubscribersId } =
+ this.selectedFolderStore;
if (treeFolders.length > 0) {
+ treeFolders.forEach((f) => {
+ deleteSocketSubscribersId(`DIR-${f.id}`);
+ });
+
socketHelper.emit({
command: "unsubscribe",
data: {
@@ -39,6 +45,10 @@ class TreeFoldersStore {
},
});
+ treeFolders.forEach((f) => {
+ addSocketSubscribersId(`DIR-${f.id}`);
+ });
+
socketHelper.emit({
command: "subscribe",
data: {
diff --git a/packages/components/selector/sub-components/Item/index.tsx b/packages/components/selector/sub-components/Item/index.tsx
index da3f9908d8..72b5ba44ff 100644
--- a/packages/components/selector/sub-components/Item/index.tsx
+++ b/packages/components/selector/sub-components/Item/index.tsx
@@ -22,6 +22,7 @@ const compareFunction = (prevProps: ItemProps, nextProps: ItemProps) => {
return (
prevItem?.id === nextItem?.id &&
+ prevItem?.label === nextItem?.label &&
prevItem?.isSelected === nextItem?.isSelected
);
};
diff --git a/products/ASC.Files/Core/Configuration/ProductEntryPoint.cs b/products/ASC.Files/Core/Configuration/ProductEntryPoint.cs
index 69fbc89504..bcfee8004e 100644
--- a/products/ASC.Files/Core/Configuration/ProductEntryPoint.cs
+++ b/products/ASC.Files/Core/Configuration/ProductEntryPoint.cs
@@ -142,7 +142,7 @@ public class ProductEntryPoint : Product
public override async Task> GetAuditEventsAsync(DateTime scheduleDate, Guid userId, Tenant tenant, WhatsNewType whatsNewType)
{
- IEnumerable events;
+ IEnumerable events;
_tenantManager.SetCurrentTenant(tenant);
if (whatsNewType == WhatsNewType.RoomsActivity)
diff --git a/products/ASC.Files/Core/Core/Dao/TeamlabDao/FolderDao.cs b/products/ASC.Files/Core/Core/Dao/TeamlabDao/FolderDao.cs
index 6b85105c74..976006162b 100644
--- a/products/ASC.Files/Core/Core/Dao/TeamlabDao/FolderDao.cs
+++ b/products/ASC.Files/Core/Core/Dao/TeamlabDao/FolderDao.cs
@@ -212,7 +212,7 @@ internal class FolderDao : AbstractDao, IFolderDao
{
return 0;
}
-
+
await using var filesDbContext = _dbContextFactory.CreateDbContext();
if (filterType == FilterType.None && subjectId == default && string.IsNullOrEmpty(searchText) && !withSubfolders && !excludeSubject)
@@ -225,7 +225,7 @@ internal class FolderDao : AbstractDao, IFolderDao
return await q.CountAsync();
}
- public async IAsyncEnumerable> GetFoldersAsync(int parentId, OrderBy orderBy, FilterType filterType, bool subjectGroup, Guid subjectID, string searchText, bool withSubfolders = false,
+ public async IAsyncEnumerable> GetFoldersAsync(int parentId, OrderBy orderBy, FilterType filterType, bool subjectGroup, Guid subjectID, string searchText, bool withSubfolders = false,
bool excludeSubject = false, int offset = 0, int count = -1)
{
if (CheckInvalidFilter(filterType) || count == 0)
@@ -320,10 +320,10 @@ internal class FolderDao : AbstractDao, IFolderDao
{
var roomTypes = new List
{
- FolderType.CustomRoom,
- FolderType.ReviewRoom,
- FolderType.FillingFormsRoom,
- FolderType.EditingRoom,
+ FolderType.CustomRoom,
+ FolderType.ReviewRoom,
+ FolderType.FillingFormsRoom,
+ FolderType.EditingRoom,
FolderType.ReadOnlyRoom,
FolderType.PublicRoom,
};
@@ -528,6 +528,7 @@ internal class FolderDao : AbstractDao, IFolderDao
await Queries.DeleteBunchObjectsAsync(filesDbContext, TenantID, folderId.ToString());
+ await filesDbContext.SaveChangesAsync();
await tx.CommitAsync();
await RecalculateFoldersCountAsync(parent);
});
@@ -1216,14 +1217,14 @@ internal class FolderDao : AbstractDao, IFolderDao
{
var roomTypes = new List
{
- FolderType.CustomRoom,
- FolderType.ReviewRoom,
- FolderType.FillingFormsRoom,
- FolderType.EditingRoom,
+ FolderType.CustomRoom,
+ FolderType.ReviewRoom,
+ FolderType.FillingFormsRoom,
+ FolderType.EditingRoom,
FolderType.ReadOnlyRoom,
FolderType.PublicRoom
};
-
+
Expression> filter = f => roomTypes.Contains(f.FolderType);
await foreach (var e in GetFeedsInternalAsync(tenant, from, to, filter, null))
@@ -1295,16 +1296,16 @@ internal class FolderDao : AbstractDao, IFolderDao
public async IAsyncEnumerable GetTenantsWithRoomsFeedsAsync(DateTime fromTime)
{
- var roomTypes = new List
- {
- FolderType.CustomRoom,
- FolderType.ReviewRoom,
- FolderType.FillingFormsRoom,
- FolderType.EditingRoom,
+ var roomTypes = new List
+ {
+ FolderType.CustomRoom,
+ FolderType.ReviewRoom,
+ FolderType.FillingFormsRoom,
+ FolderType.EditingRoom,
FolderType.ReadOnlyRoom,
FolderType.PublicRoom,
};
-
+
Expression> filter = f => roomTypes.Contains(f.FolderType);
await foreach (var q in GetTenantsWithFeeds(fromTime, filter, true))
@@ -1331,7 +1332,7 @@ internal class FolderDao : AbstractDao, IFolderDao
if (rootFolderType != FolderType.VirtualRooms && rootFolderType != FolderType.Archive)
{
- return (-1,"");
+ return (-1, "");
}
var rootFolderId = Convert.ToInt32(fileEntry.RootId);
@@ -1348,7 +1349,7 @@ internal class FolderDao : AbstractDao, IFolderDao
{
return (entryId, fileEntry.Title);
}
-
+
await using var filesDbContext = _dbContextFactory.CreateDbContext();
var parentFolders = await Queries.ParentIdTitlePairAsync(filesDbContext, folderId).ToListAsync();
@@ -1525,7 +1526,7 @@ internal class FolderDao : AbstractDao, IFolderDao
{
return (await _globalStore.GetStoreAsync()).CreateDataWriteOperator(chunkedUploadSession, sessionHolder);
}
-
+
private async Task> GetFoldersQueryWithFilters(int parentId, OrderBy orderBy, bool subjectGroup, Guid subjectId, string searchText, bool withSubfolders, bool excludeSubject,
FilesDbContext filesDbContext)
{
diff --git a/products/ASC.Files/Core/Services/WCFService/FileOperations/FileMoveCopyOperation.cs b/products/ASC.Files/Core/Services/WCFService/FileOperations/FileMoveCopyOperation.cs
index b410cbbd7f..1a53a8beea 100644
--- a/products/ASC.Files/Core/Services/WCFService/FileOperations/FileMoveCopyOperation.cs
+++ b/products/ASC.Files/Core/Services/WCFService/FileOperations/FileMoveCopyOperation.cs
@@ -46,7 +46,7 @@ internal class FileMoveCopyOperationData : FileOperationData
public FileConflictResolveType ResolveType { get; }
public IDictionary Headers { get; }
- public FileMoveCopyOperationData(IEnumerable folders, IEnumerable files, Tenant tenant, JsonElement toFolderId, bool copy, FileConflictResolveType resolveType,
+ public FileMoveCopyOperationData(IEnumerable folders, IEnumerable files, Tenant tenant, JsonElement toFolderId, bool copy, FileConflictResolveType resolveType,
ExternalShareData externalShareData, bool holdResult = true, IDictionary headers = null)
: base(folders, files, tenant, externalShareData, holdResult)
{
@@ -471,7 +471,7 @@ class FileMoveCopyOperation : FileOperation, T>
newFolderId = await FolderDao.MoveFolderAsync(folder.Id, toFolderId, CancellationToken);
var (name, value) = await tenantQuotaFeatureStatHelper.GetStatAsync();
- _ = quotaSocketManager.ChangeQuotaUsedValueAsync(name, value);
+ _ = quotaSocketManager.ChangeQuotaUsedValueAsync(name, value);
}
else
{
@@ -581,7 +581,7 @@ class FileMoveCopyOperation : FileOperation, T>
{
this[Err] = FilesCommonResource.ErrorMassage_SecurityException_MoveFile;
}
- else if (checkPermissions && !await FilesSecurity.CanDownloadAsync(file))
+ else if (checkPermissions && file.RootFolderType != FolderType.TRASH && !await FilesSecurity.CanDownloadAsync(file))
{
this[Err] = FilesCommonResource.ErrorMassage_SecurityException;
}
@@ -722,7 +722,7 @@ class FileMoveCopyOperation : FileOperation, T>
{
foreach (var size in _thumbnailSettings.Sizes)
{
- await(await globalStorage.GetStoreAsync()).CopyAsync(String.Empty,
+ await (await globalStorage.GetStoreAsync()).CopyAsync(String.Empty,
FileDao.GetUniqThumbnailPath(file, size.Width, size.Height),
String.Empty,
fileDao.GetUniqThumbnailPath(newFile, size.Width, size.Height));
diff --git a/products/ASC.People/Server/Api/UserController.cs b/products/ASC.People/Server/Api/UserController.cs
index cd1373cd02..bc5fffa717 100644
--- a/products/ASC.People/Server/Api/UserController.cs
+++ b/products/ASC.People/Server/Api/UserController.cs
@@ -24,6 +24,8 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
+using Microsoft.AspNetCore.RateLimiting;
+
namespace ASC.People.Api;
public class UserController : PeopleControllerBase
@@ -1221,7 +1223,8 @@ public class UserController : PeopleControllerBase
/// false
[AllowNotPayment]
[AllowAnonymous]
- [HttpPost("password")]
+ [HttpPost("password")]
+ [EnableRateLimiting("sensitive_api")]
public async Task