diff --git a/MareAPI b/MareAPI index 3b17590..deb911c 160000 --- a/MareAPI +++ b/MareAPI @@ -1 +1 @@ -Subproject commit 3b175900c10a9a152a168b1bd6fee390aa58e8e0 +Subproject commit deb911cb0a0e0abb2bfe4df9c243e09a70db4000 diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Functions.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Functions.cs index 5872bc5..4fa753d 100644 --- a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Functions.cs +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Functions.cs @@ -263,6 +263,8 @@ public partial class MareHub { IsTemporary = group.IsTemporary, ExpiresAt = group.ExpiresAt, + AutoDetectVisible = group.AutoDetectVisible, + PasswordTemporarilyDisabled = group.PasswordTemporarilyDisabled, }).ConfigureAwait(false); } else diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Groups.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Groups.cs index 19228b8..eeeac36 100644 --- a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Groups.cs +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Groups.cs @@ -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 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> 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 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 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 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(); } diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.McdfShare.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.McdfShare.cs new file mode 100644 index 0000000..f0b9915 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.McdfShare.cs @@ -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> 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> 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 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(); + share.Nonce = dto.Nonce ?? Array.Empty(); + share.Salt = dto.Salt ?? Array.Empty(); + share.Tag = dto.Tag ?? Array.Empty(); + 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 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 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 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 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)); + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Data/MareDbContext.cs b/MareSynchronosServer/MareSynchronosShared/Data/MareDbContext.cs index 9b03f1e..98c998d 100644 --- a/MareSynchronosServer/MareSynchronosShared/Data/MareDbContext.cs +++ b/MareSynchronosServer/MareSynchronosShared/Data/MareDbContext.cs @@ -51,6 +51,9 @@ public class MareDbContext : DbContext public DbSet CharaDataOriginalFiles { get; set; } public DbSet CharaDataPoses { get; set; } public DbSet CharaDataAllowances { get; set; } + public DbSet McdfShares { get; set; } + public DbSet McdfShareAllowedUsers { get; set; } + public DbSet McdfShareAllowedGroups { get; set; } protected override void OnModelCreating(ModelBuilder mb) { @@ -127,5 +130,28 @@ public class MareDbContext : DbContext mb.Entity().HasIndex(c => c.ParentId); 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); + + mb.Entity().ToTable("mcdf_shares"); + mb.Entity().HasIndex(s => s.OwnerUID); + mb.Entity().HasOne(s => s.Owner).WithMany().HasForeignKey(s => s.OwnerUID).OnDelete(DeleteBehavior.Cascade); + mb.Entity().Property(s => s.Description).HasColumnType("text"); + mb.Entity().Property(s => s.CipherData).HasColumnType("bytea"); + mb.Entity().Property(s => s.Nonce).HasColumnType("bytea"); + mb.Entity().Property(s => s.Salt).HasColumnType("bytea"); + mb.Entity().Property(s => s.Tag).HasColumnType("bytea"); + mb.Entity().Property(s => s.CreatedUtc).HasColumnType("timestamp with time zone"); + mb.Entity().Property(s => s.UpdatedUtc).HasColumnType("timestamp with time zone"); + mb.Entity().Property(s => s.ExpiresAtUtc).HasColumnType("timestamp with time zone"); + mb.Entity().Property(s => s.DownloadCount).HasColumnType("integer"); + mb.Entity().HasMany(s => s.AllowedIndividuals).WithOne(a => a.Share).HasForeignKey(a => a.ShareId).OnDelete(DeleteBehavior.Cascade); + mb.Entity().HasMany(s => s.AllowedSyncshells).WithOne(a => a.Share).HasForeignKey(a => a.ShareId).OnDelete(DeleteBehavior.Cascade); + + mb.Entity().ToTable("mcdf_share_allowed_users"); + mb.Entity().HasKey(u => new { u.ShareId, u.AllowedIndividualUid }); + mb.Entity().HasIndex(u => u.AllowedIndividualUid); + + mb.Entity().ToTable("mcdf_share_allowed_groups"); + mb.Entity().HasKey(g => new { g.ShareId, g.AllowedGroupGid }); + mb.Entity().HasIndex(g => g.AllowedGroupGid); } } diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20250921095000_SyncshellDiscovery.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20250921095000_SyncshellDiscovery.cs new file mode 100644 index 0000000..8630597 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20250921095000_SyncshellDiscovery.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + /// + public partial class SyncshellDiscovery : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "auto_detect_visible", + table: "groups", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "password_temporarily_disabled", + table: "groups", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "auto_detect_visible", + table: "groups"); + + migrationBuilder.DropColumn( + name: "password_temporarily_disabled", + table: "groups"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20250921095500_McdfShare.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20250921095500_McdfShare.cs new file mode 100644 index 0000000..f297b45 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20250921095500_McdfShare.cs @@ -0,0 +1,106 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + /// + public partial class McdfShare : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "mcdf_shares", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + owner_uid = table.Column(type: "character varying(10)", nullable: false), + description = table.Column(type: "text", nullable: false), + cipher_data = table.Column(type: "bytea", nullable: false), + nonce = table.Column(type: "bytea", nullable: false), + salt = table.Column(type: "bytea", nullable: false), + tag = table.Column(type: "bytea", nullable: false), + created_utc = table.Column(type: "timestamp with time zone", nullable: false), + updated_utc = table.Column(type: "timestamp with time zone", nullable: true), + expires_at_utc = table.Column(type: "timestamp with time zone", nullable: true), + download_count = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_mcdf_shares", x => x.id); + table.ForeignKey( + name: "fk_mcdf_shares_users_owner_uid", + column: x => x.owner_uid, + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "mcdf_share_allowed_groups", + columns: table => new + { + share_id = table.Column(type: "uuid", nullable: false), + allowed_group_gid = table.Column(type: "character varying(20)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_mcdf_share_allowed_groups", x => new { x.share_id, x.allowed_group_gid }); + table.ForeignKey( + name: "fk_mcdf_share_allowed_groups_mcdf_shares_share_id", + column: x => x.share_id, + principalTable: "mcdf_shares", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "mcdf_share_allowed_users", + columns: table => new + { + share_id = table.Column(type: "uuid", nullable: false), + allowed_individual_uid = table.Column(type: "character varying(10)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_mcdf_share_allowed_users", x => new { x.share_id, x.allowed_individual_uid }); + table.ForeignKey( + name: "fk_mcdf_share_allowed_users_mcdf_shares_share_id", + column: x => x.share_id, + principalTable: "mcdf_shares", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_mcdf_share_allowed_groups_allowed_group_gid", + table: "mcdf_share_allowed_groups", + column: "allowed_group_gid"); + + migrationBuilder.CreateIndex( + name: "ix_mcdf_share_allowed_users_allowed_individual_uid", + table: "mcdf_share_allowed_users", + column: "allowed_individual_uid"); + + migrationBuilder.CreateIndex( + name: "ix_mcdf_shares_owner_uid", + table: "mcdf_shares", + column: "owner_uid"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "mcdf_share_allowed_groups"); + + migrationBuilder.DropTable( + name: "mcdf_share_allowed_users"); + + migrationBuilder.DropTable( + name: "mcdf_shares"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/MareDbContextModelSnapshot.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/MareDbContextModelSnapshot.cs index a4bf759..2d77d17 100644 --- a/MareSynchronosServer/MareSynchronosShared/Migrations/MareDbContextModelSnapshot.cs +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/MareDbContextModelSnapshot.cs @@ -454,6 +454,10 @@ namespace MareSynchronosServer.Migrations .HasColumnType("boolean") .HasColumnName("disable_vfx"); + b.Property("AutoDetectVisible") + .HasColumnType("boolean") + .HasColumnName("auto_detect_visible"); + b.Property("HashedPassword") .HasColumnType("text") .HasColumnName("hashed_password"); @@ -462,10 +466,22 @@ namespace MareSynchronosServer.Migrations .HasColumnType("boolean") .HasColumnName("is_temporary"); + b.Property("PasswordTemporarilyDisabled") + .HasColumnType("boolean") + .HasColumnName("password_temporarily_disabled"); + b.Property("InvitesEnabled") .HasColumnType("boolean") .HasColumnName("invites_enabled"); + b.Property("AutoDetectVisible") + .HasColumnType("boolean") + .HasColumnName("auto_detect_visible"); + + b.Property("PasswordTemporarilyDisabled") + .HasColumnType("boolean") + .HasColumnName("password_temporarily_disabled"); + b.Property("OwnerUID") .HasColumnType("character varying(10)") .HasColumnName("owner_uid"); @@ -479,6 +495,99 @@ namespace MareSynchronosServer.Migrations b.ToTable("groups", (string)null); }); + modelBuilder.Entity("MareSynchronosShared.Models.McdfShare", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CipherData") + .HasColumnType("bytea") + .HasColumnName("cipher_data"); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_utc"); + + b.Property("DownloadCount") + .HasColumnType("integer") + .HasColumnName("download_count"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ExpiresAtUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at_utc"); + + b.Property("Nonce") + .HasColumnType("bytea") + .HasColumnName("nonce"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.Property("Salt") + .HasColumnType("bytea") + .HasColumnName("salt"); + + b.Property("Tag") + .HasColumnType("bytea") + .HasColumnName("tag"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_utc"); + + b.HasKey("Id") + .HasName("pk_mcdf_shares"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_mcdf_shares_owner_uid"); + + b.ToTable("mcdf_shares", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.McdfShareAllowedGroup", b => + { + b.Property("ShareId") + .HasColumnType("uuid") + .HasColumnName("share_id"); + + b.Property("AllowedGroupGid") + .HasColumnType("character varying(20)") + .HasColumnName("allowed_group_gid"); + + b.HasKey("ShareId", "AllowedGroupGid") + .HasName("pk_mcdf_share_allowed_groups"); + + b.HasIndex("AllowedGroupGid") + .HasDatabaseName("ix_mcdf_share_allowed_groups_allowed_group_gid"); + + b.ToTable("mcdf_share_allowed_groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.McdfShareAllowedUser", b => + { + b.Property("ShareId") + .HasColumnType("uuid") + .HasColumnName("share_id"); + + b.Property("AllowedIndividualUid") + .HasColumnType("character varying(10)") + .HasColumnName("allowed_individual_uid"); + + b.HasKey("ShareId", "AllowedIndividualUid") + .HasName("pk_mcdf_share_allowed_users"); + + b.HasIndex("AllowedIndividualUid") + .HasDatabaseName("ix_mcdf_share_allowed_users_allowed_individual_uid"); + + b.ToTable("mcdf_share_allowed_users", (string)null); + }); + modelBuilder.Entity("MareSynchronosShared.Models.GroupBan", b => { b.Property("GroupGID") @@ -745,6 +854,13 @@ namespace MareSynchronosServer.Migrations b.Navigation("User"); }); + modelBuilder.Entity("MareSynchronosShared.Models.McdfShare", b => + { + b.Navigation("AllowedIndividuals"); + + b.Navigation("AllowedSyncshells"); + }); + modelBuilder.Entity("MareSynchronosShared.Models.CharaData", b => { b.HasOne("MareSynchronosShared.Models.User", "Uploader") @@ -943,6 +1059,42 @@ namespace MareSynchronosServer.Migrations b.Navigation("Group"); }); + modelBuilder.Entity("MareSynchronosShared.Models.McdfShare", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_mcdf_shares_users_owner_uid"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.McdfShareAllowedGroup", b => + { + b.HasOne("MareSynchronosShared.Models.McdfShare", "Share") + .WithMany("AllowedSyncshells") + .HasForeignKey("ShareId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_mcdf_share_allowed_groups_mcdf_shares_share_id"); + + b.Navigation("Share"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.McdfShareAllowedUser", b => + { + b.HasOne("MareSynchronosShared.Models.McdfShare", "Share") + .WithMany("AllowedIndividuals") + .HasForeignKey("ShareId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_mcdf_share_allowed_users_mcdf_shares_share_id"); + + b.Navigation("Share"); + }); + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => { b.HasOne("MareSynchronosShared.Models.User", "User") diff --git a/MareSynchronosServer/MareSynchronosShared/Models/Group.cs b/MareSynchronosServer/MareSynchronosShared/Models/Group.cs index e1f0ff2..0d7b95f 100644 --- a/MareSynchronosServer/MareSynchronosShared/Models/Group.cs +++ b/MareSynchronosServer/MareSynchronosShared/Models/Group.cs @@ -19,4 +19,6 @@ public class Group public bool DisableVFX { get; set; } public bool IsTemporary { get; set; } public DateTime? ExpiresAt { get; set; } + public bool AutoDetectVisible { get; set; } + public bool PasswordTemporarilyDisabled { get; set; } } diff --git a/MareSynchronosServer/MareSynchronosShared/Models/McdfShare.cs b/MareSynchronosServer/MareSynchronosShared/Models/McdfShare.cs new file mode 100644 index 0000000..858b135 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Models/McdfShare.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace MareSynchronosShared.Models; + +public class McdfShare +{ + [Key] + public Guid Id { get; set; } + [MaxLength(10)] + public string OwnerUID { get; set; } = string.Empty; + public User Owner { get; set; } = null!; + public string Description { get; set; } = string.Empty; + public byte[] CipherData { get; set; } = Array.Empty(); + public byte[] Nonce { get; set; } = Array.Empty(); + public byte[] Salt { get; set; } = Array.Empty(); + public byte[] Tag { get; set; } = Array.Empty(); + public DateTime CreatedUtc { get; set; } + public DateTime? UpdatedUtc { get; set; } + public DateTime? ExpiresAtUtc { get; set; } + public int DownloadCount { get; set; } + public ICollection AllowedIndividuals { get; set; } = new HashSet(); + public ICollection AllowedSyncshells { get; set; } = new HashSet(); +} + +public class McdfShareAllowedUser +{ + public Guid ShareId { get; set; } + public McdfShare Share { get; set; } = null!; + [MaxLength(10)] + public string AllowedIndividualUid { get; set; } = string.Empty; +} + +public class McdfShareAllowedGroup +{ + public Guid ShareId { get; set; } + public McdfShare Share { get; set; } = null!; + [MaxLength(20)] + public string AllowedGroupGid { get; set; } = string.Empty; +}