From f9df60dc883be2a735ead9162f4a7e24ba983443 Mon Sep 17 00:00:00 2001 From: SirConstance Date: Sat, 20 Sep 2025 09:02:53 +0200 Subject: [PATCH] Ajout des syncshell perma & temporaire --- .../Hubs/MareHub.Functions.cs | 8 +- .../Hubs/MareHub.Groups.cs | 115 ++++++++++++++++-- .../Data/MareDbContext.cs | 2 +- .../20250920090000_GroupTemporary.cs | 40 ++++++ .../Migrations/MareDbContextModelSnapshot.cs | 8 ++ .../MareSynchronosShared/Models/Group.cs | 5 +- 6 files changed, 166 insertions(+), 12 deletions(-) create mode 100644 MareSynchronosServer/MareSynchronosShared/Migrations/20250920090000_GroupTemporary.cs diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Functions.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Functions.cs index c1a7944..5872bc5 100644 --- a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Functions.cs +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Functions.cs @@ -259,7 +259,11 @@ public partial class MareHub var user = await DbContext.Users.SingleAsync(u => u.UID == groupHasMigrated.Item2).ConfigureAwait(false); await Clients.Users(groupPairsWithoutSelf.Select(p => p.GroupUserUID)).Client_GroupSendInfo(new GroupInfoDto(group.ToGroupData(), - user.ToUserData(), group.GetGroupPermissions())).ConfigureAwait(false); + user.ToUserData(), group.GetGroupPermissions()) + { + IsTemporary = group.IsTemporary, + ExpiresAt = group.ExpiresAt, + }).ConfigureAwait(false); } else { @@ -292,4 +296,4 @@ public partial class MareHub await UserGroupLeave(groupUserPair, allUserPairs, ident, userUid).ConfigureAwait(false); } } -} \ No newline at end of file +} diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Groups.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Groups.cs index 8f4075e..19228b8 100644 --- a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Groups.cs +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Groups.cs @@ -144,7 +144,11 @@ public partial class MareHub var groupPairs = await DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).AsNoTracking().ToListAsync().ConfigureAwait(false); - await Clients.Users(groupPairs).Client_GroupSendInfo(new GroupInfoDto(group.ToGroupData(), newOwnerPair.GroupUser.ToUserData(), group.GetGroupPermissions())).ConfigureAwait(false); + await Clients.Users(groupPairs).Client_GroupSendInfo(new GroupInfoDto(group.ToGroupData(), newOwnerPair.GroupUser.ToUserData(), group.GetGroupPermissions()) + { + IsTemporary = group.IsTemporary, + ExpiresAt = group.ExpiresAt, + }).ConfigureAwait(false); } [Authorize(Policy = "Identified")] @@ -232,9 +236,8 @@ public partial class MareHub sanitizedAlias = sanitizedAlias[..50]; } - var normalizedAlias = sanitizedAlias.ToLowerInvariant(); var aliasExists = await DbContext.Groups - .AnyAsync(g => g.Alias != null && g.Alias.ToLower() == normalizedAlias) + .AnyAsync(g => g.Alias != null && EF.Functions.ILike(g.Alias!, sanitizedAlias)) .ConfigureAwait(false); if (aliasExists) { @@ -249,6 +252,8 @@ public partial class MareHub InvitesEnabled = true, OwnerUID = UserUID, Alias = sanitizedAlias, + IsTemporary = false, + ExpiresAt = null, }; GroupPair initialPair = new() @@ -265,12 +270,98 @@ public partial class MareHub var self = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); - await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(newGroup.ToGroupData(), self.ToUserData(), GroupPermissions.NoneSet, GroupUserPermissions.NoneSet, GroupUserInfo.None)) - .ConfigureAwait(false); + await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(newGroup.ToGroupData(), self.ToUserData(), GroupPermissions.NoneSet, GroupUserPermissions.NoneSet, GroupUserInfo.None) + { + IsTemporary = newGroup.IsTemporary, + ExpiresAt = newGroup.ExpiresAt, + }).ConfigureAwait(false); _logger.LogCallInfo(MareHubLogger.Args(gid)); - return new GroupPasswordDto(newGroup.ToGroupData(), passwd); + return new GroupPasswordDto(newGroup.ToGroupData(), passwd) + { + IsTemporary = newGroup.IsTemporary, + ExpiresAt = newGroup.ExpiresAt, + }; + } + + [Authorize(Policy = "Identified")] + public async Task GroupCreateTemporary(DateTime expiresAtUtc) + { + _logger.LogCallInfo(); + + var now = DateTime.UtcNow; + if (expiresAtUtc.Kind == DateTimeKind.Unspecified) + { + expiresAtUtc = DateTime.SpecifyKind(expiresAtUtc, DateTimeKind.Utc); + } + + if (expiresAtUtc <= now) + { + throw new System.Exception("Expiration must be in the future."); + } + + if (expiresAtUtc > now.AddDays(7)) + { + throw new System.Exception("Temporary syncshells may not last longer than 7 days."); + } + + var existingGroupsByUser = await DbContext.Groups.CountAsync(u => u.OwnerUID == UserUID).ConfigureAwait(false); + var existingJoinedGroups = await DbContext.GroupPairs.CountAsync(u => u.GroupUserUID == UserUID).ConfigureAwait(false); + if (existingGroupsByUser >= _maxExistingGroupsByUser || existingJoinedGroups >= _maxJoinedGroupsByUser) + { + throw new System.Exception($"Max groups for user is {_maxExistingGroupsByUser}, max joined groups is {_maxJoinedGroupsByUser}."); + } + + var gid = StringUtils.GenerateRandomString(9); + while (await DbContext.Groups.AnyAsync(g => g.GID == "UMB-" + gid).ConfigureAwait(false)) + { + gid = StringUtils.GenerateRandomString(9); + } + gid = "UMB-" + gid; + + var passwd = StringUtils.GenerateRandomString(16); + var sha = SHA256.Create(); + var hashedPw = StringUtils.Sha256String(passwd); + + Group newGroup = new() + { + GID = gid, + HashedPassword = hashedPw, + InvitesEnabled = true, + OwnerUID = UserUID, + Alias = null, + IsTemporary = true, + ExpiresAt = expiresAtUtc, + }; + + GroupPair initialPair = new() + { + GroupGID = newGroup.GID, + GroupUserUID = UserUID, + IsPaused = false, + IsPinned = true, + }; + + await DbContext.Groups.AddAsync(newGroup).ConfigureAwait(false); + await DbContext.GroupPairs.AddAsync(initialPair).ConfigureAwait(false); + await DbContext.SaveChangesAsync().ConfigureAwait(false); + + var self = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); + + await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(newGroup.ToGroupData(), self.ToUserData(), GroupPermissions.NoneSet, GroupUserPermissions.NoneSet, GroupUserInfo.None) + { + IsTemporary = newGroup.IsTemporary, + ExpiresAt = newGroup.ExpiresAt, + }).ConfigureAwait(false); + + _logger.LogCallInfo(MareHubLogger.Args(gid, "Temporary", expiresAtUtc)); + + return new GroupPasswordDto(newGroup.ToGroupData(), passwd) + { + IsTemporary = newGroup.IsTemporary, + ExpiresAt = newGroup.ExpiresAt, + }; } [Authorize(Policy = "Identified")] @@ -395,7 +486,11 @@ 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())).ConfigureAwait(false); + await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(group.ToGroupData(), group.Owner.ToUserData(), group.GetGroupPermissions(), newPair.GetGroupPairPermissions(), newPair.GetGroupPairUserInfo()) + { + IsTemporary = group.IsTemporary, + ExpiresAt = group.ExpiresAt, + }).ConfigureAwait(false); var self = DbContext.Users.Single(u => u.UID == UserUID); @@ -547,7 +642,11 @@ public partial class MareHub var groups = await DbContext.GroupPairs.Include(g => g.Group).Include(g => g.Group.Owner).Where(g => g.GroupUserUID == UserUID).AsNoTracking().ToListAsync().ConfigureAwait(false); return groups.Select(g => new GroupFullInfoDto(g.Group.ToGroupData(), g.Group.Owner.ToUserData(), - g.Group.GetGroupPermissions(), g.GetGroupPairPermissions(), g.GetGroupPairUserInfo())).ToList(); + g.Group.GetGroupPermissions(), g.GetGroupPairPermissions(), g.GetGroupPairUserInfo()) + { + IsTemporary = g.Group.IsTemporary, + ExpiresAt = g.Group.ExpiresAt, + }).ToList(); } [Authorize(Policy = "Identified")] diff --git a/MareSynchronosServer/MareSynchronosShared/Data/MareDbContext.cs b/MareSynchronosServer/MareSynchronosShared/Data/MareDbContext.cs index 0e78bb5..9b03f1e 100644 --- a/MareSynchronosServer/MareSynchronosShared/Data/MareDbContext.cs +++ b/MareSynchronosServer/MareSynchronosShared/Data/MareDbContext.cs @@ -128,4 +128,4 @@ public class MareDbContext : DbContext mb.Entity().HasOne(u => u.AllowedGroup).WithMany().HasForeignKey(u => u.AllowedGroupGID).OnDelete(DeleteBehavior.Cascade); mb.Entity().HasOne(u => u.AllowedUser).WithMany().HasForeignKey(u => u.AllowedUserUID).OnDelete(DeleteBehavior.Cascade); } -} \ No newline at end of file +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20250920090000_GroupTemporary.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20250920090000_GroupTemporary.cs new file mode 100644 index 0000000..58b23b9 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20250920090000_GroupTemporary.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + /// + public partial class GroupTemporary : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "expires_at", + table: "groups", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "is_temporary", + table: "groups", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "expires_at", + table: "groups"); + + migrationBuilder.DropColumn( + name: "is_temporary", + table: "groups"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/MareDbContextModelSnapshot.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/MareDbContextModelSnapshot.cs index 7f857eb..a4bf759 100644 --- a/MareSynchronosServer/MareSynchronosShared/Migrations/MareDbContextModelSnapshot.cs +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/MareDbContextModelSnapshot.cs @@ -341,6 +341,10 @@ namespace MareSynchronosServer.Migrations .HasColumnType("boolean") .HasColumnName("disable_vfx"); + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + b.Property("IsPaused") .HasColumnType("boolean") .HasColumnName("is_paused"); @@ -454,6 +458,10 @@ namespace MareSynchronosServer.Migrations .HasColumnType("text") .HasColumnName("hashed_password"); + b.Property("IsTemporary") + .HasColumnType("boolean") + .HasColumnName("is_temporary"); + b.Property("InvitesEnabled") .HasColumnType("boolean") .HasColumnName("invites_enabled"); diff --git a/MareSynchronosServer/MareSynchronosShared/Models/Group.cs b/MareSynchronosServer/MareSynchronosShared/Models/Group.cs index 72f35d7..e1f0ff2 100644 --- a/MareSynchronosServer/MareSynchronosShared/Models/Group.cs +++ b/MareSynchronosServer/MareSynchronosShared/Models/Group.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; namespace MareSynchronosShared.Models; @@ -16,4 +17,6 @@ public class Group public bool DisableSounds { get; set; } public bool DisableAnimations { get; set; } public bool DisableVFX { get; set; } + public bool IsTemporary { get; set; } + public DateTime? ExpiresAt { get; set; } }