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 SendUserPasswordAsync(MemberRequestDto inDto) { if (_authContext.IsAuthenticated) diff --git a/web/ASC.Web.Api/Api/ConnectionsController.cs b/web/ASC.Web.Api/Api/ConnectionsController.cs index b0c416ff95..35b231eda7 100644 --- a/web/ASC.Web.Api/Api/ConnectionsController.cs +++ b/web/ASC.Web.Api/Api/ConnectionsController.cs @@ -95,7 +95,7 @@ public class ConnectionsController : ControllerBase { var user = await _userManager.GetUsersAsync(_securityContext.CurrentAccount.ID); var loginEvents = await _dbLoginEventsManager.GetLoginEventsAsync(user.TenantId, user.Id); - var tasks = loginEvents.ConvertAll(async r => await ConvertAsync(r)); + var tasks = loginEvents.ConvertAll(async r => await _geolocationHelper.AddGeolocationAsync(r)); var listLoginEvents = (await Task.WhenAll(tasks)).ToList(); var loginEventId = GetLoginEventIdFromCookie(); if (loginEventId != 0) @@ -118,7 +118,7 @@ public class ConnectionsController : ControllerBase var browser = MessageSettings.GetBrowser(clientInfo); var ip = MessageSettings.GetIP(request); - var baseEvent = new CustomEvent + var baseEvent = new BaseEvent { Id = 0, Platform = platformAndDevice, @@ -127,7 +127,7 @@ public class ConnectionsController : ControllerBase IP = ip }; - listLoginEvents.Add(await ConvertAsync(baseEvent)); + listLoginEvents.Add(await _geolocationHelper.AddGeolocationAsync(baseEvent)); } } @@ -280,45 +280,4 @@ public class ConnectionsController : ControllerBase var loginEventId = _cookieStorage.GetLoginEventIdFromCookie(cookie); return loginEventId; } - - private async Task ConvertAsync(BaseEvent baseEvent) - { - var location = await GetGeolocationAsync(baseEvent.IP); - return new CustomEvent - { - Id = baseEvent.Id, - IP = baseEvent.IP, - Platform = baseEvent.Platform, - Browser = baseEvent.Browser, - Date = baseEvent.Date, - Country = location[0], - City = location[1] - }; - } - - private async Task GetGeolocationAsync(string ip) - { - try - { - var location = await _geolocationHelper.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 }; - } - } - - private class CustomEvent : BaseEvent - { - public string Country { get; set; } - - public string City { get; set; } - } } diff --git a/web/ASC.Web.Api/Api/SecurityController.cs b/web/ASC.Web.Api/Api/SecurityController.cs index aa1ae1c7f2..33829b6326 100644 --- a/web/ASC.Web.Api/Api/SecurityController.cs +++ b/web/ASC.Web.Api/Api/SecurityController.cs @@ -24,9 +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 AuditEventDto = ASC.Web.Api.ApiModel.ResponseDto.AuditEventDto; -using LoginEventDto = ASC.Web.Api.ApiModel.ResponseDto.LoginEventDto; - namespace ASC.Web.Api.Controllers; /// diff --git a/web/ASC.Web.Api/ApiModels/ResponseDto/AuditEventDto.cs b/web/ASC.Web.Api/ApiModels/ResponseDto/AuditEventDto.cs index 5333d992d5..2465149b2b 100644 --- a/web/ASC.Web.Api/ApiModels/ResponseDto/AuditEventDto.cs +++ b/web/ASC.Web.Api/ApiModels/ResponseDto/AuditEventDto.cs @@ -58,6 +58,14 @@ public class AuditEventDto /// System.String, System public string IP { get; set; } + /// Country + /// System.String, System + public string Country { get; set; } + + /// City + /// System.String, System + public string City { get; set; } + /// Browser /// System.String, System public string Browser { get; set; } @@ -94,7 +102,7 @@ public class AuditEventDto /// System.String, System public string Context { get; set; } - public AuditEventDto(AuditTrail.Models.AuditEventDto auditEvent, AuditActionMapper auditActionMapper) + public AuditEventDto(AuditTrail.Models.AuditEvent auditEvent, AuditActionMapper auditActionMapper) { Id = auditEvent.Id; Date = new ApiDateTime(auditEvent.Date, TimeSpan.Zero); @@ -103,6 +111,8 @@ public class AuditEventDto Action = auditEvent.ActionText; ActionId = (MessageAction)auditEvent.Action; IP = auditEvent.IP; + Country = auditEvent.Country; + City = auditEvent.City; Browser = auditEvent.Browser; Platform = auditEvent.Platform; Page = auditEvent.Page; diff --git a/web/ASC.Web.Api/ApiModels/ResponseDto/LoginEventDto.cs b/web/ASC.Web.Api/ApiModels/ResponseDto/LoginEventDto.cs index 5712b35eda..7e15b0e178 100644 --- a/web/ASC.Web.Api/ApiModels/ResponseDto/LoginEventDto.cs +++ b/web/ASC.Web.Api/ApiModels/ResponseDto/LoginEventDto.cs @@ -62,6 +62,14 @@ public class LoginEventDto /// System.String, System public string IP { get; set; } + /// Country + /// System.String, System + public string Country { get; set; } + + /// City + /// System.String, System + public string City { get; set; } + /// Browser /// System.String, System public string Browser { get; set; } @@ -74,7 +82,7 @@ public class LoginEventDto /// System.String, System public string Page { get; set; } - public LoginEventDto(AuditTrail.Models.LoginEventDto loginEvent) + public LoginEventDto(AuditTrail.Models.LoginEvent loginEvent) { Id = loginEvent.Id; Date = new ApiDateTime(loginEvent.Date, TimeSpan.Zero); @@ -84,6 +92,8 @@ public class LoginEventDto Action = loginEvent.ActionText; ActionId = (MessageAction)loginEvent.Action; IP = loginEvent.IP; + Country = loginEvent.Country; + City = loginEvent.City; Browser = loginEvent.Browser; Platform = loginEvent.Platform; Page = loginEvent.Page;