Merge branch 'develop' into bugfix/infinity-loader

This commit is contained in:
Ilya Oleshko 2023-08-22 11:13:09 +03:00
commit 4a35d3a499
45 changed files with 795 additions and 229 deletions

View File

@ -26,6 +26,8 @@
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="7.0.1" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.3" />
<PackageReference Include="RedisRateLimiting" Version="1.0.11" />
<PackageReference Include="RedisRateLimiting.AspNetCore" Version="1.0.8" />
<PackageReference Include="StackExchange.Redis.Extensions.AspNetCore" Version="9.1.0" />
<PackageReference Include="StackExchange.Redis.Extensions.Newtonsoft" Version="9.1.0" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="8.0.0" />

View File

@ -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]

View File

@ -100,6 +100,88 @@ public abstract class BaseStartup
}
});
var redisOptions = _configuration.GetSection("Redis").Get<RedisConfiguration>().ConfigurationOptions;
var connectionMultiplexer = ConnectionMultiplexer.Connect(redisOptions);
services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.CreateChained(
PartitionedRateLimiter.Create<HttpContext, string>(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, string>(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<EFLoggerFactory>();
services.AddBaseDbContextPool<AccountLinkContext>()
@ -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)

View File

@ -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;

View File

@ -151,7 +151,7 @@ public class InvitationLinkHelper
return linkId == default ? (ValidationResult.Invalid, default) : (ValidationResult.Ok, linkId);
}
private async Task<AuditEvent> GetLinkVisitMessageAsync(string email, string key)
private async Task<DbAuditEvent> 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<MessagesContext, string, string, Task<AuditEvent>> AuditEventsAsync =
public static readonly Func<MessagesContext, string, string, Task<DbAuditEvent>> AuditEventsAsync =
EF.CompileAsyncQuery(
(MessagesContext ctx, string target, string description) =>
ctx.AuditEvents.FirstOrDefault(a => a.Target == target && a.DescriptionRaw == description));

View File

@ -61,7 +61,7 @@ public class DbLoginEventsManager
_mapper = mapper;
}
public async Task<LoginEvent> GetByIdAsync(int id)
public async Task<DbLoginEvent> 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<LoginEvent>, List<BaseEvent>>(loginInfo);
return _mapper.Map<List<DbLoginEvent>, List<BaseEvent>>(loginInfo);
}
public async Task LogOutEventAsync(int loginEventId)
@ -135,7 +135,7 @@ public class DbLoginEventsManager
static file class Queries
{
public static readonly Func<MessagesContext, int, Guid, IEnumerable<int>, DateTime, IAsyncEnumerable<LoginEvent>>
public static readonly Func<MessagesContext, int, Guid, IEnumerable<int>, DateTime, IAsyncEnumerable<DbLoginEvent>>
LoginEventsAsync = EF.CompileAsyncQuery(
(MessagesContext ctx, int tenantId, Guid userId, IEnumerable<int> loginActions, DateTime date) =>
ctx.LoginEvents

View File

@ -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)
{

View File

@ -28,8 +28,8 @@ namespace ASC.MessagingSystem.EF.Context;
public class MessagesContext : DbContext
{
public DbSet<AuditEvent> AuditEvents { get; set; }
public DbSet<LoginEvent> LoginEvents { get; set; }
public DbSet<DbAuditEvent> AuditEvents { get; set; }
public DbSet<DbLoginEvent> LoginEvents { get; set; }
public DbSet<DbWebstudioSettings> WebstudioSettings { get; set; }
public DbSet<DbTenant> Tenants { get; set; }
public DbSet<User> Users { get; set; }

View File

@ -28,7 +28,7 @@ using Profile = AutoMapper.Profile;
namespace ASC.MessagingSystem.EF.Model;
public class AuditEvent : MessageEvent, IMapFrom<EventMessage>
public class DbAuditEvent : MessageEvent, IMapFrom<EventMessage>
{
public string Initiator { get; set; }
public string Target { get; set; }
@ -37,8 +37,8 @@ public class AuditEvent : MessageEvent, IMapFrom<EventMessage>
public void Mapping(Profile profile)
{
profile.CreateMap<MessageEvent, AuditEvent>();
profile.CreateMap<EventMessage, AuditEvent>()
profile.CreateMap<MessageEvent, DbAuditEvent>();
profile.CreateMap<EventMessage, DbAuditEvent>()
.ConvertUsing<EventTypeConverter>();
}
}
@ -47,7 +47,7 @@ public static class AuditEventExtension
{
public static ModelBuilderWrapper AddAuditEvent(this ModelBuilderWrapper modelBuilder)
{
modelBuilder.Entity<AuditEvent>().Navigation(e => e.Tenant).AutoInclude(false);
modelBuilder.Entity<DbAuditEvent>().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<AuditEvent>(entity =>
modelBuilder.Entity<DbAuditEvent>(entity =>
{
entity.ToTable("audit_events")
.HasCharSet("utf8");
@ -133,7 +133,7 @@ public static class AuditEventExtension
}
public static void PgSqlAddAuditEvent(this ModelBuilder modelBuilder)
{
modelBuilder.Entity<AuditEvent>(entity =>
modelBuilder.Entity<DbAuditEvent>(entity =>
{
entity.ToTable("audit_events", "onlyoffice");

View File

@ -28,7 +28,7 @@ using Profile = AutoMapper.Profile;
namespace ASC.MessagingSystem.EF.Model;
public class LoginEvent : MessageEvent, IMapFrom<EventMessage>
public class DbLoginEvent : MessageEvent, IMapFrom<EventMessage>
{
public string Login { get; set; }
public bool Active { get; set; }
@ -37,8 +37,8 @@ public class LoginEvent : MessageEvent, IMapFrom<EventMessage>
public void Mapping(Profile profile)
{
profile.CreateMap<MessageEvent, LoginEvent>();
profile.CreateMap<EventMessage, LoginEvent>()
profile.CreateMap<MessageEvent, DbLoginEvent>();
profile.CreateMap<EventMessage, DbLoginEvent>()
.ConvertUsing<EventTypeConverter>();
}
}
@ -47,7 +47,7 @@ public static class LoginEventsExtension
{
public static ModelBuilderWrapper AddLoginEvents(this ModelBuilderWrapper modelBuilder)
{
modelBuilder.Entity<LoginEvent>().Navigation(e => e.Tenant).AutoInclude(false);
modelBuilder.Entity<DbLoginEvent>().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<LoginEvent>(entity =>
modelBuilder.Entity<DbLoginEvent>(entity =>
{
entity.ToTable("login_events")
.HasCharSet("utf8");
@ -131,7 +131,7 @@ public static class LoginEventsExtension
}
public static void PgSqlAddLoginEvents(this ModelBuilder modelBuilder)
{
modelBuilder.Entity<LoginEvent>(entity =>
modelBuilder.Entity<DbLoginEvent>(entity =>
{
entity.ToTable("login_events", "onlyoffice");

View File

@ -55,6 +55,33 @@ public class GeolocationHelper
_cache = cache;
}
public async Task<BaseEvent> AddGeolocationAsync(BaseEvent baseEvent)
{
var location = await GetGeolocationAsync(baseEvent.IP);
baseEvent.Country = location[0];
baseEvent.City = location[1];
return baseEvent;
}
public async Task<string[]> 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<IPGeolocationInfo> GetIPGeolocationAsync(IPAddress address)
{
try

View File

@ -28,7 +28,7 @@ using Profile = AutoMapper.Profile;
namespace ASC.AuditTrail.Models;
public class BaseEvent : IMapFrom<LoginEvent>
public class BaseEvent : IMapFrom<DbLoginEvent>
{
public int Id { get; set; }
public int TenantId { get; set; }
@ -37,7 +37,13 @@ public class BaseEvent : IMapFrom<LoginEvent>
public IList<string> 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<LoginEvent>
public virtual void Mapping(Profile profile)
{
profile.CreateMap<LoginEvent, BaseEvent>()
profile.CreateMap<DbLoginEvent, BaseEvent>()
.ForMember(r => r.IP, opt => opt.MapFrom<BaseEventTypeIpResolver>())
.ForMember(r => r.Date, opt => opt.MapFrom<BaseEventTypeDateResolver>())
;

View File

@ -27,9 +27,9 @@
namespace ASC.MessagingSystem.Mapping;
[Scope]
public class BaseEventTypeIpResolver : IValueResolver<LoginEvent, BaseEvent, string>
public class BaseEventTypeIpResolver : IValueResolver<DbLoginEvent, BaseEvent, string>
{
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<LoginEvent, BaseEvent, str
}
[Scope]
public class BaseEventTypeDateResolver : IValueResolver<LoginEvent, BaseEvent, DateTime>
public class BaseEventTypeDateResolver : IValueResolver<DbLoginEvent, BaseEvent, DateTime>
{
private readonly TenantUtil _tenantUtil;
@ -54,7 +54,7 @@ public class BaseEventTypeDateResolver : IValueResolver<LoginEvent, BaseEvent, D
_tenantUtil = tenantUtil;
}
public DateTime Resolve(LoginEvent source, BaseEvent destination, DateTime destMember, ResolutionContext context)
public DateTime Resolve(DbLoginEvent source, BaseEvent destination, DateTime destMember, ResolutionContext context)
{
return _tenantUtil.DateTimeFromUtc(source.Date);
}

View File

@ -28,12 +28,12 @@
namespace ASC.MessagingSystem.Mapping;
[Scope]
public class EventTypeConverter : ITypeConverter<EventMessage, LoginEvent>, ITypeConverter<EventMessage, AuditEvent>
public class EventTypeConverter : ITypeConverter<EventMessage, DbLoginEvent>, ITypeConverter<EventMessage, DbAuditEvent>
{
public LoginEvent Convert(EventMessage source, LoginEvent destination, ResolutionContext context)
public DbLoginEvent Convert(EventMessage source, DbLoginEvent destination, ResolutionContext context)
{
var messageEvent = context.Mapper.Map<EventMessage, MessageEvent>(source);
var loginEvent = context.Mapper.Map<MessageEvent, LoginEvent>(messageEvent);
var loginEvent = context.Mapper.Map<MessageEvent, DbLoginEvent>(messageEvent);
loginEvent.Login = source.Initiator;
loginEvent.Active = source.Active;
@ -50,10 +50,10 @@ public class EventTypeConverter : ITypeConverter<EventMessage, LoginEvent>, 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<EventMessage, MessageEvent>(source);
var auditEvent = context.Mapper.Map<MessageEvent, AuditEvent>(messageEvent);
var auditEvent = context.Mapper.Map<MessageEvent, DbAuditEvent>(messageEvent);
auditEvent.Initiator = source.Initiator;
auditEvent.Target = source.Target?.ToString();

View File

@ -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<EventMessage, LoginEvent>(message);
var loginEvent = _mapper.Map<EventMessage, DbLoginEvent>(message);
await ef.LoginEvents.AddAsync(loginEvent);
}
else
{
var auditEvent = _mapper.Map<EventMessage, AuditEvent>(message);
var auditEvent = _mapper.Map<EventMessage, DbAuditEvent>(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<EventMessage, LoginEvent>(message);
var loginEvent = _mapper.Map<EventMessage, DbLoginEvent>(message);
ef.LoginEvents.Add(loginEvent);
}
else
{
var auditEvent = _mapper.Map<EventMessage, AuditEvent>(message);
var auditEvent = _mapper.Map<EventMessage, DbAuditEvent>(message);
ef.AuditEvents.Add(auditEvent);
}
}
@ -245,7 +245,7 @@ public class MessagesRepository : IDisposable
private async Task<int> AddLoginEventAsync(EventMessage message, MessagesContext dbContext)
{
var loginEvent = _mapper.Map<EventMessage, LoginEvent>(message);
var loginEvent = _mapper.Map<EventMessage, DbLoginEvent>(message);
await dbContext.LoginEvents.AddAsync(loginEvent);
await dbContext.SaveChangesAsync();
@ -255,7 +255,7 @@ public class MessagesRepository : IDisposable
private async Task<int> AddAuditEventAsync(EventMessage message, MessagesContext dbContext)
{
var auditEvent = _mapper.Map<EventMessage, AuditEvent>(message);
var auditEvent = _mapper.Map<EventMessage, DbAuditEvent>(message);
await dbContext.AuditEvents.AddAsync(auditEvent);
await dbContext.SaveChangesAsync();

View File

@ -68,8 +68,8 @@ public class MigrationContext : DbContext
public DbSet<InstanceRegistration> InstanceRegistrations { get; set; }
public DbSet<AuditEvent> AuditEvents { get; set; }
public DbSet<LoginEvent> LoginEvents { get; set; }
public DbSet<DbAuditEvent> AuditEvents { get; set; }
public DbSet<DbLoginEvent> LoginEvents { get; set; }
public DbSet<BackupRecord> Backups { get; set; }
public DbSet<BackupSchedule> Schedules { get; set; }

View File

@ -186,6 +186,15 @@ namespace ASC.AuditTrail {
}
}
/// <summary>
/// Looks up a localized string similar to City.
/// </summary>
public static string CityCol {
get {
return ResourceManager.GetString("CityCol", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Color Theme Changed.
/// </summary>
@ -213,6 +222,15 @@ namespace ASC.AuditTrail {
}
}
/// <summary>
/// Looks up a localized string similar to Country.
/// </summary>
public static string CountryCol {
get {
return ResourceManager.GetString("CountryCol", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Create.
/// </summary>

View File

@ -777,4 +777,10 @@
<data name="TrashEmptied" xml:space="preserve">
<value>Trash emptied</value>
</data>
<data name="CityCol" xml:space="preserve">
<value>City</value>
</data>
<data name="CountryCol" xml:space="preserve">
<value>Country</value>
</data>
</root>

View File

@ -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;

View File

@ -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)
{

View File

@ -26,7 +26,7 @@
namespace ASC.AuditTrail.Models;
public class AuditEventDto : BaseEvent, IMapFrom<AuditEventQuery>
public class AuditEvent : BaseEvent, IMapFrom<AuditEventQuery>
{
public string Initiator { get; set; }
@ -48,9 +48,9 @@ public class AuditEventDto : BaseEvent, IMapFrom<AuditEventQuery>
public override void Mapping(Profile profile)
{
profile.CreateMap<AuditEvent, AuditEventDto>();
profile.CreateMap<DbAuditEvent, AuditEvent>();
profile.CreateMap<AuditEventQuery, AuditEventDto>()
profile.CreateMap<AuditEventQuery, AuditEvent>()
.ConvertUsing<EventTypeConverter>();
}
}

View File

@ -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; }

View File

@ -26,16 +26,16 @@
namespace ASC.AuditTrail.Models;
public class LoginEventDto : BaseEvent, IMapFrom<LoginEventQuery>
public class LoginEvent : BaseEvent, IMapFrom<LoginEventQuery>
{
public string Login { get; set; }
public int Action { get; set; }
public override void Mapping(Profile profile)
{
profile.CreateMap<LoginEvent, LoginEventDto>();
profile.CreateMap<DbLoginEvent, LoginEvent>();
profile.CreateMap<LoginEventQuery, LoginEventDto>()
profile.CreateMap<LoginEventQuery, LoginEvent>()
.ConvertUsing<EventTypeConverter>();
}
}

View File

@ -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; }

View File

@ -29,8 +29,8 @@ using ASC.Core.Tenants;
namespace ASC.AuditTrail.Models.Mappings;
[Scope]
internal class EventTypeConverter : ITypeConverter<LoginEventQuery, LoginEventDto>,
ITypeConverter<AuditEventQuery, AuditEventDto>
internal class EventTypeConverter : ITypeConverter<LoginEventQuery, LoginEvent>,
ITypeConverter<AuditEventQuery, AuditEvent>
{
private readonly UserFormatter _userFormatter;
private readonly AuditActionMapper _auditActionMapper;
@ -49,9 +49,9 @@ internal class EventTypeConverter : ITypeConverter<LoginEventQuery, LoginEventDt
_tenantUtil = tenantUtil;
}
public LoginEventDto Convert(LoginEventQuery source, LoginEventDto destination, ResolutionContext context)
public LoginEvent Convert(LoginEventQuery source, LoginEvent destination, ResolutionContext context)
{
var result = context.Mapper.Map<LoginEventDto>(source.Event);
var result = context.Mapper.Map<LoginEvent>(source.Event);
if (source.Event.DescriptionRaw != null)
{
@ -95,11 +95,11 @@ internal class EventTypeConverter : ITypeConverter<LoginEventQuery, LoginEventDt
return result;
}
public AuditEventDto Convert(AuditEventQuery source, AuditEventDto destination, ResolutionContext context)
public AuditEvent Convert(AuditEventQuery source, AuditEvent destination, ResolutionContext context)
{
var target = source.Event.Target;
source.Event.Target = null;
var result = context.Mapper.Map<AuditEventDto>(source.Event);
var result = context.Mapper.Map<AuditEvent>(source.Event);
result.Target = _messageTarget.Parse(target);

View File

@ -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<MessagesContext> _dbContextFactory;
private readonly IMapper _mapper;
private readonly GeolocationHelper _geolocationHelper;
public AuditEventsRepository(
AuditActionMapper auditActionMapper,
TenantManager tenantManager,
IDbContextFactory<MessagesContext> dbContextFactory,
IMapper mapper)
IMapper mapper,
GeolocationHelper geolocationHelper)
{
_auditActionMapper = auditActionMapper;
_tenantManager = tenantManager;
_dbContextFactory = dbContextFactory;
_mapper = mapper;
_geolocationHelper = geolocationHelper;
}
public async Task<IEnumerable<AuditEventDto>> GetByFilterAsync(
public async Task<IEnumerable<AuditEvent>> GetByFilterAsync(
Guid? userId = null,
ProductType? productType = null,
ModuleType? moduleType = null,
@ -77,7 +78,7 @@ public class AuditEventsRepository
withoutUserId);
}
public async Task<IEnumerable<AuditEventDto>> GetByFilterWithActionsAsync(
public async Task<IEnumerable<AuditEvent>> GetByFilterWithActionsAsync(
Guid? userId = null,
ProductType? productType = null,
ModuleType? moduleType = null,
@ -204,7 +205,13 @@ public class AuditEventsRepository
{
query = query.Take(limit);
}
return _mapper.Map<List<AuditEventQuery>, IEnumerable<AuditEventDto>>(await query.ToListAsync());
var events = _mapper.Map<List<AuditEventQuery>, IEnumerable<AuditEvent>>(await query.ToListAsync());
foreach(var e in events)
{
await _geolocationHelper.AddGeolocationAsync(e);
}
return events;
}
private static void FindByEntry(IQueryable<AuditEventQuery> q, EntryType entry, string target, IEnumerable<KeyValuePair<MessageAction, MessageMaps>> actions)

View File

@ -32,18 +32,21 @@ public class LoginEventsRepository
private readonly TenantManager _tenantManager;
private readonly IDbContextFactory<MessagesContext> _dbContextFactory;
private readonly IMapper _mapper;
private readonly GeolocationHelper _geolocationHelper;
public LoginEventsRepository(
TenantManager tenantManager,
IDbContextFactory<MessagesContext> dbContextFactory,
IMapper mapper)
IMapper mapper,
GeolocationHelper geolocationHelper)
{
_tenantManager = tenantManager;
_dbContextFactory = dbContextFactory;
_mapper = mapper;
_geolocationHelper = geolocationHelper;
}
public async Task<IEnumerable<LoginEventDto>> GetByFilterAsync(
public async Task<IEnumerable<LoginEvent>> GetByFilterAsync(
Guid? login = null,
MessageAction? action = null,
DateTime? fromDate = null,
@ -108,7 +111,13 @@ public class LoginEventsRepository
}
}
return _mapper.Map<List<LoginEventQuery>, IEnumerable<LoginEventDto>>(await query.ToListAsync());
var events = _mapper.Map<List<LoginEventQuery>, IEnumerable<LoginEvent>>(await query.ToListAsync());
foreach (var e in events)
{
await _geolocationHelper.AddGeolocationAsync(e);
}
return events;
}
}

View File

@ -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<string>;
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<void>;
};
export type FilesSelectorProps = {
@ -152,4 +169,9 @@ export type FilesSelectorProps = {
descriptionText?: string;
setSelectedItems: () => void;
includeFolder?: boolean;
socketHelper: any;
socketSubscribersId: Set<string>;
};

View File

@ -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]
);

View File

@ -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,

View File

@ -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 | number>(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;

View File

@ -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<boolean>(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,
};
}

View File

@ -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;

View File

@ -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,
},
});
}
};

View File

@ -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) {

View File

@ -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: {

View File

@ -22,6 +22,7 @@ const compareFunction = (prevProps: ItemProps, nextProps: ItemProps) => {
return (
prevItem?.id === nextItem?.id &&
prevItem?.label === nextItem?.label &&
prevItem?.isSelected === nextItem?.isSelected
);
};

View File

@ -142,7 +142,7 @@ public class ProductEntryPoint : Product
public override async Task<IEnumerable<ActivityInfo>> GetAuditEventsAsync(DateTime scheduleDate, Guid userId, Tenant tenant, WhatsNewType whatsNewType)
{
IEnumerable<AuditEventDto> events;
IEnumerable<AuditEvent> events;
_tenantManager.SetCurrentTenant(tenant);
if (whatsNewType == WhatsNewType.RoomsActivity)

View File

@ -212,7 +212,7 @@ internal class FolderDao : AbstractDao, IFolderDao<int>
{
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<int>
return await q.CountAsync();
}
public async IAsyncEnumerable<Folder<int>> GetFoldersAsync(int parentId, OrderBy orderBy, FilterType filterType, bool subjectGroup, Guid subjectID, string searchText, bool withSubfolders = false,
public async IAsyncEnumerable<Folder<int>> 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<int>
{
var roomTypes = new List<FolderType>
{
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<int>
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<int>
{
var roomTypes = new List<FolderType>
{
FolderType.CustomRoom,
FolderType.ReviewRoom,
FolderType.FillingFormsRoom,
FolderType.EditingRoom,
FolderType.CustomRoom,
FolderType.ReviewRoom,
FolderType.FillingFormsRoom,
FolderType.EditingRoom,
FolderType.ReadOnlyRoom,
FolderType.PublicRoom
};
Expression<Func<DbFolder, bool>> 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<int>
public async IAsyncEnumerable<int> GetTenantsWithRoomsFeedsAsync(DateTime fromTime)
{
var roomTypes = new List<FolderType>
{
FolderType.CustomRoom,
FolderType.ReviewRoom,
FolderType.FillingFormsRoom,
FolderType.EditingRoom,
var roomTypes = new List<FolderType>
{
FolderType.CustomRoom,
FolderType.ReviewRoom,
FolderType.FillingFormsRoom,
FolderType.EditingRoom,
FolderType.ReadOnlyRoom,
FolderType.PublicRoom,
};
Expression<Func<DbFolder, bool>> filter = f => roomTypes.Contains(f.FolderType);
await foreach (var q in GetTenantsWithFeeds(fromTime, filter, true))
@ -1331,7 +1332,7 @@ internal class FolderDao : AbstractDao, IFolderDao<int>
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<int>
{
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<int>
{
return (await _globalStore.GetStoreAsync()).CreateDataWriteOperator(chunkedUploadSession, sessionHolder);
}
private async Task<IQueryable<DbFolder>> GetFoldersQueryWithFilters(int parentId, OrderBy orderBy, bool subjectGroup, Guid subjectId, string searchText, bool withSubfolders, bool excludeSubject,
FilesDbContext filesDbContext)
{

View File

@ -46,7 +46,7 @@ internal class FileMoveCopyOperationData<T> : FileOperationData<T>
public FileConflictResolveType ResolveType { get; }
public IDictionary<string, StringValues> Headers { get; }
public FileMoveCopyOperationData(IEnumerable<T> folders, IEnumerable<T> files, Tenant tenant, JsonElement toFolderId, bool copy, FileConflictResolveType resolveType,
public FileMoveCopyOperationData(IEnumerable<T> folders, IEnumerable<T> files, Tenant tenant, JsonElement toFolderId, bool copy, FileConflictResolveType resolveType,
ExternalShareData externalShareData, bool holdResult = true, IDictionary<string, StringValues> headers = null)
: base(folders, files, tenant, externalShareData, holdResult)
{
@ -471,7 +471,7 @@ class FileMoveCopyOperation<T> : FileOperation<FileMoveCopyOperationData<T>, T>
newFolderId = await FolderDao.MoveFolderAsync(folder.Id, toFolderId, CancellationToken);
var (name, value) = await tenantQuotaFeatureStatHelper.GetStatAsync<CountRoomFeature, int>();
_ = quotaSocketManager.ChangeQuotaUsedValueAsync(name, value);
_ = quotaSocketManager.ChangeQuotaUsedValueAsync(name, value);
}
else
{
@ -581,7 +581,7 @@ class FileMoveCopyOperation<T> : FileOperation<FileMoveCopyOperationData<T>, 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<T> : FileOperation<FileMoveCopyOperationData<T>, 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));

View File

@ -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
/// <requiresAuthorization>false</requiresAuthorization>
[AllowNotPayment]
[AllowAnonymous]
[HttpPost("password")]
[HttpPost("password")]
[EnableRateLimiting("sensitive_api")]
public async Task<object> SendUserPasswordAsync(MemberRequestDto inDto)
{
if (_authContext.IsAuthenticated)

View File

@ -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<CustomEvent> 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<string[]> 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; }
}
}

View File

@ -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;
/// <summary>

View File

@ -58,6 +58,14 @@ public class AuditEventDto
/// <type>System.String, System</type>
public string IP { get; set; }
/// <summary>Country</summary>
/// <type>System.String, System</type>
public string Country { get; set; }
/// <summary>City</summary>
/// <type>System.String, System</type>
public string City { get; set; }
/// <summary>Browser</summary>
/// <type>System.String, System</type>
public string Browser { get; set; }
@ -94,7 +102,7 @@ public class AuditEventDto
/// <type>System.String, System</type>
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;

View File

@ -62,6 +62,14 @@ public class LoginEventDto
/// <type>System.String, System</type>
public string IP { get; set; }
/// <summary>Country</summary>
/// <type>System.String, System</type>
public string Country { get; set; }
/// <summary>City</summary>
/// <type>System.String, System</type>
public string City { get; set; }
/// <summary>Browser</summary>
/// <type>System.String, System</type>
public string Browser { get; set; }
@ -74,7 +82,7 @@ public class LoginEventDto
/// <type>System.String, System</type>
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;