Merge branch 'develop' into feature/sso-mobile-layout

This commit is contained in:
Viktor Fomin 2023-08-21 22:08:23 +03:00
commit 1d2479aec8
71 changed files with 997 additions and 312 deletions

View File

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

View File

@ -24,6 +24,8 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
using Role = ASC.Common.Security.Authorizing.Role;
namespace ASC.Api.Core.Auth;
[Scope]

View File

@ -100,6 +100,88 @@ public abstract class BaseStartup
}
});
var redisOptions = _configuration.GetSection("Redis").Get<RedisConfiguration>().ConfigurationOptions;
var connectionMultiplexer = ConnectionMultiplexer.Connect(redisOptions);
services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.CreateChained(
PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
var userId = httpContext?.User?.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Sid)?.Value;
if (userId == null)
{
return RateLimitPartition.GetNoLimiter("no_limiter");
}
var permitLimit = 1500;
string partitionKey;
partitionKey = $"sw_{userId}";
return RedisRateLimitPartition.GetSlidingWindowRateLimiter(partitionKey, key => new RedisSlidingWindowRateLimiterOptions
{
PermitLimit = permitLimit,
Window = TimeSpan.FromMinutes(1),
ConnectionMultiplexerFactory = () => connectionMultiplexer
});
}),
PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
var userId = httpContext?.User?.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Sid)?.Value;
string partitionKey;
int permitLimit;
if (userId == null)
{
return RateLimitPartition.GetNoLimiter("no_limiter");
}
if (String.Compare(httpContext.Request.Method, "GET", true) == 0)
{
permitLimit = 50;
partitionKey = $"cr_read_{userId}";
}
else
{
permitLimit = 15;
partitionKey = $"cr_write_{userId}";
}
return RedisRateLimitPartition.GetConcurrencyRateLimiter(partitionKey, key => new RedisConcurrencyRateLimiterOptions
{
PermitLimit = permitLimit,
QueueLimit = 0,
ConnectionMultiplexerFactory = () => connectionMultiplexer
});
}
));
options.AddPolicy("sensitive_api", httpContext => {
var userId = httpContext?.User?.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Sid)?.Value;
if (userId == null)
{
return RateLimitPartition.GetNoLimiter("no_limiter");
}
var permitLimit = 5;
var partitionKey = $"sensitive_api_{userId}";
return RedisRateLimitPartition.GetSlidingWindowRateLimiter(partitionKey, key => new RedisSlidingWindowRateLimiterOptions
{
PermitLimit = permitLimit,
Window = TimeSpan.FromMinutes(1),
ConnectionMultiplexerFactory = () => connectionMultiplexer
});
});
options.OnRejected = (context, ct) => RateLimitMetadata.OnRejected(context.HttpContext, context.Lease, ct);
});
services.AddScoped<EFLoggerFactory>();
services.AddBaseDbContextPool<AccountLinkContext>()
@ -311,6 +393,8 @@ public abstract class BaseStartup
app.UseAuthentication();
app.UseRateLimiter();
app.UseAuthorization();
app.UseCultureMiddleware();
@ -346,6 +430,8 @@ public abstract class BaseStartup
await context.Response.WriteAsync($"{Environment.MachineName} running {CustomHealthCheck.Running}");
});
});
}
public void ConfigureContainer(ContainerBuilder builder)

View File

@ -41,6 +41,7 @@ global using System.Text.Encodings.Web;
global using System.Text.Json;
global using System.Text.Json.Serialization;
global using System.Text.RegularExpressions;
global using System.Threading.RateLimiting;
global using System.Web;
global using System.Xml.Linq;
@ -150,6 +151,10 @@ global using NLog.Web;
global using RabbitMQ.Client;
global using RedisRateLimiting;
global using RedisRateLimiting.AspNetCore;
global using StackExchange.Redis;
global using StackExchange.Redis.Extensions.Core.Configuration;
global using StackExchange.Redis.Extensions.Newtonsoft;

View File

@ -151,7 +151,7 @@ public class InvitationLinkHelper
return linkId == default ? (ValidationResult.Invalid, default) : (ValidationResult.Ok, linkId);
}
private async Task<AuditEvent> GetLinkVisitMessageAsync(string email, string key)
private async Task<DbAuditEvent> GetLinkVisitMessageAsync(string email, string key)
{
await using var context = _dbContextFactory.CreateDbContext();
@ -188,7 +188,7 @@ public class LinkValidationResult
static file class Queries
{
public static readonly Func<MessagesContext, string, string, Task<AuditEvent>> AuditEventsAsync =
public static readonly Func<MessagesContext, string, string, Task<DbAuditEvent>> AuditEventsAsync =
EF.CompileAsyncQuery(
(MessagesContext ctx, string target, string description) =>
ctx.AuditEvents.FirstOrDefault(a => a.Target == target && a.DescriptionRaw == description));

View File

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

View File

@ -564,6 +564,16 @@ public class EFUserService : IUserService
}
await userDbContext.AddOrUpdateAsync(q => q.Photos, userPhoto);
var userEntity = new User
{
Id = id,
LastModified = DateTime.UtcNow,
TenantId = tenant
};
userDbContext.Users.Attach(userEntity);
userDbContext.Entry(userEntity).Property(x => x.LastModified).IsModified = true;
}
else if (userPhoto != null)
{

View File

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

View File

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

View File

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

View File

@ -55,6 +55,33 @@ public class GeolocationHelper
_cache = cache;
}
public async Task<BaseEvent> AddGeolocationAsync(BaseEvent baseEvent)
{
var location = await GetGeolocationAsync(baseEvent.IP);
baseEvent.Country = location[0];
baseEvent.City = location[1];
return baseEvent;
}
public async Task<string[]> GetGeolocationAsync(string ip)
{
try
{
var location = await GetIPGeolocationAsync(IPAddress.Parse(ip));
if (string.IsNullOrEmpty(location.Key))
{
return new string[] { string.Empty, string.Empty };
}
var regionInfo = new RegionInfo(location.Key).EnglishName;
return new string[] { regionInfo, location.City };
}
catch (Exception ex)
{
_logger.ErrorWithException(ex);
return new string[] { string.Empty, string.Empty };
}
}
public async Task<IPGeolocationInfo> GetIPGeolocationAsync(IPAddress address)
{
try

View File

@ -28,7 +28,7 @@ using Profile = AutoMapper.Profile;
namespace ASC.AuditTrail.Models;
public class BaseEvent : IMapFrom<LoginEvent>
public class BaseEvent : IMapFrom<DbLoginEvent>
{
public int Id { get; set; }
public int TenantId { get; set; }
@ -37,7 +37,13 @@ public class BaseEvent : IMapFrom<LoginEvent>
public IList<string> Description { get; set; }
[Event("IpCol")]
public string IP { get; set; }
public string IP { get; set; }
[Event("CountryCol")]
public string Country { get; set; }
[Event("CityCol")]
public string City { get; set; }
[Event("BrowserCol")]
public string Browser { get; set; }
@ -59,7 +65,7 @@ public class BaseEvent : IMapFrom<LoginEvent>
public virtual void Mapping(Profile profile)
{
profile.CreateMap<LoginEvent, BaseEvent>()
profile.CreateMap<DbLoginEvent, BaseEvent>()
.ForMember(r => r.IP, opt => opt.MapFrom<BaseEventTypeIpResolver>())
.ForMember(r => r.Date, opt => opt.MapFrom<BaseEventTypeDateResolver>())
;

View File

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

View File

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

View File

@ -174,12 +174,12 @@ public class MessagesRepository : IDisposable
// messages with action code < 2000 are related to login-history
if ((int)message.Action < 2000)
{
var loginEvent = _mapper.Map<EventMessage, LoginEvent>(message);
var loginEvent = _mapper.Map<EventMessage, DbLoginEvent>(message);
await ef.LoginEvents.AddAsync(loginEvent);
}
else
{
var auditEvent = _mapper.Map<EventMessage, AuditEvent>(message);
var auditEvent = _mapper.Map<EventMessage, DbAuditEvent>(message);
await ef.AuditEvents.AddAsync(auditEvent);
}
}
@ -230,12 +230,12 @@ public class MessagesRepository : IDisposable
// messages with action code < 2000 are related to login-history
if ((int)message.Action < 2000)
{
var loginEvent = _mapper.Map<EventMessage, LoginEvent>(message);
var loginEvent = _mapper.Map<EventMessage, DbLoginEvent>(message);
ef.LoginEvents.Add(loginEvent);
}
else
{
var auditEvent = _mapper.Map<EventMessage, AuditEvent>(message);
var auditEvent = _mapper.Map<EventMessage, DbAuditEvent>(message);
ef.AuditEvents.Add(auditEvent);
}
}
@ -245,7 +245,7 @@ public class MessagesRepository : IDisposable
private async Task<int> AddLoginEventAsync(EventMessage message, MessagesContext dbContext)
{
var loginEvent = _mapper.Map<EventMessage, LoginEvent>(message);
var loginEvent = _mapper.Map<EventMessage, DbLoginEvent>(message);
await dbContext.LoginEvents.AddAsync(loginEvent);
await dbContext.SaveChangesAsync();
@ -255,7 +255,7 @@ public class MessagesRepository : IDisposable
private async Task<int> AddAuditEventAsync(EventMessage message, MessagesContext dbContext)
{
var auditEvent = _mapper.Map<EventMessage, AuditEvent>(message);
var auditEvent = _mapper.Map<EventMessage, DbAuditEvent>(message);
await dbContext.AuditEvents.AddAsync(auditEvent);
await dbContext.SaveChangesAsync();

View File

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

View File

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

View File

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

View File

@ -35,7 +35,9 @@ global using ASC.AuditTrail.Types;
global using ASC.Common;
global using ASC.Common.Mapping;
global using ASC.Core;
global using ASC.Core.Common.EF;
global using ASC.Core.Users;
global using ASC.Geolocation;
global using ASC.MessagingSystem.Core;
global using ASC.MessagingSystem.EF.Context;
global using ASC.MessagingSystem.EF.Model;

View File

@ -46,7 +46,7 @@ public class AuditActionMapper
};
}
public string GetActionText(MessageMaps action, AuditEventDto evt)
public string GetActionText(MessageMaps action, AuditEvent evt)
{
if (action == null)
{
@ -78,7 +78,7 @@ public class AuditActionMapper
}
}
public string GetActionText(MessageMaps action, LoginEventDto evt)
public string GetActionText(MessageMaps action, LoginEvent evt)
{
if (action == null)
{

View File

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

View File

@ -28,7 +28,7 @@ namespace ASC.AuditTrail.Models;
public class AuditEventQuery
{
public AuditEvent Event { get; set; }
public DbAuditEvent Event { get; set; }
public string UserName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }

View File

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

View File

@ -28,7 +28,7 @@ namespace ASC.AuditTrail.Models;
public class LoginEventQuery
{
public LoginEvent Event { get; set; }
public DbLoginEvent Event { get; set; }
public string UserName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }

View File

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

View File

@ -24,8 +24,6 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
using ASC.Core.Common.EF;
namespace ASC.AuditTrail.Repositories;
[Scope(Additional = typeof(AuditEventsRepositoryExtensions))]
@ -35,20 +33,23 @@ public class AuditEventsRepository
private readonly TenantManager _tenantManager;
private readonly IDbContextFactory<MessagesContext> _dbContextFactory;
private readonly IMapper _mapper;
private readonly GeolocationHelper _geolocationHelper;
public AuditEventsRepository(
AuditActionMapper auditActionMapper,
TenantManager tenantManager,
IDbContextFactory<MessagesContext> dbContextFactory,
IMapper mapper)
IMapper mapper,
GeolocationHelper geolocationHelper)
{
_auditActionMapper = auditActionMapper;
_tenantManager = tenantManager;
_dbContextFactory = dbContextFactory;
_mapper = mapper;
_geolocationHelper = geolocationHelper;
}
public async Task<IEnumerable<AuditEventDto>> GetByFilterAsync(
public async Task<IEnumerable<AuditEvent>> GetByFilterAsync(
Guid? userId = null,
ProductType? productType = null,
ModuleType? moduleType = null,
@ -77,7 +78,7 @@ public class AuditEventsRepository
withoutUserId);
}
public async Task<IEnumerable<AuditEventDto>> GetByFilterWithActionsAsync(
public async Task<IEnumerable<AuditEvent>> GetByFilterWithActionsAsync(
Guid? userId = null,
ProductType? productType = null,
ModuleType? moduleType = null,
@ -204,7 +205,13 @@ public class AuditEventsRepository
{
query = query.Take(limit);
}
return _mapper.Map<List<AuditEventQuery>, IEnumerable<AuditEventDto>>(await query.ToListAsync());
var events = _mapper.Map<List<AuditEventQuery>, IEnumerable<AuditEvent>>(await query.ToListAsync());
foreach(var e in events)
{
await _geolocationHelper.AddGeolocationAsync(e);
}
return events;
}
private static void FindByEntry(IQueryable<AuditEventQuery> q, EntryType entry, string target, IEnumerable<KeyValuePair<MessageAction, MessageMaps>> actions)

View File

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

View File

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

View File

@ -73,7 +73,6 @@ const withLoader = (WrappedComponent) => (Loader) => {
isLoadingFilesFind,
isInit: isPublicRoom ? true : isInit,
showBodyLoader,
isPublicRoom,
accountsViewAs,
};
}

View File

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

View File

@ -171,7 +171,7 @@ const getIconUrl = (extension: string, isImage: boolean, isMedia: boolean) => {
return iconSize32.get(path);
};
const convertFoldersToItems = (
export const convertFoldersToItems = (
folders: any,
disabledItems: any[],
filterParam?: string
@ -215,9 +215,17 @@ const convertFoldersToItems = (
return items;
};
const convertFilesToItems = (files: any, filterParam?: string) => {
export const convertFilesToItems = (files: any, filterParam?: string) => {
const items = files.map((file: any) => {
const { id, title, security, parentId, rootFolderType, fileExst } = file;
const {
id,
title,
security,
parentId,
folderId,
rootFolderType,
fileExst,
} = file;
const isImage = file.viewAccessability.ImageView;
const isMedia = file.viewAccessability.MediaView;
@ -231,9 +239,8 @@ const convertFilesToItems = (files: any, filterParam?: string) => {
label: title.replace(fileExst, ""),
title,
icon,
security,
parentId,
parentId: parentId || folderId,
rootFolderType,
isFolder: false,
isDisabled: !filterParam,
@ -259,6 +266,7 @@ export const useFilesHelper = ({
onSelectTreeNode,
setSelectedTreeNode,
filterParam,
getRootData,
}: useFilesHelpersProps) => {
const getFileList = React.useCallback(
async (
@ -305,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]
);

View File

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

View File

@ -0,0 +1,235 @@
import React from "react";
import { convertFilesToItems, convertFoldersToItems } from "./useFilesHelper";
import {
Item,
setItemsCallback,
useSocketHelperProps,
} from "../FilesSelector.types";
import { convertRoomsToItems } from "./useRoomsHelper";
const useSocketHelper = ({
socketHelper,
socketSubscribersId,
setItems,
setBreadCrumbs,
setTotal,
disabledItems,
filterParam,
}: useSocketHelperProps) => {
const subscribedId = React.useRef<null | number>(null);
const subscribe = (id: number) => {
const roomParts = `DIR-${id}`;
if (socketSubscribersId.has(roomParts)) return (subscribedId.current = id);
if (subscribedId.current && !socketSubscribersId.has(roomParts)) {
unsubscribe(subscribedId.current, false);
}
socketHelper.emit({
command: "subscribe",
data: {
roomParts: `DIR-${id}`,
individual: true,
},
});
subscribedId.current = id;
};
const unsubscribe = (id: number, clear = true) => {
if (clear) {
subscribedId.current = null;
}
if (id && !socketSubscribersId.has(`DIR-${id}`)) {
socketHelper.emit({
command: "unsubscribe",
data: {
roomParts: `DIR-${id}`,
individual: true,
},
});
}
};
const addItem = React.useCallback((opt: any) => {
if (!opt?.data) return;
const data = JSON.parse(opt.data);
if (
data.folderId
? data.folderId !== subscribedId.current
: data.parentId !== subscribedId.current
)
return;
let item: null | Item = null;
if (opt?.type === "file") {
item = convertFilesToItems([data], filterParam)[0];
} else if (opt?.type === "folder") {
item = !!data.roomType
? convertRoomsToItems([data])[0]
: convertFoldersToItems([data], disabledItems, filterParam)[0];
}
const callback: setItemsCallback = (value: Item[] | null) => {
if (!item || !value) return value;
if (opt.type === "folder") {
setTotal((value) => value + 1);
return [item, ...value];
}
if (opt.type === "file") {
let idx = 0;
for (let i = 0; i < value.length - 1; i++) {
if (!value[i].isFolder) break;
idx = i + 1;
}
const newValue = [...value];
newValue.splice(idx, 0, item);
setTotal((value) => value + 1);
return newValue;
}
return value;
};
setItems(callback);
}, []);
const updateItem = React.useCallback((opt: any) => {
if (!opt?.data) return;
const data = JSON.parse(opt.data);
if (
((data.folderId && data.folderId !== subscribedId.current) ||
(data.parentId && data.parentId !== subscribedId.current)) &&
data.id !== subscribedId.current
)
return;
let item: null | Item = null;
if (opt?.type === "file") {
item = convertFilesToItems([data], filterParam)[0];
} else if (opt?.type === "folder") {
item = !!data.roomType
? convertRoomsToItems([data])[0]
: convertFoldersToItems([data], disabledItems, filterParam)[0];
}
if (item?.id === subscribedId.current) {
return setBreadCrumbs((value) => {
if (!value) return value;
const newValue = [...value];
if (newValue[newValue.length - 1].id === item?.id) {
newValue[newValue.length - 1].label = item.label;
}
return newValue;
});
}
const callback: setItemsCallback = (value: Item[] | null) => {
if (!item || !value) return value;
if (opt.type === "folder") {
const idx = value.findIndex((v) => v.id === item?.id && v.isFolder);
if (idx > -1) {
const newValue = [...value];
newValue.splice(idx, 1, item);
return newValue;
}
setBreadCrumbs((breadCrumbsValue) => {
return breadCrumbsValue;
});
}
if (opt.type === "file") {
const idx = value.findIndex((v) => v.id === item?.id && !v.isFolder);
if (idx > -1) {
const newValue = [...value];
newValue.splice(idx, 1, item);
return [...newValue];
}
}
return value;
};
setItems(callback);
}, []);
const deleteItem = React.useCallback((opt: any) => {
const callback: setItemsCallback = (value: Item[] | null) => {
if (!value) return value;
if (opt.type === "folder") {
const newValue = value.filter((v) => +v.id !== +opt?.id || !v.isFolder);
if (newValue.length !== value.length) {
setTotal((value) => value - 1);
}
return newValue;
}
if (opt.type === "file") {
const newValue = value.filter((v) => +v.id !== +opt?.id || v.isFolder);
if (newValue.length !== value.length) {
setTotal((value) => value - 1);
}
return newValue;
}
return value;
};
setItems(callback);
}, []);
React.useEffect(() => {
socketHelper.on("s:modify-folder", async (opt: any) => {
switch (opt?.cmd) {
case "create":
addItem(opt);
break;
case "update":
updateItem(opt);
break;
case "delete":
deleteItem(opt);
break;
}
});
}, [addItem, updateItem, deleteItem]);
return { subscribe, unsubscribe };
};
export default useSocketHelper;

View File

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

View File

@ -56,12 +56,14 @@ export const getIsDisabled = (
isRequestRunning?: boolean,
security?: Security,
filterParam?: string,
isFileSelected?: boolean
isFileSelected?: boolean,
includeFolder?: boolean
) => {
if (isFirstLoad) return true;
if (isRequestRunning) return true;
if (!!filterParam) return !isFileSelected;
if (sameId && !isCopy) return true;
if (sameId && isCopy && includeFolder) return true;
if (isRooms) return true;
if (isRoot) return true;
if (isCopy) return !security?.CopyTo;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"])(

View File

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

View File

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

View File

@ -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 && (

View File

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

View File

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

View File

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

View File

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

View File

@ -254,6 +254,17 @@ const ClientRoutes = [
</ErrorBoundary>
</PublicRoute>
),
errorElement: <Error404 />,
children: [
{
index: true,
element: (
<PublicRoute>
<FilesView />
</PublicRoute>
),
},
],
},
{
path: "/wizard",

View File

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

View File

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

View File

@ -165,6 +165,21 @@ class FilesStore {
const { socketHelper } = authStore.settingsStore;
socketHelper.on("s:modify-folder", async (opt) => {
const { socketSubscribersId } = this.selectedFolderStore;
if (opt && opt.data) {
const data = JSON.parse(opt.data);
const pathParts = data.folderId
? `DIR-${data.folderId}`
: `DIR-${data.parentId}`;
if (
!socketSubscribersId.has(pathParts) &&
!socketSubscribersId.has(`DIR-${data.id}`)
)
return;
}
console.log("[WS] s:modify-folder", opt);
if (!(this.clientLoadingStore.isLoading || this.operationAction))
@ -200,6 +215,11 @@ class FilesStore {
});
socketHelper.on("refresh-folder", (id) => {
const { socketSubscribersId } = this.selectedFolderStore;
const pathParts = `DIR-${id}`;
if (!socketSubscribersId.has(pathParts)) return;
if (!id || this.clientLoadingStore.isLoading) return;
//console.log(
@ -216,6 +236,11 @@ class FilesStore {
});
socketHelper.on("s:markasnew-folder", ({ folderId, count }) => {
const { socketSubscribersId } = this.selectedFolderStore;
const pathParts = `DIR-${folderId}`;
if (!socketSubscribersId.has(pathParts)) return;
console.log(`[WS] markasnew-folder ${folderId}:${count}`);
const foundIndex =
@ -229,6 +254,11 @@ class FilesStore {
});
socketHelper.on("s:markasnew-file", ({ fileId, count }) => {
const { socketSubscribersId } = this.selectedFolderStore;
const pathParts = `FILE-${fileId}`;
if (!socketSubscribersId.has(pathParts)) return;
console.log(`[WS] markasnew-file ${fileId}:${count}`);
const foundIndex = fileId && this.files.findIndex((x) => x.id === fileId);
@ -246,6 +276,11 @@ class FilesStore {
//WAIT FOR RESPONSES OF EDITING FILE
socketHelper.on("s:start-edit-file", (id) => {
const { socketSubscribersId } = this.selectedFolderStore;
const pathParts = `FILE-${id}`;
if (!socketSubscribersId.has(pathParts)) return;
const foundIndex = this.files.findIndex((x) => x.id === id);
if (foundIndex == -1) return;
@ -264,6 +299,11 @@ class FilesStore {
});
socketHelper.on("s:stop-edit-file", (id) => {
const { socketSubscribersId } = this.selectedFolderStore;
const pathParts = `FILE-${id}`;
if (!socketSubscribersId.has(pathParts)) return;
const foundIndex = this.files.findIndex((x) => x.id === id);
if (foundIndex == -1) return;
@ -811,9 +851,14 @@ class FilesStore {
setFiles = (files) => {
const { socketHelper } = this.authStore.settingsStore;
const { addSocketSubscribersId, deleteSocketSubscribersId } =
this.selectedFolderStore;
if (files.length === 0 && this.files.length === 0) return;
if (this.files?.length > 0) {
this.files.forEach((f) => {
deleteSocketSubscribersId(`FILE-${f.id}`);
});
socketHelper.emit({
command: "unsubscribe",
data: {
@ -826,6 +871,10 @@ class FilesStore {
this.files = files;
if (this.files?.length > 0) {
this.files.forEach((f) => {
addSocketSubscribersId(`FILE-${f.id}`);
});
socketHelper.emit({
command: "subscribe",
data: {
@ -843,33 +892,38 @@ class FilesStore {
};
setFolders = (folders) => {
const { addSocketSubscribersId, deleteSocketSubscribersId } =
this.selectedFolderStore;
const { socketHelper } = this.authStore.settingsStore;
if (folders.length === 0 && this.folders.length === 0) return;
if (this.folders?.length > 0) {
this.folders.forEach((f) =>
socketHelper.emit({
command: "unsubscribe",
data: {
roomParts: `DIR-${f.id}`,
individual: true,
},
})
);
this.folders.forEach((f) => {
deleteSocketSubscribersId(`DIR-${f.id}`);
});
socketHelper.emit({
command: "unsubscribe",
data: {
roomParts: this.folders.map((f) => `DIR-${f.id}`),
individual: true,
},
});
}
this.folders = folders;
if (this.folders?.length > 0) {
this.folders.forEach((f) =>
socketHelper.emit({
command: "subscribe",
data: {
roomParts: `DIR-${f.id}`,
individual: true,
},
})
);
this.folders.forEach((f) => {
addSocketSubscribersId(`DIR-${f.id}`);
});
socketHelper.emit({
command: "subscribe",
data: {
roomParts: this.folders.map((f) => `DIR-${f.id}`),
individual: true,
},
});
}
};

View File

@ -32,6 +32,8 @@ class SelectedFolderStore {
settingsStore = null;
security = null;
socketSubscribersId = new Set();
constructor(settingsStore) {
makeAutoObservable(this);
this.settingsStore = settingsStore;
@ -69,6 +71,7 @@ class SelectedFolderStore {
this.tags = null;
this.rootFolderId = null;
this.security = null;
this.socketSubscribersId = new Set();
};
setParentId = (parentId) => {
@ -112,6 +115,14 @@ class SelectedFolderStore {
};
};
addSocketSubscribersId = (path) => {
this.socketSubscribersId.add(path);
};
deleteSocketSubscribersId = (path) => {
this.socketSubscribersId.delete(path);
};
setSelectedFolder = (selectedFolder) => {
const { socketHelper } = this.settingsStore;
@ -120,6 +131,8 @@ class SelectedFolderStore {
command: "unsubscribe",
data: { roomParts: `DIR-${this.id}`, individual: true },
});
this.deleteSocketSubscribersId(`DIR-${this.id}`);
}
if (selectedFolder) {
@ -127,6 +140,8 @@ class SelectedFolderStore {
command: "subscribe",
data: { roomParts: `DIR-${selectedFolder.id}`, individual: true },
});
this.addSocketSubscribersId(`DIR-${selectedFolder.id}`);
}
if (!selectedFolder) {

View File

@ -29,8 +29,14 @@ class TreeFoldersStore {
listenTreeFolders = (treeFolders) => {
const { socketHelper } = this.authStore.settingsStore;
const { addSocketSubscribersId, deleteSocketSubscribersId } =
this.selectedFolderStore;
if (treeFolders.length > 0) {
treeFolders.forEach((f) => {
deleteSocketSubscribersId(`DIR-${f.id}`);
});
socketHelper.emit({
command: "unsubscribe",
data: {
@ -39,6 +45,10 @@ class TreeFoldersStore {
},
});
treeFolders.forEach((f) => {
addSocketSubscribersId(`DIR-${f.id}`);
});
socketHelper.emit({
command: "subscribe",
data: {

View File

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

View File

@ -143,6 +143,13 @@ const StyledContainer = styled.div`
.title-icon {
min-width: 16px;
min-height: 16px;
svg {
path,
rect {
fill: ${({ theme }) => theme.navigation.publicIcon};
}
}
}
}

View File

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

View File

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

View File

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

View File

@ -206,6 +206,10 @@ const StyledTableGroupMenu = styled.div`
}
}
}
.scroll-body {
display: flex;
}
`;
StyledTableGroupMenu.defaultProps = { theme: Base };

View File

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

View File

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

View File

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

View File

@ -212,7 +212,7 @@ internal class FolderDao : AbstractDao, IFolderDao<int>
{
return 0;
}
await using var filesDbContext = _dbContextFactory.CreateDbContext();
if (filterType == FilterType.None && subjectId == default && string.IsNullOrEmpty(searchText) && !withSubfolders && !excludeSubject)
@ -225,7 +225,7 @@ internal class FolderDao : AbstractDao, IFolderDao<int>
return await q.CountAsync();
}
public async IAsyncEnumerable<Folder<int>> GetFoldersAsync(int parentId, OrderBy orderBy, FilterType filterType, bool subjectGroup, Guid subjectID, string searchText, bool withSubfolders = false,
public async IAsyncEnumerable<Folder<int>> GetFoldersAsync(int parentId, OrderBy orderBy, FilterType filterType, bool subjectGroup, Guid subjectID, string searchText, bool withSubfolders = false,
bool excludeSubject = false, int offset = 0, int count = -1)
{
if (CheckInvalidFilter(filterType) || count == 0)
@ -320,10 +320,10 @@ internal class FolderDao : AbstractDao, IFolderDao<int>
{
var roomTypes = new List<FolderType>
{
FolderType.CustomRoom,
FolderType.ReviewRoom,
FolderType.FillingFormsRoom,
FolderType.EditingRoom,
FolderType.CustomRoom,
FolderType.ReviewRoom,
FolderType.FillingFormsRoom,
FolderType.EditingRoom,
FolderType.ReadOnlyRoom,
FolderType.PublicRoom,
};
@ -528,6 +528,7 @@ internal class FolderDao : AbstractDao, IFolderDao<int>
await Queries.DeleteBunchObjectsAsync(filesDbContext, TenantID, folderId.ToString());
await filesDbContext.SaveChangesAsync();
await tx.CommitAsync();
await RecalculateFoldersCountAsync(parent);
});
@ -1216,14 +1217,14 @@ internal class FolderDao : AbstractDao, IFolderDao<int>
{
var roomTypes = new List<FolderType>
{
FolderType.CustomRoom,
FolderType.ReviewRoom,
FolderType.FillingFormsRoom,
FolderType.EditingRoom,
FolderType.CustomRoom,
FolderType.ReviewRoom,
FolderType.FillingFormsRoom,
FolderType.EditingRoom,
FolderType.ReadOnlyRoom,
FolderType.PublicRoom
};
Expression<Func<DbFolder, bool>> filter = f => roomTypes.Contains(f.FolderType);
await foreach (var e in GetFeedsInternalAsync(tenant, from, to, filter, null))
@ -1295,16 +1296,16 @@ internal class FolderDao : AbstractDao, IFolderDao<int>
public async IAsyncEnumerable<int> GetTenantsWithRoomsFeedsAsync(DateTime fromTime)
{
var roomTypes = new List<FolderType>
{
FolderType.CustomRoom,
FolderType.ReviewRoom,
FolderType.FillingFormsRoom,
FolderType.EditingRoom,
var roomTypes = new List<FolderType>
{
FolderType.CustomRoom,
FolderType.ReviewRoom,
FolderType.FillingFormsRoom,
FolderType.EditingRoom,
FolderType.ReadOnlyRoom,
FolderType.PublicRoom,
};
Expression<Func<DbFolder, bool>> filter = f => roomTypes.Contains(f.FolderType);
await foreach (var q in GetTenantsWithFeeds(fromTime, filter, true))
@ -1331,7 +1332,7 @@ internal class FolderDao : AbstractDao, IFolderDao<int>
if (rootFolderType != FolderType.VirtualRooms && rootFolderType != FolderType.Archive)
{
return (-1,"");
return (-1, "");
}
var rootFolderId = Convert.ToInt32(fileEntry.RootId);
@ -1348,7 +1349,7 @@ internal class FolderDao : AbstractDao, IFolderDao<int>
{
return (entryId, fileEntry.Title);
}
await using var filesDbContext = _dbContextFactory.CreateDbContext();
var parentFolders = await Queries.ParentIdTitlePairAsync(filesDbContext, folderId).ToListAsync();
@ -1525,7 +1526,7 @@ internal class FolderDao : AbstractDao, IFolderDao<int>
{
return (await _globalStore.GetStoreAsync()).CreateDataWriteOperator(chunkedUploadSession, sessionHolder);
}
private async Task<IQueryable<DbFolder>> GetFoldersQueryWithFilters(int parentId, OrderBy orderBy, bool subjectGroup, Guid subjectId, string searchText, bool withSubfolders, bool excludeSubject,
FilesDbContext filesDbContext)
{

View File

@ -46,7 +46,7 @@ internal class FileMoveCopyOperationData<T> : FileOperationData<T>
public FileConflictResolveType ResolveType { get; }
public IDictionary<string, StringValues> Headers { get; }
public FileMoveCopyOperationData(IEnumerable<T> folders, IEnumerable<T> files, Tenant tenant, JsonElement toFolderId, bool copy, FileConflictResolveType resolveType,
public FileMoveCopyOperationData(IEnumerable<T> folders, IEnumerable<T> files, Tenant tenant, JsonElement toFolderId, bool copy, FileConflictResolveType resolveType,
ExternalShareData externalShareData, bool holdResult = true, IDictionary<string, StringValues> headers = null)
: base(folders, files, tenant, externalShareData, holdResult)
{
@ -471,7 +471,7 @@ class FileMoveCopyOperation<T> : FileOperation<FileMoveCopyOperationData<T>, T>
newFolderId = await FolderDao.MoveFolderAsync(folder.Id, toFolderId, CancellationToken);
var (name, value) = await tenantQuotaFeatureStatHelper.GetStatAsync<CountRoomFeature, int>();
_ = quotaSocketManager.ChangeQuotaUsedValueAsync(name, value);
_ = quotaSocketManager.ChangeQuotaUsedValueAsync(name, value);
}
else
{
@ -581,7 +581,7 @@ class FileMoveCopyOperation<T> : FileOperation<FileMoveCopyOperationData<T>, T>
{
this[Err] = FilesCommonResource.ErrorMassage_SecurityException_MoveFile;
}
else if (checkPermissions && !await FilesSecurity.CanDownloadAsync(file))
else if (checkPermissions && file.RootFolderType != FolderType.TRASH && !await FilesSecurity.CanDownloadAsync(file))
{
this[Err] = FilesCommonResource.ErrorMassage_SecurityException;
}
@ -722,7 +722,7 @@ class FileMoveCopyOperation<T> : FileOperation<FileMoveCopyOperationData<T>, T>
{
foreach (var size in _thumbnailSettings.Sizes)
{
await(await globalStorage.GetStoreAsync()).CopyAsync(String.Empty,
await (await globalStorage.GetStoreAsync()).CopyAsync(String.Empty,
FileDao.GetUniqThumbnailPath(file, size.Width, size.Height),
String.Empty,
fileDao.GetUniqThumbnailPath(newFile, size.Width, size.Height));

View File

@ -24,6 +24,8 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
using Microsoft.AspNetCore.RateLimiting;
namespace ASC.People.Api;
public class UserController : PeopleControllerBase
@ -1221,7 +1223,8 @@ public class UserController : PeopleControllerBase
/// <requiresAuthorization>false</requiresAuthorization>
[AllowNotPayment]
[AllowAnonymous]
[HttpPost("password")]
[HttpPost("password")]
[EnableRateLimiting("sensitive_api")]
public async Task<object> SendUserPasswordAsync(MemberRequestDto inDto)
{
if (_authContext.IsAuthenticated)

View File

@ -95,7 +95,7 @@ public class ConnectionsController : ControllerBase
{
var user = await _userManager.GetUsersAsync(_securityContext.CurrentAccount.ID);
var loginEvents = await _dbLoginEventsManager.GetLoginEventsAsync(user.TenantId, user.Id);
var tasks = loginEvents.ConvertAll(async r => await ConvertAsync(r));
var tasks = loginEvents.ConvertAll(async r => await _geolocationHelper.AddGeolocationAsync(r));
var listLoginEvents = (await Task.WhenAll(tasks)).ToList();
var loginEventId = GetLoginEventIdFromCookie();
if (loginEventId != 0)
@ -118,7 +118,7 @@ public class ConnectionsController : ControllerBase
var browser = MessageSettings.GetBrowser(clientInfo);
var ip = MessageSettings.GetIP(request);
var baseEvent = new CustomEvent
var baseEvent = new BaseEvent
{
Id = 0,
Platform = platformAndDevice,
@ -127,7 +127,7 @@ public class ConnectionsController : ControllerBase
IP = ip
};
listLoginEvents.Add(await ConvertAsync(baseEvent));
listLoginEvents.Add(await _geolocationHelper.AddGeolocationAsync(baseEvent));
}
}
@ -280,45 +280,4 @@ public class ConnectionsController : ControllerBase
var loginEventId = _cookieStorage.GetLoginEventIdFromCookie(cookie);
return loginEventId;
}
private async Task<CustomEvent> ConvertAsync(BaseEvent baseEvent)
{
var location = await GetGeolocationAsync(baseEvent.IP);
return new CustomEvent
{
Id = baseEvent.Id,
IP = baseEvent.IP,
Platform = baseEvent.Platform,
Browser = baseEvent.Browser,
Date = baseEvent.Date,
Country = location[0],
City = location[1]
};
}
private async Task<string[]> GetGeolocationAsync(string ip)
{
try
{
var location = await _geolocationHelper.GetIPGeolocationAsync(IPAddress.Parse(ip));
if (string.IsNullOrEmpty(location.Key))
{
return new string[] { string.Empty, string.Empty };
}
var regionInfo = new RegionInfo(location.Key).EnglishName;
return new string[] { regionInfo, location.City };
}
catch (Exception ex)
{
_logger.ErrorWithException(ex);
return new string[] { string.Empty, string.Empty };
}
}
private class CustomEvent : BaseEvent
{
public string Country { get; set; }
public string City { get; set; }
}
}

View File

@ -24,9 +24,6 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
using AuditEventDto = ASC.Web.Api.ApiModel.ResponseDto.AuditEventDto;
using LoginEventDto = ASC.Web.Api.ApiModel.ResponseDto.LoginEventDto;
namespace ASC.Web.Api.Controllers;
/// <summary>

View File

@ -58,6 +58,14 @@ public class AuditEventDto
/// <type>System.String, System</type>
public string IP { get; set; }
/// <summary>Country</summary>
/// <type>System.String, System</type>
public string Country { get; set; }
/// <summary>City</summary>
/// <type>System.String, System</type>
public string City { get; set; }
/// <summary>Browser</summary>
/// <type>System.String, System</type>
public string Browser { get; set; }
@ -94,7 +102,7 @@ public class AuditEventDto
/// <type>System.String, System</type>
public string Context { get; set; }
public AuditEventDto(AuditTrail.Models.AuditEventDto auditEvent, AuditActionMapper auditActionMapper)
public AuditEventDto(AuditTrail.Models.AuditEvent auditEvent, AuditActionMapper auditActionMapper)
{
Id = auditEvent.Id;
Date = new ApiDateTime(auditEvent.Date, TimeSpan.Zero);
@ -103,6 +111,8 @@ public class AuditEventDto
Action = auditEvent.ActionText;
ActionId = (MessageAction)auditEvent.Action;
IP = auditEvent.IP;
Country = auditEvent.Country;
City = auditEvent.City;
Browser = auditEvent.Browser;
Platform = auditEvent.Platform;
Page = auditEvent.Page;

View File

@ -62,6 +62,14 @@ public class LoginEventDto
/// <type>System.String, System</type>
public string IP { get; set; }
/// <summary>Country</summary>
/// <type>System.String, System</type>
public string Country { get; set; }
/// <summary>City</summary>
/// <type>System.String, System</type>
public string City { get; set; }
/// <summary>Browser</summary>
/// <type>System.String, System</type>
public string Browser { get; set; }
@ -74,7 +82,7 @@ public class LoginEventDto
/// <type>System.String, System</type>
public string Page { get; set; }
public LoginEventDto(AuditTrail.Models.LoginEventDto loginEvent)
public LoginEventDto(AuditTrail.Models.LoginEvent loginEvent)
{
Id = loginEvent.Id;
Date = new ApiDateTime(loginEvent.Date, TimeSpan.Zero);
@ -84,6 +92,8 @@ public class LoginEventDto
Action = loginEvent.ActionText;
ActionId = (MessageAction)loginEvent.Action;
IP = loginEvent.IP;
Country = loginEvent.Country;
City = loginEvent.City;
Browser = loginEvent.Browser;
Platform = loginEvent.Platform;
Page = loginEvent.Page;