This commit is contained in:
Stanley Dimant
2022-10-04 14:51:49 +02:00
56 changed files with 3759 additions and 1237 deletions

View File

@@ -0,0 +1,4 @@
[*.cs]
# MA0048: File name must match type name
dotnet_diagnostic.MA0048.severity = suggestion

View File

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

View File

@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "6.0.6",
"version": "6.0.9",
"commands": [
"dotnet-ef"
]

View File

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

View File

@@ -8,10 +8,10 @@ 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;
@@ -102,13 +102,13 @@ namespace MareSynchronosServer.Hubs
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
return users.Where(c => !string.IsNullOrEmpty(_clientIdentService.GetCharacterIdentForUid(c.UID).Result)).Select(async b => new OnlineUserDto
{
CharacterNameHash = _clientIdentService.GetCharacterIdentForUid(b.UID),
CharacterNameHash = await _clientIdentService.GetCharacterIdentForUid(b.UID).ConfigureAwait(false),
UID = b.UID,
IsModerator = b.IsModerator,
IsAdmin = b.IsAdmin
}).ToList();
}).Select(c => c.Result).ToList();
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
@@ -135,7 +135,7 @@ namespace MareSynchronosServer.Hubs
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminUpdateOrAddBannedUser, dto).ConfigureAwait(false);
var bannedUser = _clientIdentService.GetUidForCharacterIdent(dto.CharacterHash);
var bannedUser = await _clientIdentService.GetUidForCharacterIdent(dto.CharacterHash).ConfigureAwait(false);
if (!string.IsNullOrEmpty(bannedUser))
{
await Clients.User(bannedUser).SendAsync(Api.OnAdminForcedReconnect).ConfigureAwait(false);
@@ -168,5 +168,4 @@ namespace MareSynchronosServer.Hubs
await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminUpdateOrAddForbiddenFile, dto).ConfigureAwait(false);
}
}
}

View File

@@ -16,15 +16,15 @@ 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()
{
_logger.LogInformation("User {AuthenticatedUserId} aborted upload", AuthenticatedUserId);
_logger.LogCallInfo(Api.SendFileAbortUpload);
var userId = AuthenticatedUserId;
var notUploadedFiles = _dbContext.Files.Where(f => !f.Uploaded && f.Uploader.UID == userId).ToList();
_dbContext.RemoveRange(notUploadedFiles);
@@ -35,14 +35,14 @@ namespace MareSynchronosServer.Hubs
[HubMethodName(Api.SendFileDeleteAllFiles)]
public async Task DeleteAllFiles()
{
_logger.LogInformation("User {AuthenticatedUserId} deleted all their files", AuthenticatedUserId);
_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 }
{ "Authorization", Context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.Authentication, StringComparison.Ordinal))?.Value }
};
_ = await _fileServiceClient.DeleteFilesAsync(request, headers).ConfigureAwait(false);
}
@@ -51,6 +51,8 @@ namespace MareSynchronosServer.Hubs
[HubMethodName(Api.InvokeGetFilesSizes)]
public async Task<List<DownloadFileDto>> GetFilesSizes(List<string> hashes)
{
_logger.LogCallInfo(Api.InvokeGetFilesSizes, hashes.Count.ToString());
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);
@@ -60,14 +62,14 @@ namespace MareSynchronosServer.Hubs
request.Hash.AddRange(hashes);
Metadata headers = new Metadata()
{
{ "Authorization", Context.User!.Claims.SingleOrDefault(c => c.Type == ClaimTypes.Authentication)?.Value }
{ "Authorization", Context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.Authentication, StringComparison.Ordinal))?.Value }
};
var grpcResponse = await _fileServiceClient.GetFileSizesAsync(request, headers).ConfigureAwait(false);
foreach (var hash in grpcResponse.HashToFileSize)
{
var forbiddenFile = forbiddenFiles.SingleOrDefault(f => f.Hash == hash.Key);
var downloadFile = allFiles.SingleOrDefault(f => f.Hash == hash.Key);
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));
response.Add(new DownloadFileDto
{
@@ -87,6 +89,7 @@ namespace MareSynchronosServer.Hubs
[HubMethodName(Api.InvokeFileIsUploadFinished)]
public async Task<bool> IsUploadFinished()
{
_logger.LogCallInfo(Api.InvokeFileIsUploadFinished);
var userUid = AuthenticatedUserId;
return await _dbContext.Files.AsNoTracking()
.AnyAsync(f => f.Uploader.UID == userUid && !f.Uploaded).ConfigureAwait(false);
@@ -96,9 +99,9 @@ namespace MareSynchronosServer.Hubs
[HubMethodName(Api.InvokeFileSendFiles)]
public async Task<List<UploadFileDto>> SendFiles(List<string> fileListHashes)
{
var userSentHashes = new HashSet<string>(fileListHashes.Distinct());
_logger.LogInformation("User {AuthenticatedUserId} sending files: {count}", AuthenticatedUserId, userSentHashes.Count);
var notCoveredFiles = new Dictionary<string, UploadFileDto>();
var userSentHashes = new HashSet<string>(fileListHashes.Distinct(StringComparer.Ordinal), StringComparer.Ordinal);
_logger.LogCallInfo(Api.InvokeFileSendFiles, userSentHashes.Count.ToString());
var notCoveredFiles = new Dictionary<string, UploadFileDto>(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);
@@ -122,7 +125,8 @@ namespace MareSynchronosServer.Hubs
}
if (existingFiles.ContainsKey(file)) { continue; }
_logger.LogInformation("User {AuthenticatedUserId} needs upload: {file}", AuthenticatedUserId, file);
_logger.LogCallInfo(Api.InvokeFileSendFiles, file, "Missing");
var userId = AuthenticatedUserId;
fileCachesToUpload.Add(new FileCache()
{
@@ -146,9 +150,9 @@ namespace MareSynchronosServer.Hubs
[HubMethodName(Api.SendFileUploadFileStreamAsync)]
public async Task UploadFileStreamAsync(string hash, IAsyncEnumerable<byte[]> fileContent)
{
_logger.LogInformation("User {AuthenticatedUserId} uploading file: {hash}", AuthenticatedUserId, hash);
_logger.LogCallInfo(Api.SendFileUploadFileStreamAsync, hash);
var relatedFile = _dbContext.Files.SingleOrDefault(f => f.Hash == hash && f.Uploader.UID == AuthenticatedUserId && f.Uploaded == false);
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;
@@ -188,7 +192,7 @@ namespace MareSynchronosServer.Hubs
return;
}
_logger.LogInformation("User {AuthenticatedUserId} upload finished: {hash}, size: {length}", AuthenticatedUserId, hash, length);
_logger.LogCallInfo(Api.SendFileUploadFileStreamAsync, hash, "Uploaded");
try
{
@@ -196,10 +200,10 @@ namespace MareSynchronosServer.Hubs
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)
var computedHashString = BitConverter.ToString(computedHash).Replace("-", "", StringComparison.Ordinal);
if (!string.Equals(hash, computedHashString, StringComparison.Ordinal))
{
_logger.LogWarning("Computed file hash was not expected file hash. Computed: {computedHashString}, Expected {hash}", computedHashString, hash);
_logger.LogCallWarning(Api.SendFileUploadFileStreamAsync, hash, "Invalid", computedHashString);
_dbContext.Remove(relatedFile);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
@@ -208,7 +212,7 @@ namespace MareSynchronosServer.Hubs
Metadata headers = new Metadata()
{
{ "Authorization", Context.User!.Claims.SingleOrDefault(c => c.Type == ClaimTypes.Authentication)?.Value }
{ "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);
@@ -224,13 +228,15 @@ namespace MareSynchronosServer.Hubs
Uploader = AuthenticatedUserId
}).ConfigureAwait(false);
}
await streamingCall.RequestStream.CompleteAsync();
await streamingCall.RequestStream.CompleteAsync().ConfigureAwait(false);
tempFileStream.Close();
await tempFileStream.DisposeAsync();
await tempFileStream.DisposeAsync().ConfigureAwait(false);
_logger.LogCallInfo(Api.SendFileUploadFileStreamAsync, hash, "Pushed");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Upload failed");
_logger.LogCallWarning(Api.SendFileUploadFileStreamAsync, "Failed", hash, ex.Message);
_dbContext.Remove(relatedFile);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
}
@@ -239,5 +245,4 @@ namespace MareSynchronosServer.Hubs
File.Delete(tempFileName);
}
}
}
}

View File

@@ -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<List<PausedEntry>> 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<List<string>> 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<List<string>> 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<User> GetAuthenticatedUserUntrackedAsync()
{
return await _dbContext.Users.AsNoTrackingWithIdentityResolution().SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false);
}
private async Task UserGroupLeave(GroupPair groupUserPair, List<PausedEntry> 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<GroupPair> 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);
}
}
}
}

View File

@@ -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<GroupCreatedDto> 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<List<GroupDto>> 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<List<GroupPairDto>> 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<GroupPairDto>();
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<bool> 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<bool> 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);
}
}
}
}

View File

@@ -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,22 +12,23 @@ 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()
{
_logger.LogInformation("User {AuthenticatedUserId} deleted their account", AuthenticatedUserId);
_logger.LogCallInfo(Api.SendUserDeleteAccount);
string userid = AuthenticatedUserId;
var userEntry = await _dbContext.Users.SingleAsync(u => u.UID == userid).ConfigureAwait(false);
var charaIdent = _clientIdentService.GetCharacterIdentForUid(userid);
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)
{
@@ -40,7 +42,6 @@ namespace MareSynchronosServer.Hubs
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)
@@ -55,8 +56,11 @@ namespace MareSynchronosServer.Hubs
}, charaIdent).ConfigureAwait(false);
}
_mareMetrics.DecGauge(MetricsAPI.GaugePairs, ownPairData.Count + otherPairData.Count);
_mareMetrics.DecGauge(MetricsAPI.GaugePairsPaused, ownPairData.Count(c => c.IsPaused));
foreach (var pair in groupPairs)
{
await GroupLeave(pair.GroupGID).ConfigureAwait(false);
}
_mareMetrics.IncCounter(MetricsAPI.CounterUsersRegisteredDeleted, 1);
_dbContext.RemoveRange(otherPairData);
@@ -69,32 +73,20 @@ namespace MareSynchronosServer.Hubs
[HubMethodName(Api.InvokeUserGetOnlineCharacters)]
public async Task<List<string>> GetOnlineCharacters()
{
_logger.LogInformation("User {AuthenticatedUserId} requested online characters", AuthenticatedUserId);
_logger.LogCallInfo(Api.InvokeUserGetOnlineCharacters);
var ownUser = await GetAuthenticatedUserUntrackedAsync().ConfigureAwait(false);
var ownIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).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();
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<List<ClientPairDto>> GetPairedClients()
{
_logger.LogCallInfo(Api.InvokeUserGetPairedClients);
string userid = AuthenticatedUserId;
var query =
from userToOther in _dbContext.ClientPairs
@@ -135,56 +127,46 @@ namespace MareSynchronosServer.Hubs
[HubMethodName(Api.InvokeUserPushCharacterDataToVisibleClients)]
public async Task PushCharacterDataToVisibleClients(CharacterCacheDto characterCache, List<string> visibleCharacterIds)
{
_logger.LogInformation("User {AuthenticatedUserId} pushing character data to {visibleCharacterIds} visible clients", AuthenticatedUserId, visibleCharacterIds.Count);
_logger.LogCallInfo(Api.InvokeUserPushCharacterDataToVisibleClients, visibleCharacterIds.Count);
var user = await GetAuthenticatedUserUntrackedAsync().ConfigureAwait(false);
var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
var query =
from userToOther in _dbContext.ClientPairs
join otherToUser in _dbContext.ClientPairs
on new
{
user = userToOther.UserUID,
other = userToOther.OtherUserUID
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));
} equals new
{
user = otherToUser.OtherUserUID,
other = otherToUser.UserUID
}
where
userToOther.UserUID == user.UID
&& !userToOther.IsPaused
&& !otherToUser.IsPaused
select otherToUser.UserUID;
var ownIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false);
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);
_logger.LogCallInfo(Api.InvokeUserPushCharacterDataToVisibleClients, visibleCharacterIds.Count, allPairedUsersDict.Count());
await Clients.Users(otherEntries).SendAsync(Api.OnUserReceiveCharacterData, characterCache, ownIdent).ConfigureAwait(false);
await Clients.Users(allPairedUsersDict.Select(f => f.Key)).SendAsync(Api.OnUserReceiveCharacterData, characterCache, ownIdent).ConfigureAwait(false);
_mareMetrics.IncCounter(MetricsAPI.CounterUserPushData);
_mareMetrics.IncCounter(MetricsAPI.CounterUserPushDataTo, otherEntries.Count);
_mareMetrics.IncCounter(MetricsAPI.CounterUserPushDataTo, allPairedUsersDict.Count());
}
[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);
_logger.LogCallInfo(Api.SendUserPairedClientAddition, uid);
var otherUser = await _dbContext.Users
.SingleOrDefaultAsync(u => u.UID == uid || u.Alias == uid).ConfigureAwait(false);
// 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.OtherUser.UID == otherUser.UID).ConfigureAwait(false);
p.User.UID == AuthenticatedUserId && p.OtherUserUID == uid).ConfigureAwait(false);
if (otherUser == null || existingEntry != null) return;
_logger.LogInformation("User {AuthenticatedUserId} adding {uid} to whitelist", AuthenticatedUserId, uid);
// 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,
@@ -193,6 +175,8 @@ namespace MareSynchronosServer.Hubs
};
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()
@@ -202,10 +186,16 @@ namespace MareSynchronosServer.Hubs
IsPaused = false,
IsPausedFromOthers = otherEntry?.IsPaused ?? false,
IsSynced = otherEntry != null
}, string.Empty).ConfigureAwait(false);
if (otherEntry != null)
{
var userIdent = _clientIdentService.GetCharacterIdentForUid(user.UID);
}).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()
{
@@ -214,10 +204,14 @@ namespace MareSynchronosServer.Hubs
IsPaused = otherEntry.IsPaused,
IsPausedFromOthers = false,
IsSynced = true
}, userIdent).ConfigureAwait(false);
}).ConfigureAwait(false);
var otherIdent = _clientIdentService.GetCharacterIdentForUid(otherUser.UID);
if (!string.IsNullOrEmpty(otherIdent))
// 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);
@@ -226,23 +220,22 @@ namespace MareSynchronosServer.Hubs
}
}
_mareMetrics.IncGauge(MetricsAPI.GaugePairs);
}
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendUserPairedClientPauseChange)]
public async Task SendPairedClientPauseChange(string otherUserUid, bool isPaused)
{
if (otherUserUid == AuthenticatedUserId) return;
_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;
_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);
_logger.LogCallInfo(Api.SendUserPairedClientPauseChange, otherUserUid, isPaused, "Success");
var otherEntry = OppositeEntry(otherUserUid);
await Clients.User(AuthenticatedUserId)
@@ -252,7 +245,7 @@ namespace MareSynchronosServer.Hubs
IsPaused = isPaused,
IsPausedFromOthers = otherEntry?.IsPaused ?? false,
IsSynced = otherEntry != null
}, otherCharaIdent).ConfigureAwait(false);
}).ConfigureAwait(false);
if (otherEntry != null)
{
await Clients.User(otherUserUid).SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto()
@@ -261,65 +254,98 @@ namespace MareSynchronosServer.Hubs
IsPaused = otherEntry.IsPaused,
IsPausedFromOthers = isPaused,
IsSynced = true
}, selfCharaIdent).ConfigureAwait(false);
}
}).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)
public async Task SendPairedClientRemoval(string otherUserUid)
{
if (uid == AuthenticatedUserId) return;
_logger.LogCallInfo(Api.SendUserPairedClientRemoval, otherUserUid);
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);
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);
var otherEntry = OppositeEntry(uid);
var otherIdent = _clientIdentService.GetCharacterIdentForUid(otherUser.UID);
await Clients.User(sender.UID)
_logger.LogCallInfo(Api.SendUserPairedClientRemoval, otherUserUid, "Success");
await Clients.User(AuthenticatedUserId)
.SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto()
{
OtherUID = otherUser.UID,
OtherUID = otherUserUid,
IsRemoved = true
}, otherIdent).ConfigureAwait(false);
if (otherEntry != null)
}).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()
{
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,
OtherUID = AuthenticatedUserId,
IsPausedFromOthers = false,
IsSynced = false
}, ownIdent).ConfigureAwait(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);
}
_mareMetrics.DecGauge(MetricsAPI.GaugePairs);
// 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);
}
}

View File

@@ -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<MareHub> _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<MareHub> 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<string>("CdnFullUrl"));
var config = configuration.GetRequiredSection("MareSynchronos");
_cdnFullUri = new Uri(config.GetValue<string>("CdnFullUrl"));
_shardName = config.GetValue("ShardName", "Main");
_maxExistingGroupsByUser = config.GetValue<int>("MaxExistingGroupsByUser", 3);
_maxJoinedGroupsByUser = config.GetValue<int>("MaxJoinedGroupsByUser", 6);
_maxGroupUserCount = config.GetValue<int>("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<bool> 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<User> GetAuthenticatedUserUntrackedAsync()
{
return await _dbContext.Users.AsNoTrackingWithIdentityResolution().SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false);
}
}

View File

@@ -26,6 +26,10 @@
<PackageReference Include="Grpc.Net.Client" Version="2.47.0" />
<PackageReference Include="Karambolo.Extensions.Logging.File" Version="3.3.1" />
<PackageReference Include="lz4net" Version="1.0.15.93" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.733">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="6.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.8" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="6.0.8" />

View File

@@ -9,10 +9,10 @@ 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)
{
var hostBuilder = CreateHostBuilder(args);
@@ -23,7 +23,7 @@ namespace MareSynchronosServer
using var context = services.GetRequiredService<MareDbContext>();
var secondaryServer = Environment.GetEnvironmentVariable("SECONDARY_SERVER");
if (string.IsNullOrEmpty(secondaryServer) || secondaryServer == "0")
if (string.IsNullOrEmpty(secondaryServer) || string.Equals(secondaryServer, "0", StringComparison.Ordinal))
{
context.Database.Migrate();
context.SaveChanges();
@@ -42,7 +42,7 @@ namespace MareSynchronosServer
metrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, context.ClientPairs.Count(p => p.IsPaused));
}
if (args.Length == 0 || args[0] != "dry")
if (args.Length == 0 || !string.Equals(args[0], "dry", StringComparison.Ordinal))
{
host.Run();
}
@@ -62,5 +62,4 @@ namespace MareSynchronosServer
});
webBuilder.UseStartup<Startup>();
});
}
}

View File

@@ -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<SystemInfoService> _logger;
private readonly IHubContext<MareHub> _hubContext;
private Timer _timer;
private string _shardName;
public SystemInfoDto SystemInfoDto { get; private set; } = new();
public SystemInfoService(MareMetrics mareMetrics, IClientIdentificationService clientIdentService, ILogger<SystemInfoService> logger, IHubContext<MareHub> hubContext)
public SystemInfoService(MareMetrics mareMetrics, IConfiguration configuration, IServiceProvider services, IClientIdentificationService clientIdentService, ILogger<SystemInfoService> logger, IHubContext<MareHub> 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<MareDbContext>()!;
_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));
}
}

View File

@@ -22,11 +22,12 @@ 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)
{
Configuration = configuration;
@@ -122,6 +123,7 @@ namespace MareSynchronosServer
hubOptions.EnableDetailedErrors = true;
hubOptions.MaximumParallelInvocationsPerClient = 10;
hubOptions.StreamBufferCapacity = 200;
hubOptions.AddFilter<SignalRLimitFilter>();
});
@@ -186,5 +188,4 @@ namespace MareSynchronosServer
});
});
}
}
}

View File

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

View File

@@ -0,0 +1,28 @@
using MareSynchronosServer.Hubs;
using Microsoft.Extensions.Logging;
namespace MareSynchronosServer.Utils;
public class MareHubLogger
{
private readonly MareHub _hub;
private readonly ILogger<MareHub> _logger;
public MareHubLogger(MareHub hub, ILogger<MareHub> 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);
}
}

View File

@@ -0,0 +1,8 @@
namespace MareSynchronosServer.Utils;
public enum PauseInfo
{
NoConnection,
Paused,
Unpaused
}

View File

@@ -0,0 +1,7 @@
namespace MareSynchronosServer.Utils;
public record PauseState
{
public string GID { get; set; }
public bool IsPaused { get; set; }
}

View File

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

View File

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

View File

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

View File

@@ -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,15 +10,14 @@ 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<CleanupService> _logger;
@@ -43,7 +43,7 @@ namespace MareSynchronosServices
return Task.CompletedTask;
}
private void CleanUp(object state)
private async void CleanUp(object state)
{
using var scope = _services.CreateScope();
using var dbContext = scope.ServiceProvider.GetService<MareDbContext>()!;
@@ -98,7 +98,7 @@ namespace MareSynchronosServices
foreach (var user in usersToRemove)
{
PurgeUser(user, dbContext);
await PurgeUser(user, dbContext);
}
}
@@ -116,7 +116,7 @@ namespace MareSynchronosServices
dbContext.SaveChanges();
}
public void PurgeUser(User user, MareDbContext dbContext)
public async Task PurgeUser(User user, MareDbContext dbContext)
{
var lodestone = dbContext.LodeStoneAuth.SingleOrDefault(a => a.User.UID == user.UID);
@@ -138,10 +138,36 @@ namespace MareSynchronosServices
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)
{
var groupPairs = await dbContext.GroupPairs.Where(g => g.GroupGID == groupPair.GroupGID).ToListAsync().ConfigureAwait(false);
if (!groupPairs.Any())
{
_logger.LogInformation("Group {gid} has no new owner, deleting", groupPair.GroupGID);
dbContext.Remove(groupPair.Group);
}
else
{
var groupHasMigrated = await SharedDbFunctions.MigrateOrDeleteGroup(dbContext, groupPair.Group, groupPairs, _configuration.GetValue<int>("MaxExistingGroupsByUser", 3)).ConfigureAwait(false);
continue;
}
}
else
{
dbContext.Remove(groupPair);
}
dbContext.SaveChanges();
}
_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);
@@ -160,5 +186,4 @@ namespace MareSynchronosServices
{
_timer?.Dispose();
}
}
}

View File

@@ -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<SocketSlashCommand> verificationQueue = new();
private ConcurrentDictionary<ulong, DateTime> LastVanityChange = new();
private ConcurrentDictionary<string, DateTime> 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<EmbedBuilder> HandleVanityUid(EmbedBuilder eb, ulong id, string newUid)
private async Task<EmbedBuilder> 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<MareDbContext>();
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<EmbedBuilder> 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 between 5 and 15 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<MareDbContext>();
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,11 +758,32 @@ public class DiscordBot : IHostedService
}
await Task.Delay(100);
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<ulong>())}");
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
{
logger.LogInformation("No roles for command defined, no cleanup performed");
@@ -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);

View File

@@ -5,10 +5,10 @@ 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<AuthenticationService> _logger;
private readonly MareDbContext _dbContext;
private readonly SecretKeyAuthenticationHandler _authHandler;
@@ -38,5 +38,4 @@ namespace MareSynchronosServices.Services
_authHandler.ClearUnauthorizedUsers();
return Task.FromResult(new Empty());
}
}
}

View File

@@ -1,23 +1,17 @@
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<AuthenticationSchemeOptions>
{
public class SecretKeyGrpcAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public const string AuthScheme = "SecretKeyGrpcAuth";
private readonly AuthService.AuthServiceClient _authClient;
@@ -59,5 +53,4 @@ namespace MareSynchronosShared.Authentication
return AuthenticateResult.Success(ticket);
}
}
}

View File

@@ -39,13 +39,14 @@ public class MareDbContext : DbContext
public DbSet<Auth> Auth { get; set; }
public DbSet<LodeStoneAuth> LodeStoneAuth { get; set; }
public DbSet<BannedRegistrations> BannedRegistrations { get; set; }
public DbSet<Group> Groups { get; set; }
public DbSet<GroupPair> GroupPairs { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Auth>().ToTable("auth");
modelBuilder.Entity<User>().ToTable("users");
//modelBuilder.Entity<User>().HasIndex(c => c.CharacterIdentification);
modelBuilder.Entity<FileCache>().ToTable("file_caches");
modelBuilder.Entity<FileCache>().HasIndex(c => c.UploaderUID);
modelBuilder.Entity<ClientPair>().ToTable("client_pairs");
@@ -56,5 +57,11 @@ public class MareDbContext : DbContext
modelBuilder.Entity<Banned>().ToTable("banned_users");
modelBuilder.Entity<LodeStoneAuth>().ToTable("lodestone_auth");
modelBuilder.Entity<BannedRegistrations>().ToTable("banned_registrations");
modelBuilder.Entity<Group>().ToTable("groups");
modelBuilder.Entity<Group>().HasIndex(c => c.OwnerUID);
modelBuilder.Entity<GroupPair>().ToTable("group_pairs");
modelBuilder.Entity<GroupPair>().HasKey(u => new { u.GroupGID, u.GroupUserUID });
modelBuilder.Entity<GroupPair>().HasIndex(c => c.GroupUserUID);
modelBuilder.Entity<GroupPair>().HasIndex(c => c.GroupGID);
}
}

View File

@@ -1,10 +1,9 @@
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)
{
if (!string.IsNullOrEmpty(accessor.HttpContext.Request.Headers["CF-CONNECTING-IP"]))
@@ -26,5 +25,4 @@ namespace MareSynchronosServer
return accessor.HttpContext.Connection.RemoteIpAddress.ToString();
}
}
}

View File

@@ -13,6 +13,10 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="EFCore.NamingConventions" Version="6.0.0" />
<PackageReference Include="Grpc.AspNetCore" Version="2.47.0" />
@@ -33,6 +37,7 @@
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="6.0.8" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.6" />
<PackageReference Include="prometheus-net" Version="6.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,6 +1,4 @@
using MareSynchronosShared.Data;
using Microsoft.Extensions.DependencyInjection;
using Prometheus;
using Prometheus;
namespace MareSynchronosShared.Metrics;

View File

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

View File

@@ -1,5 +1,4 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -1,5 +1,4 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -1,5 +1,4 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -0,0 +1,389 @@
// <auto-generated />
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<string>("HashedKey")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("hashed_key");
b.Property<string>("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<string>("CharacterIdentification")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("character_identification");
b.Property<string>("Reason")
.HasColumnType("text")
.HasColumnName("reason");
b.Property<byte[]>("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<string>("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<string>("UserUID")
.HasMaxLength(10)
.HasColumnType("character varying(10)")
.HasColumnName("user_uid");
b.Property<string>("OtherUserUID")
.HasMaxLength(10)
.HasColumnType("character varying(10)")
.HasColumnName("other_user_uid");
b.Property<bool>("AllowReceivingMessages")
.HasColumnType("boolean")
.HasColumnName("allow_receiving_messages");
b.Property<bool>("IsPaused")
.HasColumnType("boolean")
.HasColumnName("is_paused");
b.Property<byte[]>("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<string>("Hash")
.HasMaxLength(40)
.HasColumnType("character varying(40)")
.HasColumnName("hash");
b.Property<byte[]>("Timestamp")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("bytea")
.HasColumnName("timestamp");
b.Property<bool>("Uploaded")
.HasColumnType("boolean")
.HasColumnName("uploaded");
b.Property<string>("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<string>("Hash")
.HasMaxLength(40)
.HasColumnType("character varying(40)")
.HasColumnName("hash");
b.Property<string>("ForbiddenBy")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("forbidden_by");
b.Property<byte[]>("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<string>("GID")
.HasMaxLength(14)
.HasColumnType("character varying(14)")
.HasColumnName("gid");
b.Property<string>("Alias")
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("alias");
b.Property<string>("HashedPassword")
.HasColumnType("text")
.HasColumnName("hashed_password");
b.Property<bool>("InvitesEnabled")
.HasColumnType("boolean")
.HasColumnName("invites_enabled");
b.Property<string>("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<string>("GroupGID")
.HasColumnType("character varying(14)")
.HasColumnName("group_gid");
b.Property<string>("GroupUserUID")
.HasColumnType("character varying(10)")
.HasColumnName("group_user_uid");
b.Property<bool>("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<decimal>("DiscordId")
.ValueGeneratedOnAdd()
.HasColumnType("numeric(20,0)")
.HasColumnName("discord_id");
b.Property<string>("HashedLodestoneId")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("hashed_lodestone_id");
b.Property<string>("LodestoneAuthString")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("lodestone_auth_string");
b.Property<DateTime?>("StartedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("started_at");
b.Property<string>("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<string>("UID")
.HasMaxLength(10)
.HasColumnType("character varying(10)")
.HasColumnName("uid");
b.Property<string>("Alias")
.HasMaxLength(10)
.HasColumnType("character varying(10)")
.HasColumnName("alias");
b.Property<bool>("IsAdmin")
.HasColumnType("boolean")
.HasColumnName("is_admin");
b.Property<bool>("IsModerator")
.HasColumnType("boolean")
.HasColumnName("is_moderator");
b.Property<DateTime>("LastLoggedIn")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_logged_in");
b.Property<byte[]>("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
}
}
}

View File

@@ -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<string>(
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<string>(type: "character varying(14)", maxLength: 14, nullable: false),
owner_uid = table.Column<string>(type: "character varying(10)", nullable: true),
alias = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
invites_enabled = table.Column<bool>(type: "boolean", nullable: false),
hashed_password = table.Column<string>(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<string>(type: "character varying(14)", nullable: false),
group_user_uid = table.Column<string>(type: "character varying(10)", nullable: false),
is_paused = table.Column<bool>(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<string>(
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<string>(
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");
}
}
}

View File

@@ -0,0 +1,389 @@
// <auto-generated />
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<string>("HashedKey")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("hashed_key");
b.Property<string>("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<string>("CharacterIdentification")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("character_identification");
b.Property<string>("Reason")
.HasColumnType("text")
.HasColumnName("reason");
b.Property<byte[]>("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<string>("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<string>("UserUID")
.HasMaxLength(10)
.HasColumnType("character varying(10)")
.HasColumnName("user_uid");
b.Property<string>("OtherUserUID")
.HasMaxLength(10)
.HasColumnType("character varying(10)")
.HasColumnName("other_user_uid");
b.Property<bool>("AllowReceivingMessages")
.HasColumnType("boolean")
.HasColumnName("allow_receiving_messages");
b.Property<bool>("IsPaused")
.HasColumnType("boolean")
.HasColumnName("is_paused");
b.Property<byte[]>("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<string>("Hash")
.HasMaxLength(40)
.HasColumnType("character varying(40)")
.HasColumnName("hash");
b.Property<byte[]>("Timestamp")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("bytea")
.HasColumnName("timestamp");
b.Property<bool>("Uploaded")
.HasColumnType("boolean")
.HasColumnName("uploaded");
b.Property<string>("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<string>("Hash")
.HasMaxLength(40)
.HasColumnType("character varying(40)")
.HasColumnName("hash");
b.Property<string>("ForbiddenBy")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("forbidden_by");
b.Property<byte[]>("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<string>("GID")
.HasMaxLength(20)
.HasColumnType("character varying(20)")
.HasColumnName("gid");
b.Property<string>("Alias")
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("alias");
b.Property<string>("HashedPassword")
.HasColumnType("text")
.HasColumnName("hashed_password");
b.Property<bool>("InvitesEnabled")
.HasColumnType("boolean")
.HasColumnName("invites_enabled");
b.Property<string>("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<string>("GroupGID")
.HasColumnType("character varying(20)")
.HasColumnName("group_gid");
b.Property<string>("GroupUserUID")
.HasColumnType("character varying(10)")
.HasColumnName("group_user_uid");
b.Property<bool>("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<decimal>("DiscordId")
.ValueGeneratedOnAdd()
.HasColumnType("numeric(20,0)")
.HasColumnName("discord_id");
b.Property<string>("HashedLodestoneId")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("hashed_lodestone_id");
b.Property<string>("LodestoneAuthString")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("lodestone_auth_string");
b.Property<DateTime?>("StartedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("started_at");
b.Property<string>("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<string>("UID")
.HasMaxLength(10)
.HasColumnType("character varying(10)")
.HasColumnName("uid");
b.Property<string>("Alias")
.HasMaxLength(10)
.HasColumnType("character varying(10)")
.HasColumnName("alias");
b.Property<bool>("IsAdmin")
.HasColumnType("boolean")
.HasColumnName("is_admin");
b.Property<bool>("IsModerator")
.HasColumnType("boolean")
.HasColumnName("is_moderator");
b.Property<DateTime>("LastLoggedIn")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_logged_in");
b.Property<byte[]>("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
}
}
}

View File

@@ -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<string>(
name: "gid",
table: "groups",
type: "character varying(20)",
maxLength: 20,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(14)",
oldMaxLength: 14);
migrationBuilder.AlterColumn<string>(
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<string>(
name: "gid",
table: "groups",
type: "character varying(14)",
maxLength: 14,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(20)",
oldMaxLength: 20);
migrationBuilder.AlterColumn<string>(
name: "group_gid",
table: "group_pairs",
type: "character varying(14)",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(20)");
}
}
}

View File

@@ -0,0 +1,393 @@
// <auto-generated />
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<string>("HashedKey")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("hashed_key");
b.Property<string>("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<string>("CharacterIdentification")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("character_identification");
b.Property<string>("Reason")
.HasColumnType("text")
.HasColumnName("reason");
b.Property<byte[]>("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<string>("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<string>("UserUID")
.HasMaxLength(10)
.HasColumnType("character varying(10)")
.HasColumnName("user_uid");
b.Property<string>("OtherUserUID")
.HasMaxLength(10)
.HasColumnType("character varying(10)")
.HasColumnName("other_user_uid");
b.Property<bool>("AllowReceivingMessages")
.HasColumnType("boolean")
.HasColumnName("allow_receiving_messages");
b.Property<bool>("IsPaused")
.HasColumnType("boolean")
.HasColumnName("is_paused");
b.Property<byte[]>("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<string>("Hash")
.HasMaxLength(40)
.HasColumnType("character varying(40)")
.HasColumnName("hash");
b.Property<byte[]>("Timestamp")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("bytea")
.HasColumnName("timestamp");
b.Property<bool>("Uploaded")
.HasColumnType("boolean")
.HasColumnName("uploaded");
b.Property<string>("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<string>("Hash")
.HasMaxLength(40)
.HasColumnType("character varying(40)")
.HasColumnName("hash");
b.Property<string>("ForbiddenBy")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("forbidden_by");
b.Property<byte[]>("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<string>("GID")
.HasMaxLength(20)
.HasColumnType("character varying(20)")
.HasColumnName("gid");
b.Property<string>("Alias")
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("alias");
b.Property<string>("HashedPassword")
.HasColumnType("text")
.HasColumnName("hashed_password");
b.Property<bool>("InvitesEnabled")
.HasColumnType("boolean")
.HasColumnName("invites_enabled");
b.Property<string>("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<string>("GroupGID")
.HasColumnType("character varying(20)")
.HasColumnName("group_gid");
b.Property<string>("GroupUserUID")
.HasColumnType("character varying(10)")
.HasColumnName("group_user_uid");
b.Property<bool>("IsPaused")
.HasColumnType("boolean")
.HasColumnName("is_paused");
b.Property<bool>("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<decimal>("DiscordId")
.ValueGeneratedOnAdd()
.HasColumnType("numeric(20,0)")
.HasColumnName("discord_id");
b.Property<string>("HashedLodestoneId")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("hashed_lodestone_id");
b.Property<string>("LodestoneAuthString")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("lodestone_auth_string");
b.Property<DateTime?>("StartedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("started_at");
b.Property<string>("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<string>("UID")
.HasMaxLength(10)
.HasColumnType("character varying(10)")
.HasColumnName("uid");
b.Property<string>("Alias")
.HasMaxLength(10)
.HasColumnType("character varying(10)")
.HasColumnName("alias");
b.Property<bool>("IsAdmin")
.HasColumnType("boolean")
.HasColumnName("is_admin");
b.Property<bool>("IsModerator")
.HasColumnType("boolean")
.HasColumnName("is_moderator");
b.Property<DateTime>("LastLoggedIn")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_logged_in");
b.Property<byte[]>("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
}
}
}

View File

@@ -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<bool>(
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");
}
}
}

View File

@@ -171,6 +171,69 @@ namespace MareSynchronosServer.Migrations
b.ToTable("forbidden_upload_entries", (string)null);
});
modelBuilder.Entity("MareSynchronosShared.Models.Group", b =>
{
b.Property<string>("GID")
.HasMaxLength(20)
.HasColumnType("character varying(20)")
.HasColumnName("gid");
b.Property<string>("Alias")
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("alias");
b.Property<string>("HashedPassword")
.HasColumnType("text")
.HasColumnName("hashed_password");
b.Property<bool>("InvitesEnabled")
.HasColumnType("boolean")
.HasColumnName("invites_enabled");
b.Property<string>("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<string>("GroupGID")
.HasColumnType("character varying(20)")
.HasColumnName("group_gid");
b.Property<string>("GroupUserUID")
.HasColumnType("character varying(10)")
.HasColumnName("group_user_uid");
b.Property<bool>("IsPaused")
.HasColumnType("boolean")
.HasColumnName("is_paused");
b.Property<bool>("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<decimal>("DiscordId")
@@ -213,15 +276,10 @@ namespace MareSynchronosServer.Migrations
.HasColumnName("uid");
b.Property<string>("Alias")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasMaxLength(10)
.HasColumnType("character varying(10)")
.HasColumnName("alias");
b.Property<string>("CharacterIdentification")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("character_identification");
b.Property<bool>("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")

View File

@@ -1,14 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace MareSynchronosShared.Models
namespace MareSynchronosShared.Models;
public class Auth
{
public class Auth
{
[Key]
[MaxLength(64)]
public string HashedKey { get; set; }
public string UserUID { get; set; }
public User User { get; set; }
}
}

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
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; }
@@ -14,5 +14,4 @@ namespace MareSynchronosShared.Models
public bool AllowReceivingMessages { get; set; } = false;
[Timestamp]
public byte[] Timestamp { get; set; }
}
}

View File

@@ -1,9 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace MareSynchronosShared.Models
namespace MareSynchronosShared.Models;
public class FileCache
{
public class FileCache
{
[Key]
[MaxLength(40)]
public string Hash { get; set; }
@@ -13,5 +13,4 @@ namespace MareSynchronosShared.Models
public bool Uploaded { get; set; }
[Timestamp]
public byte[] Timestamp { get; set; }
}
}

View File

@@ -1,9 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace MareSynchronosShared.Models
namespace MareSynchronosShared.Models;
public class ForbiddenUploadEntry
{
public class ForbiddenUploadEntry
{
[Key]
[MaxLength(40)]
public string Hash { get; set; }
@@ -11,5 +11,4 @@ namespace MareSynchronosShared.Models
public string ForbiddenBy { get; set; }
[Timestamp]
public byte[] Timestamp { get; set; }
}
}

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace MareSynchronosShared.Models
namespace MareSynchronosShared.Models;
public class LodeStoneAuth
{
public class LodeStoneAuth
{
[Key]
public ulong DiscordId { get; set; }
[MaxLength(100)]
@@ -12,5 +12,4 @@ namespace MareSynchronosShared.Models
public string? LodestoneAuthString { get; set; }
public User? User { get; set; }
public DateTime? StartedAt { get; set; }
}
}

View File

@@ -1,14 +1,12 @@
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; }
@@ -19,5 +17,4 @@ namespace MareSynchronosShared.Models
public DateTime LastLoggedIn { get; set; }
[MaxLength(10)]
public string Alias { get; set; }
}
}

View File

@@ -12,40 +12,43 @@ public abstract class BaseClientIdentificationService : IClientIdentificationSer
this.metrics = metrics;
}
public virtual int GetOnlineUsers()
public virtual Task<int> GetOnlineUsers()
{
return OnlineClients.Count;
return Task.FromResult(OnlineClients.Count);
}
public string? GetUidForCharacterIdent(string characterIdent)
public Task<string?> GetUidForCharacterIdent(string characterIdent)
{
var result = OnlineClients.SingleOrDefault(u =>
string.Compare(u.Value, characterIdent, StringComparison.InvariantCultureIgnoreCase) == 0);
return result.Equals(new KeyValuePair<string, string>()) ? null : result.Key;
return Task.FromResult(result.Equals(new KeyValuePair<string, string>()) ? null : result.Key);
}
public virtual string? GetCharacterIdentForUid(string uid)
public virtual Task<string?> 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)

View File

@@ -21,14 +21,14 @@ public class DistributedClientIdentificationService : BaseClientIdentificationSe
this.configuration = configuration.GetSection("MareSynchronos");
}
public override int GetOnlineUsers()
public override async Task<int> GetOnlineUsers()
{
try
{
var redis = configuration.GetValue<string>("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<string?> 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)

View File

@@ -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<int> GetOnlineUsers();
Task<string?> GetUidForCharacterIdent(string characterIdent);
Task<string?> GetCharacterIdentForUid(string uid);
Task MarkUserOnline(string uid, string charaIdent);
Task MarkUserOffline(string uid);
}

View File

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

View File

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

View File

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