Préparation feature 2.0

This commit is contained in:
2025-11-01 22:46:55 +01:00
parent e484616ecd
commit b2b9ecad7b
10 changed files with 762 additions and 9 deletions

View File

@@ -263,6 +263,8 @@ public partial class MareHub
{
IsTemporary = group.IsTemporary,
ExpiresAt = group.ExpiresAt,
AutoDetectVisible = group.AutoDetectVisible,
PasswordTemporarilyDisabled = group.PasswordTemporarilyDisabled,
}).ConfigureAwait(false);
}
else

View File

@@ -148,6 +148,8 @@ public partial class MareHub
{
IsTemporary = group.IsTemporary,
ExpiresAt = group.ExpiresAt,
AutoDetectVisible = group.AutoDetectVisible,
PasswordTemporarilyDisabled = group.PasswordTemporarilyDisabled,
}).ConfigureAwait(false);
}
@@ -274,6 +276,8 @@ public partial class MareHub
{
IsTemporary = newGroup.IsTemporary,
ExpiresAt = newGroup.ExpiresAt,
AutoDetectVisible = newGroup.AutoDetectVisible,
PasswordTemporarilyDisabled = newGroup.PasswordTemporarilyDisabled,
}).ConfigureAwait(false);
_logger.LogCallInfo(MareHubLogger.Args(gid));
@@ -353,6 +357,8 @@ public partial class MareHub
{
IsTemporary = newGroup.IsTemporary,
ExpiresAt = newGroup.ExpiresAt,
AutoDetectVisible = newGroup.AutoDetectVisible,
PasswordTemporarilyDisabled = newGroup.PasswordTemporarilyDisabled,
}).ConfigureAwait(false);
_logger.LogCallInfo(MareHubLogger.Args(gid, "Temporary", expiresAtUtc));
@@ -449,22 +455,130 @@ public partial class MareHub
_logger.LogCallInfo(MareHubLogger.Args(dto.Group));
var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid).ConfigureAwait(false);
var groupGid = group?.GID ?? string.Empty;
var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID).ConfigureAwait(false);
var hashedPw = StringUtils.Sha256String(dto.Password);
return await JoinGroupInternal(group, aliasOrGid, hashedPw, allowPasswordless: false, skipInviteCheck: false).ConfigureAwait(false);
}
[Authorize(Policy = "Identified")]
public async Task<bool> SyncshellDiscoveryJoin(GroupDto dto)
{
var gid = dto.Group.GID.Trim();
_logger.LogCallInfo(MareHubLogger.Args(dto.Group));
var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false);
return await JoinGroupInternal(group, gid, hashedPassword: null, allowPasswordless: true, skipInviteCheck: true).ConfigureAwait(false);
}
[Authorize(Policy = "Identified")]
public async Task<List<SyncshellDiscoveryEntryDto>> SyncshellDiscoveryList()
{
_logger.LogCallInfo();
var groups = await DbContext.Groups.AsNoTracking()
.Include(g => g.Owner)
.Where(g => g.AutoDetectVisible && (!g.IsTemporary || g.ExpiresAt == null || g.ExpiresAt > DateTime.UtcNow))
.ToListAsync().ConfigureAwait(false);
var groupIds = groups.Select(g => g.GID).ToArray();
var memberCounts = await DbContext.GroupPairs.AsNoTracking()
.Where(p => groupIds.Contains(p.GroupGID))
.GroupBy(p => p.GroupGID)
.Select(g => new { g.Key, Count = g.Count() })
.ToDictionaryAsync(k => k.Key, k => k.Count, StringComparer.OrdinalIgnoreCase)
.ConfigureAwait(false);
return groups.Select(g => new SyncshellDiscoveryEntryDto
{
GID = g.GID,
Alias = g.Alias,
OwnerUID = g.OwnerUID,
OwnerAlias = g.Owner.Alias,
MemberCount = memberCounts.TryGetValue(g.GID, out var count) ? count : 0,
AutoAcceptPairs = g.InvitesEnabled,
Description = null,
}).ToList();
}
[Authorize(Policy = "Identified")]
public async Task<SyncshellDiscoveryStateDto?> SyncshellDiscoveryGetState(GroupDto dto)
{
_logger.LogCallInfo(MareHubLogger.Args(dto.Group));
var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
if (!hasRights) return null;
return new SyncshellDiscoveryStateDto
{
GID = group.GID,
AutoDetectVisible = group.AutoDetectVisible,
PasswordTemporarilyDisabled = group.PasswordTemporarilyDisabled,
};
}
[Authorize(Policy = "Identified")]
public async Task<bool> SyncshellDiscoverySetVisibility(SyncshellDiscoveryVisibilityRequestDto dto)
{
_logger.LogCallInfo(MareHubLogger.Args(dto));
var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.GID).ConfigureAwait(false);
if (!hasRights) return false;
group.AutoDetectVisible = dto.AutoDetectVisible;
group.PasswordTemporarilyDisabled = dto.AutoDetectVisible;
await DbContext.SaveChangesAsync().ConfigureAwait(false);
await DbContext.Entry(group).Reference(g => g.Owner).LoadAsync().ConfigureAwait(false);
var groupPairs = await DbContext.GroupPairs.AsNoTracking().Where(p => p.GroupGID == group.GID).Select(p => p.GroupUserUID).ToListAsync().ConfigureAwait(false);
await Clients.Users(groupPairs).Client_GroupSendInfo(new GroupInfoDto(group.ToGroupData(), group.Owner.ToUserData(), group.GetGroupPermissions())
{
IsTemporary = group.IsTemporary,
ExpiresAt = group.ExpiresAt,
AutoDetectVisible = group.AutoDetectVisible,
PasswordTemporarilyDisabled = group.PasswordTemporarilyDisabled,
}).ConfigureAwait(false);
return true;
}
private async Task<bool> JoinGroupInternal(Group? group, string aliasOrGid, string? hashedPassword, bool allowPasswordless, bool skipInviteCheck)
{
if (group == null) return false;
var groupGid = group.GID;
var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID).ConfigureAwait(false);
var existingUserCount = await DbContext.GroupPairs.AsNoTracking().CountAsync(g => g.GroupGID == groupGid).ConfigureAwait(false);
var joinedGroups = await DbContext.GroupPairs.CountAsync(g => g.GroupUserUID == UserUID).ConfigureAwait(false);
var isBanned = await DbContext.GroupBans.AnyAsync(g => g.GroupGID == groupGid && g.BannedUserUID == UserUID).ConfigureAwait(false);
var oneTimeInvite = await DbContext.GroupTempInvites.SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == hashedPw).ConfigureAwait(false);
if (group == null
|| (!string.Equals(group.HashedPassword, hashedPw, StringComparison.Ordinal) && oneTimeInvite == null)
GroupTempInvite? oneTimeInvite = null;
if (!string.IsNullOrEmpty(hashedPassword))
{
oneTimeInvite = await DbContext.GroupTempInvites.SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == hashedPassword).ConfigureAwait(false);
}
if (allowPasswordless && !group.AutoDetectVisible)
{
return false;
}
bool passwordBypass = group.PasswordTemporarilyDisabled || allowPasswordless;
bool passwordMatches = !string.IsNullOrEmpty(hashedPassword) && string.Equals(group.HashedPassword, hashedPassword, StringComparison.Ordinal);
bool hasValidCredential = passwordBypass || passwordMatches || oneTimeInvite != null;
if (!hasValidCredential
|| existingPair != null
|| existingUserCount >= _maxGroupUserCount
|| !group.InvitesEnabled
|| (!skipInviteCheck && !group.InvitesEnabled)
|| joinedGroups >= _maxJoinedGroupsByUser
|| isBanned)
{
return false;
}
if (oneTimeInvite != null)
{
@@ -486,13 +600,17 @@ public partial class MareHub
_logger.LogCallInfo(MareHubLogger.Args(aliasOrGid, "Success"));
await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(group.ToGroupData(), group.Owner.ToUserData(), group.GetGroupPermissions(), newPair.GetGroupPairPermissions(), newPair.GetGroupPairUserInfo())
var owner = group.Owner ?? await DbContext.Users.AsNoTracking().SingleAsync(u => u.UID == group.OwnerUID).ConfigureAwait(false);
await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(group.ToGroupData(), owner.ToUserData(), group.GetGroupPermissions(), newPair.GetGroupPairPermissions(), newPair.GetGroupPairUserInfo())
{
IsTemporary = group.IsTemporary,
ExpiresAt = group.ExpiresAt,
AutoDetectVisible = group.AutoDetectVisible,
PasswordTemporarilyDisabled = group.PasswordTemporarilyDisabled,
}).ConfigureAwait(false);
var self = DbContext.Users.Single(u => u.UID == UserUID);
var self = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false);
var groupPairs = await DbContext.GroupPairs.Include(p => p.GroupUser).Where(p => p.GroupGID == group.GID && p.GroupUserUID != UserUID).ToListAsync().ConfigureAwait(false);
@@ -646,6 +764,8 @@ public partial class MareHub
{
IsTemporary = g.Group.IsTemporary,
ExpiresAt = g.Group.ExpiresAt,
AutoDetectVisible = g.Group.AutoDetectVisible,
PasswordTemporarilyDisabled = g.Group.PasswordTemporarilyDisabled,
}).ToList();
}

View File

@@ -0,0 +1,264 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MareSynchronos.API.Dto.McdfShare;
using MareSynchronosServer.Utils;
using MareSynchronosShared.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
namespace MareSynchronosServer.Hubs;
public partial class MareHub
{
[Authorize(Policy = "Identified")]
public async Task<List<McdfShareEntryDto>> McdfShareGetOwn()
{
_logger.LogCallInfo();
var shares = await DbContext.McdfShares.AsNoTracking()
.Include(s => s.Owner)
.Include(s => s.AllowedIndividuals)
.Include(s => s.AllowedSyncshells)
.Where(s => s.OwnerUID == UserUID)
.OrderByDescending(s => s.CreatedUtc)
.ToListAsync().ConfigureAwait(false);
return shares.Select(s => MapShareEntryDto(s, true)).ToList();
}
[Authorize(Policy = "Identified")]
public async Task<List<McdfShareEntryDto>> McdfShareGetShared()
{
_logger.LogCallInfo();
var userGroups = await DbContext.GroupPairs.AsNoTracking()
.Where(p => p.GroupUserUID == UserUID)
.Select(p => p.GroupGID.ToUpperInvariant())
.ToListAsync().ConfigureAwait(false);
var shares = await DbContext.McdfShares.AsNoTracking()
.Include(s => s.Owner)
.Include(s => s.AllowedIndividuals)
.Include(s => s.AllowedSyncshells)
.Where(s => s.OwnerUID != UserUID)
.OrderByDescending(s => s.CreatedUtc)
.ToListAsync().ConfigureAwait(false);
var now = DateTime.UtcNow;
var accessible = shares.Where(s => ShareAccessibleToUser(s, userGroups) && (!s.ExpiresAtUtc.HasValue || s.ExpiresAtUtc > now)).ToList();
return accessible.Select(s => MapShareEntryDto(s, false)).ToList();
}
[Authorize(Policy = "Identified")]
public async Task<bool> McdfShareUpload(McdfShareUploadRequestDto dto)
{
_logger.LogCallInfo(MareHubLogger.Args(dto.ShareId));
var normalizedUsers = dto.AllowedIndividuals
.Where(s => !string.IsNullOrWhiteSpace(s))
.Select(NormalizeUid)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
var normalizedGroups = dto.AllowedSyncshells
.Where(s => !string.IsNullOrWhiteSpace(s))
.Select(NormalizeGroup)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
var share = await DbContext.McdfShares
.Include(s => s.AllowedIndividuals)
.Include(s => s.AllowedSyncshells)
.SingleOrDefaultAsync(s => s.Id == dto.ShareId)
.ConfigureAwait(false);
if (share != null && !string.Equals(share.OwnerUID, UserUID, StringComparison.Ordinal))
{
return false;
}
var now = DateTime.UtcNow;
if (share == null)
{
share = new McdfShare
{
Id = dto.ShareId,
OwnerUID = UserUID,
CreatedUtc = now,
};
DbContext.McdfShares.Add(share);
}
share.Description = dto.Description ?? string.Empty;
share.CipherData = dto.CipherData ?? Array.Empty<byte>();
share.Nonce = dto.Nonce ?? Array.Empty<byte>();
share.Salt = dto.Salt ?? Array.Empty<byte>();
share.Tag = dto.Tag ?? Array.Empty<byte>();
share.ExpiresAtUtc = dto.ExpiresAtUtc;
share.UpdatedUtc = now;
share.AllowedIndividuals.Clear();
foreach (var uid in normalizedUsers)
{
share.AllowedIndividuals.Add(new McdfShareAllowedUser
{
ShareId = share.Id,
AllowedIndividualUid = uid,
});
}
share.AllowedSyncshells.Clear();
foreach (var gid in normalizedGroups)
{
share.AllowedSyncshells.Add(new McdfShareAllowedGroup
{
ShareId = share.Id,
AllowedGroupGid = gid,
});
}
await DbContext.SaveChangesAsync().ConfigureAwait(false);
return true;
}
[Authorize(Policy = "Identified")]
public async Task<McdfShareEntryDto?> McdfShareUpdate(McdfShareUpdateRequestDto dto)
{
_logger.LogCallInfo(MareHubLogger.Args(dto.ShareId));
var share = await DbContext.McdfShares
.Include(s => s.AllowedIndividuals)
.Include(s => s.AllowedSyncshells)
.Include(s => s.Owner)
.SingleOrDefaultAsync(s => s.Id == dto.ShareId && s.OwnerUID == UserUID)
.ConfigureAwait(false);
if (share == null) return null;
share.Description = dto.Description ?? string.Empty;
share.ExpiresAtUtc = dto.ExpiresAtUtc;
share.UpdatedUtc = DateTime.UtcNow;
var normalizedUsers = dto.AllowedIndividuals
.Where(s => !string.IsNullOrWhiteSpace(s))
.Select(NormalizeUid)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
var normalizedGroups = dto.AllowedSyncshells
.Where(s => !string.IsNullOrWhiteSpace(s))
.Select(NormalizeGroup)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
share.AllowedIndividuals.Clear();
foreach (var uid in normalizedUsers)
{
share.AllowedIndividuals.Add(new McdfShareAllowedUser
{
ShareId = share.Id,
AllowedIndividualUid = uid,
});
}
share.AllowedSyncshells.Clear();
foreach (var gid in normalizedGroups)
{
share.AllowedSyncshells.Add(new McdfShareAllowedGroup
{
ShareId = share.Id,
AllowedGroupGid = gid,
});
}
await DbContext.SaveChangesAsync().ConfigureAwait(false);
return MapShareEntryDto(share, true);
}
[Authorize(Policy = "Identified")]
public async Task<bool> McdfShareDelete(Guid shareId)
{
_logger.LogCallInfo(MareHubLogger.Args(shareId));
var share = await DbContext.McdfShares.SingleOrDefaultAsync(s => s.Id == shareId && s.OwnerUID == UserUID).ConfigureAwait(false);
if (share == null) return false;
DbContext.McdfShares.Remove(share);
await DbContext.SaveChangesAsync().ConfigureAwait(false);
return true;
}
[Authorize(Policy = "Identified")]
public async Task<McdfSharePayloadDto?> McdfShareDownload(Guid shareId)
{
_logger.LogCallInfo(MareHubLogger.Args(shareId));
var share = await DbContext.McdfShares
.Include(s => s.AllowedIndividuals)
.Include(s => s.AllowedSyncshells)
.SingleOrDefaultAsync(s => s.Id == shareId)
.ConfigureAwait(false);
if (share == null) return null;
var userGroups = await DbContext.GroupPairs.AsNoTracking()
.Where(p => p.GroupUserUID == UserUID)
.Select(p => p.GroupGID.ToUpperInvariant())
.ToListAsync().ConfigureAwait(false);
bool isOwner = string.Equals(share.OwnerUID, UserUID, StringComparison.Ordinal);
if (!isOwner && (!ShareAccessibleToUser(share, userGroups) || (share.ExpiresAtUtc.HasValue && share.ExpiresAtUtc.Value <= DateTime.UtcNow)))
{
return null;
}
share.DownloadCount++;
await DbContext.SaveChangesAsync().ConfigureAwait(false);
return new McdfSharePayloadDto
{
ShareId = share.Id,
Description = share.Description,
CipherData = share.CipherData,
Nonce = share.Nonce,
Salt = share.Salt,
Tag = share.Tag,
CreatedUtc = share.CreatedUtc,
ExpiresAtUtc = share.ExpiresAtUtc,
};
}
private static string NormalizeUid(string candidate) => candidate.Trim().ToUpperInvariant();
private static string NormalizeGroup(string candidate) => candidate.Trim().ToUpperInvariant();
private static McdfShareEntryDto MapShareEntryDto(McdfShare share, bool isOwner)
{
return new McdfShareEntryDto
{
Id = share.Id,
Description = share.Description,
CreatedUtc = share.CreatedUtc,
UpdatedUtc = share.UpdatedUtc,
ExpiresAtUtc = share.ExpiresAtUtc,
DownloadCount = share.DownloadCount,
IsOwner = isOwner,
OwnerUid = share.OwnerUID,
OwnerAlias = share.Owner?.Alias ?? string.Empty,
AllowedIndividuals = share.AllowedIndividuals.Select(i => i.AllowedIndividualUid).OrderBy(s => s, StringComparer.OrdinalIgnoreCase).ToList(),
AllowedSyncshells = share.AllowedSyncshells.Select(g => g.AllowedGroupGid).OrderBy(s => s, StringComparer.OrdinalIgnoreCase).ToList(),
};
}
private bool ShareAccessibleToUser(McdfShare share, IReadOnlyCollection<string> userGroups)
{
if (string.Equals(share.OwnerUID, UserUID, StringComparison.Ordinal)) return true;
bool allowedByUser = share.AllowedIndividuals.Any(i => string.Equals(i.AllowedIndividualUid, UserUID, StringComparison.OrdinalIgnoreCase));
if (allowedByUser) return true;
if (share.AllowedSyncshells.Count == 0) return false;
var allowedGroups = share.AllowedSyncshells.Select(g => g.AllowedGroupGid).ToHashSet(StringComparer.OrdinalIgnoreCase);
return userGroups.Any(g => allowedGroups.Contains(g));
}
}