Merge branch 'develop' into bugfix/infinity-loader
This commit is contained in:
commit
4a35d3a499
@ -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" />
|
||||
|
@ -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]
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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));
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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; }
|
@ -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");
|
||||
|
@ -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");
|
||||
|
@ -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
|
||||
|
@ -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>())
|
||||
;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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; }
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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;
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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>();
|
||||
}
|
||||
}
|
@ -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; }
|
||||
|
@ -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>();
|
||||
}
|
||||
}
|
@ -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; }
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>;
|
||||
};
|
||||
|
@ -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]
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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;
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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: {
|
||||
|
@ -22,6 +22,7 @@ const compareFunction = (prevProps: ItemProps, nextProps: ItemProps) => {
|
||||
|
||||
return (
|
||||
prevItem?.id === nextItem?.id &&
|
||||
prevItem?.label === nextItem?.label &&
|
||||
prevItem?.isSelected === nextItem?.isSelected
|
||||
);
|
||||
};
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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));
|
||||
|
@ -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)
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user