Merge branch 'develop' into feature/sso-mobile-layout
This commit is contained in:
commit
1d2479aec8
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,9 @@
|
||||
"DisableNotifications": "Disable notifications",
|
||||
"Document": "Document",
|
||||
"DocumentEdited": "Cannot perform the action because the document is being edited.",
|
||||
"PasswordLink": " Add a password to protect your link.",
|
||||
"ChooseExpirationDate": "Limit availability period for this link by setting an expiration date.",
|
||||
"PreventDownloadFilesAndFolders": "Disable downloads of files and folders from this room to secure your data.",
|
||||
"EditRoom": "Edit room",
|
||||
"EmptyFile": "Empty file",
|
||||
"EmptyFilterDescriptionText": "No files or folders match this filter. Try a different one or clear filter to view all files. ",
|
||||
@ -138,6 +141,7 @@
|
||||
"ShareRoom": "Share room",
|
||||
"DownloadAll": "Download all",
|
||||
"PublicRoom": "Public room",
|
||||
"RoomAvailableViaExternalLink": "Room available via external link",
|
||||
"PasswordSuccessfullyCopied": "Password successfully copied",
|
||||
"LinkAddedSuccessfully": "Link added successfully",
|
||||
"MoveToPublicRoomTitle": "Move to Public room",
|
||||
@ -149,5 +153,8 @@
|
||||
"LinkValidUntil": "This link will be valid until",
|
||||
"NoExternalLinks": "No external links",
|
||||
"AllLinksAreDisabled": "All links are disabled",
|
||||
"MaximumNumberOfExternalLinksCreated": "Maximum number of external links created"
|
||||
"MaximumNumberOfExternalLinksCreated": "Maximum number of external links created",
|
||||
"AddNewExternalLink": "Add new external link",
|
||||
"CopyLinkPassword": "Copy link password",
|
||||
"ShowLinkActions": "Show link actions"
|
||||
}
|
||||
|
@ -73,7 +73,6 @@ const withLoader = (WrappedComponent) => (Loader) => {
|
||||
isLoadingFilesFind,
|
||||
isInit: isPublicRoom ? true : isInit,
|
||||
showBodyLoader,
|
||||
isPublicRoom,
|
||||
accountsViewAs,
|
||||
};
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ export type Item = {
|
||||
isFolder: boolean;
|
||||
isDisabled?: boolean;
|
||||
security: Security;
|
||||
roomType: number;
|
||||
};
|
||||
|
||||
export type BreadCrumb = {
|
||||
@ -41,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;
|
||||
@ -80,6 +97,7 @@ export type useFilesHelpersProps = {
|
||||
onSelectTreeNode?: (treeNode: any) => void;
|
||||
setSelectedTreeNode: (treeNode: any) => void;
|
||||
filterParam?: string;
|
||||
getRootData?: () => Promise<void>;
|
||||
};
|
||||
|
||||
export type FilesSelectorProps = {
|
||||
@ -88,6 +106,7 @@ export type FilesSelectorProps = {
|
||||
withoutImmediatelyClose: boolean;
|
||||
isThirdParty: boolean;
|
||||
isEditorDialog: boolean;
|
||||
setMoveToPublicRoomVisible: (visible: boolean, operationData: object) => void;
|
||||
|
||||
onClose?: () => void;
|
||||
|
||||
@ -150,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,68 +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 } = folderInfo;
|
||||
|
||||
return {
|
||||
label: title,
|
||||
id: id,
|
||||
isRoom: parentId === 0 && rootFolderType === FolderType.Rooms,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
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]
|
||||
);
|
||||
|
@ -23,12 +23,16 @@ const getRoomLogo = (roomType: number) => {
|
||||
case RoomsType.EditingRoom:
|
||||
path = "editing.svg";
|
||||
break;
|
||||
|
||||
case RoomsType.PublicRoom:
|
||||
path = "public.svg";
|
||||
break;
|
||||
}
|
||||
|
||||
return iconSize32.get(path);
|
||||
};
|
||||
|
||||
const convertRoomsToItems = (rooms: any) => {
|
||||
export const convertRoomsToItems = (rooms: any) => {
|
||||
const items = rooms.map((room: any) => {
|
||||
const {
|
||||
id,
|
||||
@ -55,6 +59,7 @@ const convertRoomsToItems = (rooms: any) => {
|
||||
parentId,
|
||||
rootFolderType,
|
||||
isFolder: true,
|
||||
roomType,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -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;
|
@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
// @ts-ignore
|
||||
import Loaders from "@docspace/common/components/Loaders";
|
||||
import { FolderType } from "@docspace/common/constants";
|
||||
import { FolderType, RoomsType } from "@docspace/common/constants";
|
||||
|
||||
import Aside from "@docspace/components/aside";
|
||||
import Backdrop from "@docspace/components/backdrop";
|
||||
@ -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,12 @@ const FilesSelector = ({
|
||||
|
||||
descriptionText,
|
||||
setSelectedItems,
|
||||
|
||||
includeFolder,
|
||||
|
||||
socketHelper,
|
||||
socketSubscribersId,
|
||||
setMoveToPublicRoomVisible,
|
||||
}: FilesSelectorProps) => {
|
||||
const { t } = useTranslation(["Files", "Common", "Translations"]);
|
||||
|
||||
@ -112,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,
|
||||
@ -161,6 +178,7 @@ const FilesSelector = ({
|
||||
onSelectTreeNode,
|
||||
setSelectedTreeNode,
|
||||
filterParam,
|
||||
getRootData,
|
||||
});
|
||||
|
||||
const onSelectAction = (item: Item) => {
|
||||
@ -174,6 +192,7 @@ const FilesSelector = ({
|
||||
id: item.id,
|
||||
isRoom:
|
||||
item.parentId === 0 && item.rootFolderType === FolderType.Rooms,
|
||||
roomType: item.roomType,
|
||||
},
|
||||
]);
|
||||
setSelectedItemId(item.id);
|
||||
@ -191,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);
|
||||
@ -292,6 +318,10 @@ const FilesSelector = ({
|
||||
fileName: string,
|
||||
isChecked: boolean
|
||||
) => {
|
||||
const isPublic =
|
||||
breadCrumbs.findIndex((f: any) => f.roomType === RoomsType.PublicRoom) >
|
||||
-1;
|
||||
|
||||
if ((isMove || isCopy || isRestoreAll) && !isEditorDialog) {
|
||||
const folderTitle = breadCrumbs[breadCrumbs.length - 1].label;
|
||||
|
||||
@ -329,6 +359,11 @@ const FilesSelector = ({
|
||||
},
|
||||
};
|
||||
|
||||
if (isPublic) {
|
||||
setMoveToPublicRoomVisible(true, operationData);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsRequestRunning(true);
|
||||
setSelectedItems();
|
||||
checkFileConflicts(selectedItemId, folderIds, fileIds)
|
||||
@ -408,7 +443,8 @@ const FilesSelector = ({
|
||||
isRequestRunning,
|
||||
selectedItemSecurity,
|
||||
filterParam,
|
||||
!!selectedFileInfo
|
||||
!!selectedFileInfo,
|
||||
includeFolder
|
||||
);
|
||||
|
||||
return (
|
||||
@ -510,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;
|
||||
@ -544,12 +585,18 @@ export default inject(
|
||||
conflictResolveDialogVisible,
|
||||
isFolderActions,
|
||||
setIsFolderActions,
|
||||
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
|
||||
@ -576,6 +623,9 @@ export default inject(
|
||||
}
|
||||
});
|
||||
|
||||
const includeFolder =
|
||||
selectionsWithoutEditing.filter((i: any) => i.isFolder).length > 0;
|
||||
|
||||
return {
|
||||
currentFolderId,
|
||||
fromFolderId,
|
||||
@ -600,6 +650,10 @@ export default inject(
|
||||
setRestoreAllPanelVisible,
|
||||
setIsFolderActions,
|
||||
setSelectedItems,
|
||||
includeFolder,
|
||||
socketHelper,
|
||||
socketSubscribersId,
|
||||
setMoveToPublicRoomVisible,
|
||||
};
|
||||
}
|
||||
)(observer(FilesSelector));
|
||||
|
@ -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;
|
||||
|
@ -92,13 +92,13 @@ const ThirdPartyStorage = ({
|
||||
|
||||
return (
|
||||
<StyledThirdPartyStorage>
|
||||
<ToggleParam
|
||||
{/* <ToggleParam
|
||||
id="shared_third-party-storage-toggle"
|
||||
title={t("Common:ThirdPartyStorage")}
|
||||
description={t("ThirdPartyStorageDescription")}
|
||||
isChecked={storageLocation.isThirdparty}
|
||||
onCheckedChange={onChangeIsThirdparty}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
{storageLocation.isThirdparty && (
|
||||
<ThirdPartyComboBox
|
||||
@ -137,11 +137,8 @@ const ThirdPartyStorage = ({
|
||||
export default inject(({ auth, settingsStore, dialogsStore }) => {
|
||||
const { currentColorScheme } = auth.settingsStore;
|
||||
|
||||
const {
|
||||
openConnectWindow,
|
||||
saveThirdParty,
|
||||
deleteThirdParty,
|
||||
} = settingsStore.thirdPartyStore;
|
||||
const { openConnectWindow, saveThirdParty, deleteThirdParty } =
|
||||
settingsStore.thirdPartyStore;
|
||||
|
||||
const {
|
||||
setConnectItem,
|
||||
|
@ -25,6 +25,7 @@ const MoveToPublicRoomComponent = (props) => {
|
||||
setMovingInProgress,
|
||||
itemOperationToFolder,
|
||||
clearActiveOperations,
|
||||
setSelectedItems,
|
||||
} = props;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@ -69,6 +70,7 @@ const MoveToPublicRoomComponent = (props) => {
|
||||
setIsLoading(true);
|
||||
}, 500);
|
||||
|
||||
setSelectedItems();
|
||||
checkFileConflicts(destFolderId, folderIds, fileIds)
|
||||
.then(async (conflicts) => {
|
||||
if (conflicts.length) {
|
||||
@ -153,7 +155,8 @@ export default inject(
|
||||
moveToPublicRoomData,
|
||||
} = dialogsStore;
|
||||
|
||||
const { setConflictDialogData, checkFileConflicts } = filesActionsStore;
|
||||
const { setConflictDialogData, checkFileConflicts, setSelectedItems } =
|
||||
filesActionsStore;
|
||||
const { itemOperationToFolder, clearActiveOperations } = uploadDataStore;
|
||||
|
||||
return {
|
||||
@ -169,6 +172,7 @@ export default inject(
|
||||
setMovingInProgress,
|
||||
itemOperationToFolder,
|
||||
clearActiveOperations,
|
||||
setSelectedItems,
|
||||
};
|
||||
}
|
||||
)(observer(MoveToPublicRoomDialog));
|
||||
|
@ -9,6 +9,7 @@ const LimitTimeBlock = (props) => {
|
||||
setExpirationDate,
|
||||
setIsExpired,
|
||||
isExpired,
|
||||
language,
|
||||
} = props;
|
||||
|
||||
const onChange = (date) => {
|
||||
@ -35,6 +36,7 @@ const LimitTimeBlock = (props) => {
|
||||
minDate={minDate}
|
||||
openDate={new Date()}
|
||||
hasError={isExpired}
|
||||
locale={language}
|
||||
/>
|
||||
</ToggleBlock>
|
||||
);
|
||||
|
@ -86,7 +86,7 @@ const PasswordAccessBlock = (props) => {
|
||||
isDisabled={isLoading}
|
||||
onClick={onCleanClick}
|
||||
>
|
||||
{t("Clean")}
|
||||
{t("Files:Clean")}
|
||||
</Link>
|
||||
<Link
|
||||
fontSize="13px"
|
||||
@ -96,7 +96,7 @@ const PasswordAccessBlock = (props) => {
|
||||
isDisabled={isLoading}
|
||||
onClick={onCopyClick}
|
||||
>
|
||||
{t("CopyPassword")}
|
||||
{t("Files:CopyPassword")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -42,6 +42,7 @@ const EditLinkPanel = (props) => {
|
||||
isDenyDownload,
|
||||
link,
|
||||
date,
|
||||
language,
|
||||
} = props;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@ -219,6 +220,7 @@ const EditLinkPanel = (props) => {
|
||||
expirationDate={expirationDate}
|
||||
setExpirationDate={setExpirationDate}
|
||||
setIsExpired={setIsExpired}
|
||||
language={language}
|
||||
/>
|
||||
</div>
|
||||
</StyledScrollbar>
|
||||
@ -296,6 +298,7 @@ export default inject(({ auth, dialogsStore, publicRoomStore }) => {
|
||||
unsavedChangesDialogVisible,
|
||||
setUnsavedChangesDialog,
|
||||
link: link ?? template,
|
||||
language: auth.language,
|
||||
};
|
||||
})(
|
||||
withTranslation(["SharingPanel", "Common", "Files"])(observer(EditLinkPanel))
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import React, { useState } from "react";
|
||||
import copy from "copy-to-clipboard";
|
||||
import Text from "@docspace/components/text";
|
||||
import Link from "@docspace/components/link";
|
||||
@ -9,28 +9,34 @@ import IconButton from "@docspace/components/icon-button";
|
||||
import Button from "@docspace/components/button";
|
||||
import CopyReactSvgUrl from "PUBLIC_DIR/images/copy.react.svg?url";
|
||||
import { StyledBody } from "./StyledEmbeddingPanel";
|
||||
import { objectToGetParams } from "@docspace/common/utils";
|
||||
|
||||
const EmbeddingBody = ({ t, embeddingLink }) => {
|
||||
const EmbeddingBody = ({ t, link, roomId }) => {
|
||||
const [size, setSize] = useState("auto");
|
||||
const [widthValue, setWidthValue] = useState("100%");
|
||||
const [heightValue, setHeightValue] = useState("100%");
|
||||
|
||||
const getIframe = useCallback(
|
||||
() =>
|
||||
`<iframe src="${embeddingLink}" width="${widthValue}" height="${heightValue}" frameborder="0" scrolling="no" allowtransparency> </iframe>`,
|
||||
[embeddingLink, widthValue, heightValue]
|
||||
);
|
||||
const [link, setLink] = useState(getIframe());
|
||||
const config = {
|
||||
width: `${widthValue}`,
|
||||
height: `${heightValue}`,
|
||||
frameId: "ds-frame",
|
||||
init: true,
|
||||
showHeader: true,
|
||||
showTitle: true,
|
||||
showMenu: false,
|
||||
showFilter: true,
|
||||
rootPath: "/rooms/shared/",
|
||||
id: roomId,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const link = getIframe();
|
||||
setLink(link);
|
||||
}, [embeddingLink, widthValue, heightValue]);
|
||||
const scriptUrl = `${window.location.origin}/static/scripts/api.js`;
|
||||
const params = objectToGetParams(config);
|
||||
const codeBlock = `<div id="${config.frameId}">Fallback text</div>\n<script src="${scriptUrl}${params}"></script>`;
|
||||
|
||||
const onChangeWidth = (e) => setWidthValue(e.target.value);
|
||||
const onChangeHeight = (e) => setHeightValue(e.target.value);
|
||||
const onCopyLink = () => {
|
||||
copy(link);
|
||||
copy(codeBlock);
|
||||
toastr.success(t("EmbeddingPanel:CodeCopySuccess"));
|
||||
};
|
||||
|
||||
@ -127,14 +133,16 @@ const EmbeddingBody = ({ t, embeddingLink }) => {
|
||||
/> */}
|
||||
</div>
|
||||
<div className="embedding-panel_code-container">
|
||||
<Text className="embedding-panel_text">{t("EmbedCode")}:</Text>
|
||||
<Text className="embedding-panel_text">
|
||||
{t("EmbeddingPanel:EmbedCode")}:
|
||||
</Text>
|
||||
<IconButton
|
||||
className="embedding-panel_copy-icon"
|
||||
size="16"
|
||||
iconName={CopyReactSvgUrl}
|
||||
onClick={onCopyLink}
|
||||
/>
|
||||
<Textarea isReadOnly value={link} heightTextArea={150} />
|
||||
<Textarea isReadOnly value={codeBlock} heightTextArea={150} />
|
||||
</div>
|
||||
</div>
|
||||
</StyledBody>
|
||||
|
@ -8,9 +8,7 @@ import { StyledEmbeddingPanel, StyledScrollbar } from "./StyledEmbeddingPanel";
|
||||
import EmbeddingBody from "./EmbeddingBody";
|
||||
|
||||
const EmbeddingPanelComponent = (props) => {
|
||||
const { t, link, visible, setEmbeddingPanelIsVisible } = props;
|
||||
|
||||
const embeddingLink = "embeddingLinkembeddingLinkembeddingLinkembeddingLink";
|
||||
const { t, link, roomId, visible, setEmbeddingPanelIsVisible } = props;
|
||||
|
||||
const scrollRef = useRef(null);
|
||||
|
||||
@ -22,7 +20,7 @@ const EmbeddingPanelComponent = (props) => {
|
||||
(e.key === "Esc" || e.key === "Escape") && onClose();
|
||||
|
||||
useEffect(() => {
|
||||
scrollRef.current && scrollRef.current.view.focus();
|
||||
scrollRef.current && scrollRef.current?.view?.focus();
|
||||
|
||||
document.addEventListener("keyup", onKeyPress);
|
||||
|
||||
@ -44,7 +42,7 @@ const EmbeddingPanelComponent = (props) => {
|
||||
</Heading>
|
||||
</div>
|
||||
<StyledScrollbar ref={scrollRef} stype="mediumBlack">
|
||||
<EmbeddingBody t={t} embeddingLink={link} />
|
||||
<EmbeddingBody t={t} link={link} roomId={roomId} />
|
||||
</StyledScrollbar>
|
||||
</Aside>
|
||||
</StyledEmbeddingPanel>
|
||||
@ -59,6 +57,7 @@ export default inject(({ dialogsStore }) => {
|
||||
visible: embeddingPanelIsVisible,
|
||||
setEmbeddingPanelIsVisible,
|
||||
link: linkParams?.link?.sharedTo?.shareLink,
|
||||
roomId: linkParams?.roomId,
|
||||
};
|
||||
})(
|
||||
withTranslation(["Files", "EmbeddingPanel"])(
|
||||
|
@ -13,6 +13,7 @@ import EyeReactSvgUrl from "PUBLIC_DIR/images/eye.react.svg?url";
|
||||
import SettingsReactSvgUrl from "PUBLIC_DIR/images/catalog.settings.react.svg?url";
|
||||
import ShareReactSvgUrl from "PUBLIC_DIR/images/share.react.svg?url";
|
||||
import CodeReactSvgUrl from "PUBLIC_DIR/images/code.react.svg?url";
|
||||
import CopyToReactSvgUrl from "PUBLIC_DIR/images/copyTo.react.svg?url";
|
||||
import OutlineReactSvgUrl from "PUBLIC_DIR/images/outline-true.react.svg?url";
|
||||
import LockedReactSvgUrl from "PUBLIC_DIR/images/locked.react.svg?url";
|
||||
import LoadedReactSvgUrl from "PUBLIC_DIR/images/loaded.react.svg?url";
|
||||
@ -86,13 +87,13 @@ const LinkRow = (props) => {
|
||||
.finally(() => setIsLoading(false));
|
||||
};
|
||||
|
||||
const onLockClick = () => {
|
||||
const onCopyPassword = () => {
|
||||
copy(password);
|
||||
toastr.success(t("Files:PasswordSuccessfullyCopied"));
|
||||
};
|
||||
|
||||
const onEmbeddingClick = () => {
|
||||
setLinkParams({ link });
|
||||
setLinkParams({ link, roomId });
|
||||
setEmbeddingPanelIsVisible(true);
|
||||
};
|
||||
|
||||
@ -130,6 +131,14 @@ const LinkRow = (props) => {
|
||||
icon: CodeReactSvgUrl,
|
||||
onClick: onEmbeddingClick,
|
||||
},
|
||||
|
||||
!disabled && {
|
||||
key: "copy-link-settings-key",
|
||||
label: t("SharingPanel:CopyExternalLink"),
|
||||
icon: CopyToReactSvgUrl,
|
||||
onClick: onCopyExternalLink,
|
||||
},
|
||||
|
||||
disabled
|
||||
? {
|
||||
key: "enable-link-key",
|
||||
@ -160,7 +169,6 @@ const LinkRow = (props) => {
|
||||
return (
|
||||
<StyledLinkRow {...rest} isExpired={isExpired}>
|
||||
<Avatar
|
||||
className="avatar"
|
||||
size="min"
|
||||
source={EyeReactSvgUrl}
|
||||
roleIcon={expiryDate ? <ClockReactSvg /> : null}
|
||||
@ -198,7 +206,8 @@ const LinkRow = (props) => {
|
||||
className="locked-icon"
|
||||
size={16}
|
||||
iconName={LockedReactSvgUrl}
|
||||
onClick={onLockClick}
|
||||
onClick={onCopyPassword}
|
||||
title={t("Files:CopyLinkPassword")}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
@ -206,12 +215,17 @@ const LinkRow = (props) => {
|
||||
size={16}
|
||||
iconName={CopyReactSvgUrl}
|
||||
onClick={onCopyExternalLink}
|
||||
title={t("SharingPanel:CopyExternalLink")}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isArchiveFolder && (
|
||||
<ContextMenuButton getData={getData} isDisabled={false} />
|
||||
<ContextMenuButton
|
||||
getData={getData}
|
||||
isDisabled={false}
|
||||
title={t("Files:ShowLinkActions")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</StyledLinkRow>
|
||||
|
@ -4,6 +4,7 @@ import PeopleIcon from "PUBLIC_DIR/images/people.react.svg?url";
|
||||
import CrossReactSvg from "PUBLIC_DIR/images/cross.react.svg?url";
|
||||
import IconButton from "@docspace/components/icon-button";
|
||||
import { StyledPublicRoomBar } from "./StyledPublicRoom";
|
||||
import Text from "@docspace/components/text";
|
||||
|
||||
const PublicRoomBar = (props) => {
|
||||
const { headerText, bodyText, iconName, onClose, ...rest } = props;
|
||||
@ -15,9 +16,13 @@ const PublicRoomBar = (props) => {
|
||||
<div className="header-icon">
|
||||
<ReactSVG src={iconName ? iconName : PeopleIcon} />
|
||||
</div>
|
||||
<div>{headerText}</div>
|
||||
<Text className="text-container_header" fontWeight={600}>
|
||||
{headerText}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="body-container">{bodyText}</div>
|
||||
<Text className="text-container_body" fontWeight={400}>
|
||||
{bodyText}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* <IconButton
|
||||
|
@ -29,7 +29,7 @@ const PublicRoomBlock = (props) => {
|
||||
<>
|
||||
{externalLinks.length > 0 && !isArchiveFolder && (
|
||||
<PublicRoomBar
|
||||
headerText={t("Files:PublicRoom")}
|
||||
headerText={t("Files:RoomAvailableViaExternalLink")}
|
||||
bodyText={t("CreateEditRoomDialog:PublicRoomBarDescription")}
|
||||
/>
|
||||
)}
|
||||
@ -56,6 +56,7 @@ const PublicRoomBlock = (props) => {
|
||||
onClick={onAddNewLink}
|
||||
size={16}
|
||||
isDisabled={externalLinks.length >= LINKS_LIMIT_COUNT}
|
||||
title={t("Files:AddNewExternalLink")}
|
||||
/>
|
||||
|
||||
{externalLinks.length >= LINKS_LIMIT_COUNT && (
|
||||
|
@ -4,7 +4,7 @@ import commonIconsStyles from "@docspace/components/utils/common-icons-style";
|
||||
|
||||
const StyledPublicRoomBar = styled.div`
|
||||
display: flex;
|
||||
background-color: #f8f9f9;
|
||||
background-color: ${(props) => props.theme.infoBlock.background};
|
||||
color: #333;
|
||||
font-size: 12px;
|
||||
padding: 12px 16px;
|
||||
@ -25,9 +25,12 @@ const StyledPublicRoomBar = styled.div`
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.body-container {
|
||||
color: #555f65;
|
||||
font-weight: 400;
|
||||
.text-container_header {
|
||||
color: ${(props) => props.theme.infoBlock.headerColor};
|
||||
}
|
||||
|
||||
.text-container_body {
|
||||
color: ${(props) => props.theme.infoBlock.descriptionColor};
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
@ -87,12 +90,13 @@ const StyledLinkRow = styled.div`
|
||||
}
|
||||
|
||||
.avatar_role-wrapper {
|
||||
${({ isExpired }) =>
|
||||
isExpired &&
|
||||
${({ isExpired, theme }) =>
|
||||
css`
|
||||
svg {
|
||||
path {
|
||||
fill: #f98e86;
|
||||
fill: ${isExpired
|
||||
? theme.infoPanel.links.iconErrorColor
|
||||
: theme.infoPanel.links.iconColor};
|
||||
}
|
||||
}
|
||||
`}
|
||||
|
@ -938,7 +938,7 @@ const SectionHeaderContent = (props) => {
|
||||
const isRoot =
|
||||
isLoading && stateIsRoot
|
||||
? stateIsRoot
|
||||
: isRootFolder || isAccountsPage || isSettingsPage || isPublicRoom;
|
||||
: isRootFolder || isAccountsPage || isSettingsPage;
|
||||
|
||||
const currentTitle = isSettingsPage
|
||||
? t("Common:Settings")
|
||||
|
@ -1,10 +1,9 @@
|
||||
import React from "react";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { useLocation, Outlet } from "react-router-dom";
|
||||
import Section from "@docspace/common/components/Section";
|
||||
import SectionHeaderContent from "../Home/Section/Header";
|
||||
import SectionFilterContent from "../Home/Section/Filter";
|
||||
import SectionBodyContent from "../Home/Section/Body";
|
||||
import FilesPanels from "../../components/FilesPanels";
|
||||
|
||||
import { RoomSharingDialog } from "../../components/dialogs";
|
||||
@ -62,7 +61,7 @@ const PublicRoomPage = (props) => {
|
||||
)}
|
||||
|
||||
<Section.SectionBody>
|
||||
<SectionBodyContent />
|
||||
<Outlet />
|
||||
</Section.SectionBody>
|
||||
</Section>
|
||||
|
||||
@ -83,8 +82,7 @@ export default inject(
|
||||
clientLoadingStore,
|
||||
}) => {
|
||||
const { withPaging } = auth.settingsStore;
|
||||
const { validatePublicRoomKey, isLoaded, isLoading, roomStatus, roomId } =
|
||||
publicRoomStore;
|
||||
const { isLoaded, isLoading, roomStatus, roomId } = publicRoomStore;
|
||||
|
||||
const { fetchFiles, isEmptyPage } = filesStore;
|
||||
const { getFilesSettings } = settingsStore;
|
||||
@ -113,7 +111,6 @@ export default inject(
|
||||
getFilesSettings,
|
||||
|
||||
withPaging,
|
||||
validatePublicRoomKey,
|
||||
|
||||
showSecondaryProgressBar,
|
||||
secondaryProgressBarValue,
|
||||
|
@ -21,6 +21,7 @@ const PublicRoom = (props) => {
|
||||
validatePublicRoomKey,
|
||||
getFilesSettings,
|
||||
setPublicRoomKey,
|
||||
setIsArticleLoading,
|
||||
} = props;
|
||||
|
||||
const navigate = useNavigate();
|
||||
@ -38,6 +39,7 @@ const PublicRoom = (props) => {
|
||||
const fetchRoomFiles = async () => {
|
||||
setPublicRoomKey(key);
|
||||
await getFilesSettings();
|
||||
setIsArticleLoading(false);
|
||||
|
||||
const filterObj = FilesFilter.getFilter(window.location);
|
||||
|
||||
@ -94,22 +96,26 @@ const PublicRoom = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default inject(({ auth, publicRoomStore, settingsStore }) => {
|
||||
const { validatePublicRoomKey, isLoaded, isLoading, roomStatus, roomId } =
|
||||
publicRoomStore;
|
||||
export default inject(
|
||||
({ auth, publicRoomStore, settingsStore, clientLoadingStore }) => {
|
||||
const { validatePublicRoomKey, isLoaded, isLoading, roomStatus, roomId } =
|
||||
publicRoomStore;
|
||||
|
||||
const { getFilesSettings } = settingsStore;
|
||||
const { setPublicRoomKey } = auth.settingsStore;
|
||||
const { getFilesSettings } = settingsStore;
|
||||
const { setPublicRoomKey } = auth.settingsStore;
|
||||
const { setIsArticleLoading } = clientLoadingStore;
|
||||
|
||||
return {
|
||||
roomId,
|
||||
isLoaded,
|
||||
isLoading,
|
||||
roomStatus,
|
||||
return {
|
||||
roomId,
|
||||
isLoaded,
|
||||
isLoading,
|
||||
roomStatus,
|
||||
|
||||
getFilesSettings,
|
||||
getFilesSettings,
|
||||
|
||||
validatePublicRoomKey,
|
||||
setPublicRoomKey,
|
||||
};
|
||||
})(observer(PublicRoom));
|
||||
validatePublicRoomKey,
|
||||
setPublicRoomKey,
|
||||
setIsArticleLoading,
|
||||
};
|
||||
}
|
||||
)(observer(PublicRoom));
|
||||
|
@ -254,6 +254,17 @@ const ClientRoutes = [
|
||||
</ErrorBoundary>
|
||||
</PublicRoute>
|
||||
),
|
||||
errorElement: <Error404 />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: (
|
||||
<PublicRoute>
|
||||
<FilesView />
|
||||
</PublicRoute>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/wizard",
|
||||
|
@ -4,8 +4,6 @@ const SHOW_LOADER_TIMER = 500;
|
||||
const MIN_LOADER_TIMER = 500;
|
||||
|
||||
class ClientLoadingStore {
|
||||
publicRoomStore;
|
||||
|
||||
isLoaded = false;
|
||||
firstLoad = true;
|
||||
|
||||
@ -33,10 +31,8 @@ class ClientLoadingStore {
|
||||
body: null,
|
||||
};
|
||||
|
||||
constructor(publicRoomStore) {
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
|
||||
this.publicRoomStore = publicRoomStore;
|
||||
}
|
||||
|
||||
setIsLoaded = (isLoaded) => {
|
||||
@ -217,7 +213,7 @@ class ClientLoadingStore {
|
||||
|
||||
get isLoading() {
|
||||
return (
|
||||
(this.isArticleLoading && !this.publicRoomStore.isPublicRoom) ||
|
||||
this.isArticleLoading ||
|
||||
this.pendingSectionLoaders.header ||
|
||||
this.pendingSectionLoaders.filter ||
|
||||
this.pendingSectionLoaders.body
|
||||
@ -225,7 +221,6 @@ class ClientLoadingStore {
|
||||
}
|
||||
|
||||
get showArticleLoader() {
|
||||
if (this.publicRoomStore.isPublicRoom) return false;
|
||||
return this.isArticleLoading;
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,7 @@ import saveAs from "file-saver";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import config from "PACKAGE_FILE";
|
||||
import toastr from "@docspace/components/toast/toastr";
|
||||
import { ShareAccessRights } from "@docspace/common/constants";
|
||||
import { ShareAccessRights, RoomsType } from "@docspace/common/constants";
|
||||
import combineUrl from "@docspace/common/utils/combineUrl";
|
||||
import {
|
||||
isMobile as isMobileUtils,
|
||||
@ -620,7 +620,7 @@ class ContextOptionsStore {
|
||||
return promise;
|
||||
};
|
||||
|
||||
onClickInviteUsers = (e) => {
|
||||
onClickInviteUsers = (e, item) => {
|
||||
const data = (e.currentTarget && e.currentTarget.dataset) || e;
|
||||
|
||||
const { action } = data;
|
||||
@ -635,7 +635,10 @@ class ContextOptionsStore {
|
||||
visible: true,
|
||||
roomId: action ? action : e,
|
||||
hideSelector: false,
|
||||
defaultAccess: ShareAccessRights.ReadOnly,
|
||||
defaultAccess:
|
||||
item.roomType === RoomsType.PublicRoom
|
||||
? ShareAccessRights.RoomManager
|
||||
: ShareAccessRights.ReadOnly,
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1035,7 +1038,7 @@ class ContextOptionsStore {
|
||||
key: "invite-users-to-room",
|
||||
label: t("Common:InviteUsers"),
|
||||
icon: PersonReactSvgUrl,
|
||||
onClick: (e) => this.onClickInviteUsers(e),
|
||||
onClick: (e) => this.onClickInviteUsers(e, item),
|
||||
disabled: false,
|
||||
action: item.id,
|
||||
},
|
||||
|
@ -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: {
|
||||
|
@ -58,7 +58,7 @@ const treeFoldersStore = new TreeFoldersStore(selectedFolderStore, authStore);
|
||||
|
||||
const publicRoomStore = new PublicRoomStore();
|
||||
|
||||
const clientLoadingStore = new ClientLoadingStore(publicRoomStore);
|
||||
const clientLoadingStore = new ClientLoadingStore();
|
||||
|
||||
const settingsStore = new SettingsStore(
|
||||
thirdPartyStore,
|
||||
|
@ -143,6 +143,13 @@ const StyledContainer = styled.div`
|
||||
.title-icon {
|
||||
min-width: 16px;
|
||||
min-height: 16px;
|
||||
|
||||
svg {
|
||||
path,
|
||||
rect {
|
||||
fill: ${({ theme }) => theme.navigation.publicIcon};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -131,8 +131,8 @@ export const RoomsType = Object.freeze({
|
||||
EditingRoom: 2,
|
||||
// ReviewRoom: 3, //TODO: Restore when certs will be done
|
||||
// ReadOnlyRoom: 4, //TODO: Restore when certs will be done
|
||||
CustomRoom: 5,
|
||||
PublicRoom: 6,
|
||||
CustomRoom: 5,
|
||||
});
|
||||
|
||||
export const RoomsTypeValues = Object.freeze(
|
||||
|
@ -118,6 +118,13 @@ const DatePicker = (props) => {
|
||||
);
|
||||
|
||||
const handleClick = (e) => {
|
||||
if (
|
||||
e.target.classList.contains("nav-thumb-vertical") ||
|
||||
e.target.classList.contains("nav-thumb-horizontal")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
!selectorRef?.current?.contains(e.target) &&
|
||||
!calendarRef?.current?.contains(e.target) &&
|
||||
!selectedItemRef?.current?.contains(e.target) &&
|
||||
@ -125,9 +132,9 @@ const DatePicker = (props) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("click", handleClick, { capture: true });
|
||||
document.addEventListener("mousedown", handleClick, { capture: true });
|
||||
return () =>
|
||||
document.removeEventListener("click", handleClick, { capture: true });
|
||||
document.removeEventListener("mousedown", handleClick, { capture: true });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -22,6 +22,7 @@ const compareFunction = (prevProps: ItemProps, nextProps: ItemProps) => {
|
||||
|
||||
return (
|
||||
prevItem?.id === nextItem?.id &&
|
||||
prevItem?.label === nextItem?.label &&
|
||||
prevItem?.isSelected === nextItem?.isSelected
|
||||
);
|
||||
};
|
||||
|
@ -206,6 +206,10 @@ const StyledTableGroupMenu = styled.div`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-body {
|
||||
display: flex;
|
||||
}
|
||||
`;
|
||||
|
||||
StyledTableGroupMenu.defaultProps = { theme: Base };
|
||||
|
@ -1939,6 +1939,7 @@ const Base = {
|
||||
expanderColor: black,
|
||||
background: white,
|
||||
rootFolderTitleColor: "#A3A9AE",
|
||||
publicIcon: black,
|
||||
|
||||
icon: {
|
||||
fill: "#316DAA",
|
||||
@ -2013,6 +2014,11 @@ const Base = {
|
||||
closeButtonSize: "17px",
|
||||
closeButtonBg: "transparent",
|
||||
|
||||
links: {
|
||||
iconColor: "#3B72A7",
|
||||
iconErrorColor: "rgba(242, 28, 14, 0.5)", //"#F21C0E",
|
||||
},
|
||||
|
||||
members: {
|
||||
iconColor: "#A3A9AE",
|
||||
iconHoverColor: "#657077",
|
||||
@ -3121,6 +3127,12 @@ const Base = {
|
||||
errorColor: "#F21C0E",
|
||||
},
|
||||
},
|
||||
|
||||
infoBlock: {
|
||||
background: "#F8F9F9",
|
||||
headerColor: "#333",
|
||||
descriptionColor: "#555F65",
|
||||
},
|
||||
};
|
||||
|
||||
export default Base;
|
||||
|
@ -1935,6 +1935,7 @@ const Dark = {
|
||||
expanderColor: "#eeeeee",
|
||||
background: black,
|
||||
rootFolderTitleColor: "#858585",
|
||||
publicIcon: "#858585",
|
||||
|
||||
icon: {
|
||||
fill: "#E06A1B",
|
||||
@ -2009,6 +2010,11 @@ const Dark = {
|
||||
closeButtonSize: "12px",
|
||||
closeButtonBg: "#a2a2a2",
|
||||
|
||||
links: {
|
||||
iconColor: "#858585",
|
||||
iconErrorColor: "rgba(242, 28, 14, 0.5)", //"#F21C0E",
|
||||
},
|
||||
|
||||
members: {
|
||||
iconColor: "#A3A9AE",
|
||||
iconHoverColor: "#ffffff",
|
||||
@ -3124,6 +3130,12 @@ const Dark = {
|
||||
errorColor: "#F21C0E",
|
||||
},
|
||||
},
|
||||
|
||||
infoBlock: {
|
||||
background: "#282828",
|
||||
headerColor: "#FFF",
|
||||
descriptionColor: "#ADADAD",
|
||||
},
|
||||
};
|
||||
|
||||
export default Dark;
|
||||
|
@ -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