diff --git a/MareAPI b/MareAPI index 9dc1e90..2d5d9d9 160000 --- a/MareAPI +++ b/MareAPI @@ -1 +1 @@ -Subproject commit 9dc1e901aa453e65bb432b2b288c1ff00021700a +Subproject commit 2d5d9d9d1cc87eebcf89cddc182e2c47fe31a8b4 diff --git a/MareSynchronosServer/.editorconfig b/MareSynchronosServer/.editorconfig new file mode 100644 index 0000000..6cf5454 --- /dev/null +++ b/MareSynchronosServer/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# MA0048: File name must match type name +dotnet_diagnostic.MA0048.severity = suggestion diff --git a/MareSynchronosServer/MareSynchronosServer.sln b/MareSynchronosServer/MareSynchronosServer.sln index 4e7c962..6edf0f4 100644 --- a/MareSynchronosServer/MareSynchronosServer.sln +++ b/MareSynchronosServer/MareSynchronosServer.sln @@ -11,9 +11,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronosServerTest", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronosShared", "MareSynchronosShared\MareSynchronosShared.csproj", "{67B1461D-E215-4BA8-A64D-E1836724D5E6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MareSynchronosStaticFilesServer", "MareSynchronosStaticFilesServer\MareSynchronosStaticFilesServer.csproj", "{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronosStaticFilesServer", "MareSynchronosStaticFilesServer\MareSynchronosStaticFilesServer.csproj", "{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MareSynchronosServices", "MareSynchronosServices\MareSynchronosServices.csproj", "{E29C8677-AB44-4950-9EB1-D8E70B710A56}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronosServices", "MareSynchronosServices\MareSynchronosServices.csproj", "{E29C8677-AB44-4950-9EB1-D8E70B710A56}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7D5C2B87-5CC9-4FE7-AD13-4C13F6600683}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/MareSynchronosServer/MareSynchronosServer/.config/dotnet-tools.json b/MareSynchronosServer/MareSynchronosServer/.config/dotnet-tools.json index c9b7f4f..b8c6c1f 100644 --- a/MareSynchronosServer/MareSynchronosServer/.config/dotnet-tools.json +++ b/MareSynchronosServer/MareSynchronosServer/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "6.0.6", + "version": "6.0.9", "commands": [ "dotnet-ef" ] diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/IdBasedUserIdProvider.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/IdBasedUserIdProvider.cs deleted file mode 100644 index ef08d56..0000000 --- a/MareSynchronosServer/MareSynchronosServer/Hubs/IdBasedUserIdProvider.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Linq; -using System.Security.Claims; -using Microsoft.AspNetCore.SignalR; - -namespace MareSynchronosServer.Hubs -{ - public class IdBasedUserIdProvider : IUserIdProvider - { - public string GetUserId(HubConnectionContext context) - { - return context.User!.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; - } - } -} diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Admin.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Admin.cs index 605cdb1..6c6b9f6 100644 --- a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Admin.cs +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Admin.cs @@ -8,165 +8,164 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; -namespace MareSynchronosServer.Hubs +namespace MareSynchronosServer.Hubs; + +public partial class MareHub { - public partial class MareHub + private bool IsAdmin => _dbContext.Users.Single(b => b.UID == AuthenticatedUserId).IsAdmin; + + private bool IsModerator => _dbContext.Users.Single(b => b.UID == AuthenticatedUserId).IsModerator || IsAdmin; + + private List OnlineAdmins => _dbContext.Users.Where(u => (u.IsModerator || u.IsAdmin)).Select(u => u.UID).ToList(); + + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.SendAdminChangeModeratorStatus)] + public async Task ChangeModeratorStatus(string uid, bool isModerator) { - private bool IsAdmin => _dbContext.Users.Single(b => b.UID == AuthenticatedUserId).IsAdmin; + if (!IsAdmin) return; + var user = await _dbContext.Users.SingleOrDefaultAsync(u => u.UID == uid).ConfigureAwait(false); - private bool IsModerator => _dbContext.Users.Single(b => b.UID == AuthenticatedUserId).IsModerator || IsAdmin; + if (user == null) return; - private List OnlineAdmins => _dbContext.Users.Where(u => (u.IsModerator || u.IsAdmin)).Select(u => u.UID).ToList(); + user.IsModerator = isModerator; + _dbContext.Update(user); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + await Clients.Users(user.UID).SendAsync(Api.OnAdminForcedReconnect).ConfigureAwait(false); + } - [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] - [HubMethodName(Api.SendAdminChangeModeratorStatus)] - public async Task ChangeModeratorStatus(string uid, bool isModerator) + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.SendAdminDeleteBannedUser)] + public async Task DeleteBannedUser(BannedUserDto dto) + { + if (!IsModerator || string.IsNullOrEmpty(dto.CharacterHash)) return; + + var existingUser = + await _dbContext.BannedUsers.SingleOrDefaultAsync(b => b.CharacterIdentification == dto.CharacterHash).ConfigureAwait(false); + if (existingUser == null) { - if (!IsAdmin) return; - var user = await _dbContext.Users.SingleOrDefaultAsync(u => u.UID == uid).ConfigureAwait(false); - - if (user == null) return; - - user.IsModerator = isModerator; - _dbContext.Update(user); - await _dbContext.SaveChangesAsync().ConfigureAwait(false); - await Clients.Users(user.UID).SendAsync(Api.OnAdminForcedReconnect).ConfigureAwait(false); + return; } - [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] - [HubMethodName(Api.SendAdminDeleteBannedUser)] - public async Task DeleteBannedUser(BannedUserDto dto) + _dbContext.Remove(existingUser); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminDeleteBannedUser, dto).ConfigureAwait(false); + } + + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.SendAdminDeleteForbiddenFile)] + public async Task DeleteForbiddenFile(ForbiddenFileDto dto) + { + if (!IsAdmin || string.IsNullOrEmpty(dto.Hash)) return; + + var existingFile = + await _dbContext.ForbiddenUploadEntries.SingleOrDefaultAsync(b => b.Hash == dto.Hash).ConfigureAwait(false); + if (existingFile == null) { - if (!IsModerator || string.IsNullOrEmpty(dto.CharacterHash)) return; - - var existingUser = - await _dbContext.BannedUsers.SingleOrDefaultAsync(b => b.CharacterIdentification == dto.CharacterHash).ConfigureAwait(false); - if (existingUser == null) - { - return; - } - - _dbContext.Remove(existingUser); - await _dbContext.SaveChangesAsync().ConfigureAwait(false); - await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminDeleteBannedUser, dto).ConfigureAwait(false); + return; } - [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] - [HubMethodName(Api.SendAdminDeleteForbiddenFile)] - public async Task DeleteForbiddenFile(ForbiddenFileDto dto) + _dbContext.Remove(existingFile); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminDeleteForbiddenFile, dto).ConfigureAwait(false); + } + + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.InvokeAdminGetBannedUsers)] + public async Task> GetBannedUsers() + { + if (!IsModerator) return null; + + return await _dbContext.BannedUsers.AsNoTracking().Select(b => new BannedUserDto() { - if (!IsAdmin || string.IsNullOrEmpty(dto.Hash)) return; + CharacterHash = b.CharacterIdentification, + Reason = b.Reason + }).ToListAsync().ConfigureAwait(false); + } - var existingFile = - await _dbContext.ForbiddenUploadEntries.SingleOrDefaultAsync(b => b.Hash == dto.Hash).ConfigureAwait(false); - if (existingFile == null) + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.InvokeAdminGetForbiddenFiles)] + public async Task> GetForbiddenFiles() + { + if (!IsModerator) return null; + + return await _dbContext.ForbiddenUploadEntries.AsNoTracking().Select(b => new ForbiddenFileDto() + { + Hash = b.Hash, + ForbiddenBy = b.ForbiddenBy + }).ToListAsync().ConfigureAwait(false); + } + + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.InvokeAdminGetOnlineUsers)] + public async Task> AdminGetOnlineUsers() + { + if (!IsModerator) return null; + + var users = await _dbContext.Users.AsNoTracking().ToListAsync().ConfigureAwait(false); + return users.Where(c => !string.IsNullOrEmpty(_clientIdentService.GetCharacterIdentForUid(c.UID).Result)).Select(async b => new OnlineUserDto + { + CharacterNameHash = await _clientIdentService.GetCharacterIdentForUid(b.UID).ConfigureAwait(false), + UID = b.UID, + IsModerator = b.IsModerator, + IsAdmin = b.IsAdmin + }).Select(c => c.Result).ToList(); + } + + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.SendAdminUpdateOrAddBannedUser)] + public async Task UpdateOrAddBannedUser(BannedUserDto dto) + { + if (!IsModerator || string.IsNullOrEmpty(dto.CharacterHash)) return; + + var existingUser = + await _dbContext.BannedUsers.SingleOrDefaultAsync(b => b.CharacterIdentification == dto.CharacterHash).ConfigureAwait(false); + if (existingUser != null) + { + existingUser.Reason = dto.Reason; + _dbContext.Update(existingUser); + } + else + { + await _dbContext.BannedUsers.AddAsync(new Banned { - return; - } - - _dbContext.Remove(existingFile); - await _dbContext.SaveChangesAsync().ConfigureAwait(false); - await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminDeleteForbiddenFile, dto).ConfigureAwait(false); + CharacterIdentification = dto.CharacterHash, + Reason = dto.Reason + }).ConfigureAwait(false); } - [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] - [HubMethodName(Api.InvokeAdminGetBannedUsers)] - public async Task> GetBannedUsers() + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminUpdateOrAddBannedUser, dto).ConfigureAwait(false); + var bannedUser = await _clientIdentService.GetUidForCharacterIdent(dto.CharacterHash).ConfigureAwait(false); + if (!string.IsNullOrEmpty(bannedUser)) { - if (!IsModerator) return null; - - return await _dbContext.BannedUsers.AsNoTracking().Select(b => new BannedUserDto() - { - CharacterHash = b.CharacterIdentification, - Reason = b.Reason - }).ToListAsync().ConfigureAwait(false); - } - - [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] - [HubMethodName(Api.InvokeAdminGetForbiddenFiles)] - public async Task> GetForbiddenFiles() - { - if (!IsModerator) return null; - - return await _dbContext.ForbiddenUploadEntries.AsNoTracking().Select(b => new ForbiddenFileDto() - { - Hash = b.Hash, - ForbiddenBy = b.ForbiddenBy - }).ToListAsync().ConfigureAwait(false); - } - - [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] - [HubMethodName(Api.InvokeAdminGetOnlineUsers)] - public async Task> AdminGetOnlineUsers() - { - if (!IsModerator) return null; - - var users = await _dbContext.Users.AsNoTracking().ToListAsync().ConfigureAwait(false); - return users.Where(c => !string.IsNullOrEmpty(_clientIdentService.GetCharacterIdentForUid(c.UID))).Select(b => new OnlineUserDto - { - CharacterNameHash = _clientIdentService.GetCharacterIdentForUid(b.UID), - UID = b.UID, - IsModerator = b.IsModerator, - IsAdmin = b.IsAdmin - }).ToList(); - } - - [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] - [HubMethodName(Api.SendAdminUpdateOrAddBannedUser)] - public async Task UpdateOrAddBannedUser(BannedUserDto dto) - { - if (!IsModerator || string.IsNullOrEmpty(dto.CharacterHash)) return; - - var existingUser = - await _dbContext.BannedUsers.SingleOrDefaultAsync(b => b.CharacterIdentification == dto.CharacterHash).ConfigureAwait(false); - if (existingUser != null) - { - existingUser.Reason = dto.Reason; - _dbContext.Update(existingUser); - } - else - { - await _dbContext.BannedUsers.AddAsync(new Banned - { - CharacterIdentification = dto.CharacterHash, - Reason = dto.Reason - }).ConfigureAwait(false); - } - - await _dbContext.SaveChangesAsync().ConfigureAwait(false); - await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminUpdateOrAddBannedUser, dto).ConfigureAwait(false); - var bannedUser = _clientIdentService.GetUidForCharacterIdent(dto.CharacterHash); - if (!string.IsNullOrEmpty(bannedUser)) - { - await Clients.User(bannedUser).SendAsync(Api.OnAdminForcedReconnect).ConfigureAwait(false); - } - } - - [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] - [HubMethodName(Api.SendAdminUpdateOrAddForbiddenFile)] - public async Task UpdateOrAddForbiddenFile(ForbiddenFileDto dto) - { - if (!IsAdmin || string.IsNullOrEmpty(dto.Hash)) return; - - var existingForbiddenFile = - await _dbContext.ForbiddenUploadEntries.SingleOrDefaultAsync(b => b.Hash == dto.Hash).ConfigureAwait(false); - if (existingForbiddenFile != null) - { - existingForbiddenFile.ForbiddenBy = dto.ForbiddenBy; - _dbContext.Update(existingForbiddenFile); - } - else - { - await _dbContext.ForbiddenUploadEntries.AddAsync(new ForbiddenUploadEntry - { - Hash = dto.Hash, - ForbiddenBy = dto.ForbiddenBy - }).ConfigureAwait(false); - } - - await _dbContext.SaveChangesAsync().ConfigureAwait(false); - - await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminUpdateOrAddForbiddenFile, dto).ConfigureAwait(false); + await Clients.User(bannedUser).SendAsync(Api.OnAdminForcedReconnect).ConfigureAwait(false); } } + + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.SendAdminUpdateOrAddForbiddenFile)] + public async Task UpdateOrAddForbiddenFile(ForbiddenFileDto dto) + { + if (!IsAdmin || string.IsNullOrEmpty(dto.Hash)) return; + + var existingForbiddenFile = + await _dbContext.ForbiddenUploadEntries.SingleOrDefaultAsync(b => b.Hash == dto.Hash).ConfigureAwait(false); + if (existingForbiddenFile != null) + { + existingForbiddenFile.ForbiddenBy = dto.ForbiddenBy; + _dbContext.Update(existingForbiddenFile); + } + else + { + await _dbContext.ForbiddenUploadEntries.AddAsync(new ForbiddenUploadEntry + { + Hash = dto.Hash, + ForbiddenBy = dto.ForbiddenBy + }).ConfigureAwait(false); + } + + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + + await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminUpdateOrAddForbiddenFile, dto).ConfigureAwait(false); + } } diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Files.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Files.cs index 3a89f58..df56a78 100644 --- a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Files.cs +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Files.cs @@ -16,228 +16,233 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace MareSynchronosServer.Hubs +namespace MareSynchronosServer.Hubs; + +public partial class MareHub { - public partial class MareHub + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.SendFileAbortUpload)] + public async Task AbortUpload() { - [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] - [HubMethodName(Api.SendFileAbortUpload)] - public async Task AbortUpload() - { - _logger.LogInformation("User {AuthenticatedUserId} aborted upload", AuthenticatedUserId); - var userId = AuthenticatedUserId; - var notUploadedFiles = _dbContext.Files.Where(f => !f.Uploaded && f.Uploader.UID == userId).ToList(); - _dbContext.RemoveRange(notUploadedFiles); - await _dbContext.SaveChangesAsync().ConfigureAwait(false); - } + _logger.LogCallInfo(Api.SendFileAbortUpload); + var userId = AuthenticatedUserId; + var notUploadedFiles = _dbContext.Files.Where(f => !f.Uploaded && f.Uploader.UID == userId).ToList(); + _dbContext.RemoveRange(notUploadedFiles); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + } - [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] - [HubMethodName(Api.SendFileDeleteAllFiles)] - public async Task DeleteAllFiles() - { - _logger.LogInformation("User {AuthenticatedUserId} deleted all their files", AuthenticatedUserId); + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.SendFileDeleteAllFiles)] + public async Task DeleteAllFiles() + { + _logger.LogCallInfo(Api.SendFileDeleteAllFiles); - var ownFiles = await _dbContext.Files.Where(f => f.Uploaded && f.Uploader.UID == AuthenticatedUserId).ToListAsync().ConfigureAwait(false); - var request = new DeleteFilesRequest(); - request.Hash.AddRange(ownFiles.Select(f => f.Hash)); - Metadata headers = new Metadata() - { - { "Authorization", Context.User!.Claims.SingleOrDefault(c => c.Type == ClaimTypes.Authentication)?.Value } - }; - _ = await _fileServiceClient.DeleteFilesAsync(request, headers).ConfigureAwait(false); - } - - [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] - [HubMethodName(Api.InvokeGetFilesSizes)] - public async Task> GetFilesSizes(List hashes) - { - var allFiles = await _dbContext.Files.Where(f => hashes.Contains(f.Hash)).ToListAsync().ConfigureAwait(false); - var forbiddenFiles = await _dbContext.ForbiddenUploadEntries. - Where(f => hashes.Contains(f.Hash)).ToListAsync().ConfigureAwait(false); - List response = new(); - - FileSizeRequest request = new FileSizeRequest(); - request.Hash.AddRange(hashes); - Metadata headers = new Metadata() - { - { "Authorization", Context.User!.Claims.SingleOrDefault(c => c.Type == ClaimTypes.Authentication)?.Value } - }; - var grpcResponse = await _fileServiceClient.GetFileSizesAsync(request, headers).ConfigureAwait(false); - - foreach (var hash in grpcResponse.HashToFileSize) + var ownFiles = await _dbContext.Files.Where(f => f.Uploaded && f.Uploader.UID == AuthenticatedUserId).ToListAsync().ConfigureAwait(false); + var request = new DeleteFilesRequest(); + request.Hash.AddRange(ownFiles.Select(f => f.Hash)); + Metadata headers = new Metadata() { - var forbiddenFile = forbiddenFiles.SingleOrDefault(f => f.Hash == hash.Key); - var downloadFile = allFiles.SingleOrDefault(f => f.Hash == hash.Key); + { "Authorization", Context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.Authentication, StringComparison.Ordinal))?.Value } + }; + _ = await _fileServiceClient.DeleteFilesAsync(request, headers).ConfigureAwait(false); + } - response.Add(new DownloadFileDto - { - FileExists = hash.Value > 0, - ForbiddenBy = forbiddenFile?.ForbiddenBy ?? string.Empty, - IsForbidden = forbiddenFile != null, - Hash = hash.Key, - Size = hash.Value, - Url = new Uri(_cdnFullUri, hash.Key.ToUpperInvariant()).ToString() - }); - } + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.InvokeGetFilesSizes)] + public async Task> GetFilesSizes(List hashes) + { + _logger.LogCallInfo(Api.InvokeGetFilesSizes, hashes.Count.ToString()); - return response; - } + var allFiles = await _dbContext.Files.Where(f => hashes.Contains(f.Hash)).ToListAsync().ConfigureAwait(false); + var forbiddenFiles = await _dbContext.ForbiddenUploadEntries. + Where(f => hashes.Contains(f.Hash)).ToListAsync().ConfigureAwait(false); + List response = new(); - [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] - [HubMethodName(Api.InvokeFileIsUploadFinished)] - public async Task IsUploadFinished() + FileSizeRequest request = new FileSizeRequest(); + request.Hash.AddRange(hashes); + Metadata headers = new Metadata() { - var userUid = AuthenticatedUserId; - return await _dbContext.Files.AsNoTracking() - .AnyAsync(f => f.Uploader.UID == userUid && !f.Uploaded).ConfigureAwait(false); - } + { "Authorization", Context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.Authentication, StringComparison.Ordinal))?.Value } + }; + var grpcResponse = await _fileServiceClient.GetFileSizesAsync(request, headers).ConfigureAwait(false); - [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] - [HubMethodName(Api.InvokeFileSendFiles)] - public async Task> SendFiles(List fileListHashes) + foreach (var hash in grpcResponse.HashToFileSize) { - var userSentHashes = new HashSet(fileListHashes.Distinct()); - _logger.LogInformation("User {AuthenticatedUserId} sending files: {count}", AuthenticatedUserId, userSentHashes.Count); - var notCoveredFiles = new Dictionary(); - var forbiddenFiles = await _dbContext.ForbiddenUploadEntries.AsNoTracking().Where(f => userSentHashes.Contains(f.Hash)).ToDictionaryAsync(f => f.Hash, f => f).ConfigureAwait(false); - var existingFiles = await _dbContext.Files.AsNoTracking().Where(f => userSentHashes.Contains(f.Hash)).ToDictionaryAsync(f => f.Hash, f => f).ConfigureAwait(false); - var uploader = await _dbContext.Users.SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false); + var forbiddenFile = forbiddenFiles.SingleOrDefault(f => string.Equals(f.Hash, hash.Key, StringComparison.Ordinal)); + var downloadFile = allFiles.SingleOrDefault(f => string.Equals(f.Hash, hash.Key, StringComparison.Ordinal)); - List fileCachesToUpload = new(); - foreach (var file in userSentHashes) + response.Add(new DownloadFileDto { - // Skip empty file hashes, duplicate file hashes, forbidden file hashes and existing file hashes - if (string.IsNullOrEmpty(file)) { continue; } - if (notCoveredFiles.ContainsKey(file)) { continue; } - if (forbiddenFiles.ContainsKey(file)) - { - notCoveredFiles[file] = new UploadFileDto() - { - ForbiddenBy = forbiddenFiles[file].ForbiddenBy, - Hash = file, - IsForbidden = true - }; + FileExists = hash.Value > 0, + ForbiddenBy = forbiddenFile?.ForbiddenBy ?? string.Empty, + IsForbidden = forbiddenFile != null, + Hash = hash.Key, + Size = hash.Value, + Url = new Uri(_cdnFullUri, hash.Key.ToUpperInvariant()).ToString() + }); + } - continue; - } - if (existingFiles.ContainsKey(file)) { continue; } + return response; + } - _logger.LogInformation("User {AuthenticatedUserId} needs upload: {file}", AuthenticatedUserId, file); - var userId = AuthenticatedUserId; - fileCachesToUpload.Add(new FileCache() - { - Hash = file, - Uploaded = false, - Uploader = uploader - }); + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.InvokeFileIsUploadFinished)] + public async Task IsUploadFinished() + { + _logger.LogCallInfo(Api.InvokeFileIsUploadFinished); + var userUid = AuthenticatedUserId; + return await _dbContext.Files.AsNoTracking() + .AnyAsync(f => f.Uploader.UID == userUid && !f.Uploaded).ConfigureAwait(false); + } + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.InvokeFileSendFiles)] + public async Task> SendFiles(List fileListHashes) + { + var userSentHashes = new HashSet(fileListHashes.Distinct(StringComparer.Ordinal), StringComparer.Ordinal); + _logger.LogCallInfo(Api.InvokeFileSendFiles, userSentHashes.Count.ToString()); + var notCoveredFiles = new Dictionary(StringComparer.Ordinal); + var forbiddenFiles = await _dbContext.ForbiddenUploadEntries.AsNoTracking().Where(f => userSentHashes.Contains(f.Hash)).ToDictionaryAsync(f => f.Hash, f => f).ConfigureAwait(false); + var existingFiles = await _dbContext.Files.AsNoTracking().Where(f => userSentHashes.Contains(f.Hash)).ToDictionaryAsync(f => f.Hash, f => f).ConfigureAwait(false); + var uploader = await _dbContext.Users.SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false); + + List fileCachesToUpload = new(); + foreach (var file in userSentHashes) + { + // Skip empty file hashes, duplicate file hashes, forbidden file hashes and existing file hashes + if (string.IsNullOrEmpty(file)) { continue; } + if (notCoveredFiles.ContainsKey(file)) { continue; } + if (forbiddenFiles.ContainsKey(file)) + { notCoveredFiles[file] = new UploadFileDto() { + ForbiddenBy = forbiddenFiles[file].ForbiddenBy, Hash = file, + IsForbidden = true }; + + continue; } - //Save bulk - await _dbContext.Files.AddRangeAsync(fileCachesToUpload).ConfigureAwait(false); - await _dbContext.SaveChangesAsync().ConfigureAwait(false); - return notCoveredFiles.Values.ToList(); + if (existingFiles.ContainsKey(file)) { continue; } + + _logger.LogCallInfo(Api.InvokeFileSendFiles, file, "Missing"); + + var userId = AuthenticatedUserId; + fileCachesToUpload.Add(new FileCache() + { + Hash = file, + Uploaded = false, + Uploader = uploader + }); + + notCoveredFiles[file] = new UploadFileDto() + { + Hash = file, + }; } + //Save bulk + await _dbContext.Files.AddRangeAsync(fileCachesToUpload).ConfigureAwait(false); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + return notCoveredFiles.Values.ToList(); + } - [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] - [HubMethodName(Api.SendFileUploadFileStreamAsync)] - public async Task UploadFileStreamAsync(string hash, IAsyncEnumerable fileContent) + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.SendFileUploadFileStreamAsync)] + public async Task UploadFileStreamAsync(string hash, IAsyncEnumerable fileContent) + { + _logger.LogCallInfo(Api.SendFileUploadFileStreamAsync, hash); + + var relatedFile = _dbContext.Files.SingleOrDefault(f => f.Hash == hash && f.Uploader.UID == AuthenticatedUserId && !f.Uploaded); + if (relatedFile == null) return; + var forbiddenFile = _dbContext.ForbiddenUploadEntries.SingleOrDefault(f => f.Hash == hash); + if (forbiddenFile != null) return; + + var tempFileName = Path.GetTempFileName(); + using var fileStream = new FileStream(tempFileName, FileMode.OpenOrCreate); + long length = 0; + try { - _logger.LogInformation("User {AuthenticatedUserId} uploading file: {hash}", AuthenticatedUserId, hash); + await foreach (var chunk in fileContent.ConfigureAwait(false)) + { + length += chunk.Length; + await fileStream.WriteAsync(chunk).ConfigureAwait(false); + } - var relatedFile = _dbContext.Files.SingleOrDefault(f => f.Hash == hash && f.Uploader.UID == AuthenticatedUserId && f.Uploaded == false); - if (relatedFile == null) return; - var forbiddenFile = _dbContext.ForbiddenUploadEntries.SingleOrDefault(f => f.Hash == hash); - if (forbiddenFile != null) return; - - var tempFileName = Path.GetTempFileName(); - using var fileStream = new FileStream(tempFileName, FileMode.OpenOrCreate); - long length = 0; + await fileStream.FlushAsync().ConfigureAwait(false); + await fileStream.DisposeAsync().ConfigureAwait(false); + } + catch + { try { - await foreach (var chunk in fileContent.ConfigureAwait(false)) - { - length += chunk.Length; - await fileStream.WriteAsync(chunk).ConfigureAwait(false); - } - await fileStream.FlushAsync().ConfigureAwait(false); await fileStream.DisposeAsync().ConfigureAwait(false); + _dbContext.Files.Remove(relatedFile); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); } catch { - try - { - await fileStream.FlushAsync().ConfigureAwait(false); - await fileStream.DisposeAsync().ConfigureAwait(false); - _dbContext.Files.Remove(relatedFile); - await _dbContext.SaveChangesAsync().ConfigureAwait(false); - } - catch - { - // already removed - } - finally - { - File.Delete(tempFileName); - } - - return; - } - - _logger.LogInformation("User {AuthenticatedUserId} upload finished: {hash}, size: {length}", AuthenticatedUserId, hash, length); - - try - { - var decodedFile = LZ4.LZ4Codec.Unwrap(await File.ReadAllBytesAsync(tempFileName).ConfigureAwait(false)); - using var sha1 = SHA1.Create(); - using var ms = new MemoryStream(decodedFile); - var computedHash = await sha1.ComputeHashAsync(ms).ConfigureAwait(false); - var computedHashString = BitConverter.ToString(computedHash).Replace("-", ""); - if (hash != computedHashString) - { - _logger.LogWarning("Computed file hash was not expected file hash. Computed: {computedHashString}, Expected {hash}", computedHashString, hash); - _dbContext.Remove(relatedFile); - await _dbContext.SaveChangesAsync().ConfigureAwait(false); - - return; - } - - Metadata headers = new Metadata() - { - { "Authorization", Context.User!.Claims.SingleOrDefault(c => c.Type == ClaimTypes.Authentication)?.Value } - }; - var streamingCall = _fileServiceClient.UploadFile(headers); - using var tempFileStream = new FileStream(tempFileName, FileMode.Open, FileAccess.Read); - int size = 1024 * 1024; - byte[] data = new byte[size]; - int readBytes; - while ((readBytes = tempFileStream.Read(data, 0, size)) > 0) - { - await streamingCall.RequestStream.WriteAsync(new UploadFileRequest() - { - FileData = ByteString.CopyFrom(data, 0, readBytes), - Hash = computedHashString, - Uploader = AuthenticatedUserId - }).ConfigureAwait(false); - } - await streamingCall.RequestStream.CompleteAsync(); - tempFileStream.Close(); - await tempFileStream.DisposeAsync(); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Upload failed"); - _dbContext.Remove(relatedFile); - await _dbContext.SaveChangesAsync().ConfigureAwait(false); + // already removed } finally { File.Delete(tempFileName); } + + return; + } + + _logger.LogCallInfo(Api.SendFileUploadFileStreamAsync, hash, "Uploaded"); + + try + { + var decodedFile = LZ4.LZ4Codec.Unwrap(await File.ReadAllBytesAsync(tempFileName).ConfigureAwait(false)); + using var sha1 = SHA1.Create(); + using var ms = new MemoryStream(decodedFile); + var computedHash = await sha1.ComputeHashAsync(ms).ConfigureAwait(false); + var computedHashString = BitConverter.ToString(computedHash).Replace("-", "", StringComparison.Ordinal); + if (!string.Equals(hash, computedHashString, StringComparison.Ordinal)) + { + _logger.LogCallWarning(Api.SendFileUploadFileStreamAsync, hash, "Invalid", computedHashString); + _dbContext.Remove(relatedFile); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + + return; + } + + Metadata headers = new Metadata() + { + { "Authorization", Context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.Authentication, StringComparison.Ordinal))?.Value } + }; + var streamingCall = _fileServiceClient.UploadFile(headers); + using var tempFileStream = new FileStream(tempFileName, FileMode.Open, FileAccess.Read); + int size = 1024 * 1024; + byte[] data = new byte[size]; + int readBytes; + while ((readBytes = tempFileStream.Read(data, 0, size)) > 0) + { + await streamingCall.RequestStream.WriteAsync(new UploadFileRequest() + { + FileData = ByteString.CopyFrom(data, 0, readBytes), + Hash = computedHashString, + Uploader = AuthenticatedUserId + }).ConfigureAwait(false); + } + await streamingCall.RequestStream.CompleteAsync().ConfigureAwait(false); + tempFileStream.Close(); + await tempFileStream.DisposeAsync().ConfigureAwait(false); + + _logger.LogCallInfo(Api.SendFileUploadFileStreamAsync, hash, "Pushed"); + } + catch (Exception ex) + { + _logger.LogCallWarning(Api.SendFileUploadFileStreamAsync, "Failed", hash, ex.Message); + _dbContext.Remove(relatedFile); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + } + finally + { + File.Delete(tempFileName); } } } diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Functions.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Functions.cs new file mode 100644 index 0000000..5b490ea --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Functions.cs @@ -0,0 +1,110 @@ +using MareSynchronosShared.Models; +using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System; +using Microsoft.AspNetCore.SignalR; +using System.Globalization; +using MareSynchronos.API; +using MareSynchronosServer.Utils; +using System.Security.Claims; +using Microsoft.Extensions.Logging; + +namespace MareSynchronosServer.Hubs; + +public partial class MareHub +{ + private async Task> GetAllPairedClientsWithPauseState(string? uid = null) + { + uid ??= AuthenticatedUserId; + + var query = await (from userPair in _dbContext.ClientPairs + join otherUserPair in _dbContext.ClientPairs on userPair.OtherUserUID equals otherUserPair.UserUID + where otherUserPair.OtherUserUID == uid && userPair.UserUID == uid + select new + { + UID = Convert.ToString(userPair.OtherUserUID), + GID = "DIRECT", + PauseState = (userPair.IsPaused || otherUserPair.IsPaused) + }) + .Union( + (from userGroupPair in _dbContext.GroupPairs + join otherGroupPair in _dbContext.GroupPairs on userGroupPair.GroupGID equals otherGroupPair.GroupGID + where + userGroupPair.GroupUserUID == uid + && otherGroupPair.GroupUserUID != uid + select new + { + UID = Convert.ToString(otherGroupPair.GroupUserUID), + GID = Convert.ToString(otherGroupPair.GroupGID), + PauseState = (userGroupPair.IsPaused || otherGroupPair.IsPaused) + }) + ).ToListAsync().ConfigureAwait(false); + + return query.GroupBy(g => g.UID, g => (g.GID, g.PauseState), + (key, g) => new PausedEntry + { + UID = key, + PauseStates = g.Select(p => new PauseState() { GID = string.Equals(p.GID, "DIRECT", StringComparison.Ordinal) ? null : p.GID, IsPaused = p.PauseState }) + .ToList() + }, StringComparer.Ordinal).ToList(); + } + + private async Task> GetAllPairedUnpausedUsers(string? uid = null) + { + uid ??= AuthenticatedUserId; + var ret = await GetAllPairedClientsWithPauseState(uid).ConfigureAwait(false); + return ret.Where(k => !k.IsPaused).Select(k => k.UID).ToList(); + } + + private async Task> SendDataToAllPairedUsers(string apiMethod, object arg) + { + var usersToSendDataTo = await GetAllPairedUnpausedUsers().ConfigureAwait(false); + await Clients.Users(usersToSendDataTo).SendAsync(apiMethod, arg).ConfigureAwait(false); + + return usersToSendDataTo; + } + + public string AuthenticatedUserId => Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.NameIdentifier, StringComparison.Ordinal))?.Value ?? "Unknown"; + + protected async Task GetAuthenticatedUserUntrackedAsync() + { + return await _dbContext.Users.AsNoTrackingWithIdentityResolution().SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false); + } + + private async Task UserGroupLeave(GroupPair groupUserPair, List allUserPairs, string userIdent, string? uid = null) + { + uid ??= AuthenticatedUserId; + var userPair = allUserPairs.SingleOrDefault(p => string.Equals(p.UID, groupUserPair.GroupUserUID, StringComparison.Ordinal)); + if (userPair != null) + { + if (userPair.IsDirectlyPaused != PauseInfo.NoConnection) return; + if (userPair.IsPausedPerGroup is PauseInfo.Unpaused) return; + } + + var groupUserIdent = await _clientIdentService.GetCharacterIdentForUid(groupUserPair.GroupUserUID).ConfigureAwait(false); + if (!string.IsNullOrEmpty(groupUserIdent)) + { + await Clients.User(uid).SendAsync(Api.OnUserRemoveOnlinePairedPlayer, groupUserIdent).ConfigureAwait(false); + await Clients.User(groupUserPair.GroupUserUID).SendAsync(Api.OnUserRemoveOnlinePairedPlayer, userIdent).ConfigureAwait(false); + } + } + + private async Task SendGroupDeletedToAll(List groupUsers) + { + foreach (var pair in groupUsers) + { + var pairIdent = await _clientIdentService.GetCharacterIdentForUid(pair.GroupUserUID).ConfigureAwait(false); + if (string.IsNullOrEmpty(pairIdent)) continue; + + var pairs = await GetAllPairedClientsWithPauseState(pair.GroupUserUID).ConfigureAwait(false); + + foreach (var groupUserPair in groupUsers.Where(g => !string.Equals(g.GroupUserUID, pair.GroupUserUID, StringComparison.Ordinal))) + { + await UserGroupLeave(groupUserPair, pairs, pairIdent, pair.GroupUserUID).ConfigureAwait(false); + } + } + } + +} diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Groups.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Groups.cs new file mode 100644 index 0000000..4ae2b56 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Groups.cs @@ -0,0 +1,548 @@ +using MareSynchronos.API; +using MareSynchronosServer.Utils; +using MareSynchronosShared.Authentication; +using MareSynchronosShared.Data; +using MareSynchronosShared.Models; +using MareSynchronosShared.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Threading.Tasks; + +namespace MareSynchronosServer.Hubs; + +public partial class MareHub +{ + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.InvokeGroupCreate)] + public async Task CreateGroup() + { + _logger.LogCallInfo(Api.InvokeGroupCreate); + var existingGroupsByUser = _dbContext.Groups.Count(u => u.OwnerUID == AuthenticatedUserId); + var existingJoinedGroups = _dbContext.GroupPairs.Count(u => u.GroupUserUID == AuthenticatedUserId); + if (existingGroupsByUser >= _maxExistingGroupsByUser || existingJoinedGroups >= _maxJoinedGroupsByUser) + { + throw new System.Exception($"Max groups for user is {_maxExistingGroupsByUser}, max joined groups is {_maxJoinedGroupsByUser}."); + } + + var gid = StringUtils.GenerateRandomString(12); + while (await _dbContext.Groups.AnyAsync(g => g.GID == "MSS-" + gid).ConfigureAwait(false)) + { + gid = StringUtils.GenerateRandomString(12); + } + gid = "MSS-" + 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 = AuthenticatedUserId + }; + + GroupPair initialPair = new() + { + GroupGID = newGroup.GID, + GroupUserUID = AuthenticatedUserId, + 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 = _dbContext.Users.Single(u => u.UID == AuthenticatedUserId); + + await Clients.User(AuthenticatedUserId).SendAsync(Api.OnGroupChange, new GroupDto() + { + GID = newGroup.GID, + OwnedBy = string.IsNullOrEmpty(self.Alias) ? self.UID : self.Alias, + IsDeleted = false, + IsPaused = false, + InvitesEnabled = true + }).ConfigureAwait(false); + + _logger.LogCallInfo(Api.InvokeGroupCreate, gid); + + return new GroupCreatedDto() + { + GID = newGroup.GID, + Password = passwd + }; + } + + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.InvokeGroupGetGroups)] + public async Task> GetGroups() + { + _logger.LogCallInfo(Api.InvokeGroupGetGroups); + + var groups = await _dbContext.GroupPairs.Include(g => g.Group).Include(g => g.Group.Owner).Where(g => g.GroupUserUID == AuthenticatedUserId).ToListAsync().ConfigureAwait(false); + + return groups.Select(g => new GroupDto() + { + GID = g.GroupGID, + Alias = g.Group.Alias, + InvitesEnabled = g.Group.InvitesEnabled, + OwnedBy = string.IsNullOrEmpty(g.Group.Owner.Alias) ? g.Group.Owner.UID : g.Group.Owner.Alias, + IsPaused = g.IsPaused + }).ToList(); + } + + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.InvokeGroupGetUsersInGroup)] + public async Task> GetUsersInGroup(string gid) + { + _logger.LogCallInfo(Api.InvokeGroupGetUsersInGroup, gid); + + var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false); + var existingPair = await _dbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == AuthenticatedUserId).ConfigureAwait(false); + if (group == null || existingPair == null) return new List(); + + var allPairs = await _dbContext.GroupPairs.Include(g => g.GroupUser).Where(g => g.GroupGID == gid && g.GroupUserUID != AuthenticatedUserId).ToListAsync().ConfigureAwait(false); + return allPairs.Select(p => new GroupPairDto() + { + GroupGID = gid, + IsPaused = p.IsPaused, + IsRemoved = false, + UserUID = p.GroupUser.UID, + UserAlias = p.GroupUser.Alias, + IsPinned = p.IsPinned + }).ToList(); + } + + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.SendGroupChangeInviteState)] + public async Task GroupChangeInviteState(string gid, bool enabled) + { + _logger.LogCallInfo(Api.SendGroupChangeInviteState, gid, enabled.ToString()); + + var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false); + if (group == null || !string.Equals(group.OwnerUID, AuthenticatedUserId, StringComparison.Ordinal)) return; + + group.InvitesEnabled = enabled; + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + + _logger.LogCallInfo(Api.SendGroupChangeInviteState, gid, enabled.ToString(), "Success"); + + var groupPairs = _dbContext.GroupPairs.Where(p => p.GroupGID == gid).Select(p => p.GroupUserUID).ToList(); + await Clients.Users(groupPairs).SendAsync(Api.OnGroupChange, new GroupDto() + { + GID = gid, + InvitesEnabled = enabled, + }).ConfigureAwait(false); + } + + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.SendGroupDelete)] + public async Task GroupDelete(string gid) + { + _logger.LogCallInfo(Api.SendGroupDelete, gid); + + var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false); + if (group == null || !string.Equals(group.OwnerUID, AuthenticatedUserId, StringComparison.Ordinal)) return; + + _logger.LogCallInfo(Api.SendGroupDelete, gid, "Success"); + + var groupPairs = await _dbContext.GroupPairs.Where(p => p.GroupGID == gid).ToListAsync().ConfigureAwait(false); + _dbContext.RemoveRange(groupPairs); + _dbContext.Remove(group); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + + await Clients.Users(groupPairs.Select(g => g.GroupUserUID)).SendAsync(Api.OnGroupChange, new GroupDto() + { + GID = group.GID, + IsDeleted = true, + }).ConfigureAwait(false); + + + await SendGroupDeletedToAll(groupPairs).ConfigureAwait(false); + } + + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.InvokeGroupJoin)] + public async Task GroupJoin(string gid, string password) + { + _logger.LogCallInfo(Api.InvokeGroupJoin, gid); + + var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid || g.Alias == gid).ConfigureAwait(false); + var existingPair = await _dbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == AuthenticatedUserId).ConfigureAwait(false); + var hashedPw = StringUtils.Sha256String(password); + var existingUserCount = await _dbContext.GroupPairs.CountAsync(g => g.GroupGID == gid).ConfigureAwait(false); + var joinedGroups = await _dbContext.GroupPairs.CountAsync(g => g.GroupUserUID == AuthenticatedUserId).ConfigureAwait(false); + if (group == null + || !string.Equals(group.HashedPassword, hashedPw, StringComparison.Ordinal) + || existingPair != null + || existingUserCount >= _maxGroupUserCount + || !group.InvitesEnabled + || joinedGroups >= _maxJoinedGroupsByUser) + return false; + + GroupPair newPair = new() + { + GroupGID = group.GID, + GroupUserUID = AuthenticatedUserId + }; + + await _dbContext.GroupPairs.AddAsync(newPair).ConfigureAwait(false); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + + _logger.LogCallInfo(Api.InvokeGroupJoin, gid, "Success"); + + await Clients.User(AuthenticatedUserId).SendAsync(Api.OnGroupChange, new GroupDto() + { + GID = group.GID, + OwnedBy = group.OwnerUID, + IsDeleted = false, + IsPaused = false, + Alias = group.Alias, + InvitesEnabled = true + }).ConfigureAwait(false); + + var self = _dbContext.Users.Single(u => u.UID == AuthenticatedUserId); + + var groupPairs = await _dbContext.GroupPairs.Where(p => p.GroupGID == group.GID && p.GroupUserUID != AuthenticatedUserId).ToListAsync().ConfigureAwait(false); + await Clients.Users(groupPairs.Select(p => p.GroupUserUID)).SendAsync(Api.OnGroupUserChange, new GroupPairDto() + { + GroupGID = group.GID, + IsPaused = false, + IsRemoved = false, + UserUID = AuthenticatedUserId, + UserAlias = self.Alias, + IsPinned = false + }).ConfigureAwait(false); + + var allUserPairs = await GetAllPairedClientsWithPauseState().ConfigureAwait(false); + + var userIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false); + foreach (var groupUserPair in groupPairs) + { + var userPair = allUserPairs.Single(p => string.Equals(p.UID, groupUserPair.GroupUserUID, StringComparison.Ordinal)); + if (userPair.IsDirectlyPaused != PauseInfo.NoConnection) continue; + if (userPair.IsPausedExcludingGroup(gid) is PauseInfo.Unpaused) continue; + if (userPair.IsPausedPerGroup is PauseInfo.Paused) continue; + + var groupUserIdent = await _clientIdentService.GetCharacterIdentForUid(groupUserPair.GroupUserUID).ConfigureAwait(false); + if (!string.IsNullOrEmpty(groupUserIdent)) + { + await Clients.User(AuthenticatedUserId).SendAsync(Api.OnUserAddOnlinePairedPlayer, groupUserIdent).ConfigureAwait(false); + await Clients.User(groupUserPair.GroupUserUID).SendAsync(Api.OnUserAddOnlinePairedPlayer, userIdent).ConfigureAwait(false); + } + } + + return true; + } + + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.SendGroupLeave)] + public async Task GroupLeave(string gid) + { + _logger.LogCallInfo(Api.SendGroupLeave, gid); + + var groupPair = await _dbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == AuthenticatedUserId).ConfigureAwait(false); + if (groupPair == null) return; + + var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false); + + var groupPairs = await _dbContext.GroupPairs.Where(p => p.GroupGID == group.GID).ToListAsync().ConfigureAwait(false); + var groupPairsWithoutSelf = groupPairs.Where(p => !string.Equals(p.GroupUserUID, AuthenticatedUserId, StringComparison.Ordinal)).ToList(); + + _dbContext.GroupPairs.Remove(groupPair); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + + await Clients.User(AuthenticatedUserId).SendAsync(Api.OnGroupChange, new GroupDto() + { + GID = group.GID, + IsDeleted = true + }).ConfigureAwait(false); + + bool ownerHasLeft = string.Equals(group.OwnerUID, AuthenticatedUserId, StringComparison.Ordinal); + if (ownerHasLeft) + { + if (!groupPairsWithoutSelf.Any()) + { + _logger.LogCallInfo(Api.SendGroupLeave, gid, "Deleted"); + + _dbContext.Remove(group); + } + else + { + var groupHasMigrated = await SharedDbFunctions.MigrateOrDeleteGroup(_dbContext, group, groupPairsWithoutSelf, _maxExistingGroupsByUser).ConfigureAwait(false); + + if (groupHasMigrated.Item1) + { + _logger.LogCallInfo(Api.SendGroupLeave, gid, "Migrated", groupHasMigrated.Item2); + + await Clients.Users(groupPairsWithoutSelf.Select(p => p.GroupUserUID)).SendAsync(Api.OnGroupChange, new GroupDto() + { + GID = group.GID, + OwnedBy = groupHasMigrated.Item2, + Alias = null + }).ConfigureAwait(false); + } + else + { + _logger.LogCallInfo(Api.SendGroupLeave, gid, "Deleted"); + + await Clients.Users(groupPairsWithoutSelf.Select(p => p.GroupUserUID)).SendAsync(Api.OnGroupChange, new GroupDto() + { + GID = group.GID, + IsDeleted = true + }).ConfigureAwait(false); + + await SendGroupDeletedToAll(groupPairs).ConfigureAwait(false); + + return; + } + } + } + + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + + _logger.LogCallInfo(Api.SendGroupLeave, gid, "Success"); + + await Clients.Users(groupPairsWithoutSelf.Select(p => p.GroupUserUID)).SendAsync(Api.OnGroupUserChange, new GroupPairDto() + { + GroupGID = group.GID, + IsRemoved = true, + UserUID = AuthenticatedUserId, + }).ConfigureAwait(false); + + var allUserPairs = await GetAllPairedClientsWithPauseState().ConfigureAwait(false); + + var userIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false); + foreach (var groupUserPair in groupPairsWithoutSelf) + { + await UserGroupLeave(groupUserPair, allUserPairs, userIdent).ConfigureAwait(false); + } + } + + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.SendGroupPause)] + public async Task GroupChangePauseState(string gid, bool isPaused) + { + _logger.LogCallInfo(Api.SendGroupPause, gid, isPaused); + + var groupPair = await _dbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == AuthenticatedUserId).ConfigureAwait(false); + if (groupPair == null) return; + + groupPair.IsPaused = isPaused; + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + + _logger.LogCallInfo(Api.SendGroupPause, gid, isPaused, "Success"); + + var groupPairs = await _dbContext.GroupPairs.Where(p => p.GroupGID == gid && p.GroupUserUID != AuthenticatedUserId).ToListAsync().ConfigureAwait(false); + await Clients.Users(groupPairs.Select(p => p.GroupUserUID)).SendAsync(Api.OnGroupUserChange, new GroupPairDto() + { + GroupGID = gid, + IsPaused = isPaused, + UserUID = AuthenticatedUserId, + }).ConfigureAwait(false); + + await Clients.User(AuthenticatedUserId).SendAsync(Api.OnGroupChange, new GroupDto + { + GID = gid, + IsPaused = isPaused + }).ConfigureAwait(false); + + var allUserPairs = await GetAllPairedClientsWithPauseState().ConfigureAwait(false); + + var userIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false); + foreach (var groupUserPair in groupPairs) + { + var userPair = allUserPairs.SingleOrDefault(p => string.Equals(p.UID, groupUserPair.GroupUserUID, StringComparison.Ordinal)); + if (userPair != null) + { + if (userPair.IsDirectlyPaused != PauseInfo.NoConnection) continue; + if (userPair.IsPausedExcludingGroup(gid) is PauseInfo.Unpaused) continue; + } + + var groupUserIdent = await _clientIdentService.GetCharacterIdentForUid(groupUserPair.GroupUserUID).ConfigureAwait(false); + if (!string.IsNullOrEmpty(groupUserIdent)) + { + await Clients.User(AuthenticatedUserId).SendAsync(isPaused ? Api.OnUserRemoveOnlinePairedPlayer : Api.OnUserAddOnlinePairedPlayer, groupUserIdent).ConfigureAwait(false); + await Clients.User(groupUserPair.GroupUserUID).SendAsync(isPaused ? Api.OnUserRemoveOnlinePairedPlayer : Api.OnUserAddOnlinePairedPlayer, userIdent).ConfigureAwait(false); + } + } + } + + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.SendGroupRemoveUser)] + public async Task GroupRemoveUser(string gid, string uid) + { + _logger.LogCallInfo(Api.SendGroupRemoveUser, gid, uid); + + var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false); + if (group == null || !string.Equals(group.OwnerUID, AuthenticatedUserId, StringComparison.Ordinal)) return; + var groupPair = await _dbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == uid).ConfigureAwait(false); + if (groupPair == null) return; + + _logger.LogCallInfo(Api.SendGroupRemoveUser, gid, uid, "Success"); + + _dbContext.GroupPairs.Remove(groupPair); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + + var groupPairs = _dbContext.GroupPairs.Where(p => p.GroupGID == group.GID).ToList(); + await Clients.Users(groupPairs.Select(p => p.GroupUserUID)).SendAsync(Api.OnGroupUserChange, new GroupPairDto() + { + GroupGID = group.GID, + IsRemoved = true, + UserUID = uid, + }).ConfigureAwait(false); + + var userIdent = await _clientIdentService.GetCharacterIdentForUid(uid).ConfigureAwait(false); + if (userIdent == null) return; + + await Clients.User(uid).SendAsync(Api.OnGroupChange, new GroupDto() + { + GID = gid, + IsDeleted = true, + }).ConfigureAwait(false); + + var allUserPairs = await GetAllPairedClientsWithPauseState(uid).ConfigureAwait(false); + + foreach (var groupUserPair in groupPairs) + { + await UserGroupLeave(groupUserPair, allUserPairs, userIdent, uid).ConfigureAwait(false); + } + } + + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.SendGroupChangeOwner)] + public async Task ChangeOwnership(string gid, string uid) + { + _logger.LogCallInfo(Api.SendGroupChangeOwner, gid, uid); + + var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false); + if (group == null || !string.Equals(group.OwnerUID, AuthenticatedUserId, StringComparison.Ordinal)) return; + var groupPair = await _dbContext.GroupPairs.Include(g => g.GroupUser).SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == uid).ConfigureAwait(false); + if (groupPair == null) return; + var ownedShells = await _dbContext.Groups.CountAsync(g => g.OwnerUID == uid).ConfigureAwait(false); + if (ownedShells >= _maxExistingGroupsByUser) return; + + var prevOwner = await _dbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == AuthenticatedUserId).ConfigureAwait(false); + prevOwner.IsPinned = false; + group.Owner = groupPair.GroupUser; + group.Alias = null; + groupPair.IsPinned = true; + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + + _logger.LogCallInfo(Api.SendGroupChangeOwner, gid, uid, "Success"); + + var groupPairs = await _dbContext.GroupPairs.Where(p => p.GroupGID == gid).Select(p => p.GroupUserUID).ToListAsync().ConfigureAwait(false); + + await Clients.Users(groupPairs).SendAsync(Api.OnGroupChange, new GroupDto() + { + GID = gid, + OwnedBy = string.IsNullOrEmpty(group.Owner.Alias) ? group.Owner.UID : group.Owner.Alias, + Alias = null + }).ConfigureAwait(false); + + await Clients.Users(groupPairs.Where(p => !string.Equals(p, uid, StringComparison.Ordinal))).SendAsync(Api.OnGroupUserChange, new GroupPairDto() + { + GroupGID = gid, + UserUID = uid, + IsPinned = true + }).ConfigureAwait(false); + } + + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.InvokeGroupChangePassword)] + public async Task ChangeGroupPassword(string gid, string password) + { + _logger.LogCallInfo(Api.InvokeGroupChangePassword, gid); + + var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false); + if (group == null || !string.Equals(group.OwnerUID, AuthenticatedUserId, StringComparison.Ordinal)) return false; + + if (password.Length < 10) return false; + + _logger.LogCallInfo(Api.InvokeGroupChangePassword, gid, "Success"); + + group.HashedPassword = StringUtils.Sha256String(password); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + + return true; + } + + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.SendGroupChangePinned)] + public async Task ChangePinned(string gid, string uid, bool isPinned) + { + _logger.LogCallInfo(Api.SendGroupChangePinned, gid, uid, isPinned); + + var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false); + if (group == null || !string.Equals(group.OwnerUID, AuthenticatedUserId, StringComparison.Ordinal)) return; + var groupPair = await _dbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == uid).ConfigureAwait(false); + if (groupPair == null) return; + + groupPair.IsPinned = isPinned; + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + + _logger.LogCallInfo(Api.InvokeGroupChangePassword, gid, uid, isPinned, "Success"); + + var groupPairs = await _dbContext.GroupPairs.Where(p => p.GroupGID == gid).Select(p => p.GroupUserUID).ToListAsync().ConfigureAwait(false); + + await Clients.Users(groupPairs.Where(p => !string.Equals(p, uid, StringComparison.Ordinal))).SendAsync(Api.OnGroupUserChange, new GroupPairDto() + { + GroupGID = gid, + UserUID = uid, + IsPinned = isPinned + }).ConfigureAwait(false); + } + + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.SendGroupClear)] + public async Task ClearGroup(string gid) + { + _logger.LogCallInfo(Api.SendGroupClear, gid); + + var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false); + if (group == null || !string.Equals(group.OwnerUID, AuthenticatedUserId, StringComparison.Ordinal)) return; + + var groupPairs = await _dbContext.GroupPairs.Where(p => p.GroupGID == gid).ToListAsync().ConfigureAwait(false); + + await Clients.Users(groupPairs.Where(p => !p.IsPinned).Select(g => g.GroupUserUID)).SendAsync(Api.OnGroupChange, new GroupDto() + { + GID = group.GID, + IsDeleted = true, + }).ConfigureAwait(false); + + _logger.LogCallInfo(Api.SendGroupClear, gid, "Success"); + + var notPinned = groupPairs.Where(g => !g.IsPinned).ToList(); + + _dbContext.GroupPairs.RemoveRange(notPinned); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + + foreach (var pair in notPinned) + { + await Clients.Users(groupPairs.Where(p => p.IsPinned).Select(g => g.GroupUserUID)).SendAsync(Api.OnGroupUserChange, new GroupPairDto() + { + GroupGID = pair.GroupGID, + IsRemoved = true, + UserUID = pair.GroupUserUID + }).ConfigureAwait(false); + + var pairIdent = await _clientIdentService.GetCharacterIdentForUid(pair.GroupUserUID).ConfigureAwait(false); + if (string.IsNullOrEmpty(pairIdent)) continue; + + var allUserPairs = await GetAllPairedClientsWithPauseState(pair.GroupUserUID).ConfigureAwait(false); + + foreach (var groupUserPair in groupPairs.Where(p => !string.Equals(p.GroupUserUID, pair.GroupUserUID, StringComparison.Ordinal))) + { + await UserGroupLeave(groupUserPair, allUserPairs, pairIdent).ConfigureAwait(false); + } + } + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.User.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.User.cs index 8078428..66d5156 100644 --- a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.User.cs +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.User.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading.Tasks; using MareSynchronos.API; +using MareSynchronosServer.Utils; using MareSynchronosShared.Authentication; using MareSynchronosShared.Metrics; using MareSynchronosShared.Models; @@ -11,315 +12,340 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace MareSynchronosServer.Hubs +namespace MareSynchronosServer.Hubs; + +public partial class MareHub { - public partial class MareHub + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.SendUserDeleteAccount)] + public async Task DeleteAccount() { - [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] - [HubMethodName(Api.SendUserDeleteAccount)] - public async Task DeleteAccount() + _logger.LogCallInfo(Api.SendUserDeleteAccount); + + string userid = AuthenticatedUserId; + var userEntry = await _dbContext.Users.SingleAsync(u => u.UID == userid).ConfigureAwait(false); + var charaIdent = await _clientIdentService.GetCharacterIdentForUid(userid).ConfigureAwait(false); + var ownPairData = await _dbContext.ClientPairs.Where(u => u.User.UID == userid).ToListAsync().ConfigureAwait(false); + var auth = await _dbContext.Auth.SingleAsync(u => u.UserUID == userid).ConfigureAwait(false); + var lodestone = await _dbContext.LodeStoneAuth.SingleOrDefaultAsync(a => a.User.UID == userid).ConfigureAwait(false); + var groupPairs = await _dbContext.GroupPairs.Where(g => g.GroupUserUID == userid).ToListAsync().ConfigureAwait(false); + + if (lodestone != null) { - _logger.LogInformation("User {AuthenticatedUserId} deleted their account", AuthenticatedUserId); - - string userid = AuthenticatedUserId; - var userEntry = await _dbContext.Users.SingleAsync(u => u.UID == userid).ConfigureAwait(false); - var charaIdent = _clientIdentService.GetCharacterIdentForUid(userid); - var ownPairData = await _dbContext.ClientPairs.Where(u => u.User.UID == userid).ToListAsync().ConfigureAwait(false); - var auth = await _dbContext.Auth.SingleAsync(u => u.UserUID == userid).ConfigureAwait(false); - var lodestone = await _dbContext.LodeStoneAuth.SingleOrDefaultAsync(a => a.User.UID == userid).ConfigureAwait(false); - - if (lodestone != null) - { - _dbContext.Remove(lodestone); - } - - while (_dbContext.Files.Any(f => f.Uploader == userEntry)) - { - await Task.Delay(1000).ConfigureAwait(false); - } - - await _authServiceClient.RemoveAuthAsync(new RemoveAuthRequest() { Uid = userid }).ConfigureAwait(false); - - - _dbContext.RemoveRange(ownPairData); - await _dbContext.SaveChangesAsync().ConfigureAwait(false); - var otherPairData = await _dbContext.ClientPairs.Include(u => u.User) - .Where(u => u.OtherUser.UID == userid).ToListAsync().ConfigureAwait(false); - foreach (var pair in otherPairData) - { - await Clients.User(pair.User.UID) - .SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto() - { - OtherUID = userid, - IsRemoved = true - }, charaIdent).ConfigureAwait(false); - } - - _mareMetrics.DecGauge(MetricsAPI.GaugePairs, ownPairData.Count + otherPairData.Count); - _mareMetrics.DecGauge(MetricsAPI.GaugePairsPaused, ownPairData.Count(c => c.IsPaused)); - _mareMetrics.IncCounter(MetricsAPI.CounterUsersRegisteredDeleted, 1); - - _dbContext.RemoveRange(otherPairData); - _dbContext.Remove(userEntry); - _dbContext.Remove(auth); - await _dbContext.SaveChangesAsync().ConfigureAwait(false); + _dbContext.Remove(lodestone); } - [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] - [HubMethodName(Api.InvokeUserGetOnlineCharacters)] - public async Task> GetOnlineCharacters() + while (_dbContext.Files.Any(f => f.Uploader == userEntry)) { - _logger.LogInformation("User {AuthenticatedUserId} requested online characters", AuthenticatedUserId); - - var ownUser = await GetAuthenticatedUserUntrackedAsync().ConfigureAwait(false); - - var otherUsers = await _dbContext.ClientPairs.AsNoTracking() - .Include(u => u.User) - .Include(u => u.OtherUser) - .Where(w => w.User.UID == ownUser.UID && !w.IsPaused) - //.Where(w => !string.IsNullOrEmpty(w.OtherUser.CharacterIdentification)) - .Select(e => e.OtherUser).ToListAsync().ConfigureAwait(false); - var otherOnlineUsers = - otherUsers.Where(u => !string.IsNullOrEmpty(_clientIdentService.GetCharacterIdentForUid(u.UID))); - var otherEntries = await _dbContext.ClientPairs.AsNoTracking() - .Include(u => u.User) - .Where(u => otherOnlineUsers.Any(e => e == u.User) && u.OtherUser == ownUser && !u.IsPaused) - .ToListAsync().ConfigureAwait(false); - var ownIdent = _clientIdentService.GetCharacterIdentForUid(ownUser.UID); - - await Clients.Users(otherEntries.Select(e => e.User.UID)).SendAsync(Api.OnUserAddOnlinePairedPlayer, ownIdent).ConfigureAwait(false); - return otherEntries.Select(e => _clientIdentService.GetCharacterIdentForUid(e.User.UID)).Distinct().ToList(); + await Task.Delay(1000).ConfigureAwait(false); } - [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] - [HubMethodName(Api.InvokeUserGetPairedClients)] - public async Task> GetPairedClients() - { - string userid = AuthenticatedUserId; - var query = - from userToOther in _dbContext.ClientPairs - join otherToUser in _dbContext.ClientPairs - on new - { - user = userToOther.UserUID, - other = userToOther.OtherUserUID + await _authServiceClient.RemoveAuthAsync(new RemoveAuthRequest() { Uid = userid }).ConfigureAwait(false); - } equals new - { - user = otherToUser.OtherUserUID, - other = otherToUser.UserUID - } into leftJoin - from otherEntry in leftJoin.DefaultIfEmpty() - where - userToOther.UserUID == userid - select new + _dbContext.RemoveRange(ownPairData); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + var otherPairData = await _dbContext.ClientPairs.Include(u => u.User) + .Where(u => u.OtherUser.UID == userid).ToListAsync().ConfigureAwait(false); + foreach (var pair in otherPairData) + { + await Clients.User(pair.User.UID) + .SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto() { - userToOther.OtherUser.Alias, - userToOther.IsPaused, - OtherIsPaused = otherEntry != null && otherEntry.IsPaused, - userToOther.OtherUserUID, - IsSynced = otherEntry != null - }; - - return (await query.ToListAsync().ConfigureAwait(false)).Select(f => new ClientPairDto() - { - VanityUID = f.Alias, - IsPaused = f.IsPaused, - OtherUID = f.OtherUserUID, - IsSynced = f.IsSynced, - IsPausedFromOthers = f.OtherIsPaused - }).ToList(); + OtherUID = userid, + IsRemoved = true + }, charaIdent).ConfigureAwait(false); } - [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] - [HubMethodName(Api.InvokeUserPushCharacterDataToVisibleClients)] - public async Task PushCharacterDataToVisibleClients(CharacterCacheDto characterCache, List visibleCharacterIds) + foreach (var pair in groupPairs) { - _logger.LogInformation("User {AuthenticatedUserId} pushing character data to {visibleCharacterIds} visible clients", AuthenticatedUserId, visibleCharacterIds.Count); - - var user = await GetAuthenticatedUserUntrackedAsync().ConfigureAwait(false); - - var query = - from userToOther in _dbContext.ClientPairs - join otherToUser in _dbContext.ClientPairs - on new - { - user = userToOther.UserUID, - other = userToOther.OtherUserUID - - } equals new - { - user = otherToUser.OtherUserUID, - other = otherToUser.UserUID - } - where - userToOther.UserUID == user.UID - && !userToOther.IsPaused - && !otherToUser.IsPaused - select otherToUser.UserUID; - - var otherEntries = await query.ToListAsync().ConfigureAwait(false); - otherEntries = - otherEntries.Where(c => visibleCharacterIds.Select(c => c.ToLowerInvariant()).Contains(_clientIdentService.GetCharacterIdentForUid(c)?.ToLowerInvariant() ?? "")).ToList(); - var ownIdent = _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId); - - await Clients.Users(otherEntries).SendAsync(Api.OnUserReceiveCharacterData, characterCache, ownIdent).ConfigureAwait(false); - - _mareMetrics.IncCounter(MetricsAPI.CounterUserPushData); - _mareMetrics.IncCounter(MetricsAPI.CounterUserPushDataTo, otherEntries.Count); + await GroupLeave(pair.GroupGID).ConfigureAwait(false); } - [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] - [HubMethodName(Api.SendUserPairedClientAddition)] - public async Task SendPairedClientAddition(string uid) - { - if (uid == AuthenticatedUserId || string.IsNullOrWhiteSpace(uid)) return; - uid = uid.Trim(); - var user = await _dbContext.Users.SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false); + _mareMetrics.IncCounter(MetricsAPI.CounterUsersRegisteredDeleted, 1); - var otherUser = await _dbContext.Users - .SingleOrDefaultAsync(u => u.UID == uid || u.Alias == uid).ConfigureAwait(false); - var existingEntry = - await _dbContext.ClientPairs.AsNoTracking() - .FirstOrDefaultAsync(p => - p.User.UID == AuthenticatedUserId && p.OtherUser.UID == otherUser.UID).ConfigureAwait(false); - if (otherUser == null || existingEntry != null) return; - _logger.LogInformation("User {AuthenticatedUserId} adding {uid} to whitelist", AuthenticatedUserId, uid); - ClientPair wl = new ClientPair() + _dbContext.RemoveRange(otherPairData); + _dbContext.Remove(userEntry); + _dbContext.Remove(auth); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + } + + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.InvokeUserGetOnlineCharacters)] + public async Task> GetOnlineCharacters() + { + _logger.LogCallInfo(Api.InvokeUserGetOnlineCharacters); + + var ownIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false); + + var usersToSendOnlineTo = await SendDataToAllPairedUsers(Api.OnUserAddOnlinePairedPlayer, ownIdent).ConfigureAwait(false); + return usersToSendOnlineTo.Select(async e => await _clientIdentService.GetCharacterIdentForUid(e).ConfigureAwait(false)).Select(t => t.Result).Where(t => !string.IsNullOrEmpty(t)).Distinct(System.StringComparer.Ordinal).ToList(); + } + + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.InvokeUserGetPairedClients)] + public async Task> GetPairedClients() + { + _logger.LogCallInfo(Api.InvokeUserGetPairedClients); + + string userid = AuthenticatedUserId; + var query = + from userToOther in _dbContext.ClientPairs + join otherToUser in _dbContext.ClientPairs + on new + { + user = userToOther.UserUID, + other = userToOther.OtherUserUID + + } equals new + { + user = otherToUser.OtherUserUID, + other = otherToUser.UserUID + } into leftJoin + from otherEntry in leftJoin.DefaultIfEmpty() + where + userToOther.UserUID == userid + select new { - IsPaused = false, - OtherUser = otherUser, - User = user + userToOther.OtherUser.Alias, + userToOther.IsPaused, + OtherIsPaused = otherEntry != null && otherEntry.IsPaused, + userToOther.OtherUserUID, + IsSynced = otherEntry != null }; - await _dbContext.ClientPairs.AddAsync(wl).ConfigureAwait(false); - await _dbContext.SaveChangesAsync().ConfigureAwait(false); - var otherEntry = OppositeEntry(otherUser.UID); - await Clients.User(user.UID) - .SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto() - { - VanityUID = otherUser.Alias, - OtherUID = otherUser.UID, - IsPaused = false, - IsPausedFromOthers = otherEntry?.IsPaused ?? false, - IsSynced = otherEntry != null - }, string.Empty).ConfigureAwait(false); - if (otherEntry != null) - { - var userIdent = _clientIdentService.GetCharacterIdentForUid(user.UID); - await Clients.User(otherUser.UID).SendAsync(Api.OnUserUpdateClientPairs, - new ClientPairDto() - { - VanityUID = user.Alias, - OtherUID = user.UID, - IsPaused = otherEntry.IsPaused, - IsPausedFromOthers = false, - IsSynced = true - }, userIdent).ConfigureAwait(false); - var otherIdent = _clientIdentService.GetCharacterIdentForUid(otherUser.UID); - if (!string.IsNullOrEmpty(otherIdent)) - { - await Clients.User(user.UID) - .SendAsync(Api.OnUserAddOnlinePairedPlayer, otherIdent).ConfigureAwait(false); - await Clients.User(otherUser.UID) - .SendAsync(Api.OnUserAddOnlinePairedPlayer, userIdent).ConfigureAwait(false); - } - } - - _mareMetrics.IncGauge(MetricsAPI.GaugePairs); - } - - [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] - [HubMethodName(Api.SendUserPairedClientPauseChange)] - public async Task SendPairedClientPauseChange(string otherUserUid, bool isPaused) + return (await query.ToListAsync().ConfigureAwait(false)).Select(f => new ClientPairDto() { - if (otherUserUid == AuthenticatedUserId) return; - ClientPair pair = await _dbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == AuthenticatedUserId && w.OtherUserUID == otherUserUid).ConfigureAwait(false); - if (pair == null) return; + VanityUID = f.Alias, + IsPaused = f.IsPaused, + OtherUID = f.OtherUserUID, + IsSynced = f.IsSynced, + IsPausedFromOthers = f.OtherIsPaused + }).ToList(); + } - _logger.LogInformation("User {AuthenticatedUserId} changed pause status with {otherUserUid} to {isPaused}", AuthenticatedUserId, otherUserUid, isPaused); - pair.IsPaused = isPaused; - _dbContext.Update(pair); - await _dbContext.SaveChangesAsync().ConfigureAwait(false); - var selfCharaIdent = _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId); - var otherCharaIdent = _clientIdentService.GetCharacterIdentForUid(pair.OtherUserUID); - var otherEntry = OppositeEntry(otherUserUid); + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.InvokeUserPushCharacterDataToVisibleClients)] + public async Task PushCharacterDataToVisibleClients(CharacterCacheDto characterCache, List visibleCharacterIds) + { + _logger.LogCallInfo(Api.InvokeUserPushCharacterDataToVisibleClients, visibleCharacterIds.Count); - await Clients.User(AuthenticatedUserId) - .SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto() - { - OtherUID = otherUserUid, - IsPaused = isPaused, - IsPausedFromOthers = otherEntry?.IsPaused ?? false, - IsSynced = otherEntry != null - }, otherCharaIdent).ConfigureAwait(false); - if (otherEntry != null) + var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false); + + var allPairedUsersDict = allPairedUsers.ToDictionary(f => f, async f => await _clientIdentService.GetCharacterIdentForUid(f).ConfigureAwait(false), System.StringComparer.Ordinal) + .Where(f => visibleCharacterIds.Contains(f.Value.Result, System.StringComparer.Ordinal)); + + var ownIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false); + + _logger.LogCallInfo(Api.InvokeUserPushCharacterDataToVisibleClients, visibleCharacterIds.Count, allPairedUsersDict.Count()); + + await Clients.Users(allPairedUsersDict.Select(f => f.Key)).SendAsync(Api.OnUserReceiveCharacterData, characterCache, ownIdent).ConfigureAwait(false); + + _mareMetrics.IncCounter(MetricsAPI.CounterUserPushData); + _mareMetrics.IncCounter(MetricsAPI.CounterUserPushDataTo, allPairedUsersDict.Count()); + } + + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.SendUserPairedClientAddition)] + public async Task SendPairedClientAddition(string uid) + { + _logger.LogCallInfo(Api.SendUserPairedClientAddition, uid); + + // don't allow adding yourself or nothing + uid = uid.Trim(); + if (string.Equals(uid, AuthenticatedUserId, System.StringComparison.Ordinal) || string.IsNullOrWhiteSpace(uid)) return; + + // grab other user, check if it exists and if a pair already exists + var otherUser = await _dbContext.Users.SingleOrDefaultAsync(u => u.UID == uid || u.Alias == uid).ConfigureAwait(false); + var existingEntry = + await _dbContext.ClientPairs.AsNoTracking() + .FirstOrDefaultAsync(p => + p.User.UID == AuthenticatedUserId && p.OtherUserUID == uid).ConfigureAwait(false); + if (otherUser == null || existingEntry != null) return; + + // grab self create new client pair and save + var user = await _dbContext.Users.SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false); + + _logger.LogCallInfo(Api.SendUserPairedClientAddition, uid, "Success"); + + ClientPair wl = new ClientPair() + { + IsPaused = false, + OtherUser = otherUser, + User = user + }; + await _dbContext.ClientPairs.AddAsync(wl).ConfigureAwait(false); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + + // get the opposite entry of the client pair + var otherEntry = OppositeEntry(otherUser.UID); + await Clients.User(user.UID) + .SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto() { - await Clients.User(otherUserUid).SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto() - { - OtherUID = AuthenticatedUserId, - IsPaused = otherEntry.IsPaused, - IsPausedFromOthers = isPaused, - IsSynced = true - }, selfCharaIdent).ConfigureAwait(false); - } + VanityUID = otherUser.Alias, + OtherUID = otherUser.UID, + IsPaused = false, + IsPausedFromOthers = otherEntry?.IsPaused ?? false, + IsSynced = otherEntry != null + }).ConfigureAwait(false); + + // if there's no opposite entry do nothing + if (otherEntry == null) return; + + // check if other user is online + var otherIdent = await _clientIdentService.GetCharacterIdentForUid(otherUser.UID).ConfigureAwait(false); + if (otherIdent == null) return; + + // send push with update to other user if other user is online + await Clients.User(otherUser.UID).SendAsync(Api.OnUserUpdateClientPairs, + new ClientPairDto() + { + VanityUID = user.Alias, + OtherUID = user.UID, + IsPaused = otherEntry.IsPaused, + IsPausedFromOthers = false, + IsSynced = true + }).ConfigureAwait(false); + + // get own ident and all pairs + var userIdent = await _clientIdentService.GetCharacterIdentForUid(user.UID).ConfigureAwait(false); + var allUserPairs = await GetAllPairedClientsWithPauseState().ConfigureAwait(false); + + // if the other user has paused the main user and there was no previous group connection don't send anything + if (!otherEntry.IsPaused && allUserPairs.Any(p => string.Equals(p.UID, uid, System.StringComparison.Ordinal) && p.IsPausedPerGroup is PauseInfo.Paused or PauseInfo.NoConnection)) + { + await Clients.User(user.UID) + .SendAsync(Api.OnUserAddOnlinePairedPlayer, otherIdent).ConfigureAwait(false); + await Clients.User(otherUser.UID) + .SendAsync(Api.OnUserAddOnlinePairedPlayer, userIdent).ConfigureAwait(false); + } + } + + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.SendUserPairedClientPauseChange)] + public async Task SendPairedClientPauseChange(string otherUserUid, bool isPaused) + { + _logger.LogCallInfo(Api.SendUserPairedClientPauseChange, otherUserUid, isPaused); + + if (string.Equals(otherUserUid, AuthenticatedUserId, System.StringComparison.Ordinal)) return; + ClientPair pair = await _dbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == AuthenticatedUserId && w.OtherUserUID == otherUserUid).ConfigureAwait(false); + if (pair == null) return; + + pair.IsPaused = isPaused; + _dbContext.Update(pair); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + + _logger.LogCallInfo(Api.SendUserPairedClientPauseChange, otherUserUid, isPaused, "Success"); + + var otherEntry = OppositeEntry(otherUserUid); + + await Clients.User(AuthenticatedUserId) + .SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto() + { + OtherUID = otherUserUid, + IsPaused = isPaused, + IsPausedFromOthers = otherEntry?.IsPaused ?? false, + IsSynced = otherEntry != null + }).ConfigureAwait(false); + if (otherEntry != null) + { + await Clients.User(otherUserUid).SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto() + { + OtherUID = AuthenticatedUserId, + IsPaused = otherEntry.IsPaused, + IsPausedFromOthers = isPaused, + IsSynced = true + }).ConfigureAwait(false); + + var selfCharaIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false); + var otherCharaIdent = await _clientIdentService.GetCharacterIdentForUid(pair.OtherUserUID).ConfigureAwait(false); + + if (selfCharaIdent == null || otherCharaIdent == null || otherEntry.IsPaused) return; if (isPaused) { - _mareMetrics.IncGauge(MetricsAPI.GaugePairsPaused); + await Clients.User(AuthenticatedUserId).SendAsync(Api.OnUserRemoveOnlinePairedPlayer, otherCharaIdent).ConfigureAwait(false); + await Clients.User(otherUserUid).SendAsync(Api.OnUserRemoveOnlinePairedPlayer, selfCharaIdent).ConfigureAwait(false); } else { - _mareMetrics.DecGauge(MetricsAPI.GaugePairsPaused); + await Clients.User(AuthenticatedUserId).SendAsync(Api.OnUserAddOnlinePairedPlayer, otherCharaIdent).ConfigureAwait(false); + await Clients.User(otherUserUid).SendAsync(Api.OnUserAddOnlinePairedPlayer, selfCharaIdent).ConfigureAwait(false); } } - - [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] - [HubMethodName(Api.SendUserPairedClientRemoval)] - public async Task SendPairedClientRemoval(string uid) - { - if (uid == AuthenticatedUserId) return; - - var sender = await _dbContext.Users.SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false); - var otherUser = await _dbContext.Users.SingleOrDefaultAsync(u => u.UID == uid).ConfigureAwait(false); - if (otherUser == null) return; - _logger.LogInformation("User {AuthenticatedUserId} removed {uid} from whitelist", AuthenticatedUserId, uid); - ClientPair wl = - await _dbContext.ClientPairs.SingleOrDefaultAsync(w => w.User == sender && w.OtherUser == otherUser).ConfigureAwait(false); - if (wl == null) return; - _dbContext.ClientPairs.Remove(wl); - await _dbContext.SaveChangesAsync().ConfigureAwait(false); - var otherEntry = OppositeEntry(uid); - var otherIdent = _clientIdentService.GetCharacterIdentForUid(otherUser.UID); - await Clients.User(sender.UID) - .SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto() - { - OtherUID = otherUser.UID, - IsRemoved = true - }, otherIdent).ConfigureAwait(false); - if (otherEntry != null) - { - if (!string.IsNullOrEmpty(otherIdent)) - { - var ownIdent = _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId); - await Clients.User(sender.UID) - .SendAsync(Api.OnUserRemoveOnlinePairedPlayer, otherIdent).ConfigureAwait(false); - await Clients.User(otherUser.UID) - .SendAsync(Api.OnUserRemoveOnlinePairedPlayer, ownIdent).ConfigureAwait(false); - await Clients.User(otherUser.UID).SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto() - { - OtherUID = sender.UID, - IsPaused = otherEntry.IsPaused, - IsPausedFromOthers = false, - IsSynced = false - }, ownIdent).ConfigureAwait(false); - } - } - - _mareMetrics.DecGauge(MetricsAPI.GaugePairs); - } - - private ClientPair OppositeEntry(string otherUID) => - _dbContext.ClientPairs.AsNoTracking().SingleOrDefault(w => w.User.UID == otherUID && w.OtherUser.UID == AuthenticatedUserId); } + + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + [HubMethodName(Api.SendUserPairedClientRemoval)] + public async Task SendPairedClientRemoval(string otherUserUid) + { + _logger.LogCallInfo(Api.SendUserPairedClientRemoval, otherUserUid); + + if (string.Equals(otherUserUid, AuthenticatedUserId, System.StringComparison.Ordinal)) return; + + // check if client pair even exists + ClientPair callerPair = + await _dbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == AuthenticatedUserId && w.OtherUserUID == otherUserUid).ConfigureAwait(false); + bool callerHadPaused = callerPair.IsPaused; + if (callerPair == null) return; + + // delete from database, send update info to users pair list + _dbContext.ClientPairs.Remove(callerPair); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + + _logger.LogCallInfo(Api.SendUserPairedClientRemoval, otherUserUid, "Success"); + + await Clients.User(AuthenticatedUserId) + .SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto() + { + OtherUID = otherUserUid, + IsRemoved = true + }).ConfigureAwait(false); + + // check if opposite entry exists + var oppositeClientPair = OppositeEntry(otherUserUid); + if (oppositeClientPair == null) return; + + // check if other user is online, if no then there is no need to do anything further + var otherIdent = await _clientIdentService.GetCharacterIdentForUid(otherUserUid).ConfigureAwait(false); + if (otherIdent == null) return; + + // get own ident and + await Clients.User(otherUserUid).SendAsync(Api.OnUserUpdateClientPairs, + new ClientPairDto() + { + OtherUID = AuthenticatedUserId, + IsPausedFromOthers = false, + IsSynced = false + }).ConfigureAwait(false); + + // if the other user had paused the user the state will be offline for either, do nothing + bool otherHadPaused = oppositeClientPair.IsPaused; + if (!callerHadPaused && otherHadPaused) return; + + var allUsers = await GetAllPairedClientsWithPauseState().ConfigureAwait(false); + var pauseEntry = allUsers.SingleOrDefault(f => string.Equals(f.UID, otherUserUid, System.StringComparison.Ordinal)); + var isPausedInGroup = pauseEntry == null || pauseEntry.IsPausedPerGroup is PauseInfo.Paused or PauseInfo.NoConnection; + + // if neither user had paused each other and both are in unpaused groups, state will be online for both, do nothing + if (!callerHadPaused && !otherHadPaused && !isPausedInGroup) return; + + // if neither user had paused each other and either is not in an unpaused group with each other, change state to offline + if (!callerHadPaused && !otherHadPaused && isPausedInGroup) + { + var userIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false); + await Clients.User(AuthenticatedUserId).SendAsync(Api.OnUserRemoveOnlinePairedPlayer, otherIdent).ConfigureAwait(false); + await Clients.User(otherUserUid).SendAsync(Api.OnUserRemoveOnlinePairedPlayer, userIdent).ConfigureAwait(false); + } + + // if the caller had paused other but not the other has paused the caller and they are in an unpaused group together, change state to online + if (callerHadPaused && !otherHadPaused && !isPausedInGroup) + { + var userIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false); + await Clients.User(AuthenticatedUserId).SendAsync(Api.OnUserAddOnlinePairedPlayer, otherIdent).ConfigureAwait(false); + await Clients.User(otherUserUid).SendAsync(Api.OnUserAddOnlinePairedPlayer, userIdent).ConfigureAwait(false); + } + } + + private ClientPair OppositeEntry(string otherUID) => + _dbContext.ClientPairs.AsNoTracking().SingleOrDefault(w => w.User.UID == otherUID && w.OtherUser.UID == AuthenticatedUserId); } diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.cs index fa0646a..0f7d125 100644 --- a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.cs +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.cs @@ -4,6 +4,7 @@ using System.Security.Claims; using System.Threading.Tasks; using MareSynchronos.API; using MareSynchronosServer.Services; +using MareSynchronosServer.Utils; using MareSynchronosShared.Authentication; using MareSynchronosShared.Data; using MareSynchronosShared.Metrics; @@ -27,9 +28,14 @@ public partial class MareHub : Hub private readonly SystemInfoService _systemInfoService; private readonly IHttpContextAccessor _contextAccessor; private readonly IClientIdentificationService _clientIdentService; - private readonly ILogger _logger; + private readonly MareHubLogger _logger; private readonly MareDbContext _dbContext; private readonly Uri _cdnFullUri; + private readonly string _shardName; + private readonly int _maxExistingGroupsByUser; + private readonly int _maxJoinedGroupsByUser; + private readonly int _maxGroupUserCount; + public MareHub(MareMetrics mareMetrics, AuthService.AuthServiceClient authServiceClient, FileService.FileServiceClient fileServiceClient, MareDbContext mareDbContext, ILogger logger, SystemInfoService systemInfoService, IConfiguration configuration, IHttpContextAccessor contextAccessor, IClientIdentificationService clientIdentService) @@ -38,10 +44,15 @@ public partial class MareHub : Hub _authServiceClient = authServiceClient; _fileServiceClient = fileServiceClient; _systemInfoService = systemInfoService; - _cdnFullUri = new Uri(configuration.GetRequiredSection("MareSynchronos").GetValue("CdnFullUrl")); + var config = configuration.GetRequiredSection("MareSynchronos"); + _cdnFullUri = new Uri(config.GetValue("CdnFullUrl")); + _shardName = config.GetValue("ShardName", "Main"); + _maxExistingGroupsByUser = config.GetValue("MaxExistingGroupsByUser", 3); + _maxJoinedGroupsByUser = config.GetValue("MaxJoinedGroupsByUser", 6); + _maxGroupUserCount = config.GetValue("MaxGroupUserCount", 100); _contextAccessor = contextAccessor; _clientIdentService = clientIdentService; - _logger = logger; + _logger = new MareHubLogger(this, logger); _dbContext = mareDbContext; } @@ -51,9 +62,9 @@ public partial class MareHub : Hub { _mareMetrics.IncCounter(MetricsAPI.CounterInitializedConnections); - var userId = Context.User!.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; + var userId = Context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.NameIdentifier, StringComparison.Ordinal))?.Value; - _logger.LogInformation("Connection from {userId}, CI: {characterIdentification}", userId, characterIdentification); + _logger.LogCallInfo(Api.InvokeHeartbeat, characterIdentification); await Clients.Caller.SendAsync(Api.OnUpdateSystemInfo, _systemInfoService.SystemInfoDto).ConfigureAwait(false); @@ -62,9 +73,11 @@ public partial class MareHub : Hub if (!string.IsNullOrEmpty(userId) && !isBanned && !string.IsNullOrEmpty(characterIdentification)) { var user = (await _dbContext.Users.SingleAsync(u => u.UID == userId).ConfigureAwait(false)); - var existingIdent = _clientIdentService.GetCharacterIdentForUid(userId); - if (!string.IsNullOrEmpty(existingIdent) && characterIdentification != existingIdent) + var existingIdent = await _clientIdentService.GetCharacterIdentForUid(userId).ConfigureAwait(false); + if (!string.IsNullOrEmpty(existingIdent) && !string.Equals(characterIdentification, existingIdent, StringComparison.Ordinal)) { + _logger.LogCallInfo(Api.InvokeHeartbeat, characterIdentification, "Failure", "LoggedIn"); + return new ConnectionDto() { ServerVersion = Api.Version @@ -72,27 +85,50 @@ public partial class MareHub : Hub } user.LastLoggedIn = DateTime.UtcNow; - _clientIdentService.MarkUserOnline(user.UID, characterIdentification); + await _clientIdentService.MarkUserOnline(user.UID, characterIdentification).ConfigureAwait(false); await _dbContext.SaveChangesAsync().ConfigureAwait(false); + _logger.LogCallInfo(Api.InvokeHeartbeat, characterIdentification, "Success"); + return new ConnectionDto { ServerVersion = Api.Version, UID = string.IsNullOrEmpty(user.Alias) ? user.UID : user.Alias, IsModerator = user.IsModerator, - IsAdmin = user.IsAdmin + IsAdmin = user.IsAdmin, + ServerInfo = new ServerInfoDto() + { + MaxGroupsCreatedByUser = _maxExistingGroupsByUser, + ShardName = _shardName, + MaxGroupsJoinedByUser = _maxJoinedGroupsByUser, + MaxGroupUserCount = _maxGroupUserCount + } }; } + _logger.LogCallInfo(Api.InvokeHeartbeat, characterIdentification, "Failure"); + return new ConnectionDto() { ServerVersion = Api.Version }; } + [HubMethodName(Api.InvokeCheckClientHealth)] + [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] + public async Task CheckClientHealth() + { + var needsReconnect = string.IsNullOrEmpty(await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false)); + if (needsReconnect) + { + _logger.LogCallWarning(Api.InvokeCheckClientHealth, needsReconnect); + } + return needsReconnect; + } + public override async Task OnConnectedAsync() { - _logger.LogInformation("Connection from {ip}", _contextAccessor.GetIpAddress()); + _logger.LogCallInfo("Connect", _contextAccessor.GetIpAddress()); _mareMetrics.IncGauge(MetricsAPI.GaugeConnections); await base.OnConnectedAsync().ConfigureAwait(false); } @@ -101,50 +137,22 @@ public partial class MareHub : Hub { _mareMetrics.DecGauge(MetricsAPI.GaugeConnections); - var userCharaIdent = _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId); + var userCharaIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false); if (!string.IsNullOrEmpty(userCharaIdent)) { - var user = await _dbContext.Users.SingleOrDefaultAsync(u => u.UID == AuthenticatedUserId)!.ConfigureAwait(false); _mareMetrics.DecGauge(MetricsAPI.GaugeAuthorizedConnections); - _logger.LogInformation("Disconnect from {id}", AuthenticatedUserId); + _logger.LogCallInfo("Disconnect"); - var query = - from userToOther in _dbContext.ClientPairs - join otherToUser in _dbContext.ClientPairs - on new - { - user = userToOther.UserUID, - other = userToOther.OtherUserUID + await SendDataToAllPairedUsers(Api.OnUserRemoveOnlinePairedPlayer, userCharaIdent).ConfigureAwait(false); - } equals new - { - user = otherToUser.OtherUserUID, - other = otherToUser.UserUID - } - where - userToOther.UserUID == user.UID - && !userToOther.IsPaused - && !otherToUser.IsPaused - select otherToUser.UserUID; - var otherEntries = await query.ToListAsync().ConfigureAwait(false); + _dbContext.RemoveRange(_dbContext.Files.Where(f => !f.Uploaded && f.UploaderUID == AuthenticatedUserId)); - await Clients.Users(otherEntries).SendAsync(Api.OnUserRemoveOnlinePairedPlayer, userCharaIdent).ConfigureAwait(false); - - _dbContext.RemoveRange(_dbContext.Files.Where(f => !f.Uploaded && f.UploaderUID == user.UID)); - - _clientIdentService.MarkUserOffline(user.UID); + await _clientIdentService.MarkUserOffline(AuthenticatedUserId).ConfigureAwait(false); await _dbContext.SaveChangesAsync().ConfigureAwait(false); } await base.OnDisconnectedAsync(exception).ConfigureAwait(false); } - - protected string AuthenticatedUserId => Context.User?.Claims?.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "Unknown"; - - protected async Task GetAuthenticatedUserUntrackedAsync() - { - return await _dbContext.Users.AsNoTrackingWithIdentityResolution().SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false); - } -} \ No newline at end of file +} diff --git a/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj b/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj index a5c173b..699aa2f 100644 --- a/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj +++ b/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj @@ -26,6 +26,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/MareSynchronosServer/MareSynchronosServer/Program.cs b/MareSynchronosServer/MareSynchronosServer/Program.cs index a023e6a..a34d436 100644 --- a/MareSynchronosServer/MareSynchronosServer/Program.cs +++ b/MareSynchronosServer/MareSynchronosServer/Program.cs @@ -9,58 +9,57 @@ using Microsoft.Extensions.Logging; using MareSynchronosShared.Data; using MareSynchronosShared.Metrics; -namespace MareSynchronosServer +namespace MareSynchronosServer; + +public class Program { - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) + var hostBuilder = CreateHostBuilder(args); + var host = hostBuilder.Build(); + using (var scope = host.Services.CreateScope()) { - var hostBuilder = CreateHostBuilder(args); - var host = hostBuilder.Build(); - using (var scope = host.Services.CreateScope()) + var services = scope.ServiceProvider; + using var context = services.GetRequiredService(); + + var secondaryServer = Environment.GetEnvironmentVariable("SECONDARY_SERVER"); + if (string.IsNullOrEmpty(secondaryServer) || string.Equals(secondaryServer, "0", StringComparison.Ordinal)) { - var services = scope.ServiceProvider; - using var context = services.GetRequiredService(); + context.Database.Migrate(); + context.SaveChanges(); - var secondaryServer = Environment.GetEnvironmentVariable("SECONDARY_SERVER"); - if (string.IsNullOrEmpty(secondaryServer) || secondaryServer == "0") - { - context.Database.Migrate(); - context.SaveChanges(); - - // clean up residuals - var looseFiles = context.Files.Where(f => f.Uploaded == false); - var unfinishedRegistrations = context.LodeStoneAuth.Where(c => c.StartedAt != null); - context.RemoveRange(unfinishedRegistrations); - context.RemoveRange(looseFiles); - context.SaveChanges(); - } - - var metrics = services.GetRequiredService(); - - metrics.SetGaugeTo(MetricsAPI.GaugePairs, context.ClientPairs.Count()); - metrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, context.ClientPairs.Count(p => p.IsPaused)); + // clean up residuals + var looseFiles = context.Files.Where(f => f.Uploaded == false); + var unfinishedRegistrations = context.LodeStoneAuth.Where(c => c.StartedAt != null); + context.RemoveRange(unfinishedRegistrations); + context.RemoveRange(looseFiles); + context.SaveChanges(); } - if (args.Length == 0 || args[0] != "dry") - { - host.Run(); - } + var metrics = services.GetRequiredService(); + + metrics.SetGaugeTo(MetricsAPI.GaugePairs, context.ClientPairs.Count()); + metrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, context.ClientPairs.Count(p => p.IsPaused)); } - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .UseSystemd() - .UseConsoleLifetime() - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseContentRoot(AppContext.BaseDirectory); - webBuilder.ConfigureLogging((ctx, builder) => - { - builder.AddConfiguration(ctx.Configuration.GetSection("Logging")); - builder.AddFile(o => o.RootPath = AppContext.BaseDirectory); - }); - webBuilder.UseStartup(); - }); + if (args.Length == 0 || !string.Equals(args[0], "dry", StringComparison.Ordinal)) + { + host.Run(); + } } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseSystemd() + .UseConsoleLifetime() + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseContentRoot(AppContext.BaseDirectory); + webBuilder.ConfigureLogging((ctx, builder) => + { + builder.AddConfiguration(ctx.Configuration.GetSection("Logging")); + builder.AddFile(o => o.RootPath = AppContext.BaseDirectory); + }); + webBuilder.UseStartup(); + }); } diff --git a/MareSynchronosServer/MareSynchronosServer/Services/SystemInfoService.cs b/MareSynchronosServer/MareSynchronosServer/Services/SystemInfoService.cs index edeca4d..eae24eb 100644 --- a/MareSynchronosServer/MareSynchronosServer/Services/SystemInfoService.cs +++ b/MareSynchronosServer/MareSynchronosServer/Services/SystemInfoService.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; using MareSynchronos.API; @@ -7,6 +8,7 @@ using MareSynchronosShared.Data; using MareSynchronosShared.Metrics; using MareSynchronosShared.Services; using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -16,16 +18,19 @@ namespace MareSynchronosServer.Services; public class SystemInfoService : IHostedService, IDisposable { private readonly MareMetrics _mareMetrics; - private readonly IClientIdentificationService clientIdentService; + private readonly IServiceProvider _services; + private readonly IClientIdentificationService _clientIdentService; private readonly ILogger _logger; private readonly IHubContext _hubContext; private Timer _timer; + private string _shardName; public SystemInfoDto SystemInfoDto { get; private set; } = new(); - public SystemInfoService(MareMetrics mareMetrics, IClientIdentificationService clientIdentService, ILogger logger, IHubContext hubContext) + public SystemInfoService(MareMetrics mareMetrics, IConfiguration configuration, IServiceProvider services, IClientIdentificationService clientIdentService, ILogger logger, IHubContext hubContext) { _mareMetrics = mareMetrics; - this.clientIdentService = clientIdentService; + _services = services; + _clientIdentService = clientIdentService; _logger = logger; _hubContext = hubContext; } @@ -42,27 +47,28 @@ public class SystemInfoService : IHostedService, IDisposable private void PushSystemInfo(object state) { ThreadPool.GetAvailableThreads(out int workerThreads, out int ioThreads); - _logger.LogInformation("ThreadPool: {workerThreads} workers available, {ioThreads} IO workers available", workerThreads, ioThreads); _mareMetrics.SetGaugeTo(MetricsAPI.GaugeAvailableWorkerThreads, workerThreads); _mareMetrics.SetGaugeTo(MetricsAPI.GaugeAvailableIOWorkerThreads, ioThreads); - var secondaryServer = Environment.GetEnvironmentVariable("SECONDARY_SERVER"); - if (string.IsNullOrEmpty(secondaryServer) || secondaryServer == "0") + if (string.IsNullOrEmpty(secondaryServer) || string.Equals(secondaryServer, "0", StringComparison.Ordinal)) { SystemInfoDto = new SystemInfoDto() { - CacheUsage = 0, - CpuUsage = 0, - RAMUsage = 0, - NetworkIn = 0, - NetworkOut = 0, - OnlineUsers = clientIdentService.GetOnlineUsers(), - UploadedFiles = 0 + OnlineUsers = _clientIdentService.GetOnlineUsers().Result, }; _hubContext.Clients.All.SendAsync(Api.OnUpdateSystemInfo, SystemInfoDto); + + using var scope = _services.CreateScope(); + using var db = scope.ServiceProvider.GetService()!; + + _mareMetrics.SetGaugeTo(MetricsAPI.GaugePairs, db.ClientPairs.Count()); + _mareMetrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, db.ClientPairs.Count(p => p.IsPaused)); + _mareMetrics.SetGaugeTo(MetricsAPI.GaugeGroups, db.Groups.Count()); + _mareMetrics.SetGaugeTo(MetricsAPI.GaugeGroupPairs, db.GroupPairs.Count()); + _mareMetrics.SetGaugeTo(MetricsAPI.GaugeGroupPairsPaused, db.GroupPairs.Count(p => p.IsPaused)); } } diff --git a/MareSynchronosServer/MareSynchronosServer/Startup.cs b/MareSynchronosServer/MareSynchronosServer/Startup.cs index de7a6f9..8cf0884 100644 --- a/MareSynchronosServer/MareSynchronosServer/Startup.cs +++ b/MareSynchronosServer/MareSynchronosServer/Startup.cs @@ -22,169 +22,170 @@ using System.Collections.Generic; using MareSynchronosServer.Services; using MareSynchronosShared.Services; using System.Net.Http; +using MareSynchronosServer.Utils; -namespace MareSynchronosServer +namespace MareSynchronosServer; + +public class Startup { - public class Startup + public Startup(IConfiguration configuration) { - public Startup(IConfiguration configuration) + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddHttpContextAccessor(); + + services.Configure(Configuration.GetSection("IpRateLimiting")); + services.Configure(Configuration.GetSection("IpRateLimitPolicies")); + + services.AddMemoryCache(); + services.AddInMemoryRateLimiting(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(_ => Configuration); + + var mareConfig = Configuration.GetRequiredSection("MareSynchronos"); + + var defaultMethodConfig = new MethodConfig { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - public void ConfigureServices(IServiceCollection services) - { - services.AddHttpContextAccessor(); - - services.Configure(Configuration.GetSection("IpRateLimiting")); - services.Configure(Configuration.GetSection("IpRateLimitPolicies")); - - services.AddMemoryCache(); - services.AddInMemoryRateLimiting(); - - services.AddSingleton(); - services.AddSingleton(); - services.AddTransient(_ => Configuration); - - var mareConfig = Configuration.GetRequiredSection("MareSynchronos"); - - var defaultMethodConfig = new MethodConfig + Names = { MethodName.Default }, + RetryPolicy = new RetryPolicy { - Names = { MethodName.Default }, - RetryPolicy = new RetryPolicy - { - MaxAttempts = 100, - InitialBackoff = TimeSpan.FromSeconds(1), - MaxBackoff = TimeSpan.FromSeconds(5), - BackoffMultiplier = 1.5, - RetryableStatusCodes = { Grpc.Core.StatusCode.Unavailable } - } + MaxAttempts = 100, + InitialBackoff = TimeSpan.FromSeconds(1), + MaxBackoff = TimeSpan.FromSeconds(5), + BackoffMultiplier = 1.5, + RetryableStatusCodes = { Grpc.Core.StatusCode.Unavailable } + } + }; + + services.AddSingleton(new MareMetrics(new List + { + MetricsAPI.CounterInitializedConnections, + MetricsAPI.CounterUserPushData, + MetricsAPI.CounterUserPushDataTo, + MetricsAPI.CounterUsersRegisteredDeleted, + }, new List + { + MetricsAPI.GaugeAuthorizedConnections, + MetricsAPI.GaugeConnections, + MetricsAPI.GaugePairs, + MetricsAPI.GaugePairsPaused, + MetricsAPI.GaugeAvailableIOWorkerThreads, + MetricsAPI.GaugeAvailableWorkerThreads + })); + + services.AddGrpcClient(c => + { + c.Address = new Uri(mareConfig.GetValue("ServiceAddress")); + }).ConfigureChannel(c => + { + c.ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } }; + c.HttpHandler = new SocketsHttpHandler() + { + EnableMultipleHttp2Connections = true }; - - services.AddSingleton(new MareMetrics(new List - { - MetricsAPI.CounterInitializedConnections, - MetricsAPI.CounterUserPushData, - MetricsAPI.CounterUserPushDataTo, - MetricsAPI.CounterUsersRegisteredDeleted, - }, new List - { - MetricsAPI.GaugeAuthorizedConnections, - MetricsAPI.GaugeConnections, - MetricsAPI.GaugePairs, - MetricsAPI.GaugePairsPaused, - MetricsAPI.GaugeAvailableIOWorkerThreads, - MetricsAPI.GaugeAvailableWorkerThreads - })); - - services.AddGrpcClient(c => - { - c.Address = new Uri(mareConfig.GetValue("ServiceAddress")); - }).ConfigureChannel(c => - { - c.ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } }; - c.HttpHandler = new SocketsHttpHandler() - { - EnableMultipleHttp2Connections = true - }; - }); - services.AddGrpcClient(c => - { - c.Address = new Uri(mareConfig.GetValue("StaticFileServiceAddress")); - }).ConfigureChannel(c => - { - c.ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } }; - }); - - services.AddDbContextPool(options => - { - options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder => - { - builder.MigrationsHistoryTable("_efmigrationshistory", "public"); - builder.MigrationsAssembly("MareSynchronosShared"); - }).UseSnakeCaseNamingConvention(); - options.EnableThreadSafetyChecks(false); - }, mareConfig.GetValue("DbContextPoolSize", 1024)); - - services.AddAuthentication(options => - { - options.DefaultScheme = SecretKeyGrpcAuthenticationHandler.AuthScheme; - }).AddScheme(SecretKeyGrpcAuthenticationHandler.AuthScheme, _ => { }); - services.AddAuthorization(options => options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()); - - services.AddSingleton(); - - var signalRServiceBuilder = services.AddSignalR(hubOptions => - { - hubOptions.MaximumReceiveMessageSize = long.MaxValue; - hubOptions.EnableDetailedErrors = true; - hubOptions.MaximumParallelInvocationsPerClient = 10; - hubOptions.StreamBufferCapacity = 200; - hubOptions.AddFilter(); - }); - - // add redis related options - var redis = mareConfig.GetValue("RedisConnectionString", string.Empty); - if (!string.IsNullOrEmpty(redis)) - { - signalRServiceBuilder.AddStackExchangeRedis(redis, options => - { - options.Configuration.ChannelPrefix = "MareSynchronos"; - }); - - services.AddStackExchangeRedisCache(opt => - { - opt.Configuration = redis; - opt.InstanceName = "MareSynchronosCache:"; - }); - services.AddSingleton(); - services.AddHostedService(p => p.GetService()); - } - else - { - services.AddSingleton(); - services.AddHostedService(p => p.GetService()); - } - - services.AddHostedService(provider => provider.GetService()); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + }); + services.AddGrpcClient(c => { - if (env.IsDevelopment()) + c.Address = new Uri(mareConfig.GetValue("StaticFileServiceAddress")); + }).ConfigureChannel(c => + { + c.ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } }; + }); + + services.AddDbContextPool(options => + { + options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder => { - app.UseDeveloperExceptionPage(); - app.UseMigrationsEndPoint(); - } - else + builder.MigrationsHistoryTable("_efmigrationshistory", "public"); + builder.MigrationsAssembly("MareSynchronosShared"); + }).UseSnakeCaseNamingConvention(); + options.EnableThreadSafetyChecks(false); + }, mareConfig.GetValue("DbContextPoolSize", 1024)); + + services.AddAuthentication(options => + { + options.DefaultScheme = SecretKeyGrpcAuthenticationHandler.AuthScheme; + }).AddScheme(SecretKeyGrpcAuthenticationHandler.AuthScheme, _ => { }); + services.AddAuthorization(options => options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()); + + services.AddSingleton(); + + var signalRServiceBuilder = services.AddSignalR(hubOptions => + { + hubOptions.MaximumReceiveMessageSize = long.MaxValue; + hubOptions.EnableDetailedErrors = true; + hubOptions.MaximumParallelInvocationsPerClient = 10; + hubOptions.StreamBufferCapacity = 200; + + hubOptions.AddFilter(); + }); + + // add redis related options + var redis = mareConfig.GetValue("RedisConnectionString", string.Empty); + if (!string.IsNullOrEmpty(redis)) + { + signalRServiceBuilder.AddStackExchangeRedis(redis, options => { - app.UseExceptionHandler("/Error"); - app.UseHsts(); - } - - app.UseIpRateLimiting(); - - app.UseRouting(); - - app.UseWebSockets(); - - var metricServer = new KestrelMetricServer(4980); - metricServer.Start(); - - app.UseAuthentication(); - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapHub(Api.Path, options => - { - options.ApplicationMaxBufferSize = 5242880; - options.TransportMaxBufferSize = 5242880; - options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling; - }); + options.Configuration.ChannelPrefix = "MareSynchronos"; }); + + services.AddStackExchangeRedisCache(opt => + { + opt.Configuration = redis; + opt.InstanceName = "MareSynchronosCache:"; + }); + services.AddSingleton(); + services.AddHostedService(p => p.GetService()); } + else + { + services.AddSingleton(); + services.AddHostedService(p => p.GetService()); + } + + services.AddHostedService(provider => provider.GetService()); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseMigrationsEndPoint(); + } + else + { + app.UseExceptionHandler("/Error"); + app.UseHsts(); + } + + app.UseIpRateLimiting(); + + app.UseRouting(); + + app.UseWebSockets(); + + var metricServer = new KestrelMetricServer(4980); + metricServer.Start(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapHub(Api.Path, options => + { + options.ApplicationMaxBufferSize = 5242880; + options.TransportMaxBufferSize = 5242880; + options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling; + }); + }); } } diff --git a/MareSynchronosServer/MareSynchronosServer/Utils/IdBasedUserIdProvider.cs b/MareSynchronosServer/MareSynchronosServer/Utils/IdBasedUserIdProvider.cs new file mode 100644 index 0000000..180a5df --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Utils/IdBasedUserIdProvider.cs @@ -0,0 +1,13 @@ +using System.Linq; +using System.Security.Claims; +using Microsoft.AspNetCore.SignalR; + +namespace MareSynchronosServer.Utils; + +public class IdBasedUserIdProvider : IUserIdProvider +{ + public string GetUserId(HubConnectionContext context) + { + return context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.NameIdentifier, System.StringComparison.Ordinal))?.Value; + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/Utils/MareHubLogger.cs b/MareSynchronosServer/MareSynchronosServer/Utils/MareHubLogger.cs new file mode 100644 index 0000000..dfc359d --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Utils/MareHubLogger.cs @@ -0,0 +1,28 @@ +using MareSynchronosServer.Hubs; +using Microsoft.Extensions.Logging; + +namespace MareSynchronosServer.Utils; + +public class MareHubLogger +{ + private readonly MareHub _hub; + private readonly ILogger _logger; + + public MareHubLogger(MareHub hub, ILogger logger) + { + _hub = hub; + _logger = logger; + } + + public void LogCallInfo(string methodName, params object[] args) + { + string formattedArgs = args.Length != 0 ? "|" + string.Join(":", args) : string.Empty; + _logger.LogInformation("{uid}:{method}{args}", _hub.AuthenticatedUserId, methodName, formattedArgs); + } + + public void LogCallWarning(string methodName, params object[] args) + { + string formattedArgs = args.Length != 0 ? "|" + string.Join(":", args) : string.Empty; + _logger.LogWarning("{uid}:{method}{args}", _hub.AuthenticatedUserId, methodName, formattedArgs); + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/Utils/PauseInfo.cs b/MareSynchronosServer/MareSynchronosServer/Utils/PauseInfo.cs new file mode 100644 index 0000000..3fdb54f --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Utils/PauseInfo.cs @@ -0,0 +1,8 @@ +namespace MareSynchronosServer.Utils; + +public enum PauseInfo +{ + NoConnection, + Paused, + Unpaused +} diff --git a/MareSynchronosServer/MareSynchronosServer/Utils/PauseState.cs b/MareSynchronosServer/MareSynchronosServer/Utils/PauseState.cs new file mode 100644 index 0000000..8106f9f --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Utils/PauseState.cs @@ -0,0 +1,7 @@ +namespace MareSynchronosServer.Utils; + +public record PauseState +{ + public string GID { get; set; } + public bool IsPaused { get; set; } +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosServer/Utils/PausedEntry.cs b/MareSynchronosServer/MareSynchronosServer/Utils/PausedEntry.cs new file mode 100644 index 0000000..00fe0ae --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Utils/PausedEntry.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MareSynchronosServer.Utils; + +public record PausedEntry +{ + public string UID { get; set; } + public List PauseStates { get; set; } = new(); + + public PauseInfo IsDirectlyPaused => PauseStateWithoutGroups == null ? PauseInfo.NoConnection + : PauseStates.First(g => g.GID == null).IsPaused ? PauseInfo.Paused : PauseInfo.Unpaused; + + public PauseInfo IsPausedPerGroup => !PauseStatesWithoutDirect.Any() ? PauseInfo.NoConnection + : PauseStatesWithoutDirect.All(p => p.IsPaused) ? PauseInfo.Paused : PauseInfo.Unpaused; + + private IEnumerable PauseStatesWithoutDirect => PauseStates.Where(f => f.GID != null); + private PauseState PauseStateWithoutGroups => PauseStates.SingleOrDefault(p => p.GID == null); + + public bool IsPaused + { + get + { + var isDirectlyPaused = IsDirectlyPaused; + bool result; + if (isDirectlyPaused != PauseInfo.NoConnection) + { + result = isDirectlyPaused == PauseInfo.Paused; + } + else + { + result = IsPausedPerGroup == PauseInfo.Paused; + } + + return result; + } + } + + public PauseInfo IsPausedForSpecificGroup(string gid) + { + var state = PauseStatesWithoutDirect.SingleOrDefault(g => string.Equals(g.GID, gid, StringComparison.Ordinal)); + if (state == null) return PauseInfo.NoConnection; + return state.IsPaused ? PauseInfo.Paused : PauseInfo.NoConnection; + } + + public PauseInfo IsPausedExcludingGroup(string gid) + { + var states = PauseStatesWithoutDirect.Where(f => !string.Equals(f.GID, gid, StringComparison.Ordinal)).ToList(); + if (!states.Any()) return PauseInfo.NoConnection; + var result = states.All(p => p.IsPaused); + if (result) return PauseInfo.Paused; + return PauseInfo.Unpaused; + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/SignalRLimitFilter.cs b/MareSynchronosServer/MareSynchronosServer/Utils/SignalRLimitFilter.cs similarity index 96% rename from MareSynchronosServer/MareSynchronosServer/Hubs/SignalRLimitFilter.cs rename to MareSynchronosServer/MareSynchronosServer/Utils/SignalRLimitFilter.cs index d3a1ffe..57574a1 100644 --- a/MareSynchronosServer/MareSynchronosServer/Hubs/SignalRLimitFilter.cs +++ b/MareSynchronosServer/MareSynchronosServer/Utils/SignalRLimitFilter.cs @@ -9,7 +9,7 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace MareSynchronosServer.Hubs; +namespace MareSynchronosServer.Utils; public class SignalRLimitFilter : IHubFilter { private readonly IRateLimitProcessor _processor; @@ -42,7 +42,7 @@ public class SignalRLimitFilter : IHubFilter var counter = await _processor.ProcessRequestAsync(client, rule).ConfigureAwait(false); if (counter.Count > rule.Limit) { - var authUserId = invocationContext.Context.User.Claims?.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "Unknown"; + var authUserId = invocationContext.Context.User.Claims?.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.NameIdentifier, StringComparison.Ordinal))?.Value ?? "Unknown"; var retry = counter.Timestamp.RetryAfterFrom(rule); logger.LogWarning("Method rate limit triggered from {ip}/{authUserId}: {method}", ip, authUserId, invocationContext.HubMethodName); throw new HubException($"call limit {retry}"); diff --git a/MareSynchronosServer/MareSynchronosServer/Utils/UserPair.cs b/MareSynchronosServer/MareSynchronosServer/Utils/UserPair.cs new file mode 100644 index 0000000..1a5c367 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Utils/UserPair.cs @@ -0,0 +1,12 @@ +namespace MareSynchronosServer.Hubs; + +public partial class MareHub +{ + private record UserPair + { + public string UserUID { get; set; } + public string OtherUserUID { get; set; } + public bool UserPausedOther { get; set; } + public bool OtherPausedUser { get; set; } + } +} diff --git a/MareSynchronosServer/MareSynchronosServices/CleanupService.cs b/MareSynchronosServer/MareSynchronosServices/CleanupService.cs index 5e9233b..25f1138 100644 --- a/MareSynchronosServer/MareSynchronosServices/CleanupService.cs +++ b/MareSynchronosServer/MareSynchronosServices/CleanupService.cs @@ -2,6 +2,7 @@ using MareSynchronosShared.Data; using MareSynchronosShared.Metrics; using MareSynchronosShared.Models; +using MareSynchronosShared.Utils; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -9,156 +10,180 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace MareSynchronosServices +namespace MareSynchronosServices; + +public class CleanupService : IHostedService, IDisposable { - public class CleanupService : IHostedService, IDisposable + private readonly MareMetrics metrics; + private readonly SecretKeyAuthenticationHandler _authService; + private readonly ILogger _logger; + private readonly IServiceProvider _services; + private readonly IConfiguration _configuration; + private Timer? _timer; + + public CleanupService(MareMetrics metrics, SecretKeyAuthenticationHandler authService, ILogger logger, IServiceProvider services, IConfiguration configuration) { - private readonly MareMetrics metrics; - private readonly SecretKeyAuthenticationHandler _authService; - private readonly ILogger _logger; - private readonly IServiceProvider _services; - private readonly IConfiguration _configuration; - private Timer? _timer; + this.metrics = metrics; + _authService = authService; + _logger = logger; + _services = services; + _configuration = configuration.GetRequiredSection("MareSynchronos"); + } - public CleanupService(MareMetrics metrics, SecretKeyAuthenticationHandler authService, ILogger logger, IServiceProvider services, IConfiguration configuration) + public Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Cleanup Service started"); + + _timer = new Timer(CleanUp, null, TimeSpan.Zero, TimeSpan.FromMinutes(10)); + + return Task.CompletedTask; + } + + private async void CleanUp(object state) + { + using var scope = _services.CreateScope(); + using var dbContext = scope.ServiceProvider.GetService()!; + + try { - this.metrics = metrics; - _authService = authService; - _logger = logger; - _services = services; - _configuration = configuration.GetRequiredSection("MareSynchronos"); + _logger.LogInformation($"Cleaning up expired lodestone authentications"); + var lodestoneAuths = dbContext.LodeStoneAuth.Include(u => u.User).Where(a => a.StartedAt != null).ToList(); + List expiredAuths = new List(); + foreach (var auth in lodestoneAuths) + { + if (auth.StartedAt < DateTime.UtcNow - TimeSpan.FromMinutes(15)) + { + expiredAuths.Add(auth); + } + } + + dbContext.Users.RemoveRange(expiredAuths.Where(u => u.User != null).Select(a => a.User)); + dbContext.RemoveRange(expiredAuths); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error during expired auths cleanup"); } - public Task StartAsync(CancellationToken cancellationToken) + try { - _logger.LogInformation("Cleanup Service started"); + if (!bool.TryParse(_configuration["PurgeUnusedAccounts"], out var purgeUnusedAccounts)) + { + purgeUnusedAccounts = false; + } - _timer = new Timer(CleanUp, null, TimeSpan.Zero, TimeSpan.FromMinutes(10)); + if (purgeUnusedAccounts) + { + if (!int.TryParse(_configuration["PurgeUnusedAccountsPeriodInDays"], out var usersOlderThanDays)) + { + usersOlderThanDays = 14; + } - return Task.CompletedTask; + _logger.LogInformation("Cleaning up users older than {usersOlderThanDays} days", usersOlderThanDays); + + var allUsers = dbContext.Users.Where(u => string.IsNullOrEmpty(u.Alias)).ToList(); + List usersToRemove = new(); + foreach (var user in allUsers) + { + if (user.LastLoggedIn < (DateTime.UtcNow - TimeSpan.FromDays(usersOlderThanDays))) + { + _logger.LogInformation("User outdated: {userUID}", user.UID); + usersToRemove.Add(user); + } + } + + foreach (var user in usersToRemove) + { + await PurgeUser(user, dbContext); + } + } + + _logger.LogInformation("Cleaning up unauthorized users"); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error during user purge"); } - private void CleanUp(object state) + _authService.ClearUnauthorizedUsers(); + + _logger.LogInformation($"Cleanup complete"); + + dbContext.SaveChanges(); + } + + public async Task PurgeUser(User user, MareDbContext dbContext) + { + var lodestone = dbContext.LodeStoneAuth.SingleOrDefault(a => a.User.UID == user.UID); + + if (lodestone != null) { - using var scope = _services.CreateScope(); - using var dbContext = scope.ServiceProvider.GetService()!; + dbContext.Remove(lodestone); + } - try + _authService.RemoveAuthentication(user.UID); + + var auth = dbContext.Auth.Single(a => a.UserUID == user.UID); + + var userFiles = dbContext.Files.Where(f => f.Uploaded && f.Uploader.UID == user.UID).ToList(); + dbContext.Files.RemoveRange(userFiles); + + var ownPairData = dbContext.ClientPairs.Where(u => u.User.UID == user.UID).ToList(); + + dbContext.RemoveRange(ownPairData); + var otherPairData = dbContext.ClientPairs.Include(u => u.User) + .Where(u => u.OtherUser.UID == user.UID).ToList(); + + var userGroupPairs = dbContext.GroupPairs.Include(g => g.Group).Where(u => u.GroupUserUID == user.UID); + + foreach (var groupPair in userGroupPairs) + { + bool ownerHasLeft = string.Equals(groupPair.Group.OwnerUID, user.UID, StringComparison.Ordinal); + if (ownerHasLeft) { - _logger.LogInformation($"Cleaning up expired lodestone authentications"); - var lodestoneAuths = dbContext.LodeStoneAuth.Include(u => u.User).Where(a => a.StartedAt != null).ToList(); - List expiredAuths = new List(); - foreach (var auth in lodestoneAuths) + var groupPairs = await dbContext.GroupPairs.Where(g => g.GroupGID == groupPair.GroupGID).ToListAsync().ConfigureAwait(false); + + if (!groupPairs.Any()) { - if (auth.StartedAt < DateTime.UtcNow - TimeSpan.FromMinutes(15)) - { - expiredAuths.Add(auth); - } + _logger.LogInformation("Group {gid} has no new owner, deleting", groupPair.GroupGID); + dbContext.Remove(groupPair.Group); } - - dbContext.Users.RemoveRange(expiredAuths.Where(u => u.User != null).Select(a => a.User)); - dbContext.RemoveRange(expiredAuths); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Error during expired auths cleanup"); - } - - try - { - if (!bool.TryParse(_configuration["PurgeUnusedAccounts"], out var purgeUnusedAccounts)) + else { - purgeUnusedAccounts = false; + var groupHasMigrated = await SharedDbFunctions.MigrateOrDeleteGroup(dbContext, groupPair.Group, groupPairs, _configuration.GetValue("MaxExistingGroupsByUser", 3)).ConfigureAwait(false); + continue; } - - if (purgeUnusedAccounts) - { - if (!int.TryParse(_configuration["PurgeUnusedAccountsPeriodInDays"], out var usersOlderThanDays)) - { - usersOlderThanDays = 14; - } - - _logger.LogInformation("Cleaning up users older than {usersOlderThanDays} days", usersOlderThanDays); - - var allUsers = dbContext.Users.Where(u => string.IsNullOrEmpty(u.Alias)).ToList(); - List usersToRemove = new(); - foreach (var user in allUsers) - { - if (user.LastLoggedIn < (DateTime.UtcNow - TimeSpan.FromDays(usersOlderThanDays))) - { - _logger.LogInformation("User outdated: {userUID}", user.UID); - usersToRemove.Add(user); - } - } - - foreach (var user in usersToRemove) - { - PurgeUser(user, dbContext); - } - } - - _logger.LogInformation("Cleaning up unauthorized users"); } - catch (Exception ex) + else { - _logger.LogWarning(ex, "Error during user purge"); + dbContext.Remove(groupPair); } - _authService.ClearUnauthorizedUsers(); - - _logger.LogInformation($"Cleanup complete"); - dbContext.SaveChanges(); } - public void PurgeUser(User user, MareDbContext dbContext) - { - var lodestone = dbContext.LodeStoneAuth.SingleOrDefault(a => a.User.UID == user.UID); + _logger.LogInformation("User purged: {uid}", user.UID); - if (lodestone != null) - { - dbContext.Remove(lodestone); - } + metrics.DecGauge(MetricsAPI.GaugeUsersRegistered, 1); - _authService.RemoveAuthentication(user.UID); + dbContext.RemoveRange(otherPairData); + dbContext.Remove(auth); + dbContext.Remove(user); + } - var auth = dbContext.Auth.Single(a => a.UserUID == user.UID); + public Task StopAsync(CancellationToken cancellationToken) + { + _timer?.Change(Timeout.Infinite, 0); - var userFiles = dbContext.Files.Where(f => f.Uploaded && f.Uploader.UID == user.UID).ToList(); - dbContext.Files.RemoveRange(userFiles); + return Task.CompletedTask; + } - var ownPairData = dbContext.ClientPairs.Where(u => u.User.UID == user.UID).ToList(); - - dbContext.RemoveRange(ownPairData); - var otherPairData = dbContext.ClientPairs.Include(u => u.User) - .Where(u => u.OtherUser.UID == user.UID).ToList(); - - _logger.LogInformation("User purged: {uid}", user.UID); - - metrics.DecGauge(MetricsAPI.GaugePairs, ownPairData.Count + otherPairData.Count); - metrics.DecGauge(MetricsAPI.GaugePairsPaused, ownPairData.Count(c => c.IsPaused) + otherPairData.Count(c => c.IsPaused)); - metrics.DecGauge(MetricsAPI.GaugeUsersRegistered, 1); - - dbContext.RemoveRange(otherPairData); - dbContext.Remove(auth); - dbContext.Remove(user); - } - - public Task StopAsync(CancellationToken cancellationToken) - { - _timer?.Change(Timeout.Infinite, 0); - - return Task.CompletedTask; - } - - public void Dispose() - { - _timer?.Dispose(); - } + public void Dispose() + { + _timer?.Dispose(); } } diff --git a/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBot.cs b/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBot.cs index dabb1b5..25b6c8b 100644 --- a/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBot.cs +++ b/MareSynchronosServer/MareSynchronosServices/Discord/DiscordBot.cs @@ -9,6 +9,7 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Discord; +using Discord.Rest; using Discord.WebSocket; using MareSynchronosServices.Authentication; using MareSynchronosShared.Data; @@ -41,7 +42,9 @@ public class DiscordBot : IHostedService private readonly string[] LodestoneServers = new[] { "eu", "na", "jp", "fr", "de" }; private readonly ConcurrentQueue verificationQueue = new(); private ConcurrentDictionary LastVanityChange = new(); + private ConcurrentDictionary LastVanityGidChange = new(); private ulong vanityCommandId; + private ulong vanityGidCommandId; private Task cleanUpUserTask = null; private SemaphoreSlim semaphore; @@ -128,6 +131,18 @@ public class DiscordBot : IHostedService eb = await HandleVanityUid(eb, arg.User.Id, newUid); await arg.RespondAsync(embeds: new[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + break; + } + case "setsyncshellvanityid": + { + EmbedBuilder eb = new(); + var oldGid = (string)arg.Data.Options.First(f => f.Name == "syncshell_id").Value; + var newGid = (string)arg.Data.Options.First(f => f.Name == "vanity_syncshell_id").Value; + + eb = await HandleVanityGid(eb, arg.User.Id, oldGid, newGid); + + await arg.RespondAsync(embeds: new[] { eb.Build() }, ephemeral: true).ConfigureAwait(false); + break; } default: @@ -141,11 +156,75 @@ public class DiscordBot : IHostedService } } - private async Task HandleVanityUid(EmbedBuilder eb, ulong id, string newUid) + private async Task HandleVanityGid(EmbedBuilder eb, ulong id, string oldGid, string newGid) { + if (LastVanityGidChange.TryGetValue(oldGid, out var lastChange)) + { + var dateTimeDiff = DateTime.UtcNow.Subtract(lastChange); + if (dateTimeDiff.TotalHours < 24) + { + eb.WithTitle(("Failed to set Vanity Syncshell Id")); + eb.WithDescription( + $"You can only change the Vanity Syncshell Id once every 24h. Your last change is {dateTimeDiff} ago."); + } + } + + Regex rgx = new(@"[_\-a-zA-Z0-9]{5,20}", RegexOptions.ECMAScript); + if (!rgx.Match(newGid).Success || newGid.Length < 5 || newGid.Length > 20) + { + eb.WithTitle("Failed to set Vanity Syncshell Id"); + eb.WithDescription("The Vanity Syncshell Id must be between 5 and 20 characters and only contain letters A-Z, numbers 0-9 as well as - and _."); + return eb; + } + using var scope = services.CreateScope(); await using var db = scope.ServiceProvider.GetRequiredService(); + var lodestoneUser = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(u => u.DiscordId == id).ConfigureAwait(false); + if (lodestoneUser == null) + { + eb.WithTitle("Failed to set Vanity Syncshell Id"); + eb.WithDescription("You do not have a registered account on this server."); + return eb; + } + + var group = await db.Groups.FirstOrDefaultAsync(g => g.GID == oldGid || g.Alias == oldGid).ConfigureAwait(false); + if (group == null) + { + eb.WithTitle("Failed to set Vanity Syncshell Id"); + eb.WithDescription("The provided Syncshell Id does not exist."); + return eb; + } + + if (lodestoneUser.User.UID != group.OwnerUID) + { + eb.WithTitle("Failed to set Vanity Syncshell Id"); + eb.WithDescription("You are not the owner of this Syncshell"); + return eb; + } + + var uidExists = await db.Groups.AnyAsync(u => u.GID == newGid || u.Alias == newGid).ConfigureAwait(false); + if (uidExists) + { + eb.WithTitle("Failed to set Vanity Syncshell Id"); + eb.WithDescription("This Syncshell Id is already taken."); + return eb; + } + + group.Alias = newGid; + db.Update(group); + await db.SaveChangesAsync(); + + LastVanityGidChange[newGid] = DateTime.UtcNow; + LastVanityGidChange[oldGid] = DateTime.UtcNow; + + eb.WithTitle("Vanity Syncshell Id set"); + eb.WithDescription("The Vanity Syncshell Id was set to **" + newGid + "**." + Environment.NewLine + "For those changes to apply you will have to reconnect to Mare."); + return eb; + } + + private async Task HandleVanityUid(EmbedBuilder eb, ulong id, string newUid) + { if (LastVanityChange.TryGetValue(id, out var lastChange)) { var timeRemaining = DateTime.UtcNow.Subtract(lastChange); @@ -157,14 +236,17 @@ public class DiscordBot : IHostedService } } - Regex rgx = new("[A-Z0-9]{10}", RegexOptions.ECMAScript); - if (!rgx.Match(newUid).Success || newUid.Length != 10) + Regex rgx = new(@"[_\-a-zA-Z0-9]{5,15}", RegexOptions.ECMAScript); + if (!rgx.Match(newUid).Success || newUid.Length < 5 || newUid.Length > 15) { eb.WithTitle("Failed to set Vanity UID"); - eb.WithDescription("The Vanity UID must be 10 characters long and only contain uppercase letters A-Z and numbers 0-9."); + eb.WithDescription("The Vanity UID must be between 5 and 20 characters and only contain letters A-Z, numbers 0-9, as well as - and _."); return eb; } + using var scope = services.CreateScope(); + await using var db = scope.ServiceProvider.GetRequiredService(); + var lodestoneUser = await db.LodeStoneAuth.Include("User").SingleOrDefaultAsync(u => u.DiscordId == id).ConfigureAwait(false); if (lodestoneUser == null) { @@ -202,7 +284,7 @@ public class DiscordBot : IHostedService { if (discordAuthedUser.User != null) { - cleanupService.PurgeUser(discordAuthedUser.User, db); + await cleanupService.PurgeUser(discordAuthedUser.User, db); } else { @@ -508,6 +590,12 @@ public class DiscordBot : IHostedService vanityuid.WithDescription("Sets your Vanity UID."); vanityuid.AddOption("vanity_uid", ApplicationCommandOptionType.String, "Desired Vanity UID", isRequired: true); + var vanitygid = new SlashCommandBuilder(); + vanitygid.WithName("setsyncshellvanityid"); + vanitygid.WithDescription("Sets a Vanity GID for a Syncshell"); + vanitygid.AddOption("syncshell_id", ApplicationCommandOptionType.String, "Syncshell ID", isRequired: true); + vanitygid.AddOption("vanity_syncshell_id", ApplicationCommandOptionType.String, "Desired Vanity Syncshell ID", isRequired: true); + var recover = new SlashCommandBuilder(); recover.WithName("recover"); recover.WithDescription("Allows you to recover your account by generating a new secret key"); @@ -528,7 +616,6 @@ public class DiscordBot : IHostedService } if (!commands.Any(c => c.Name.Contains("setvanityuid"))) { - await guild.CreateApplicationCommandAsync(recover.Build()).ConfigureAwait(false); var vanityCommand = await guild.CreateApplicationCommandAsync(vanityuid.Build()).ConfigureAwait(false); vanityCommandId = vanityCommand.Id; } @@ -536,6 +623,10 @@ public class DiscordBot : IHostedService { vanityCommandId = commands.First(c => c.Name.Contains("setvanityuid")).Id; } + if (!commands.Any(c => c.Name.Contains("setsyncshellvanityid"))) + { + await guild.CreateApplicationCommandAsync(vanitygid.Build()).ConfigureAwait(false); + } if (!commands.Any(c => c.Name.Contains("recover"))) { await guild.CreateApplicationCommandAsync(recover.Build()).ConfigureAwait(false); @@ -651,6 +742,8 @@ public class DiscordBot : IHostedService { var aliasedUsers = db.LodeStoneAuth.Include("User") .Where(c => c.User != null && !string.IsNullOrEmpty(c.User.Alias)); + var aliasedGroups = db.Groups.Include(u => u.Owner) + .Where(c => !string.IsNullOrEmpty(c.Alias)); foreach (var lodestoneAuth in aliasedUsers) { @@ -665,9 +758,30 @@ public class DiscordBot : IHostedService } await Task.Delay(100); + await db.SaveChangesAsync().ConfigureAwait(false); } - await db.SaveChangesAsync().ConfigureAwait(false); + foreach (var group in aliasedGroups) + { + var lodestoneUser = await db.LodeStoneAuth.Include(u => u.User).SingleOrDefaultAsync(f => f.User.UID == group.OwnerUID); + RestGuildUser discordUser = null; + if (lodestoneUser != null) + { + discordUser = await restGuild.GetUserAsync(lodestoneUser.DiscordId).ConfigureAwait(false); + } + + logger.LogInformation($"Checking Group: {group.GID}, owned by {lodestoneUser?.User?.UID ?? string.Empty} ({lodestoneUser?.User?.Alias ?? string.Empty}), User in Roles: {string.Join(", ", discordUser?.RoleIds ?? new List())}"); + + if (lodestoneUser == null || discordUser == null || !discordUser.RoleIds.Any(u => allowedRoleIds.Contains(u))) + { + logger.LogInformation($"User {lodestoneUser.User.UID} not in allowed roles, deleting group alias"); + group.Alias = null; + db.Update(group); + } + + await Task.Delay(100); + await db.SaveChangesAsync().ConfigureAwait(false); + } } } else @@ -690,7 +804,7 @@ public class DiscordBot : IHostedService updateStatusCts = new(); while (!updateStatusCts.IsCancellationRequested) { - var onlineUsers = clientService.GetOnlineUsers(); + var onlineUsers = await clientService.GetOnlineUsers(); logger.LogInformation("Users online: " + onlineUsers); await discordClient.SetActivityAsync(new Game("Mare for " + onlineUsers + " Users")).ConfigureAwait(false); await Task.Delay(TimeSpan.FromSeconds(15)).ConfigureAwait(false); diff --git a/MareSynchronosServer/MareSynchronosServices/Services/AuthenticationService.cs b/MareSynchronosServer/MareSynchronosServices/Services/AuthenticationService.cs index d8b62e2..a6956cf 100644 --- a/MareSynchronosServer/MareSynchronosServices/Services/AuthenticationService.cs +++ b/MareSynchronosServer/MareSynchronosServices/Services/AuthenticationService.cs @@ -5,38 +5,37 @@ using MareSynchronosShared.Protos; using Microsoft.Extensions.Logging; using System.Threading.Tasks; -namespace MareSynchronosServices.Services +namespace MareSynchronosServices.Services; + +public class AuthenticationService : AuthService.AuthServiceBase { - public class AuthenticationService : AuthService.AuthServiceBase + private readonly ILogger _logger; + private readonly MareDbContext _dbContext; + private readonly SecretKeyAuthenticationHandler _authHandler; + + public AuthenticationService(ILogger logger, MareDbContext dbContext, SecretKeyAuthenticationHandler authHandler) { - private readonly ILogger _logger; - private readonly MareDbContext _dbContext; - private readonly SecretKeyAuthenticationHandler _authHandler; + _logger = logger; + _dbContext = dbContext; + _authHandler = authHandler; + } - public AuthenticationService(ILogger logger, MareDbContext dbContext, SecretKeyAuthenticationHandler authHandler) - { - _logger = logger; - _dbContext = dbContext; - _authHandler = authHandler; - } + public override async Task Authorize(AuthRequest request, ServerCallContext context) + { + return await _authHandler.AuthenticateAsync(_dbContext, request.Ip, request.SecretKey); + } - public override async Task Authorize(AuthRequest request, ServerCallContext context) - { - return await _authHandler.AuthenticateAsync(_dbContext, request.Ip, request.SecretKey); - } + public override Task RemoveAuth(RemoveAuthRequest request, ServerCallContext context) + { + _logger.LogInformation("Removing Authentication for {uid}", request.Uid); + _authHandler.RemoveAuthentication(request.Uid); + return Task.FromResult(new Empty()); + } - public override Task RemoveAuth(RemoveAuthRequest request, ServerCallContext context) - { - _logger.LogInformation("Removing Authentication for {uid}", request.Uid); - _authHandler.RemoveAuthentication(request.Uid); - return Task.FromResult(new Empty()); - } - - public override Task ClearUnauthorized(Empty request, ServerCallContext context) - { - _logger.LogInformation("Clearing unauthorized users"); - _authHandler.ClearUnauthorizedUsers(); - return Task.FromResult(new Empty()); - } + public override Task ClearUnauthorized(Empty request, ServerCallContext context) + { + _logger.LogInformation("Clearing unauthorized users"); + _authHandler.ClearUnauthorizedUsers(); + return Task.FromResult(new Empty()); } } \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Authentication/SecretKeyGrpcAuthenticationHandler.cs b/MareSynchronosServer/MareSynchronosShared/Authentication/SecretKeyGrpcAuthenticationHandler.cs index f1bc485..88d6218 100644 --- a/MareSynchronosServer/MareSynchronosShared/Authentication/SecretKeyGrpcAuthenticationHandler.cs +++ b/MareSynchronosServer/MareSynchronosShared/Authentication/SecretKeyGrpcAuthenticationHandler.cs @@ -1,63 +1,56 @@ using System.Security.Claims; -using System.Security.Cryptography; -using System.Text; using System.Text.Encodings.Web; using MareSynchronosServer; -using MareSynchronosShared.Data; -using MareSynchronosShared.Metrics; using MareSynchronosShared.Protos; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ISystemClock = Microsoft.AspNetCore.Authentication.ISystemClock; -namespace MareSynchronosShared.Authentication +namespace MareSynchronosShared.Authentication; + +public class SecretKeyGrpcAuthenticationHandler : AuthenticationHandler { + public const string AuthScheme = "SecretKeyGrpcAuth"; - public class SecretKeyGrpcAuthenticationHandler : AuthenticationHandler + private readonly AuthService.AuthServiceClient _authClient; + private readonly IHttpContextAccessor _accessor; + + public SecretKeyGrpcAuthenticationHandler(IHttpContextAccessor accessor, AuthService.AuthServiceClient authClient, + IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { - public const string AuthScheme = "SecretKeyGrpcAuth"; + this._authClient = authClient; + _accessor = accessor; + } - private readonly AuthService.AuthServiceClient _authClient; - private readonly IHttpContextAccessor _accessor; - - public SecretKeyGrpcAuthenticationHandler(IHttpContextAccessor accessor, AuthService.AuthServiceClient authClient, - IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) + protected override async Task HandleAuthenticateAsync() + { + if(!Request.Headers.TryGetValue("Authorization", out var authHeader)) { - this._authClient = authClient; - _accessor = accessor; + authHeader = string.Empty; + } + var ip = _accessor.GetIpAddress(); + + var authResult = await _authClient.AuthorizeAsync(new AuthRequest() {Ip = ip, SecretKey = authHeader}).ConfigureAwait(false); + + if (!authResult.Success) + { + return AuthenticateResult.Fail("Failed Authorization"); } - protected override async Task HandleAuthenticateAsync() + string uid = authResult.Uid; + + var claims = new List { - if(!Request.Headers.TryGetValue("Authorization", out var authHeader)) - { - authHeader = string.Empty; - } - var ip = _accessor.GetIpAddress(); + new(ClaimTypes.NameIdentifier, uid), + new(ClaimTypes.Authentication, authHeader) + }; - var authResult = await _authClient.AuthorizeAsync(new AuthRequest() {Ip = ip, SecretKey = authHeader}).ConfigureAwait(false); + var identity = new ClaimsIdentity(claims, nameof(SecretKeyGrpcAuthenticationHandler)); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, Scheme.Name); - if (!authResult.Success) - { - return AuthenticateResult.Fail("Failed Authorization"); - } - - string uid = authResult.Uid; - - var claims = new List - { - new(ClaimTypes.NameIdentifier, uid), - new(ClaimTypes.Authentication, authHeader) - }; - - var identity = new ClaimsIdentity(claims, nameof(SecretKeyGrpcAuthenticationHandler)); - var principal = new ClaimsPrincipal(identity); - var ticket = new AuthenticationTicket(principal, Scheme.Name); - - return AuthenticateResult.Success(ticket); - } + return AuthenticateResult.Success(ticket); } } diff --git a/MareSynchronosServer/MareSynchronosShared/Data/MareDbContext.cs b/MareSynchronosServer/MareSynchronosShared/Data/MareDbContext.cs index f3f50cf..7a132f6 100644 --- a/MareSynchronosServer/MareSynchronosShared/Data/MareDbContext.cs +++ b/MareSynchronosServer/MareSynchronosShared/Data/MareDbContext.cs @@ -39,13 +39,14 @@ public class MareDbContext : DbContext public DbSet Auth { get; set; } public DbSet LodeStoneAuth { get; set; } public DbSet BannedRegistrations { get; set; } + public DbSet Groups { get; set; } + public DbSet GroupPairs { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity().ToTable("auth"); modelBuilder.Entity().ToTable("users"); - //modelBuilder.Entity().HasIndex(c => c.CharacterIdentification); modelBuilder.Entity().ToTable("file_caches"); modelBuilder.Entity().HasIndex(c => c.UploaderUID); modelBuilder.Entity().ToTable("client_pairs"); @@ -56,5 +57,11 @@ public class MareDbContext : DbContext modelBuilder.Entity().ToTable("banned_users"); modelBuilder.Entity().ToTable("lodestone_auth"); modelBuilder.Entity().ToTable("banned_registrations"); + modelBuilder.Entity().ToTable("groups"); + modelBuilder.Entity().HasIndex(c => c.OwnerUID); + modelBuilder.Entity().ToTable("group_pairs"); + modelBuilder.Entity().HasKey(u => new { u.GroupGID, u.GroupUserUID }); + modelBuilder.Entity().HasIndex(c => c.GroupUserUID); + modelBuilder.Entity().HasIndex(c => c.GroupGID); } } \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Extensions.cs b/MareSynchronosServer/MareSynchronosShared/Extensions.cs index e2d63ac..0650af2 100644 --- a/MareSynchronosServer/MareSynchronosShared/Extensions.cs +++ b/MareSynchronosServer/MareSynchronosShared/Extensions.cs @@ -1,30 +1,28 @@ using Microsoft.AspNetCore.Http; -using System.Linq; -namespace MareSynchronosServer +namespace MareSynchronosServer; + +public static class Extensions { - public static class Extensions + public static string GetIpAddress(this IHttpContextAccessor accessor) { - public static string GetIpAddress(this IHttpContextAccessor accessor) + if (!string.IsNullOrEmpty(accessor.HttpContext.Request.Headers["CF-CONNECTING-IP"])) + return accessor.HttpContext.Request.Headers["CF-CONNECTING-IP"]; + + if (!string.IsNullOrEmpty(accessor.HttpContext.Request.Headers["X-Forwarded-For"])) { - if (!string.IsNullOrEmpty(accessor.HttpContext.Request.Headers["CF-CONNECTING-IP"])) - return accessor.HttpContext.Request.Headers["CF-CONNECTING-IP"]; - - if (!string.IsNullOrEmpty(accessor.HttpContext.Request.Headers["X-Forwarded-For"])) - { - return accessor.HttpContext.Request.Headers["X-Forwarded-For"]; - } - - var ipAddress = accessor.HttpContext.GetServerVariable("HTTP_X_FORWARDED_FOR"); - - if (!string.IsNullOrEmpty(ipAddress)) - { - var addresses = ipAddress.Split(','); - if (addresses.Length != 0) - return addresses.Last(); - } - - return accessor.HttpContext.Connection.RemoteIpAddress.ToString(); + return accessor.HttpContext.Request.Headers["X-Forwarded-For"]; } + + var ipAddress = accessor.HttpContext.GetServerVariable("HTTP_X_FORWARDED_FOR"); + + if (!string.IsNullOrEmpty(ipAddress)) + { + var addresses = ipAddress.Split(','); + if (addresses.Length != 0) + return addresses.Last(); + } + + return accessor.HttpContext.Connection.RemoteIpAddress.ToString(); } -} +} \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/MareSynchronosShared.csproj b/MareSynchronosServer/MareSynchronosShared/MareSynchronosShared.csproj index 7f04433..a5b16eb 100644 --- a/MareSynchronosServer/MareSynchronosShared/MareSynchronosShared.csproj +++ b/MareSynchronosServer/MareSynchronosShared/MareSynchronosShared.csproj @@ -13,6 +13,10 @@ + + + + @@ -33,6 +37,7 @@ + diff --git a/MareSynchronosServer/MareSynchronosShared/Metrics/MareMetrics.cs b/MareSynchronosServer/MareSynchronosShared/Metrics/MareMetrics.cs index 6fdb1d0..695b630 100644 --- a/MareSynchronosServer/MareSynchronosShared/Metrics/MareMetrics.cs +++ b/MareSynchronosServer/MareSynchronosShared/Metrics/MareMetrics.cs @@ -1,6 +1,4 @@ -using MareSynchronosShared.Data; -using Microsoft.Extensions.DependencyInjection; -using Prometheus; +using Prometheus; namespace MareSynchronosShared.Metrics; diff --git a/MareSynchronosServer/MareSynchronosShared/Metrics/MetricsAPI.cs b/MareSynchronosServer/MareSynchronosShared/Metrics/MetricsAPI.cs index 3b6187f..d46cc63 100644 --- a/MareSynchronosServer/MareSynchronosShared/Metrics/MetricsAPI.cs +++ b/MareSynchronosServer/MareSynchronosShared/Metrics/MetricsAPI.cs @@ -19,4 +19,7 @@ public class MetricsAPI public const string CounterAuthenticationCacheHits = "mare_auth_requests_cachehit"; public const string CounterAuthenticationFailures = "mare_auth_requests_fail"; public const string CounterAuthenticationSuccesses = "mare_auth_requests_success"; + public const string GaugeGroups = "mare_groups"; + public const string GaugeGroupPairs = "mare_groups_pairs"; + public const string GaugeGroupPairsPaused = "mare_groups_pairs_paused"; } \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220731210149_InitialCreate.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220731210149_InitialCreate.cs index 132a6b6..0fec13f 100644 --- a/MareSynchronosServer/MareSynchronosShared/Migrations/20220731210149_InitialCreate.cs +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220731210149_InitialCreate.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220801121419_AddLodestoneAuth.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220801121419_AddLodestoneAuth.cs index be72371..13bc0ac 100644 --- a/MareSynchronosServer/MareSynchronosShared/Migrations/20220801121419_AddLodestoneAuth.cs +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220801121419_AddLodestoneAuth.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.cs index 6a79460..1692083 100644 --- a/MareSynchronosServer/MareSynchronosShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.cs +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220801122103_AddNullableLodestoneAuthProperties.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220917115233_Groups.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220917115233_Groups.Designer.cs new file mode 100644 index 0000000..1a45495 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220917115233_Groups.Designer.cs @@ -0,0 +1,389 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20220917115233_Groups")] + partial class Groups + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(14) + .HasColumnType("character varying(14)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(14)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id2"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id5"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id4"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220917115233_Groups.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220917115233_Groups.cs new file mode 100644 index 0000000..39c1604 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220917115233_Groups.cs @@ -0,0 +1,123 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + public partial class Groups : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_users_character_identification", + table: "users"); + + migrationBuilder.DropColumn( + name: "character_identification", + table: "users"); + + migrationBuilder.AlterColumn( + name: "alias", + table: "users", + type: "character varying(10)", + maxLength: 10, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(100)", + oldMaxLength: 100, + oldNullable: true); + + migrationBuilder.CreateTable( + name: "groups", + columns: table => new + { + gid = table.Column(type: "character varying(14)", maxLength: 14, nullable: false), + owner_uid = table.Column(type: "character varying(10)", nullable: true), + alias = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + invites_enabled = table.Column(type: "boolean", nullable: false), + hashed_password = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_groups", x => x.gid); + table.ForeignKey( + name: "fk_groups_users_owner_temp_id5", + column: x => x.owner_uid, + principalTable: "users", + principalColumn: "uid"); + }); + + migrationBuilder.CreateTable( + name: "group_pairs", + columns: table => new + { + group_gid = table.Column(type: "character varying(14)", nullable: false), + group_user_uid = table.Column(type: "character varying(10)", nullable: false), + is_paused = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_group_pairs", x => new { x.group_gid, x.group_user_uid }); + table.ForeignKey( + name: "fk_group_pairs_groups_group_temp_id", + column: x => x.group_gid, + principalTable: "groups", + principalColumn: "gid", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_group_pairs_users_group_user_temp_id4", + column: x => x.group_user_uid, + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_group_pairs_group_gid", + table: "group_pairs", + column: "group_gid"); + + migrationBuilder.CreateIndex( + name: "ix_group_pairs_group_user_uid", + table: "group_pairs", + column: "group_user_uid"); + + migrationBuilder.CreateIndex( + name: "ix_groups_owner_uid", + table: "groups", + column: "owner_uid"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "group_pairs"); + + migrationBuilder.DropTable( + name: "groups"); + + migrationBuilder.AlterColumn( + name: "alias", + table: "users", + type: "character varying(100)", + maxLength: 100, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(10)", + oldMaxLength: 10, + oldNullable: true); + + migrationBuilder.AddColumn( + name: "character_identification", + table: "users", + type: "character varying(100)", + maxLength: 100, + nullable: true); + + migrationBuilder.CreateIndex( + name: "ix_users_character_identification", + table: "users", + column: "character_identification"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220929150304_ChangeGidLength.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220929150304_ChangeGidLength.Designer.cs new file mode 100644 index 0000000..7699c58 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220929150304_ChangeGidLength.Designer.cs @@ -0,0 +1,389 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20220929150304_ChangeGidLength")] + partial class ChangeGidLength + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id2"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id5"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id4"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20220929150304_ChangeGidLength.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20220929150304_ChangeGidLength.cs new file mode 100644 index 0000000..ec9dcc1 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20220929150304_ChangeGidLength.cs @@ -0,0 +1,51 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + public partial class ChangeGidLength : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "gid", + table: "groups", + type: "character varying(20)", + maxLength: 20, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(14)", + oldMaxLength: 14); + + migrationBuilder.AlterColumn( + name: "group_gid", + table: "group_pairs", + type: "character varying(20)", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(14)"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "gid", + table: "groups", + type: "character varying(14)", + maxLength: 14, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(20)", + oldMaxLength: 20); + + migrationBuilder.AlterColumn( + name: "group_gid", + table: "group_pairs", + type: "character varying(14)", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(20)"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20221002105428_IsPinned.Designer.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20221002105428_IsPinned.Designer.cs new file mode 100644 index 0000000..4285fe9 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20221002105428_IsPinned.Designer.cs @@ -0,0 +1,393 @@ +// +using System; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + [DbContext(typeof(MareDbContext))] + [Migration("20221002105428_IsPinned")] + partial class IsPinned + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("AllowReceivingMessages") + .HasColumnType("boolean") + .HasColumnName("allow_receiving_messages"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("alias"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Auth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_temp_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.ClientPair", b => + { + b.HasOne("MareSynchronosShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_temp_id1"); + + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_temp_id2"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.FileCache", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id5"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id4"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => + { + b.HasOne("MareSynchronosShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/20221002105428_IsPinned.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/20221002105428_IsPinned.cs new file mode 100644 index 0000000..3ee9c31 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/20221002105428_IsPinned.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MareSynchronosServer.Migrations +{ + public partial class IsPinned : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "is_pinned", + table: "group_pairs", + type: "boolean", + nullable: false, + defaultValue: false); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "is_pinned", + table: "group_pairs"); + } + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Migrations/MareDbContextModelSnapshot.cs b/MareSynchronosServer/MareSynchronosShared/Migrations/MareDbContextModelSnapshot.cs index cfa5718..3a77fc7 100644 --- a/MareSynchronosServer/MareSynchronosShared/Migrations/MareDbContextModelSnapshot.cs +++ b/MareSynchronosServer/MareSynchronosShared/Migrations/MareDbContextModelSnapshot.cs @@ -171,6 +171,69 @@ namespace MareSynchronosServer.Migrations b.ToTable("forbidden_upload_entries", (string)null); }); + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => { b.Property("DiscordId") @@ -213,15 +276,10 @@ namespace MareSynchronosServer.Migrations .HasColumnName("uid"); b.Property("Alias") - .HasMaxLength(100) - .HasColumnType("character varying(100)") + .HasMaxLength(10) + .HasColumnType("character varying(10)") .HasColumnName("alias"); - b.Property("CharacterIdentification") - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasColumnName("character_identification"); - b.Property("IsAdmin") .HasColumnType("boolean") .HasColumnName("is_admin"); @@ -243,9 +301,6 @@ namespace MareSynchronosServer.Migrations b.HasKey("UID") .HasName("pk_users"); - b.HasIndex("CharacterIdentification") - .HasDatabaseName("ix_users_character_identification"); - b.ToTable("users", (string)null); }); @@ -290,6 +345,37 @@ namespace MareSynchronosServer.Migrations b.Navigation("Uploader"); }); + modelBuilder.Entity("MareSynchronosShared.Models.Group", b => + { + b.HasOne("MareSynchronosShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_temp_id5"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("MareSynchronosShared.Models.GroupPair", b => + { + b.HasOne("MareSynchronosShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_temp_id"); + + b.HasOne("MareSynchronosShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_temp_id4"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + modelBuilder.Entity("MareSynchronosShared.Models.LodeStoneAuth", b => { b.HasOne("MareSynchronosShared.Models.User", "User") diff --git a/MareSynchronosServer/MareSynchronosShared/Models/Auth.cs b/MareSynchronosServer/MareSynchronosShared/Models/Auth.cs index 1f8bf2c..59f713d 100644 --- a/MareSynchronosServer/MareSynchronosShared/Models/Auth.cs +++ b/MareSynchronosServer/MareSynchronosShared/Models/Auth.cs @@ -1,14 +1,13 @@ using System.ComponentModel.DataAnnotations; -namespace MareSynchronosShared.Models -{ - public class Auth - { - [Key] - [MaxLength(64)] - public string HashedKey { get; set; } +namespace MareSynchronosShared.Models; - public string UserUID { get; set; } - public User User { get; set; } - } +public class Auth +{ + [Key] + [MaxLength(64)] + public string HashedKey { get; set; } + + public string UserUID { get; set; } + public User User { get; set; } } diff --git a/MareSynchronosServer/MareSynchronosShared/Models/Banned.cs b/MareSynchronosServer/MareSynchronosShared/Models/Banned.cs index b56e02a..c38e11d 100644 --- a/MareSynchronosServer/MareSynchronosShared/Models/Banned.cs +++ b/MareSynchronosServer/MareSynchronosShared/Models/Banned.cs @@ -1,14 +1,13 @@ using System.ComponentModel.DataAnnotations; -namespace MareSynchronosShared.Models +namespace MareSynchronosShared.Models; + +public class Banned { - public class Banned - { - [Key] - [MaxLength(100)] - public string CharacterIdentification { get; set; } - public string Reason { get; set; } - [Timestamp] - public byte[] Timestamp { get; set; } - } + [Key] + [MaxLength(100)] + public string CharacterIdentification { get; set; } + public string Reason { get; set; } + [Timestamp] + public byte[] Timestamp { get; set; } } diff --git a/MareSynchronosServer/MareSynchronosShared/Models/BannedRegistrations.cs b/MareSynchronosServer/MareSynchronosShared/Models/BannedRegistrations.cs index b26283a..70d2ffe 100644 --- a/MareSynchronosServer/MareSynchronosShared/Models/BannedRegistrations.cs +++ b/MareSynchronosServer/MareSynchronosShared/Models/BannedRegistrations.cs @@ -1,11 +1,10 @@ using System.ComponentModel.DataAnnotations; -namespace MareSynchronosShared.Models +namespace MareSynchronosShared.Models; + +public class BannedRegistrations { - public class BannedRegistrations - { - [Key] - [MaxLength(100)] - public string DiscordIdOrLodestoneAuth { get; set; } - } + [Key] + [MaxLength(100)] + public string DiscordIdOrLodestoneAuth { get; set; } } diff --git a/MareSynchronosServer/MareSynchronosShared/Models/ClientPair.cs b/MareSynchronosServer/MareSynchronosShared/Models/ClientPair.cs index 8cabbb5..509af61 100644 --- a/MareSynchronosServer/MareSynchronosShared/Models/ClientPair.cs +++ b/MareSynchronosServer/MareSynchronosShared/Models/ClientPair.cs @@ -1,18 +1,17 @@ using System.ComponentModel.DataAnnotations; -namespace MareSynchronosShared.Models +namespace MareSynchronosShared.Models; + +public class ClientPair { - public class ClientPair - { - [MaxLength(10)] - public string UserUID { get; set; } - public User User { get; set; } - [MaxLength(10)] - public string OtherUserUID { get; set; } - public User OtherUser { get; set; } - public bool IsPaused { get; set; } - public bool AllowReceivingMessages { get; set; } = false; - [Timestamp] - public byte[] Timestamp { get; set; } - } + [MaxLength(10)] + public string UserUID { get; set; } + public User User { get; set; } + [MaxLength(10)] + public string OtherUserUID { get; set; } + public User OtherUser { get; set; } + public bool IsPaused { get; set; } + public bool AllowReceivingMessages { get; set; } = false; + [Timestamp] + public byte[] Timestamp { get; set; } } diff --git a/MareSynchronosServer/MareSynchronosShared/Models/FileCache.cs b/MareSynchronosServer/MareSynchronosShared/Models/FileCache.cs index a239837..2429854 100644 --- a/MareSynchronosServer/MareSynchronosShared/Models/FileCache.cs +++ b/MareSynchronosServer/MareSynchronosShared/Models/FileCache.cs @@ -1,17 +1,16 @@ using System.ComponentModel.DataAnnotations; -namespace MareSynchronosShared.Models +namespace MareSynchronosShared.Models; + +public class FileCache { - public class FileCache - { - [Key] - [MaxLength(40)] - public string Hash { get; set; } - [MaxLength(10)] - public string UploaderUID { get; set; } - public User Uploader { get; set; } - public bool Uploaded { get; set; } - [Timestamp] - public byte[] Timestamp { get; set; } - } + [Key] + [MaxLength(40)] + public string Hash { get; set; } + [MaxLength(10)] + public string UploaderUID { get; set; } + public User Uploader { get; set; } + public bool Uploaded { get; set; } + [Timestamp] + public byte[] Timestamp { get; set; } } diff --git a/MareSynchronosServer/MareSynchronosShared/Models/ForbiddenUploadEntry.cs b/MareSynchronosServer/MareSynchronosShared/Models/ForbiddenUploadEntry.cs index a558423..12c946f 100644 --- a/MareSynchronosServer/MareSynchronosShared/Models/ForbiddenUploadEntry.cs +++ b/MareSynchronosServer/MareSynchronosShared/Models/ForbiddenUploadEntry.cs @@ -1,15 +1,14 @@ using System.ComponentModel.DataAnnotations; -namespace MareSynchronosShared.Models +namespace MareSynchronosShared.Models; + +public class ForbiddenUploadEntry { - public class ForbiddenUploadEntry - { - [Key] - [MaxLength(40)] - public string Hash { get; set; } - [MaxLength(100)] - public string ForbiddenBy { get; set; } - [Timestamp] - public byte[] Timestamp { get; set; } - } + [Key] + [MaxLength(40)] + public string Hash { get; set; } + [MaxLength(100)] + public string ForbiddenBy { get; set; } + [Timestamp] + public byte[] Timestamp { get; set; } } diff --git a/MareSynchronosServer/MareSynchronosShared/Models/Group.cs b/MareSynchronosServer/MareSynchronosShared/Models/Group.cs new file mode 100644 index 0000000..b5f27e4 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Models/Group.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace MareSynchronosShared.Models; + +public class Group +{ + [Key] + [MaxLength(20)] + public string GID { get; set; } + public string OwnerUID { get; set; } + public User Owner { get; set; } + [MaxLength(50)] + public string Alias { get; set; } + public bool InvitesEnabled { get; set; } + public string HashedPassword { get; set; } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Models/GroupPair.cs b/MareSynchronosServer/MareSynchronosShared/Models/GroupPair.cs new file mode 100644 index 0000000..d3cdf47 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Models/GroupPair.cs @@ -0,0 +1,11 @@ +namespace MareSynchronosShared.Models; + +public class GroupPair +{ + public string GroupGID { get; set; } + public Group Group { get; set; } + public string GroupUserUID { get; set; } + public User GroupUser { get; set; } + public bool IsPaused { get; set; } + public bool IsPinned { get; set; } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Models/LodeStoneAuth.cs b/MareSynchronosServer/MareSynchronosShared/Models/LodeStoneAuth.cs index 38448de..30afd19 100644 --- a/MareSynchronosServer/MareSynchronosShared/Models/LodeStoneAuth.cs +++ b/MareSynchronosServer/MareSynchronosShared/Models/LodeStoneAuth.cs @@ -1,16 +1,15 @@ using System.ComponentModel.DataAnnotations; -namespace MareSynchronosShared.Models +namespace MareSynchronosShared.Models; + +public class LodeStoneAuth { - public class LodeStoneAuth - { - [Key] - public ulong DiscordId { get; set; } - [MaxLength(100)] - public string HashedLodestoneId { get; set; } - [MaxLength(100)] - public string? LodestoneAuthString { get; set; } - public User? User { get; set; } - public DateTime? StartedAt { get; set; } - } + [Key] + public ulong DiscordId { get; set; } + [MaxLength(100)] + public string HashedLodestoneId { get; set; } + [MaxLength(100)] + public string? LodestoneAuthString { get; set; } + public User? User { get; set; } + public DateTime? StartedAt { get; set; } } diff --git a/MareSynchronosServer/MareSynchronosShared/Models/User.cs b/MareSynchronosServer/MareSynchronosShared/Models/User.cs index 9a0efa7..53d81a1 100644 --- a/MareSynchronosServer/MareSynchronosShared/Models/User.cs +++ b/MareSynchronosServer/MareSynchronosShared/Models/User.cs @@ -1,23 +1,20 @@ using System.ComponentModel.DataAnnotations; -namespace MareSynchronosShared.Models +namespace MareSynchronosShared.Models; + +public class User { - public class User - { - [Key] - [MaxLength(10)] - public string UID { get; set; } - //[MaxLength(100)] - //public string CharacterIdentification { get; set; } - [Timestamp] - public byte[] Timestamp { get; set; } + [Key] + [MaxLength(10)] + public string UID { get; set; } + [Timestamp] + public byte[] Timestamp { get; set; } - public bool IsModerator { get; set; } = false; + public bool IsModerator { get; set; } = false; - public bool IsAdmin { get; set; } = false; + public bool IsAdmin { get; set; } = false; - public DateTime LastLoggedIn { get; set; } - [MaxLength(10)] - public string Alias { get; set; } - } + public DateTime LastLoggedIn { get; set; } + [MaxLength(10)] + public string Alias { get; set; } } diff --git a/MareSynchronosServer/MareSynchronosShared/Services/BaseClientIdentificationService.cs b/MareSynchronosServer/MareSynchronosShared/Services/BaseClientIdentificationService.cs index 281e235..ec6ac87 100644 --- a/MareSynchronosServer/MareSynchronosShared/Services/BaseClientIdentificationService.cs +++ b/MareSynchronosServer/MareSynchronosShared/Services/BaseClientIdentificationService.cs @@ -12,40 +12,43 @@ public abstract class BaseClientIdentificationService : IClientIdentificationSer this.metrics = metrics; } - public virtual int GetOnlineUsers() + public virtual Task GetOnlineUsers() { - return OnlineClients.Count; + return Task.FromResult(OnlineClients.Count); } - public string? GetUidForCharacterIdent(string characterIdent) + public Task GetUidForCharacterIdent(string characterIdent) { var result = OnlineClients.SingleOrDefault(u => string.Compare(u.Value, characterIdent, StringComparison.InvariantCultureIgnoreCase) == 0); - return result.Equals(new KeyValuePair()) ? null : result.Key; + return Task.FromResult(result.Equals(new KeyValuePair()) ? null : result.Key); } - public virtual string? GetCharacterIdentForUid(string uid) + public virtual Task GetCharacterIdentForUid(string uid) { if (!OnlineClients.TryGetValue(uid, out var result)) { - return null; + return Task.FromResult((string?)null); } - return result; + return Task.FromResult(result); } - public virtual void MarkUserOnline(string uid, string charaIdent) + public virtual Task MarkUserOnline(string uid, string charaIdent) { OnlineClients[uid] = charaIdent; metrics.SetGaugeTo(MetricsAPI.GaugeAuthorizedConnections, OnlineClients.Count); + return Task.CompletedTask; } - public virtual void MarkUserOffline(string uid) + public virtual Task MarkUserOffline(string uid) { if (OnlineClients.TryRemove(uid, out _)) { metrics.SetGaugeTo(MetricsAPI.GaugeAuthorizedConnections, OnlineClients.Count); } + + return Task.CompletedTask; } public Task StartAsync(CancellationToken cancellationToken) diff --git a/MareSynchronosServer/MareSynchronosShared/Services/DistributedClientIdentificationService.cs b/MareSynchronosServer/MareSynchronosShared/Services/DistributedClientIdentificationService.cs index 194c200..958e1e2 100644 --- a/MareSynchronosServer/MareSynchronosShared/Services/DistributedClientIdentificationService.cs +++ b/MareSynchronosServer/MareSynchronosShared/Services/DistributedClientIdentificationService.cs @@ -21,14 +21,14 @@ public class DistributedClientIdentificationService : BaseClientIdentificationSe this.configuration = configuration.GetSection("MareSynchronos"); } - public override int GetOnlineUsers() + public override async Task GetOnlineUsers() { try { var redis = configuration.GetValue("RedisConnectionString"); - var conn = ConnectionMultiplexer.Connect(redis); + var conn = await ConnectionMultiplexer.ConnectAsync(redis).ConfigureAwait(false); var endpoint = conn.GetEndPoints().First(); - return conn.GetServer(endpoint).Keys(pattern: "*" + RedisPrefix + "*").Count(); + return await conn.GetServer(endpoint).KeysAsync(pattern: "*" + RedisPrefix + "*").CountAsync().ConfigureAwait(false); } catch (Exception ex) { @@ -37,27 +37,27 @@ public class DistributedClientIdentificationService : BaseClientIdentificationSe } } - public override string? GetCharacterIdentForUid(string uid) + public override async Task GetCharacterIdentForUid(string uid) { - var localIdent = base.GetCharacterIdentForUid(uid); + var localIdent = await base.GetCharacterIdentForUid(uid).ConfigureAwait(false); if (localIdent != null) return localIdent; - var cachedIdent = distributedCache.Get(RedisPrefix + uid); - return cachedIdent == null ? null : Encoding.UTF8.GetString(cachedIdent); + var cachedIdent = await distributedCache.GetStringAsync(RedisPrefix + uid).ConfigureAwait(false); + return cachedIdent ?? null; } - public override void MarkUserOffline(string uid) + public override async Task MarkUserOffline(string uid) { - base.MarkUserOffline(uid); - distributedCache.Remove(RedisPrefix + uid); + await base.MarkUserOffline(uid).ConfigureAwait(false); + await distributedCache.RemoveAsync(RedisPrefix + uid).ConfigureAwait(false); } - public override void MarkUserOnline(string uid, string charaIdent) + public override async Task MarkUserOnline(string uid, string charaIdent) { - base.MarkUserOnline(uid, charaIdent); - distributedCache.Set(RedisPrefix + uid, Encoding.UTF8.GetBytes(charaIdent), new DistributedCacheEntryOptions() + await base.MarkUserOnline(uid, charaIdent).ConfigureAwait(false); + await distributedCache.SetAsync(RedisPrefix + uid, Encoding.UTF8.GetBytes(charaIdent), new DistributedCacheEntryOptions() { AbsoluteExpiration = DateTime.Now.AddDays(7) - }); + }).ConfigureAwait(false); } public override Task StopAsync(CancellationToken cancellationToken) diff --git a/MareSynchronosServer/MareSynchronosShared/Services/IClientIdentificationService.cs b/MareSynchronosServer/MareSynchronosShared/Services/IClientIdentificationService.cs index b31a7ee..8f0b04d 100644 --- a/MareSynchronosServer/MareSynchronosShared/Services/IClientIdentificationService.cs +++ b/MareSynchronosServer/MareSynchronosShared/Services/IClientIdentificationService.cs @@ -4,9 +4,9 @@ namespace MareSynchronosShared.Services; public interface IClientIdentificationService : IHostedService { - int GetOnlineUsers(); - string? GetUidForCharacterIdent(string characterIdent); - string? GetCharacterIdentForUid(string uid); - void MarkUserOnline(string uid, string charaIdent); - void MarkUserOffline(string uid); + Task GetOnlineUsers(); + Task GetUidForCharacterIdent(string characterIdent); + Task GetCharacterIdentForUid(string uid); + Task MarkUserOnline(string uid, string charaIdent); + Task MarkUserOffline(string uid); } \ No newline at end of file diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/SharedDbFunctions.cs b/MareSynchronosServer/MareSynchronosShared/Utils/SharedDbFunctions.cs new file mode 100644 index 0000000..ed922a9 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Utils/SharedDbFunctions.cs @@ -0,0 +1,52 @@ +using MareSynchronosShared.Data; +using MareSynchronosShared.Models; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MareSynchronosShared.Utils; + +public static class SharedDbFunctions +{ + public static async Task<(bool, string)> MigrateOrDeleteGroup(MareDbContext context, Group group, List groupPairs, int maxGroupsByUser) + { + bool groupHasMigrated = false; + string newOwner = string.Empty; + foreach (var potentialNewOwner in groupPairs) + { + groupHasMigrated = await TryMigrateGroup(context, group, potentialNewOwner.GroupUserUID, maxGroupsByUser).ConfigureAwait(false); + + if (groupHasMigrated) + { + newOwner = potentialNewOwner.GroupUserUID; + break; + } + } + + if (!groupHasMigrated) + { + context.GroupPairs.RemoveRange(groupPairs); + context.Groups.Remove(group); + + await context.SaveChangesAsync().ConfigureAwait(false); + } + + return (groupHasMigrated, newOwner); + } + + private static async Task TryMigrateGroup(MareDbContext context, Group group, string potentialNewOwnerUid, int maxGroupsByUser) + { + var newOwnerOwnedGroups = await context.Groups.CountAsync(g => g.OwnerUID == potentialNewOwnerUid).ConfigureAwait(false); + if (newOwnerOwnedGroups >= maxGroupsByUser) + { + return false; + } + group.OwnerUID = potentialNewOwnerUid; + group.Alias = null; + await context.SaveChangesAsync().ConfigureAwait(false); + return true; + } +} diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/StringUtils.cs b/MareSynchronosServer/MareSynchronosShared/Utils/StringUtils.cs new file mode 100644 index 0000000..052bda9 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosShared/Utils/StringUtils.cs @@ -0,0 +1,31 @@ +using System.Security.Cryptography; +using System.Text; + +namespace MareSynchronosShared.Utils; + +public static class StringUtils +{ + public static string GenerateRandomString(int length, string? allowableChars = null) + { + if (string.IsNullOrEmpty(allowableChars)) + allowableChars = @"ABCDEFGHJKLMNPQRSTUVWXYZ0123456789"; + + // Generate random data + var rnd = RandomNumberGenerator.GetBytes(length); + + // Generate the output string + var allowable = allowableChars.ToCharArray(); + var l = allowable.Length; + var chars = new char[length]; + for (var i = 0; i < length; i++) + chars[i] = allowable[rnd[i] % l]; + + return new string(chars); + } + + public static string Sha256String(string input) + { + using var sha256 = SHA256.Create(); + return BitConverter.ToString(sha256.ComputeHash(Encoding.UTF8.GetBytes(input))).Replace("-", "", StringComparison.OrdinalIgnoreCase); + } +} diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/CleanupService.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/CleanupService.cs index 37e87c5..ea7089b 100644 --- a/MareSynchronosServer/MareSynchronosStaticFilesServer/CleanupService.cs +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/CleanupService.cs @@ -1,6 +1,5 @@ using MareSynchronosShared.Data; using MareSynchronosShared.Metrics; -using MareSynchronosShared.Protos; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting;