Compare commits

...

10 Commits

266 changed files with 1395 additions and 28584 deletions

Submodule MareAPI deleted from fa9b7bce43

View File

@@ -0,0 +1,36 @@
using MareSynchronos.API.Data.Enum;
using MessagePack;
using System.Text.Json.Serialization;
using System.Text.Json;
using System.Text;
using System.Security.Cryptography;
namespace MareSynchronos.API.Data;
[MessagePackObject(keyAsPropertyName: true)]
public class CharacterData
{
public CharacterData()
{
DataHash = new(() =>
{
var json = JsonSerializer.Serialize(this);
#pragma warning disable SYSLIB0021 // Type or member is obsolete
using SHA256CryptoServiceProvider cryptoProvider = new();
#pragma warning restore SYSLIB0021 // Type or member is obsolete
return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(json))).Replace("-", "", StringComparison.Ordinal);
});
}
public Dictionary<ObjectKind, string> CustomizePlusData { get; set; } = new();
[JsonIgnore]
public Lazy<string> DataHash { get; }
public Dictionary<ObjectKind, List<FileReplacementData>> FileReplacements { get; set; } = new();
public Dictionary<ObjectKind, string> GlamourerData { get; set; } = new();
public string HeelsData { get; set; } = string.Empty;
public string HonorificData { get; set; } = string.Empty;
public string ManipulationData { get; set; } = string.Empty;
public string MoodlesData { get; set; } = string.Empty;
public string PetNamesData { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,11 @@
using MessagePack;
namespace MareSynchronos.API.Data;
[MessagePackObject(keyAsPropertyName: true)]
public record ChatMessage
{
public string SenderName { get; set; } = string.Empty;
public uint SenderHomeWorldId { get; set; } = 0;
public byte[] PayloadContent { get; set; } = [];
}

View File

@@ -0,0 +1,19 @@
namespace MareSynchronos.API.Data.Comparer;
public class GroupDataComparer : IEqualityComparer<GroupData>
{
public static GroupDataComparer Instance => _instance;
private static GroupDataComparer _instance = new GroupDataComparer();
private GroupDataComparer() { }
public bool Equals(GroupData? x, GroupData? y)
{
if (x == null || y == null) return false;
return x.GID.Equals(y.GID, StringComparison.Ordinal);
}
public int GetHashCode(GroupData obj)
{
return obj.GID.GetHashCode();
}
}

View File

@@ -0,0 +1,23 @@
using MareSynchronos.API.Dto.Group;
namespace MareSynchronos.API.Data.Comparer;
public class GroupDtoComparer : IEqualityComparer<GroupDto>
{
public static GroupDtoComparer Instance => _instance;
private static GroupDtoComparer _instance = new GroupDtoComparer();
private GroupDtoComparer() { }
public bool Equals(GroupDto? x, GroupDto? y)
{
if (x == null || y == null) return false;
return x.GID.Equals(y.GID, StringComparison.Ordinal);
}
public int GetHashCode(GroupDto obj)
{
return obj.Group.GID.GetHashCode();
}
}

View File

@@ -0,0 +1,20 @@
using MareSynchronos.API.Dto.Group;
namespace MareSynchronos.API.Data.Comparer;
public class GroupPairDtoComparer : IEqualityComparer<GroupPairDto>
{
public static GroupPairDtoComparer Instance => _instance;
private static GroupPairDtoComparer _instance = new();
private GroupPairDtoComparer() { }
public bool Equals(GroupPairDto? x, GroupPairDto? y)
{
if (x == null || y == null) return false;
return x.GID.Equals(y.GID, StringComparison.Ordinal) && x.UID.Equals(y.UID, StringComparison.Ordinal);
}
public int GetHashCode(GroupPairDto obj)
{
return HashCode.Combine(obj.Group.GID.GetHashCode(), obj.User.UID.GetHashCode());
}
}

View File

@@ -0,0 +1,20 @@
namespace MareSynchronos.API.Data.Comparer;
public class UserDataComparer : IEqualityComparer<UserData>
{
public static UserDataComparer Instance => _instance;
private static UserDataComparer _instance = new();
private UserDataComparer() { }
public bool Equals(UserData? x, UserData? y)
{
if (x == null || y == null) return false;
return x.UID.Equals(y.UID, StringComparison.Ordinal);
}
public int GetHashCode(UserData obj)
{
return obj.UID.GetHashCode();
}
}

View File

@@ -0,0 +1,20 @@
using MareSynchronos.API.Dto.User;
namespace MareSynchronos.API.Data.Comparer;
public class UserDtoComparer : IEqualityComparer<UserDto>
{
public static UserDtoComparer Instance => _instance;
private static UserDtoComparer _instance = new();
private UserDtoComparer() { }
public bool Equals(UserDto? x, UserDto? y)
{
if (x == null || y == null) return false;
return x.User.UID.Equals(y.User.UID, StringComparison.Ordinal);
}
public int GetHashCode(UserDto obj)
{
return obj.User.UID.GetHashCode();
}
}

View File

@@ -0,0 +1,11 @@
namespace MareSynchronos.API.Data.Enum;
[Flags]
public enum GroupPermissions
{
NoneSet = 0x0,
DisableAnimations = 0x1,
DisableSounds = 0x2,
DisableInvites = 0x4,
DisableVFX = 0x8,
}

View File

@@ -0,0 +1,9 @@
namespace MareSynchronos.API.Data.Enum;
[Flags]
public enum GroupUserInfo
{
None = 0x0,
IsModerator = 0x2,
IsPinned = 0x4
}

View File

@@ -0,0 +1,11 @@
namespace MareSynchronos.API.Data.Enum;
[Flags]
public enum GroupUserPermissions
{
NoneSet = 0x0,
Paused = 0x1,
DisableAnimations = 0x2,
DisableSounds = 0x4,
DisableVFX = 0x8,
}

View File

@@ -0,0 +1,8 @@
namespace MareSynchronos.API.Data.Enum;
public enum MessageSeverity
{
Information,
Warning,
Error
}

View File

@@ -0,0 +1,9 @@
namespace MareSynchronos.API.Data.Enum;
public enum ObjectKind
{
Player = 0,
MinionOrMount = 1,
Companion = 2,
Pet = 3,
}

View File

@@ -0,0 +1,9 @@
namespace MareSynchronos.API.Data.Enum;
public enum TypingScope
{
Unknown = 0,
Proximity = 1, // Parler/Crier/Hurler
Party = 2,
CrossParty = 3
}

View File

@@ -0,0 +1,12 @@
namespace MareSynchronos.API.Data.Enum;
[Flags]
public enum UserPermissions
{
NoneSet = 0,
Paired = 1,
Paused = 2,
DisableAnimations = 4,
DisableSounds = 8,
DisableVFX = 16,
}

View File

@@ -0,0 +1,50 @@
using MareSynchronos.API.Data.Enum;
namespace MareSynchronos.API.Data.Extensions;
public static class GroupPermissionsExtensions
{
public static bool IsDisableAnimations(this GroupPermissions perm)
{
return perm.HasFlag(GroupPermissions.DisableAnimations);
}
public static bool IsDisableSounds(this GroupPermissions perm)
{
return perm.HasFlag(GroupPermissions.DisableSounds);
}
public static bool IsDisableInvites(this GroupPermissions perm)
{
return perm.HasFlag(GroupPermissions.DisableInvites);
}
public static bool IsDisableVFX(this GroupPermissions perm)
{
return perm.HasFlag(GroupPermissions.DisableVFX);
}
public static void SetDisableAnimations(this ref GroupPermissions perm, bool set)
{
if (set) perm |= GroupPermissions.DisableAnimations;
else perm &= ~GroupPermissions.DisableAnimations;
}
public static void SetDisableSounds(this ref GroupPermissions perm, bool set)
{
if (set) perm |= GroupPermissions.DisableSounds;
else perm &= ~GroupPermissions.DisableSounds;
}
public static void SetDisableInvites(this ref GroupPermissions perm, bool set)
{
if (set) perm |= GroupPermissions.DisableInvites;
else perm &= ~GroupPermissions.DisableInvites;
}
public static void SetDisableVFX(this ref GroupPermissions perm, bool set)
{
if (set) perm |= GroupPermissions.DisableVFX;
else perm &= ~GroupPermissions.DisableVFX;
}
}

View File

@@ -0,0 +1,28 @@
using MareSynchronos.API.Data.Enum;
namespace MareSynchronos.API.Data.Extensions;
public static class GroupUserInfoExtensions
{
public static bool IsModerator(this GroupUserInfo info)
{
return info.HasFlag(GroupUserInfo.IsModerator);
}
public static bool IsPinned(this GroupUserInfo info)
{
return info.HasFlag(GroupUserInfo.IsPinned);
}
public static void SetModerator(this ref GroupUserInfo info, bool isModerator)
{
if (isModerator) info |= GroupUserInfo.IsModerator;
else info &= ~GroupUserInfo.IsModerator;
}
public static void SetPinned(this ref GroupUserInfo info, bool isPinned)
{
if (isPinned) info |= GroupUserInfo.IsPinned;
else info &= ~GroupUserInfo.IsPinned;
}
}

View File

@@ -0,0 +1,50 @@
using MareSynchronos.API.Data.Enum;
namespace MareSynchronos.API.Data.Extensions;
public static class GroupUserPermissionsExtensions
{
public static bool IsDisableAnimations(this GroupUserPermissions perm)
{
return perm.HasFlag(GroupUserPermissions.DisableAnimations);
}
public static bool IsDisableSounds(this GroupUserPermissions perm)
{
return perm.HasFlag(GroupUserPermissions.DisableSounds);
}
public static bool IsPaused(this GroupUserPermissions perm)
{
return perm.HasFlag(GroupUserPermissions.Paused);
}
public static bool IsDisableVFX(this GroupUserPermissions perm)
{
return perm.HasFlag(GroupUserPermissions.DisableVFX);
}
public static void SetDisableAnimations(this ref GroupUserPermissions perm, bool set)
{
if (set) perm |= GroupUserPermissions.DisableAnimations;
else perm &= ~GroupUserPermissions.DisableAnimations;
}
public static void SetDisableSounds(this ref GroupUserPermissions perm, bool set)
{
if (set) perm |= GroupUserPermissions.DisableSounds;
else perm &= ~GroupUserPermissions.DisableSounds;
}
public static void SetPaused(this ref GroupUserPermissions perm, bool set)
{
if (set) perm |= GroupUserPermissions.Paused;
else perm &= ~GroupUserPermissions.Paused;
}
public static void SetDisableVFX(this ref GroupUserPermissions perm, bool set)
{
if (set) perm |= GroupUserPermissions.DisableVFX;
else perm &= ~GroupUserPermissions.DisableVFX;
}
}

View File

@@ -0,0 +1,61 @@
using MareSynchronos.API.Data.Enum;
namespace MareSynchronos.API.Data.Extensions;
public static class UserPermissionsExtensions
{
public static bool IsPaired(this UserPermissions perm)
{
return perm.HasFlag(UserPermissions.Paired);
}
public static bool IsPaused(this UserPermissions perm)
{
return perm.HasFlag(UserPermissions.Paused);
}
public static bool IsDisableAnimations(this UserPermissions perm)
{
return perm.HasFlag(UserPermissions.DisableAnimations);
}
public static bool IsDisableSounds(this UserPermissions perm)
{
return perm.HasFlag(UserPermissions.DisableSounds);
}
public static bool IsDisableVFX(this UserPermissions perm)
{
return perm.HasFlag(UserPermissions.DisableVFX);
}
public static void SetPaired(this ref UserPermissions perm, bool paired)
{
if (paired) perm |= UserPermissions.Paired;
else perm &= ~UserPermissions.Paired;
}
public static void SetPaused(this ref UserPermissions perm, bool paused)
{
if (paused) perm |= UserPermissions.Paused;
else perm &= ~UserPermissions.Paused;
}
public static void SetDisableAnimations(this ref UserPermissions perm, bool set)
{
if (set) perm |= UserPermissions.DisableAnimations;
else perm &= ~UserPermissions.DisableAnimations;
}
public static void SetDisableSounds(this ref UserPermissions perm, bool set)
{
if (set) perm |= UserPermissions.DisableSounds;
else perm &= ~UserPermissions.DisableSounds;
}
public static void SetDisableVFX(this ref UserPermissions perm, bool set)
{
if (set) perm |= UserPermissions.DisableVFX;
else perm &= ~UserPermissions.DisableVFX;
}
}

View File

@@ -0,0 +1,30 @@
using MessagePack;
using System.Text.Json.Serialization;
using System.Text.Json;
using System.Text;
using System.Security.Cryptography;
namespace MareSynchronos.API.Data;
[MessagePackObject(keyAsPropertyName: true)]
public class FileReplacementData
{
public FileReplacementData()
{
DataHash = new(() =>
{
var json = JsonSerializer.Serialize(this);
#pragma warning disable SYSLIB0021 // Type or member is obsolete
using SHA256CryptoServiceProvider cryptoProvider = new();
#pragma warning restore SYSLIB0021 // Type or member is obsolete
return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(json))).Replace("-", "", StringComparison.Ordinal);
});
}
[JsonIgnore]
public Lazy<string> DataHash { get; }
public string[] GamePaths { get; set; } = Array.Empty<string>();
public string Hash { get; set; } = string.Empty;
public string FileSwapPath { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,10 @@
using MessagePack;
namespace MareSynchronos.API.Data;
[MessagePackObject(keyAsPropertyName: true)]
public record GroupData(string GID, string? Alias = null)
{
[IgnoreMember]
public string AliasOrGID => string.IsNullOrWhiteSpace(Alias) ? GID : Alias;
}

View File

@@ -0,0 +1,14 @@
using MessagePack;
namespace MareSynchronos.API.Data;
[MessagePackObject(keyAsPropertyName: true)]
public record SignedChatMessage(ChatMessage Message, UserData Sender) : ChatMessage(Message)
{
// Sender and timestamp are set by the server
public UserData Sender { get; set; } = Sender;
public long Timestamp { get; set; } = 0;
// Signature is generated by the server as SHA256(Sender.UID | Timestamp | Destination | Message)
// Where Destination is either the receiver's UID, or the group GID
public string Signature { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,10 @@
using MessagePack;
namespace MareSynchronos.API.Data;
[MessagePackObject(keyAsPropertyName: true)]
public record UserData(string UID, string? Alias = null)
{
[IgnoreMember]
public string AliasOrUID => string.IsNullOrWhiteSpace(Alias) ? UID : Alias;
}

View File

@@ -0,0 +1,12 @@
using MessagePack;
namespace MareSynchronos.API.Dto.Account;
[MessagePackObject(keyAsPropertyName: true)]
public record RegisterReplyDto
{
public bool Success { get; set; } = false;
public string ErrorMessage { get; set; } = string.Empty;
public string UID { get; set; } = string.Empty;
public string SecretKey { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,11 @@
using MessagePack;
namespace MareSynchronos.API.Dto.Account;
[MessagePackObject(keyAsPropertyName: true)]
public record RegisterReplyV2Dto
{
public bool Success { get; set; } = false;
public string ErrorMessage { get; set; } = string.Empty;
public string UID { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,11 @@
using MareSynchronos.API.Data;
using MessagePack;
namespace MareSynchronos.API.Dto;
[MessagePackObject(keyAsPropertyName: true)]
public record AuthReplyDto
{
public string Token { get; set; } = string.Empty;
public string? WellKnown { get; set; }
}

View File

@@ -0,0 +1,9 @@
namespace MareSynchronos.API.Dto.CharaData;
public enum AccessTypeDto
{
Individuals,
ClosePairs,
AllPairs,
Public
}

View File

@@ -0,0 +1,14 @@
using MareSynchronos.API.Data;
using MessagePack;
namespace MareSynchronos.API.Dto.CharaData;
[MessagePackObject(keyAsPropertyName: true)]
public record CharaDataDownloadDto(string Id, UserData Uploader) : CharaDataDto(Id, Uploader)
{
public string GlamourerData { get; init; } = string.Empty;
public string CustomizeData { get; init; } = string.Empty;
public string ManipulationData { get; set; } = string.Empty;
public List<GamePathEntry> FileGamePaths { get; init; } = [];
public List<GamePathEntry> FileSwaps { get; init; } = [];
}

View File

@@ -0,0 +1,9 @@
using MareSynchronos.API.Data;
namespace MareSynchronos.API.Dto.CharaData;
public record CharaDataDto(string Id, UserData Uploader)
{
public string Description { get; init; } = string.Empty;
public DateTime UpdatedDate { get; init; }
}

View File

@@ -0,0 +1,88 @@
using MareSynchronos.API.Data;
using MessagePack;
namespace MareSynchronos.API.Dto.CharaData;
[MessagePackObject(keyAsPropertyName: true)]
public record CharaDataFullDto(string Id, UserData Uploader) : CharaDataDto(Id, Uploader)
{
public DateTime CreatedDate { get; init; }
public DateTime ExpiryDate { get; set; }
public string GlamourerData { get; set; } = string.Empty;
public string CustomizeData { get; set; } = string.Empty;
public string ManipulationData { get; set; } = string.Empty;
public int DownloadCount { get; set; } = 0;
public List<UserData> AllowedUsers { get; set; } = [];
public List<GroupData> AllowedGroups { get; set; } = [];
public List<GamePathEntry> FileGamePaths { get; set; } = [];
public List<GamePathEntry> FileSwaps { get; set; } = [];
public List<GamePathEntry> OriginalFiles { get; set; } = [];
public AccessTypeDto AccessType { get; set; }
public ShareTypeDto ShareType { get; set; }
public List<PoseEntry> PoseData { get; set; } = [];
}
[MessagePackObject(keyAsPropertyName: true)]
public record GamePathEntry(string HashOrFileSwap, string GamePath);
[MessagePackObject(keyAsPropertyName: true)]
public record PoseEntry(long? Id)
{
public string? Description { get; set; } = string.Empty;
public string? PoseData { get; set; } = string.Empty;
public WorldData? WorldData { get; set; }
}
[MessagePackObject]
public record struct WorldData
{
[Key(0)] public LocationInfo LocationInfo { get; set; }
[Key(1)] public float PositionX { get; set; }
[Key(2)] public float PositionY { get; set; }
[Key(3)] public float PositionZ { get; set; }
[Key(4)] public float RotationX { get; set; }
[Key(5)] public float RotationY { get; set; }
[Key(6)] public float RotationZ { get; set; }
[Key(7)] public float RotationW { get; set; }
[Key(8)] public float ScaleX { get; set; }
[Key(9)] public float ScaleY { get; set; }
[Key(10)] public float ScaleZ { get; set; }
}
[MessagePackObject]
public record struct LocationInfo
{
[Key(0)] public uint ServerId { get; set; }
[Key(1)] public uint MapId { get; set; }
[Key(2)] public uint TerritoryId { get; set; }
[Key(3)] public uint DivisionId { get; set; }
[Key(4)] public uint WardId { get; set; }
[Key(5)] public uint HouseId { get; set; }
[Key(6)] public uint RoomId { get; set; }
}
[MessagePackObject]
public record struct PoseData
{
[Key(0)] public bool IsDelta { get; set; }
[Key(1)] public Dictionary<string, BoneData> Bones { get; set; }
[Key(2)] public Dictionary<string, BoneData> MainHand { get; set; }
[Key(3)] public Dictionary<string, BoneData> OffHand { get; set; }
[Key(4)] public BoneData ModelDifference { get; set; }
}
[MessagePackObject]
public record struct BoneData
{
[Key(0)] public bool Exists { get; set; }
[Key(1)] public float PositionX { get; set; }
[Key(2)] public float PositionY { get; set; }
[Key(3)] public float PositionZ { get; set; }
[Key(4)] public float RotationX { get; set; }
[Key(5)] public float RotationY { get; set; }
[Key(6)] public float RotationZ { get; set; }
[Key(7)] public float RotationW { get; set; }
[Key(8)] public float ScaleX { get; set; }
[Key(9)] public float ScaleY { get; set; }
[Key(10)] public float ScaleZ { get; set; }
}

View File

@@ -0,0 +1,11 @@
using MareSynchronos.API.Data;
using MessagePack;
namespace MareSynchronos.API.Dto.CharaData;
[MessagePackObject(keyAsPropertyName: true)]
public record CharaDataMetaInfoDto(string Id, UserData Uploader) : CharaDataDto(Id, Uploader)
{
public bool CanBeDownloaded { get; init; }
public List<PoseEntry> PoseData { get; set; } = [];
}

View File

@@ -0,0 +1,20 @@
using MessagePack;
namespace MareSynchronos.API.Dto.CharaData;
[MessagePackObject(keyAsPropertyName: true)]
public record CharaDataUpdateDto(string Id)
{
public string? Description { get; set; }
public DateTime? ExpiryDate { get; set; }
public string? GlamourerData { get; set; }
public string? CustomizeData { get; set; }
public string? ManipulationData { get; set; }
public List<string>? AllowedUsers { get; set; }
public List<string>? AllowedGroups { get; set; }
public List<GamePathEntry>? FileGamePaths { get; set; }
public List<GamePathEntry>? FileSwaps { get; set; }
public AccessTypeDto? AccessType { get; set; }
public ShareTypeDto? ShareType { get; set; }
public List<PoseEntry>? Poses { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace MareSynchronos.API.Dto.CharaData;
public enum ShareTypeDto
{
Private,
Shared
}

View File

@@ -0,0 +1,13 @@
using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.Group;
using MareSynchronos.API.Dto.User;
using MessagePack;
namespace MareSynchronos.API.Dto.Chat;
[MessagePackObject(keyAsPropertyName: true)]
public record GroupChatMsgDto(GroupDto Group, SignedChatMessage Message)
{
public GroupDto Group = Group;
public SignedChatMessage Message = Message;
}

View File

@@ -0,0 +1,11 @@
using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.User;
using MessagePack;
namespace MareSynchronos.API.Dto.Chat;
[MessagePackObject(keyAsPropertyName: true)]
public record UserChatMsgDto(SignedChatMessage Message)
{
public SignedChatMessage Message = Message;
}

View File

@@ -0,0 +1,25 @@
using MareSynchronos.API.Data;
using MessagePack;
namespace MareSynchronos.API.Dto;
[MessagePackObject(keyAsPropertyName: true)]
public record ConnectionDto(UserData User)
{
public Version CurrentClientVersion { get; set; } = new(0, 0, 0);
public int ServerVersion { get; set; }
public bool IsAdmin { get; set; }
public bool IsModerator { get; set; }
public ServerInfo ServerInfo { get; set; } = new();
}
[MessagePackObject(keyAsPropertyName: true)]
public record ServerInfo
{
public string ShardName { get; set; } = string.Empty;
public int MaxGroupUserCount { get; set; }
public int MaxGroupsCreatedByUser { get; set; }
public int MaxGroupsJoinedByUser { get; set; }
public Uri FileServerAddress { get; set; } = new Uri("http://nonemptyuri");
public int MaxCharaData { get; set; }
}

View File

@@ -0,0 +1,14 @@
using MessagePack;
namespace MareSynchronos.API.Dto.Files;
[MessagePackObject(keyAsPropertyName: true)]
public record DownloadFileDto : ITransferFileDto
{
public bool FileExists { get; set; } = true;
public string Hash { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
public long Size { get; set; } = 0;
public bool IsForbidden { get; set; } = false;
public string ForbiddenBy { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MareSynchronos.API.Dto.Files;
public class FilesSendDto
{
public List<string> FileHashes { get; set; } = new();
public List<string> UIDs { get; set; } = new();
}

View File

@@ -0,0 +1,8 @@
namespace MareSynchronos.API.Dto.Files;
public interface ITransferFileDto
{
string Hash { get; set; }
bool IsForbidden { get; set; }
string ForbiddenBy { get; set; }
}

View File

@@ -0,0 +1,11 @@
using MessagePack;
namespace MareSynchronos.API.Dto.Files;
[MessagePackObject(keyAsPropertyName: true)]
public record UploadFileDto : ITransferFileDto
{
public string Hash { get; set; } = string.Empty;
public bool IsForbidden { get; set; } = false;
public string ForbiddenBy { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,19 @@
using MareSynchronos.API.Data;
using MessagePack;
namespace MareSynchronos.API.Dto.Group;
[MessagePackObject(keyAsPropertyName: true)]
public record BannedGroupUserDto : GroupPairDto
{
public BannedGroupUserDto(GroupData group, UserData user, string reason, DateTime bannedOn, string bannedBy) : base(group, user)
{
Reason = reason;
BannedOn = bannedOn;
BannedBy = bannedBy;
}
public string Reason { get; set; }
public DateTime BannedOn { get; set; }
public string BannedBy { get; set; }
}

View File

@@ -0,0 +1,13 @@
using MareSynchronos.API.Data;
using MessagePack;
namespace MareSynchronos.API.Dto.Group;
[MessagePackObject(keyAsPropertyName: true)]
public record GroupDto(GroupData Group)
{
public GroupData Group { get; set; } = Group;
public string GID => Group.GID;
public string? GroupAlias => Group.Alias;
public string GroupAliasOrGID => Group.AliasOrGID;
}

View File

@@ -0,0 +1,12 @@
using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum;
using MessagePack;
namespace MareSynchronos.API.Dto.Group;
[MessagePackObject(keyAsPropertyName: true)]
public record GroupFullInfoDto(GroupData Group, UserData Owner, GroupPermissions GroupPermissions, GroupUserPermissions GroupUserPermissions, GroupUserInfo GroupUserInfo) : GroupInfoDto(Group, Owner, GroupPermissions)
{
public GroupUserPermissions GroupUserPermissions { get; set; } = GroupUserPermissions;
public GroupUserInfo GroupUserInfo { get; set; } = GroupUserInfo;
}

View File

@@ -0,0 +1,22 @@
using System;
using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum;
using MessagePack;
namespace MareSynchronos.API.Dto.Group;
[MessagePackObject(keyAsPropertyName: true)]
public record GroupInfoDto(GroupData Group, UserData Owner, GroupPermissions GroupPermissions) : GroupDto(Group)
{
public GroupPermissions GroupPermissions { get; set; } = GroupPermissions;
public UserData Owner { get; set; } = Owner;
public bool AutoDetectVisible { get; set; }
public bool PasswordTemporarilyDisabled { get; set; }
public string OwnerUID => Owner.UID;
public string? OwnerAlias => Owner.Alias;
public string OwnerAliasOrUID => Owner.AliasOrUID;
public bool IsTemporary { get; set; }
public DateTime? ExpiresAt { get; set; }
}

View File

@@ -0,0 +1,12 @@
using MareSynchronos.API.Data;
using MessagePack;
namespace MareSynchronos.API.Dto.Group;
[MessagePackObject(keyAsPropertyName: true)]
public record GroupPairDto(GroupData Group, UserData User) : GroupDto(Group)
{
public string UID => User.UID;
public string? UserAlias => User.Alias;
public string UserAliasOrUID => User.AliasOrUID;
}

View File

@@ -0,0 +1,12 @@
using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum;
using MessagePack;
namespace MareSynchronos.API.Dto.Group;
[MessagePackObject(keyAsPropertyName: true)]
public record GroupPairFullInfoDto(GroupData Group, UserData User, GroupUserInfo GroupPairStatusInfo, GroupUserPermissions GroupUserPermissions) : GroupPairDto(Group, User)
{
public GroupUserInfo GroupPairStatusInfo { get; set; } = GroupPairStatusInfo;
public GroupUserPermissions GroupUserPermissions { get; set; } = GroupUserPermissions;
}

View File

@@ -0,0 +1,8 @@
using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum;
using MessagePack;
namespace MareSynchronos.API.Dto.Group;
[MessagePackObject(keyAsPropertyName: true)]
public record GroupPairUserInfoDto(GroupData Group, UserData User, GroupUserInfo GroupUserInfo) : GroupPairDto(Group, User);

View File

@@ -0,0 +1,8 @@
using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum;
using MessagePack;
namespace MareSynchronos.API.Dto.Group;
[MessagePackObject(keyAsPropertyName: true)]
public record GroupPairUserPermissionDto(GroupData Group, UserData User, GroupUserPermissions GroupPairPermissions) : GroupPairDto(Group, User);

View File

@@ -0,0 +1,12 @@
using System;
using MareSynchronos.API.Data;
using MessagePack;
namespace MareSynchronos.API.Dto.Group;
[MessagePackObject(keyAsPropertyName: true)]
public record GroupPasswordDto(GroupData Group, string Password) : GroupDto(Group)
{
public bool IsTemporary { get; set; }
public DateTime? ExpiresAt { get; set; }
}

View File

@@ -0,0 +1,8 @@
using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum;
using MessagePack;
namespace MareSynchronos.API.Dto.Group;
[MessagePackObject(keyAsPropertyName: true)]
public record GroupPermissionDto(GroupData Group, GroupPermissions Permissions) : GroupDto(Group);

View File

@@ -0,0 +1,15 @@
using MessagePack;
namespace MareSynchronos.API.Dto.Group;
[MessagePackObject(keyAsPropertyName: true)]
public sealed record SyncshellDiscoveryEntryDto
{
public string GID { get; init; } = string.Empty;
public string? Alias { get; init; }
public string OwnerUID { get; init; } = string.Empty;
public string? OwnerAlias { get; init; }
public int MemberCount { get; init; }
public bool AutoAcceptPairs { get; init; }
public string? Description { get; init; }
}

View File

@@ -0,0 +1,11 @@
using MessagePack;
namespace MareSynchronos.API.Dto.Group;
[MessagePackObject(keyAsPropertyName: true)]
public sealed record SyncshellDiscoveryStateDto
{
public string GID { get; init; } = string.Empty;
public bool AutoDetectVisible { get; init; }
public bool PasswordTemporarilyDisabled { get; init; }
}

View File

@@ -0,0 +1,16 @@
using MessagePack;
namespace MareSynchronos.API.Dto.Group;
[MessagePackObject(keyAsPropertyName: true)]
public sealed record SyncshellDiscoveryVisibilityRequestDto
{
public string GID { get; init; } = string.Empty;
public bool AutoDetectVisible { get; init; }
public int? DisplayDurationHours { get; init; }
public int[]? ActiveWeekdays { get; init; }
public string? TimeStartLocal { get; init; }
public string? TimeEndLocal { get; init; }
public string? TimeZone { get; init; }
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using MessagePack;
namespace MareSynchronos.API.Dto.McdfShare;
[MessagePackObject]
public class McdfShareEntryDto
{
[Key(0)] public Guid Id { get; set; }
[Key(1)] public string Description { get; set; } = string.Empty;
[Key(2)] public DateTime CreatedUtc { get; set; }
[Key(3)] public DateTime? UpdatedUtc { get; set; }
[Key(4)] public DateTime? ExpiresAtUtc { get; set; }
[Key(5)] public bool IsOwner { get; set; }
[Key(6)] public string OwnerUid { get; set; } = string.Empty;
[Key(7)] public string OwnerAlias { get; set; } = string.Empty;
[Key(8)] public int DownloadCount { get; set; }
[Key(9)] public List<string> AllowedIndividuals { get; set; } = [];
[Key(10)] public List<string> AllowedSyncshells { get; set; } = [];
}

View File

@@ -0,0 +1,17 @@
using System;
using MessagePack;
namespace MareSynchronos.API.Dto.McdfShare;
[MessagePackObject]
public class McdfSharePayloadDto
{
[Key(0)] public Guid ShareId { get; set; }
[Key(1)] public string Description { get; set; } = string.Empty;
[Key(2)] public byte[] CipherData { get; set; } = Array.Empty<byte>();
[Key(3)] public byte[] Nonce { get; set; } = Array.Empty<byte>();
[Key(4)] public byte[] Salt { get; set; } = Array.Empty<byte>();
[Key(5)] public byte[] Tag { get; set; } = Array.Empty<byte>();
[Key(6)] public DateTime CreatedUtc { get; set; }
[Key(7)] public DateTime? ExpiresAtUtc { get; set; }
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using MessagePack;
namespace MareSynchronos.API.Dto.McdfShare;
[MessagePackObject]
public class McdfShareUpdateRequestDto
{
[Key(0)] public Guid ShareId { get; set; }
[Key(1)] public string Description { get; set; } = string.Empty;
[Key(2)] public DateTime? ExpiresAtUtc { get; set; }
[Key(3)] public List<string> AllowedIndividuals { get; set; } = [];
[Key(4)] public List<string> AllowedSyncshells { get; set; } = [];
}

View File

@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using MessagePack;
namespace MareSynchronos.API.Dto.McdfShare;
[MessagePackObject]
public class McdfShareUploadRequestDto
{
[Key(0)] public Guid ShareId { get; set; }
[Key(1)] public string Description { get; set; } = string.Empty;
[Key(2)] public byte[] CipherData { get; set; } = Array.Empty<byte>();
[Key(3)] public byte[] Nonce { get; set; } = Array.Empty<byte>();
[Key(4)] public byte[] Salt { get; set; } = Array.Empty<byte>();
[Key(5)] public byte[] Tag { get; set; } = Array.Empty<byte>();
[Key(6)] public DateTime? ExpiresAtUtc { get; set; }
[Key(7)] public List<string> AllowedIndividuals { get; set; } = [];
[Key(8)] public List<string> AllowedSyncshells { get; set; } = [];
}

View File

@@ -0,0 +1,10 @@
using MessagePack;
namespace MareSynchronos.API.Dto;
[MessagePackObject(keyAsPropertyName: true)]
public record SystemInfoDto
{
public int OnlineUsers { get; set; }
public bool SupportsTypingState { get; set; }
}

View File

@@ -0,0 +1,7 @@
using MareSynchronos.API.Data;
using MessagePack;
namespace MareSynchronos.API.Dto.User;
[MessagePackObject(keyAsPropertyName: true)]
public record OnlineUserCharaDataDto(UserData User, CharacterData CharaData) : UserDto(User);

View File

@@ -0,0 +1,7 @@
using MareSynchronos.API.Data;
using MessagePack;
namespace MareSynchronos.API.Dto.User;
[MessagePackObject(keyAsPropertyName: true)]
public record OnlineUserIdentDto(UserData User, string Ident) : UserDto(User);

View File

@@ -0,0 +1,13 @@
using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum;
using MessagePack;
namespace MareSynchronos.API.Dto.User;
[MessagePackObject(keyAsPropertyName: true)]
public record TypingStateDto(UserData User, bool IsTyping, TypingScope Scope)
{
public UserData User { get; set; } = User;
public bool IsTyping { get; set; } = IsTyping;
public TypingScope Scope { get; set; } = Scope;
}

View File

@@ -0,0 +1,7 @@
using MareSynchronos.API.Data;
using MessagePack;
namespace MareSynchronos.API.Dto.User;
[MessagePackObject(keyAsPropertyName: true)]
public record UserCharaDataMessageDto(List<UserData> Recipients, CharacterData CharaData);

View File

@@ -0,0 +1,7 @@
using MareSynchronos.API.Data;
using MessagePack;
namespace MareSynchronos.API.Dto.User;
[MessagePackObject(keyAsPropertyName: true)]
public record UserDto(UserData User);

View File

@@ -0,0 +1,12 @@
using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum;
using MessagePack;
namespace MareSynchronos.API.Dto.User;
[MessagePackObject(keyAsPropertyName: true)]
public record UserPairDto(UserData User, UserPermissions OwnPermissions, UserPermissions OtherPermissions) : UserDto(User)
{
public UserPermissions OwnPermissions { get; set; } = OwnPermissions;
public UserPermissions OtherPermissions { get; set; } = OtherPermissions;
}

View File

@@ -0,0 +1,8 @@
using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum;
using MessagePack;
namespace MareSynchronos.API.Dto.User;
[MessagePackObject(keyAsPropertyName: true)]
public record UserPermissionsDto(UserData User, UserPermissions Permissions) : UserDto(User);

View File

@@ -0,0 +1,7 @@
using MareSynchronos.API.Data;
using MessagePack;
namespace MareSynchronos.API.Dto.User;
[MessagePackObject(keyAsPropertyName: true)]
public record UserProfileDto(UserData User, bool Disabled, bool? IsNSFW, string? ProfilePictureBase64, string? Description) : UserDto(User);

View File

@@ -0,0 +1,7 @@
using MareSynchronos.API.Data;
using MessagePack;
namespace MareSynchronos.API.Dto.User;
[MessagePackObject(keyAsPropertyName: true)]
public record UserProfileReportDto(UserData User, string ProfileReport) : UserDto(User);

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);NU1900</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MessagePack.Annotations" Version="2.5.129" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.2.32602.215
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronos.API", "MareSynchronos.API.csproj", "{CD05EE19-802F-4490-AAD8-CAD4BF1D630D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{CD05EE19-802F-4490-AAD8-CAD4BF1D630D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CD05EE19-802F-4490-AAD8-CAD4BF1D630D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CD05EE19-802F-4490-AAD8-CAD4BF1D630D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CD05EE19-802F-4490-AAD8-CAD4BF1D630D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DFB70C71-AB27-468D-A08B-218CA79BF69D}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,14 @@
namespace MareSynchronos.API.Routes;
public class MareAuth
{
public const string Auth = "/auth";
public const string Auth_CreateIdent = "createWithIdent";
public const string Auth_CreateIdentV2 = "createWithIdentV2";
public const string Auth_Register = "registerNewKey";
public const string Auth_RegisterV2 = "registerNewKeyV2";
public static Uri AuthFullPath(Uri baseUri) => new Uri(baseUri, Auth + "/" + Auth_CreateIdent);
public static Uri AuthV2FullPath(Uri baseUri) => new Uri(baseUri, Auth + "/" + Auth_CreateIdentV2);
public static Uri AuthRegisterFullPath(Uri baseUri) => new Uri(baseUri, Auth + "/" + Auth_Register);
public static Uri AuthRegisterV2FullPath(Uri baseUri) => new Uri(baseUri, Auth + "/" + Auth_RegisterV2);
}

View File

@@ -0,0 +1,45 @@
namespace MareSynchronos.API.Routes;
public class MareFiles
{
public const string Cache = "/cache";
public const string Cache_Get = "get";
public const string Request = "/request";
public const string Request_Cancel = "cancel";
public const string Request_Check = "check";
public const string Request_Enqueue = "enqueue";
public const string Request_RequestFile = "file";
public const string ServerFiles = "/files";
public const string ServerFiles_DeleteAll = "deleteAll";
public const string ServerFiles_FilesSend = "filesSend";
public const string ServerFiles_GetSizes = "getFileSizes";
public const string ServerFiles_Upload = "upload";
public const string ServerFiles_UploadRaw = "uploadRaw";
public const string ServerFiles_UploadMunged = "uploadMunged";
public const string Distribution = "/dist";
public const string Distribution_Get = "get";
public const string Main = "/main";
public const string Main_SendReady = "sendReady";
public static Uri CacheGetFullPath(Uri baseUri, Guid requestId) => new(baseUri, Cache + "/" + Cache_Get + "?requestId=" + requestId.ToString());
public static Uri RequestCancelFullPath(Uri baseUri, Guid guid) => new Uri(baseUri, Request + "/" + Request_Cancel + "?requestId=" + guid.ToString());
public static Uri RequestCheckQueueFullPath(Uri baseUri, Guid guid) => new Uri(baseUri, Request + "/" + Request_Check + "?requestId=" + guid.ToString());
public static Uri RequestEnqueueFullPath(Uri baseUri) => new(baseUri, Request + "/" + Request_Enqueue);
public static Uri RequestRequestFileFullPath(Uri baseUri, string hash) => new(baseUri, Request + "/" + Request_RequestFile + "?file=" + hash);
public static Uri ServerFilesDeleteAllFullPath(Uri baseUri) => new(baseUri, ServerFiles + "/" + ServerFiles_DeleteAll);
public static Uri ServerFilesFilesSendFullPath(Uri baseUri) => new(baseUri, ServerFiles + "/" + ServerFiles_FilesSend);
public static Uri ServerFilesGetSizesFullPath(Uri baseUri) => new(baseUri, ServerFiles + "/" + ServerFiles_GetSizes);
public static Uri ServerFilesUploadFullPath(Uri baseUri, string hash) => new(baseUri, ServerFiles + "/" + ServerFiles_Upload + "/" + hash);
public static Uri ServerFilesUploadRawFullPath(Uri baseUri, string hash) => new(baseUri, ServerFiles + "/" + ServerFiles_UploadRaw + "/" + hash);
public static Uri ServerFilesUploadMunged(Uri baseUri, string hash) => new(baseUri, ServerFiles + "/" + ServerFiles_UploadMunged + "/" + hash);
public static Uri DistributionGetFullPath(Uri baseUri, string hash) => new(baseUri, Distribution + "/" + Distribution_Get + "?file=" + hash);
public static Uri MainSendReadyFullPath(Uri baseUri, string uid, Guid request) => new(baseUri, Main + "/" + Main_SendReady + "/" + "?uid=" + uid + "&requestId=" + request.ToString());
}

View File

@@ -0,0 +1,150 @@
using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.API.Dto;
using MareSynchronos.API.Dto.CharaData;
using MareSynchronos.API.Dto.Chat;
using MareSynchronos.API.Dto.Group;
using MareSynchronos.API.Dto.User;
namespace MareSynchronos.API.SignalR;
public interface IMareHub
{
const int ApiVersion = 1029;
const string Path = "/mare";
Task<bool> CheckClientHealth();
Task Client_DownloadReady(Guid requestId);
Task Client_GroupChangePermissions(GroupPermissionDto groupPermission);
Task Client_GroupChatMsg(GroupChatMsgDto groupChatMsgDto);
Task Client_GroupDelete(GroupDto groupDto);
Task Client_GroupPairChangePermissions(GroupPairUserPermissionDto permissionDto);
Task Client_GroupPairChangeUserInfo(GroupPairUserInfoDto userInfo);
Task Client_GroupPairJoined(GroupPairFullInfoDto groupPairInfoDto);
Task Client_GroupPairLeft(GroupPairDto groupPairDto);
Task Client_GroupSendFullInfo(GroupFullInfoDto groupInfo);
Task Client_GroupSendInfo(GroupInfoDto groupInfo);
Task Client_ReceiveServerMessage(MessageSeverity messageSeverity, string message);
Task Client_UpdateSystemInfo(SystemInfoDto systemInfo);
Task Client_UserAddClientPair(UserPairDto dto);
Task Client_UserChatMsg(UserChatMsgDto chatMsgDto);
Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dataDto);
Task Client_UserReceiveUploadStatus(UserDto dto);
Task Client_UserRemoveClientPair(UserDto dto);
Task Client_UserSendOffline(UserDto dto);
Task Client_UserSendOnline(OnlineUserIdentDto dto);
Task Client_UserUpdateOtherPairPermissions(UserPermissionsDto dto);
Task Client_UserUpdateProfile(UserDto dto);
Task Client_UserUpdateSelfPairPermissions(UserPermissionsDto dto);
Task Client_UserTypingState(TypingStateDto dto);
Task Client_GposeLobbyJoin(UserData userData);
Task Client_GposeLobbyLeave(UserData userData);
Task Client_GposeLobbyPushCharacterData(CharaDataDownloadDto charaDownloadDto);
Task Client_GposeLobbyPushPoseData(UserData userData, PoseData poseData);
Task Client_GposeLobbyPushWorldData(UserData userData, WorldData worldData);
Task<ConnectionDto> GetConnectionDto();
Task GroupBanUser(GroupPairDto dto, string reason);
Task GroupChangeGroupPermissionState(GroupPermissionDto dto);
Task GroupChangeIndividualPermissionState(GroupPairUserPermissionDto dto);
Task GroupChangeOwnership(GroupPairDto groupPair);
Task<bool> GroupChangePassword(GroupPasswordDto groupPassword);
Task GroupChatSendMsg(GroupDto group, ChatMessage message);
Task GroupClear(GroupDto group);
Task<GroupPasswordDto> GroupCreate(string? alias);
Task<GroupPasswordDto> GroupCreateTemporary(DateTime expiresAtUtc);
Task<List<string>> GroupCreateTempInvite(GroupDto group, int amount);
Task GroupDelete(GroupDto group);
Task<List<BannedGroupUserDto>> GroupGetBannedUsers(GroupDto group);
Task<bool> GroupJoin(GroupPasswordDto passwordedGroup);
Task GroupLeave(GroupDto group);
Task GroupRemoveUser(GroupPairDto groupPair);
Task GroupSetUserInfo(GroupPairUserInfoDto groupPair);
Task<List<GroupFullInfoDto>> GroupsGetAll();
Task<List<GroupPairFullInfoDto>> GroupsGetUsersInGroup(GroupDto group);
Task GroupUnbanUser(GroupPairDto groupPair);
Task<int> GroupPrune(GroupDto group, int days, bool execute);
Task UserAddPair(UserDto user);
Task UserChatSendMsg(UserDto user, ChatMessage message);
Task UserDelete();
Task<List<OnlineUserIdentDto>> UserGetOnlinePairs();
Task<List<UserPairDto>> UserGetPairedClients();
Task<UserProfileDto> UserGetProfile(UserDto dto);
Task UserPushData(UserCharaDataMessageDto dto);
Task UserRemovePair(UserDto userDto);
Task UserReportProfile(UserProfileReportDto userDto);
Task UserSetPairPermissions(UserPermissionsDto userPermissions);
Task UserSetProfile(UserProfileDto userDescription);
Task UserSetTypingState(bool isTyping);
Task UserSetTypingState(bool isTyping, TypingScope scope);
Task<CharaDataFullDto?> CharaDataCreate();
Task<CharaDataFullDto?> CharaDataUpdate(CharaDataUpdateDto updateDto);
Task<bool> CharaDataDelete(string id);
Task<CharaDataMetaInfoDto?> CharaDataGetMetainfo(string id);
Task<CharaDataDownloadDto?> CharaDataDownload(string id);
Task<List<CharaDataFullDto>> CharaDataGetOwn();
Task<List<CharaDataMetaInfoDto>> CharaDataGetShared();
Task<CharaDataFullDto?> CharaDataAttemptRestore(string id);
Task<string> GposeLobbyCreate();
Task<List<UserData>> GposeLobbyJoin(string lobbyId);
Task<bool> GposeLobbyLeave();
Task GposeLobbyPushCharacterData(CharaDataDownloadDto charaDownloadDto);
Task GposeLobbyPushPoseData(PoseData poseData);
Task GposeLobbyPushWorldData(WorldData worldData);
}

View File

@@ -0,0 +1,64 @@
using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.API.Dto;
using MareSynchronos.API.Dto.CharaData;
using MareSynchronos.API.Dto.Chat;
using MareSynchronos.API.Dto.Group;
using MareSynchronos.API.Dto.User;
namespace MareSynchronos.API.SignalR;
public interface IMareHubClient : IMareHub
{
void OnDownloadReady(Action<Guid> act);
void OnGroupChangePermissions(Action<GroupPermissionDto> act);
void OnGroupChatMsg(Action<GroupChatMsgDto> groupChatMsgDto);
void OnGroupDelete(Action<GroupDto> act);
void OnGroupPairChangePermissions(Action<GroupPairUserPermissionDto> act);
void OnGroupPairChangeUserInfo(Action<GroupPairUserInfoDto> act);
void OnGroupPairJoined(Action<GroupPairFullInfoDto> act);
void OnGroupPairLeft(Action<GroupPairDto> act);
void OnGroupSendFullInfo(Action<GroupFullInfoDto> act);
void OnGroupSendInfo(Action<GroupInfoDto> act);
void OnReceiveServerMessage(Action<MessageSeverity, string> act);
void OnUpdateSystemInfo(Action<SystemInfoDto> act);
void OnUserAddClientPair(Action<UserPairDto> act);
void OnUserChatMsg(Action<UserChatMsgDto> chatMsgDto);
void OnUserTypingState(Action<TypingStateDto> act);
void OnUserReceiveCharacterData(Action<OnlineUserCharaDataDto> act);
void OnUserReceiveUploadStatus(Action<UserDto> act);
void OnUserRemoveClientPair(Action<UserDto> act);
void OnUserSendOffline(Action<UserDto> act);
void OnUserSendOnline(Action<OnlineUserIdentDto> act);
void OnUserUpdateOtherPairPermissions(Action<UserPermissionsDto> act);
void OnUserUpdateProfile(Action<UserDto> act);
void OnUserUpdateSelfPairPermissions(Action<UserPermissionsDto> act);
void OnGposeLobbyJoin(Action<UserData> act);
void OnGposeLobbyLeave(Action<UserData> act);
void OnGposeLobbyPushCharacterData(Action<CharaDataDownloadDto> act);
void OnGposeLobbyPushPoseData(Action<UserData, PoseData> act);
void OnGposeLobbyPushWorldData(Action<UserData, WorldData> act);
}

View File

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

View File

@@ -1,3 +0,0 @@
namespace MareSynchronosAuthService.Authentication;
public record SecretKeyAuthReply(bool Success, string Uid, string Alias, bool TempBan, bool Permaban);

View File

@@ -1,12 +0,0 @@
namespace MareSynchronosAuthService.Authentication;
internal record SecretKeyFailedAuthorization
{
private int failedAttempts = 1;
public int FailedAttempts => failedAttempts;
public Task ResetTask { get; set; }
public void IncreaseFailedAttempts()
{
Interlocked.Increment(ref failedAttempts);
}
}

View File

@@ -1,174 +0,0 @@
using System.Text.Json.Serialization;
using MareSynchronosAuthService.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace MareSynchronosAuthService.Controllers;
[Authorize]
[ApiController]
[Route("discovery")]
public class DiscoveryController : Controller
{
private readonly DiscoveryWellKnownProvider _provider;
private readonly DiscoveryPresenceService _presence;
public DiscoveryController(DiscoveryWellKnownProvider provider, DiscoveryPresenceService presence)
{
_provider = provider;
_presence = presence;
}
public sealed class QueryRequest
{
[JsonPropertyName("hashes")] public string[] Hashes { get; set; } = Array.Empty<string>();
[JsonPropertyName("salt")] public string SaltB64 { get; set; } = string.Empty;
}
public sealed class QueryResponseEntry
{
[JsonPropertyName("hash")] public string Hash { get; set; } = string.Empty;
[JsonPropertyName("token")] public string? Token { get; set; }
[JsonPropertyName("uid")] public string Uid { get; set; } = string.Empty;
[JsonPropertyName("displayName")] public string? DisplayName { get; set; }
}
[HttpPost("query")]
public IActionResult Query([FromBody] QueryRequest req)
{
if (_provider.IsExpired(req.SaltB64))
{
return BadRequest(new { code = "DISCOVERY_SALT_EXPIRED" });
}
var uid = User?.Claims?.FirstOrDefault(c => c.Type == MareSynchronosShared.Utils.MareClaimTypes.Uid)?.Value ?? string.Empty;
if (string.IsNullOrEmpty(uid) || req?.Hashes == null || req.Hashes.Length == 0)
return Json(Array.Empty<QueryResponseEntry>());
List<QueryResponseEntry> matches = new();
foreach (var h in req.Hashes.Distinct(StringComparer.Ordinal))
{
var (found, token, targetUid, displayName) = _presence.TryMatchAndIssueToken(uid, h);
if (found)
{
matches.Add(new QueryResponseEntry { Hash = h, Token = token, Uid = targetUid, DisplayName = displayName });
}
}
return Json(matches);
}
public sealed class RequestDto
{
[JsonPropertyName("token")] public string Token { get; set; } = string.Empty;
[JsonPropertyName("displayName")] public string? DisplayName { get; set; }
}
[HttpPost("request")]
public async Task<IActionResult> RequestPair([FromBody] RequestDto req)
{
if (string.IsNullOrEmpty(req.Token)) return BadRequest();
if (_presence.ValidateToken(req.Token, out var targetUid))
{
// Phase 3 (minimal): notify target via mare-server internal controller
try
{
var fromUid = User?.Claims?.FirstOrDefault(c => c.Type == MareSynchronosShared.Utils.MareClaimTypes.Uid)?.Value ?? string.Empty;
var fromAlias = string.IsNullOrEmpty(req.DisplayName)
? (User?.Claims?.FirstOrDefault(c => c.Type == MareSynchronosShared.Utils.MareClaimTypes.Alias)?.Value ?? string.Empty)
: req.DisplayName;
using var http = new HttpClient();
// Use same host as public (goes through nginx)
var baseUrl = $"{Request.Scheme}://{Request.Host.Value}";
var url = new Uri(new Uri(baseUrl), "/main/discovery/notifyRequest");
// Generate internal JWT
var serverToken = HttpContext.RequestServices.GetRequiredService<MareSynchronosShared.Utils.ServerTokenGenerator>().Token;
http.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", serverToken);
var payload = System.Text.Json.JsonSerializer.Serialize(new { targetUid, fromUid, fromAlias });
var resp = await http.PostAsync(url, new StringContent(payload, System.Text.Encoding.UTF8, "application/json"));
if (!resp.IsSuccessStatusCode)
{
var txt = await resp.Content.ReadAsStringAsync();
HttpContext.RequestServices.GetRequiredService<ILogger<DiscoveryController>>()
.LogWarning("notifyRequest failed: {code} {reason} {body}", (int)resp.StatusCode, resp.ReasonPhrase, txt);
}
}
catch { /* ignore */ }
return Accepted();
}
return BadRequest(new { code = "INVALID_TOKEN" });
}
public sealed class AcceptNotifyDto
{
[JsonPropertyName("targetUid")] public string TargetUid { get; set; } = string.Empty;
[JsonPropertyName("displayName")] public string? DisplayName { get; set; }
}
// Accept notification relay (sender -> auth -> main)
[HttpPost("acceptNotify")]
public async Task<IActionResult> AcceptNotify([FromBody] AcceptNotifyDto req)
{
if (string.IsNullOrEmpty(req.TargetUid)) return BadRequest();
try
{
var fromUid = User?.Claims?.FirstOrDefault(c => c.Type == MareSynchronosShared.Utils.MareClaimTypes.Uid)?.Value ?? string.Empty;
var fromAlias = string.IsNullOrEmpty(req.DisplayName)
? (User?.Claims?.FirstOrDefault(c => c.Type == MareSynchronosShared.Utils.MareClaimTypes.Alias)?.Value ?? string.Empty)
: req.DisplayName;
using var http = new HttpClient();
var baseUrl = $"{Request.Scheme}://{Request.Host.Value}";
var url = new Uri(new Uri(baseUrl), "/main/discovery/notifyAccept");
var serverToken = HttpContext.RequestServices.GetRequiredService<MareSynchronosShared.Utils.ServerTokenGenerator>().Token;
http.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", serverToken);
var payload = System.Text.Json.JsonSerializer.Serialize(new { targetUid = req.TargetUid, fromUid, fromAlias });
var resp = await http.PostAsync(url, new StringContent(payload, System.Text.Encoding.UTF8, "application/json"));
if (!resp.IsSuccessStatusCode)
{
var txt = await resp.Content.ReadAsStringAsync();
HttpContext.RequestServices.GetRequiredService<ILogger<DiscoveryController>>()
.LogWarning("notifyAccept failed: {code} {reason} {body}", (int)resp.StatusCode, resp.ReasonPhrase, txt);
}
}
catch { /* ignore */ }
return Accepted();
}
public sealed class PublishRequest
{
[JsonPropertyName("hashes")] public string[] Hashes { get; set; } = Array.Empty<string>();
[JsonPropertyName("displayName")] public string? DisplayName { get; set; }
[JsonPropertyName("salt")] public string SaltB64 { get; set; } = string.Empty;
[JsonPropertyName("allowRequests")] public bool AllowRequests { get; set; } = true;
}
[HttpPost("disable")]
public IActionResult Disable()
{
var uid = User?.Claims?.FirstOrDefault(c => c.Type == MareSynchronosShared.Utils.MareClaimTypes.Uid)?.Value ?? string.Empty;
if (string.IsNullOrEmpty(uid)) return Accepted();
_presence.Unpublish(uid);
return Accepted();
}
[HttpPost("publish")]
public IActionResult Publish([FromBody] PublishRequest req)
{
if (_provider.IsExpired(req.SaltB64))
{
return BadRequest(new { code = "DISCOVERY_SALT_EXPIRED" });
}
var uid = User?.Claims?.FirstOrDefault(c => c.Type == MareSynchronosShared.Utils.MareClaimTypes.Uid)?.Value ?? string.Empty;
if (string.IsNullOrEmpty(uid) || req?.Hashes == null || req.Hashes.Length == 0)
return Accepted();
_presence.Publish(uid, req.Hashes, req.DisplayName, req.AllowRequests);
return Accepted();
}
}

View File

@@ -1,196 +0,0 @@
using MareSynchronos.API.Dto;
using MareSynchronos.API.Dto.Account;
using MareSynchronos.API.Routes;
using MareSynchronosAuthService.Services;
using MareSynchronosShared;
using MareSynchronosShared.Data;
using MareSynchronosShared.Models;
using MareSynchronosShared.Services;
using MareSynchronosShared.Utils;
using MareSynchronosShared.Utils.Configuration;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using StackExchange.Redis.Extensions.Core.Abstractions;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace MareSynchronosAuthService.Controllers;
[AllowAnonymous]
[Route(MareAuth.Auth)]
public class JwtController : Controller
{
private readonly IHttpContextAccessor _accessor;
private readonly IRedisDatabase _redis;
private readonly IDbContextFactory<MareDbContext> _mareDbContextFactory;
private readonly GeoIPService _geoIPProvider;
private readonly SecretKeyAuthenticatorService _secretKeyAuthenticatorService;
private readonly AccountRegistrationService _accountRegistrationService;
private readonly IConfigurationService<AuthServiceConfiguration> _configuration;
public JwtController(ILogger<JwtController> logger,
IHttpContextAccessor accessor, IDbContextFactory<MareDbContext> mareDbContextFactory,
SecretKeyAuthenticatorService secretKeyAuthenticatorService,
AccountRegistrationService accountRegistrationService,
IConfigurationService<AuthServiceConfiguration> configuration,
IRedisDatabase redisDb, GeoIPService geoIPProvider)
{
_accessor = accessor;
_redis = redisDb;
_geoIPProvider = geoIPProvider;
_mareDbContextFactory = mareDbContextFactory;
_secretKeyAuthenticatorService = secretKeyAuthenticatorService;
_accountRegistrationService = accountRegistrationService;
_configuration = configuration;
}
[AllowAnonymous]
[HttpPost(MareAuth.Auth_CreateIdent)]
public async Task<IActionResult> CreateToken(string auth, string charaIdent)
{
if (string.IsNullOrEmpty(auth)) return BadRequest("No Authkey");
if (string.IsNullOrEmpty(charaIdent)) return BadRequest("No CharaIdent");
using var dbContext = await _mareDbContextFactory.CreateDbContextAsync();
var ip = _accessor.GetIpAddress();
var authResult = await _secretKeyAuthenticatorService.AuthorizeAsync(ip, auth);
var isBanned = await dbContext.BannedUsers.AsNoTracking().AnyAsync(u => u.CharacterIdentification == charaIdent).ConfigureAwait(false);
if (isBanned)
{
var authToBan = dbContext.Auth.SingleOrDefault(a => a.UserUID == authResult.Uid);
if (authToBan != null)
{
authToBan.IsBanned = true;
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
return Unauthorized("Your character is banned from using the service.");
}
if (!authResult.Success && !authResult.TempBan) return Unauthorized("The provided secret key is invalid. Verify your accounts existence and/or recover the secret key.");
if (!authResult.Success && authResult.TempBan) return Unauthorized("Due to an excessive amount of failed authentication attempts you are temporarily banned. Check your Secret Key configuration and try connecting again in 5 minutes.");
if (authResult.Permaban)
{
if (!dbContext.BannedUsers.Any(c => c.CharacterIdentification == charaIdent))
{
dbContext.BannedUsers.Add(new Banned()
{
CharacterIdentification = charaIdent,
Reason = "Autobanned CharacterIdent (" + authResult.Uid + ")",
});
await dbContext.SaveChangesAsync();
}
var lodestone = await dbContext.LodeStoneAuth.Include(a => a.User).FirstOrDefaultAsync(c => c.User.UID == authResult.Uid);
if (lodestone != null)
{
if (!dbContext.BannedRegistrations.Any(c => c.DiscordIdOrLodestoneAuth == lodestone.HashedLodestoneId))
{
dbContext.BannedRegistrations.Add(new BannedRegistrations()
{
DiscordIdOrLodestoneAuth = lodestone.HashedLodestoneId,
});
}
if (!dbContext.BannedRegistrations.Any(c => c.DiscordIdOrLodestoneAuth == lodestone.DiscordId.ToString()))
{
dbContext.BannedRegistrations.Add(new BannedRegistrations()
{
DiscordIdOrLodestoneAuth = lodestone.DiscordId.ToString(),
});
}
await dbContext.SaveChangesAsync();
}
return Unauthorized("You are permanently banned.");
}
var existingIdent = await _redis.GetAsync<string>("UID:" + authResult.Uid);
if (!string.IsNullOrEmpty(existingIdent) && !string.Equals(existingIdent, charaIdent, StringComparison.Ordinal))
return Unauthorized("Already logged in to this account. Reconnect in 60 seconds. If you keep seeing this issue, restart your game.");
var token = CreateToken(new List<Claim>()
{
new Claim(MareClaimTypes.Uid, authResult.Uid),
new Claim(MareClaimTypes.CharaIdent, charaIdent),
new Claim(MareClaimTypes.Alias, authResult.Alias),
new Claim(MareClaimTypes.Continent, await _geoIPProvider.GetCountryFromIP(_accessor)),
});
return Content(token.RawData);
}
[AllowAnonymous]
[HttpPost(MareAuth.Auth_CreateIdentV2)]
public async Task<IActionResult> CreateTokenV2(string auth, string charaIdent)
{
var tokenResponse = await CreateToken(auth, charaIdent);
var tokenContent = tokenResponse as ContentResult;
if (tokenContent == null)
return tokenResponse;
var provider = HttpContext.RequestServices.GetService<DiscoveryWellKnownProvider>();
var wk = provider?.GetWellKnownJson(Request.Scheme, Request.Host.Value)
?? _configuration.GetValueOrDefault(nameof(AuthServiceConfiguration.WellKnown), string.Empty);
return Json(new AuthReplyDto
{
Token = tokenContent.Content,
WellKnown = wk,
});
}
[AllowAnonymous]
[HttpPost(MareAuth.Auth_Register)]
public async Task<IActionResult> Register()
{
var ua = HttpContext.Request.Headers["User-Agent"][0] ?? "-";
var ip = _accessor.GetIpAddress();
// Legacy endpoint: generate a secret key for the user
var computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString());
var hashedKey = StringUtils.Sha256String(computedHash);
var dto = await _accountRegistrationService.RegisterAccountAsync(ua, ip, hashedKey);
return Json(new RegisterReplyDto()
{
Success = dto.Success,
ErrorMessage = dto.ErrorMessage,
UID = dto.UID,
SecretKey = computedHash
});
}
[AllowAnonymous]
[HttpPost(MareAuth.Auth_RegisterV2)]
public async Task<IActionResult> RegisterV2(string hashedSecretKey)
{
if (string.IsNullOrEmpty(hashedSecretKey)) return BadRequest("No HashedSecretKey");
if (hashedSecretKey.Length != 64) return BadRequest("Bad HashedSecretKey");
if (!hashedSecretKey.All(char.IsAsciiHexDigitUpper)) return BadRequest("Bad HashedSecretKey");
var ua = HttpContext.Request.Headers["User-Agent"][0] ?? "-";
var ip = _accessor.GetIpAddress();
return Json(await _accountRegistrationService.RegisterAccountAsync(ua, ip, hashedSecretKey));
}
private JwtSecurityToken CreateToken(IEnumerable<Claim> authClaims)
{
var authSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_configuration.GetValue<string>(nameof(MareConfigurationBase.Jwt))));
var token = new SecurityTokenDescriptor()
{
Subject = new ClaimsIdentity(authClaims),
SigningCredentials = new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256Signature),
};
var handler = new JwtSecurityTokenHandler();
return handler.CreateJwtSecurityToken(token);
}
}

View File

@@ -1,25 +0,0 @@
using MareSynchronosAuthService.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace MareSynchronosAuthService.Controllers;
[AllowAnonymous]
[ApiController]
public class WellKnownController : Controller
{
private readonly DiscoveryWellKnownProvider _provider;
public WellKnownController(DiscoveryWellKnownProvider provider)
{
_provider = provider;
}
[HttpGet("/.well-known/Umbra/client")]
public IActionResult Get()
{
var json = _provider.GetWellKnownJson(Request.Scheme, Request.Host.Value);
return Content(json, "application/json");
}
}

View File

@@ -1,39 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<EnableDefaultContentItems>false</EnableDefaultContentItems>
</PropertyGroup>
<ItemGroup>
<Content Include="appsettings.Development.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="IDisposableAnalyzers" Version="4.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.212">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="8.0.1" />
<PackageReference Include="StackExchange.Redis" Version="2.9.11" />
<PackageReference Include="StackExchange.Redis.Extensions.Core" Version="10.2.0" />
<PackageReference Include="StackExchange.Redis.Extensions.System.Text.Json" Version="10.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MareSynchronosShared\MareSynchronosShared.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,40 +0,0 @@
namespace MareSynchronosAuthService;
public class Program
{
public static void Main(string[] args)
{
var hostBuilder = CreateHostBuilder(args);
using var host = hostBuilder.Build();
try
{
host.Run();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
public static IHostBuilder CreateHostBuilder(string[] args)
{
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.ClearProviders();
builder.AddConsole();
});
var logger = loggerFactory.CreateLogger<Startup>();
return Host.CreateDefaultBuilder(args)
.UseSystemd()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseContentRoot(AppContext.BaseDirectory);
webBuilder.ConfigureLogging((ctx, builder) =>
{
builder.AddConfiguration(ctx.Configuration.GetSection("Logging"));
builder.AddFile(o => o.RootPath = AppContext.BaseDirectory);
});
webBuilder.UseStartup(ctx => new Startup(ctx.Configuration, logger));
});
}
}

View File

@@ -1,29 +0,0 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:37726",
"sslPort": 0
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5056",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -1,152 +0,0 @@
using System.Collections.Concurrent;
using MareSynchronos.API.Dto.Account;
using MareSynchronosShared.Data;
using MareSynchronosShared.Metrics;
using MareSynchronosShared.Services;
using MareSynchronosShared.Utils;
using MareSynchronosShared.Utils.Configuration;
using Microsoft.EntityFrameworkCore;
using System.Text.RegularExpressions;
using MareSynchronosShared.Models;
namespace MareSynchronosAuthService.Services;
internal record IpRegistrationCount
{
private int count = 1;
public int Count => count;
public Task ResetTask { get; set; }
public CancellationTokenSource ResetTaskCts { get; set; }
public void IncreaseCount()
{
Interlocked.Increment(ref count);
}
}
public class AccountRegistrationService
{
private readonly MareMetrics _metrics;
private readonly MareDbContext _mareDbContext;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly IConfigurationService<AuthServiceConfiguration> _configurationService;
private readonly ILogger<AccountRegistrationService> _logger;
private readonly ConcurrentDictionary<string, IpRegistrationCount> _registrationsPerIp = new(StringComparer.Ordinal);
private Regex _registrationUserAgentRegex = new Regex(@"^MareSynchronos/", RegexOptions.Compiled);
public AccountRegistrationService(MareMetrics metrics, MareDbContext mareDbContext,
IServiceScopeFactory serviceScopeFactory, IConfigurationService<AuthServiceConfiguration> configuration,
ILogger<AccountRegistrationService> logger)
{
_mareDbContext = mareDbContext;
_logger = logger;
_configurationService = configuration;
_metrics = metrics;
_serviceScopeFactory = serviceScopeFactory;
}
public async Task<RegisterReplyV2Dto> RegisterAccountAsync(string ua, string ip, string hashedSecretKey)
{
var reply = new RegisterReplyV2Dto();
if (!_registrationUserAgentRegex.Match(ua).Success)
{
reply.ErrorMessage = "User-Agent not allowed";
return reply;
}
if (_registrationsPerIp.TryGetValue(ip, out var registrationCount)
&& registrationCount.Count >= _configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.RegisterIpLimit), 3))
{
_logger.LogWarning("Rejecting {ip} for registration spam", ip);
if (registrationCount.ResetTask == null)
{
registrationCount.ResetTaskCts = new CancellationTokenSource();
if (registrationCount.ResetTaskCts != null)
registrationCount.ResetTaskCts.Cancel();
registrationCount.ResetTask = Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromMinutes(_configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.RegisterIpDurationInMinutes), 10))).ConfigureAwait(false);
}).ContinueWith((t) =>
{
_registrationsPerIp.Remove(ip, out _);
}, registrationCount.ResetTaskCts.Token);
}
reply.ErrorMessage = "Too many registrations from this IP. Please try again later.";
return reply;
}
var user = new User();
var hasValidUid = false;
while (!hasValidUid)
{
var uid = StringUtils.GenerateRandomString(7);
if (_mareDbContext.Users.Any(u => u.UID == uid || u.Alias == uid)) continue;
user.UID = uid;
hasValidUid = true;
}
// make the first registered user on the service to admin
if (!await _mareDbContext.Users.AnyAsync().ConfigureAwait(false))
{
user.IsAdmin = true;
}
user.LastLoggedIn = DateTime.UtcNow;
var auth = new Auth()
{
HashedKey = hashedSecretKey,
User = user,
};
await _mareDbContext.Users.AddAsync(user).ConfigureAwait(false);
await _mareDbContext.Auth.AddAsync(auth).ConfigureAwait(false);
await _mareDbContext.SaveChangesAsync().ConfigureAwait(false);
_logger.LogInformation("User registered: {userUID} from IP {ip}", user.UID, ip);
_metrics.IncCounter(MetricsAPI.CounterAccountsCreated);
reply.Success = true;
reply.UID = user.UID;
RecordIpRegistration(ip);
return reply;
}
private void RecordIpRegistration(string ip)
{
var whitelisted = _configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.WhitelistedIps), new List<string>());
if (!whitelisted.Any(w => ip.Contains(w, StringComparison.OrdinalIgnoreCase)))
{
if (_registrationsPerIp.TryGetValue(ip, out var count))
{
count.IncreaseCount();
}
else
{
count = _registrationsPerIp[ip] = new IpRegistrationCount();
if (count.ResetTaskCts != null)
count.ResetTaskCts.Cancel();
count.ResetTaskCts = new CancellationTokenSource();
count.ResetTask = Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromMinutes(_configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.RegisterIpDurationInMinutes), 10))).ConfigureAwait(false);
}).ContinueWith((t) =>
{
_registrationsPerIp.Remove(ip, out _);
}, count.ResetTaskCts.Token);
}
}
}
}

View File

@@ -1,12 +0,0 @@
using System.Collections.Concurrent;
namespace MareSynchronosAuthService.Services.Discovery;
public interface IDiscoveryPresenceStore : IDisposable
{
void Publish(string uid, IEnumerable<string> hashes, string? displayName = null, bool allowRequests = true);
void Unpublish(string uid);
(bool Found, string? Token, string TargetUid, string? DisplayName) TryMatchAndIssueToken(string requesterUid, string hash);
bool ValidateToken(string token, out string targetUid);
}

View File

@@ -1,108 +0,0 @@
using System.Collections.Concurrent;
namespace MareSynchronosAuthService.Services.Discovery;
public sealed class InMemoryPresenceStore : IDiscoveryPresenceStore
{
private readonly ConcurrentDictionary<string, (string Uid, DateTimeOffset ExpiresAt, string? DisplayName, bool AllowRequests)> _presence = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, (string TargetUid, DateTimeOffset ExpiresAt)> _tokens = new(StringComparer.Ordinal);
private readonly TimeSpan _presenceTtl;
private readonly TimeSpan _tokenTtl;
private readonly Timer _cleanupTimer;
public InMemoryPresenceStore(TimeSpan presenceTtl, TimeSpan tokenTtl)
{
_presenceTtl = presenceTtl;
_tokenTtl = tokenTtl;
_cleanupTimer = new Timer(_ => Cleanup(), null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));
}
public void Dispose()
{
_cleanupTimer.Dispose();
}
private void Cleanup()
{
var now = DateTimeOffset.UtcNow;
foreach (var kv in _presence.ToArray())
{
if (kv.Value.ExpiresAt <= now) _presence.TryRemove(kv.Key, out _);
}
foreach (var kv in _tokens.ToArray())
{
if (kv.Value.ExpiresAt <= now) _tokens.TryRemove(kv.Key, out _);
}
}
public void Publish(string uid, IEnumerable<string> hashes, string? displayName = null, bool allowRequests = true)
{
var exp = DateTimeOffset.UtcNow.Add(_presenceTtl);
foreach (var h in hashes.Distinct(StringComparer.Ordinal))
{
_presence[h] = (uid, exp, displayName, allowRequests);
}
}
public void Unpublish(string uid)
{
// Remove all presence hashes owned by this uid
foreach (var kv in _presence.ToArray())
{
if (string.Equals(kv.Value.Uid, uid, StringComparison.Ordinal))
{
_presence.TryRemove(kv.Key, out _);
}
}
}
public (bool Found, string? Token, string TargetUid, string? DisplayName) TryMatchAndIssueToken(string requesterUid, string hash)
{
if (_presence.TryGetValue(hash, out var entry))
{
// Refresh TTL for this presence whenever it is matched (regardless of AllowRequests)
var refreshed = (entry.Uid, DateTimeOffset.UtcNow.Add(_presenceTtl), entry.DisplayName, entry.AllowRequests);
_presence[hash] = refreshed;
if (string.Equals(entry.Uid, requesterUid, StringComparison.Ordinal))
return (false, null, string.Empty, null);
// Visible but requests disabled → no token
if (!entry.AllowRequests)
return (true, null, entry.Uid, entry.DisplayName);
var token = Guid.NewGuid().ToString("N");
_tokens[token] = (entry.Uid, DateTimeOffset.UtcNow.Add(_tokenTtl));
return (true, token, entry.Uid, entry.DisplayName);
}
return (false, null, string.Empty, null);
}
public bool ValidateToken(string token, out string targetUid)
{
targetUid = string.Empty;
if (_tokens.TryGetValue(token, out var info))
{
if (info.ExpiresAt > DateTimeOffset.UtcNow)
{
targetUid = info.TargetUid;
// Optional robustness: refresh TTL for all presence entries of this target
var newExp = DateTimeOffset.UtcNow.Add(_presenceTtl);
foreach (var kv in _presence.ToArray())
{
if (string.Equals(kv.Value.Uid, targetUid, StringComparison.Ordinal))
{
var v = kv.Value;
_presence[kv.Key] = (v.Uid, newExp, v.DisplayName, v.AllowRequests);
}
}
return true;
}
_tokens.TryRemove(token, out _);
}
return false;
}
}

View File

@@ -1,156 +0,0 @@
using System.Text.Json;
using Microsoft.Extensions.Logging;
using StackExchange.Redis;
namespace MareSynchronosAuthService.Services.Discovery;
public sealed class RedisPresenceStore : IDiscoveryPresenceStore
{
private readonly ILogger<RedisPresenceStore> _logger;
private readonly IDatabase _db;
private readonly TimeSpan _presenceTtl;
private readonly TimeSpan _tokenTtl;
private readonly JsonSerializerOptions _jsonOpts = new() { DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull };
public RedisPresenceStore(ILogger<RedisPresenceStore> logger, IConnectionMultiplexer mux, TimeSpan presenceTtl, TimeSpan tokenTtl)
{
_logger = logger;
_db = mux.GetDatabase();
_presenceTtl = presenceTtl;
_tokenTtl = tokenTtl;
}
public void Dispose() { }
private static string KeyForHash(string hash) => $"nd:hash:{hash}";
private static string KeyForToken(string token) => $"nd:token:{token}";
private static string KeyForUidSet(string uid) => $"nd:uid:{uid}";
public void Publish(string uid, IEnumerable<string> hashes, string? displayName = null, bool allowRequests = true)
{
var entries = hashes.Distinct(StringComparer.Ordinal).ToArray();
if (entries.Length == 0) return;
var batch = _db.CreateBatch();
foreach (var h in entries)
{
var key = KeyForHash(h);
var payload = JsonSerializer.Serialize(new Presence(uid, displayName, allowRequests), _jsonOpts);
batch.StringSetAsync(key, payload, _presenceTtl);
// Index this hash under the publisher uid for fast unpublish
batch.SetAddAsync(KeyForUidSet(uid), h);
batch.KeyExpireAsync(KeyForUidSet(uid), _presenceTtl);
}
batch.Execute();
_logger.LogDebug("RedisPresenceStore: published {count} hashes", entries.Length);
}
public void Unpublish(string uid)
{
try
{
var setKey = KeyForUidSet(uid);
var members = _db.SetMembers(setKey);
if (members is { Length: > 0 })
{
var batch = _db.CreateBatch();
foreach (var m in members)
{
var hash = (string)m;
var key = KeyForHash(hash);
// Defensive: only delete if the hash is still owned by this uid
var val = _db.StringGet(key);
if (val.HasValue)
{
try
{
var p = JsonSerializer.Deserialize<Presence>(val!);
if (p != null && string.Equals(p.Uid, uid, StringComparison.Ordinal))
{
batch.KeyDeleteAsync(key);
}
}
catch { /* ignore corrupted */ }
}
}
// Remove the uid index set itself
batch.KeyDeleteAsync(setKey);
batch.Execute();
}
else
{
// No index set: best-effort, just delete the set key in case it exists
_db.KeyDelete(setKey);
}
_logger.LogDebug("RedisPresenceStore: unpublished all hashes for uid {uid}", uid);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "RedisPresenceStore: Unpublish failed for uid {uid}", uid);
}
}
public (bool Found, string? Token, string TargetUid, string? DisplayName) TryMatchAndIssueToken(string requesterUid, string hash)
{
var key = KeyForHash(hash);
var val = _db.StringGet(key);
if (!val.HasValue) return (false, null, string.Empty, null);
try
{
var p = JsonSerializer.Deserialize<Presence>(val!);
if (p == null || string.IsNullOrEmpty(p.Uid)) return (false, null, string.Empty, null);
if (string.Equals(p.Uid, requesterUid, StringComparison.Ordinal)) return (false, null, string.Empty, null);
// Refresh TTLs for this presence whenever it is matched
_db.KeyExpire(KeyForHash(hash), _presenceTtl);
_db.KeyExpire(KeyForUidSet(p.Uid), _presenceTtl);
// Visible but requests disabled → return without token
if (!p.AllowRequests)
{
return (true, null, p.Uid, p.DisplayName);
}
var token = Guid.NewGuid().ToString("N");
_db.StringSet(KeyForToken(token), p.Uid, _tokenTtl);
return (true, token, p.Uid, p.DisplayName);
}
catch
{
return (false, null, string.Empty, null);
}
}
public bool ValidateToken(string token, out string targetUid)
{
targetUid = string.Empty;
var key = KeyForToken(token);
var val = _db.StringGet(key);
if (!val.HasValue) return false;
targetUid = val!;
try
{
var setKey = KeyForUidSet(targetUid);
var members = _db.SetMembers(setKey);
if (members is { Length: > 0 })
{
var batch = _db.CreateBatch();
foreach (var m in members)
{
var h = (string)m;
batch.KeyExpireAsync(KeyForHash(h), _presenceTtl);
}
batch.KeyExpireAsync(setKey, _presenceTtl);
batch.Execute();
}
else
{
// Still try to extend the set TTL even if empty
_db.KeyExpire(setKey, _presenceTtl);
}
}
catch { /* ignore TTL refresh issues */ }
return true;
}
private sealed record Presence(string Uid, string? DisplayName, bool AllowRequests);
}

View File

@@ -1,57 +0,0 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MareSynchronosAuthService.Services.Discovery;
namespace MareSynchronosAuthService.Services;
public class DiscoveryPresenceService : IHostedService, IDisposable
{
private readonly ILogger<DiscoveryPresenceService> _logger;
private readonly IDiscoveryPresenceStore _store;
private readonly TimeSpan _presenceTtl = TimeSpan.FromMinutes(5);
private readonly TimeSpan _tokenTtl = TimeSpan.FromMinutes(2);
public DiscoveryPresenceService(ILogger<DiscoveryPresenceService> logger, IDiscoveryPresenceStore store)
{
_logger = logger;
_store = store;
}
public Task StartAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public void Publish(string uid, IEnumerable<string> hashes, string? displayName = null, bool allowRequests = true)
{
_store.Publish(uid, hashes, displayName, allowRequests);
_logger.LogDebug("Discovery presence published for {uid} with {n} hashes", uid, hashes.Count());
}
public void Unpublish(string uid)
{
_store.Unpublish(uid);
_logger.LogDebug("Discovery presence unpublished for {uid}", uid);
}
public (bool Found, string? Token, string TargetUid, string? DisplayName) TryMatchAndIssueToken(string requesterUid, string hash)
{
var res = _store.TryMatchAndIssueToken(requesterUid, hash);
return (res.Found, res.Token, res.TargetUid, res.DisplayName);
}
public bool ValidateToken(string token, out string targetUid)
{
return _store.ValidateToken(token, out targetUid);
}
public void Dispose()
{
(_store as IDisposable)?.Dispose();
}
}

View File

@@ -1,170 +0,0 @@
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace MareSynchronosAuthService.Services;
public class DiscoveryWellKnownProvider : IHostedService
{
private readonly ILogger<DiscoveryWellKnownProvider> _logger;
private readonly object _lock = new();
private byte[] _currentSalt = Array.Empty<byte>();
private DateTimeOffset _currentSaltExpiresAt;
private byte[] _previousSalt = Array.Empty<byte>();
private DateTimeOffset _previousSaltExpiresAt;
private readonly TimeSpan _gracePeriod = TimeSpan.FromMinutes(5);
private Timer? _rotationTimer;
private readonly TimeSpan _saltTtl = TimeSpan.FromDays(30 * 6);
private readonly int _refreshSec = 86400; // 24h
public DiscoveryWellKnownProvider(ILogger<DiscoveryWellKnownProvider> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
RotateSalt();
var period = _saltTtl;
if (period.TotalMilliseconds > uint.MaxValue - 1)
{
_logger.LogInformation("DiscoveryWellKnownProvider: salt TTL {ttl} exceeds timer limit, skipping rotation timer in beta", period);
_rotationTimer = new Timer(_ => { }, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
}
else
{
_rotationTimer = new Timer(_ => RotateSalt(), null, period, period);
}
_logger.LogInformation("DiscoveryWellKnownProvider started. Salt expires at {exp}", _currentSaltExpiresAt);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_rotationTimer?.Dispose();
return Task.CompletedTask;
}
private void RotateSalt()
{
lock (_lock)
{
if (_currentSalt.Length > 0)
{
_previousSalt = _currentSalt;
_previousSaltExpiresAt = DateTimeOffset.UtcNow.Add(_gracePeriod);
}
_currentSalt = RandomNumberGenerator.GetBytes(32);
_currentSaltExpiresAt = DateTimeOffset.UtcNow.Add(_saltTtl);
}
}
public bool IsExpired(string providedSaltB64)
{
lock (_lock)
{
var now = DateTimeOffset.UtcNow;
var provided = Convert.FromBase64String(providedSaltB64);
if (_currentSalt.SequenceEqual(provided) && now <= _currentSaltExpiresAt)
return false;
if (_previousSalt.Length > 0 && _previousSalt.SequenceEqual(provided) && now <= _previousSaltExpiresAt)
return false;
return true;
}
}
public string GetWellKnownJson(string scheme, string host)
{
var isHttps = string.Equals(scheme, "https", StringComparison.OrdinalIgnoreCase);
var wsScheme = isHttps ? "wss" : "ws";
var httpScheme = isHttps ? "https" : "http";
byte[] salt;
DateTimeOffset exp;
lock (_lock)
{
salt = _currentSalt.ToArray();
exp = _currentSaltExpiresAt;
}
var root = new WellKnownRoot
{
ApiUrl = $"{wsScheme}://{host}",
HubUrl = $"{wsScheme}://{host}/mare",
Features = new() { NearbyDiscovery = true },
NearbyDiscovery = new()
{
Enabled = true,
HashAlgo = "sha256",
SaltB64 = Convert.ToBase64String(salt),
SaltExpiresAt = exp,
RefreshSec = _refreshSec,
GraceSec = (int)_gracePeriod.TotalSeconds,
Endpoints = new()
{
Publish = $"{httpScheme}://{host}/discovery/publish",
Query = $"{httpScheme}://{host}/discovery/query",
Request = $"{httpScheme}://{host}/discovery/request",
Accept = $"{httpScheme}://{host}/discovery/acceptNotify"
},
Policies = new()
{
MaxQueryBatch = 100,
MinQueryIntervalMs = 2000,
RateLimitPerMin = 30,
TokenTtlSec = 120
}
}
};
return JsonSerializer.Serialize(root, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull });
}
private sealed class WellKnownRoot
{
[JsonPropertyName("api_url")] public string ApiUrl { get; set; } = string.Empty;
[JsonPropertyName("hub_url")] public string HubUrl { get; set; } = string.Empty;
[JsonPropertyName("skip_negotiation")] public bool SkipNegotiation { get; set; } = true;
[JsonPropertyName("transports")] public string[] Transports { get; set; } = new[] { "websockets" };
[JsonPropertyName("features")] public Features Features { get; set; } = new();
[JsonPropertyName("nearby_discovery")] public Nearby NearbyDiscovery { get; set; } = new();
}
private sealed class Features
{
[JsonPropertyName("nearby_discovery")] public bool NearbyDiscovery { get; set; }
}
private sealed class Nearby
{
[JsonPropertyName("enabled")] public bool Enabled { get; set; }
[JsonPropertyName("hash_algo")] public string HashAlgo { get; set; } = "sha256";
[JsonPropertyName("salt_b64")] public string SaltB64 { get; set; } = string.Empty;
[JsonPropertyName("salt_expires_at")] public DateTimeOffset SaltExpiresAt { get; set; }
[JsonPropertyName("refresh_sec")] public int RefreshSec { get; set; }
[JsonPropertyName("grace_sec")] public int GraceSec { get; set; }
[JsonPropertyName("endpoints")] public Endpoints Endpoints { get; set; } = new();
[JsonPropertyName("policies")] public Policies Policies { get; set; } = new();
}
private sealed class Endpoints
{
[JsonPropertyName("publish")] public string? Publish { get; set; }
[JsonPropertyName("query")] public string? Query { get; set; }
[JsonPropertyName("request")] public string? Request { get; set; }
[JsonPropertyName("accept")] public string? Accept { get; set; }
}
private sealed class Policies
{
[JsonPropertyName("max_query_batch")] public int MaxQueryBatch { get; set; }
[JsonPropertyName("min_query_interval_ms")] public int MinQueryIntervalMs { get; set; }
[JsonPropertyName("rate_limit_per_min")] public int RateLimitPerMin { get; set; }
[JsonPropertyName("token_ttl_sec")] public int TokenTtlSec { get; set; }
}
}

View File

@@ -1,138 +0,0 @@
using MareSynchronosShared;
using MareSynchronosShared.Services;
using MareSynchronosShared.Utils.Configuration;
using MaxMind.GeoIP2;
namespace MareSynchronosAuthService.Services;
public class GeoIPService : IHostedService
{
private readonly ILogger<GeoIPService> _logger;
private readonly IConfigurationService<AuthServiceConfiguration> _mareConfiguration;
private bool _useGeoIP = false;
private string _cityFile = string.Empty;
private DatabaseReader? _dbReader;
private DateTime _dbLastWriteTime = DateTime.Now;
private CancellationTokenSource _fileWriteTimeCheckCts = new();
private bool _processingReload = false;
public GeoIPService(ILogger<GeoIPService> logger,
IConfigurationService<AuthServiceConfiguration> mareConfiguration)
{
_logger = logger;
_mareConfiguration = mareConfiguration;
}
public async Task<string> GetCountryFromIP(IHttpContextAccessor httpContextAccessor)
{
if (!_useGeoIP)
{
return "*";
}
try
{
var ip = httpContextAccessor.GetIpAddress();
using CancellationTokenSource waitCts = new();
waitCts.CancelAfter(TimeSpan.FromSeconds(5));
while (_processingReload) await Task.Delay(100, waitCts.Token).ConfigureAwait(false);
if (_dbReader!.TryCity(ip, out var response))
{
string? continent = response?.Continent.Code;
if (!string.IsNullOrEmpty(continent) &&
string.Equals(continent, "NA", StringComparison.Ordinal)
&& response?.Location.Longitude != null)
{
if (response.Location.Longitude < -102)
{
continent = "NA-W";
}
else
{
continent = "NA-E";
}
}
return continent ?? "*";
}
return "*";
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error handling Geo IP country in request");
return "*";
}
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("GeoIP module starting update task");
var token = _fileWriteTimeCheckCts.Token;
_ = PeriodicReloadTask(token);
return Task.CompletedTask;
}
private async Task PeriodicReloadTask(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
_processingReload = true;
var useGeoIP = _mareConfiguration.GetValueOrDefault(nameof(AuthServiceConfiguration.UseGeoIP), false);
var cityFile = _mareConfiguration.GetValueOrDefault(nameof(AuthServiceConfiguration.GeoIPDbCityFile), string.Empty);
var lastWriteTime = new FileInfo(cityFile).LastWriteTimeUtc;
if (useGeoIP && (!string.Equals(cityFile, _cityFile, StringComparison.OrdinalIgnoreCase) || lastWriteTime != _dbLastWriteTime))
{
_cityFile = cityFile;
if (!File.Exists(_cityFile)) throw new FileNotFoundException($"Could not open GeoIP City Database, path does not exist: {_cityFile}");
_dbReader?.Dispose();
_dbReader = null;
_dbReader = new DatabaseReader(_cityFile);
_dbLastWriteTime = lastWriteTime;
_ = _dbReader.City("8.8.8.8").Continent;
_logger.LogInformation($"Loaded GeoIP city file from {_cityFile}");
if (_useGeoIP != useGeoIP)
{
_logger.LogInformation("GeoIP module is now enabled");
_useGeoIP = useGeoIP;
}
}
if (_useGeoIP != useGeoIP && !useGeoIP)
{
_logger.LogInformation("GeoIP module is now disabled");
_useGeoIP = useGeoIP;
}
}
catch (Exception e)
{
_logger.LogWarning(e, "Error during periodic GeoIP module reload task, disabling GeoIP");
_useGeoIP = false;
}
finally
{
_processingReload = false;
}
await Task.Delay(TimeSpan.FromMinutes(1)).ConfigureAwait(false);
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_fileWriteTimeCheckCts.Cancel();
_fileWriteTimeCheckCts.Dispose();
_dbReader?.Dispose();
return Task.CompletedTask;
}
}

View File

@@ -1,88 +0,0 @@
using System.Collections.Concurrent;
using MareSynchronosAuthService.Authentication;
using MareSynchronosShared.Data;
using MareSynchronosShared.Metrics;
using MareSynchronosShared.Services;
using MareSynchronosShared.Utils.Configuration;
using Microsoft.EntityFrameworkCore;
namespace MareSynchronosAuthService.Services;
public class SecretKeyAuthenticatorService
{
private readonly MareMetrics _metrics;
private readonly MareDbContext _mareDbContext;
private readonly IConfigurationService<AuthServiceConfiguration> _configurationService;
private readonly ILogger<SecretKeyAuthenticatorService> _logger;
private readonly ConcurrentDictionary<string, SecretKeyFailedAuthorization> _failedAuthorizations = new(StringComparer.Ordinal);
public SecretKeyAuthenticatorService(MareMetrics metrics, MareDbContext mareDbContext,
IConfigurationService<AuthServiceConfiguration> configuration, ILogger<SecretKeyAuthenticatorService> logger)
{
_logger = logger;
_configurationService = configuration;
_metrics = metrics;
_mareDbContext = mareDbContext;
}
public async Task<SecretKeyAuthReply> AuthorizeAsync(string ip, string hashedSecretKey)
{
_metrics.IncCounter(MetricsAPI.CounterAuthenticationRequests);
if (_failedAuthorizations.TryGetValue(ip, out var existingFailedAuthorization)
&& existingFailedAuthorization.FailedAttempts > _configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.FailedAuthForTempBan), 5))
{
if (existingFailedAuthorization.ResetTask == null)
{
_logger.LogWarning("TempBan {ip} for authorization spam", ip);
existingFailedAuthorization.ResetTask = Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromMinutes(_configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.TempBanDurationInMinutes), 5))).ConfigureAwait(false);
}).ContinueWith((t) =>
{
_failedAuthorizations.Remove(ip, out _);
});
}
return new(Success: false, Uid: null, TempBan: true, Alias: null, Permaban: false);
}
var authReply = await _mareDbContext.Auth.Include(a => a.User).AsNoTracking()
.SingleOrDefaultAsync(u => u.HashedKey == hashedSecretKey).ConfigureAwait(false);
SecretKeyAuthReply reply = new(authReply != null, authReply?.UserUID, authReply?.User?.Alias ?? string.Empty, TempBan: false, authReply?.IsBanned ?? false);
if (reply.Success)
{
_metrics.IncCounter(MetricsAPI.CounterAuthenticationSuccesses);
}
else
{
return AuthenticationFailure(ip);
}
return reply;
}
private SecretKeyAuthReply AuthenticationFailure(string ip)
{
_metrics.IncCounter(MetricsAPI.CounterAuthenticationFailures);
_logger.LogWarning("Failed authorization from {ip}", ip);
var whitelisted = _configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.WhitelistedIps), new List<string>());
if (!whitelisted.Any(w => ip.Contains(w, StringComparison.OrdinalIgnoreCase)))
{
if (_failedAuthorizations.TryGetValue(ip, out var auth))
{
auth.IncreaseFailedAttempts();
}
else
{
_failedAuthorizations[ip] = new SecretKeyFailedAuthorization();
}
}
return new(Success: false, Uid: null, Alias: null, TempBan: false, Permaban: false);
}
}

View File

@@ -1,263 +0,0 @@
using MareSynchronosAuthService.Controllers;
using MareSynchronosShared.Metrics;
using MareSynchronosShared.Services;
using MareSynchronosShared.Utils;
using Microsoft.AspNetCore.Mvc.Controllers;
using StackExchange.Redis.Extensions.Core.Configuration;
using StackExchange.Redis.Extensions.System.Text.Json;
using StackExchange.Redis;
using System.Net;
using MareSynchronosAuthService.Services;
using MareSynchronosShared.RequirementHandlers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using MareSynchronosShared.Data;
using Microsoft.EntityFrameworkCore;
using Prometheus;
using Microsoft.AspNetCore.HttpOverrides;
using MareSynchronosShared.Utils.Configuration;
namespace MareSynchronosAuthService;
public class Startup
{
private readonly IConfiguration _configuration;
private ILogger<Startup> _logger;
public Startup(IConfiguration configuration, ILogger<Startup> logger)
{
_configuration = configuration;
_logger = logger;
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
{
var config = app.ApplicationServices.GetRequiredService<IConfigurationService<MareConfigurationBase>>();
// Respect X-Forwarded-* headers from the reverse proxy so generated links use the public scheme/host
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedFor
});
app.UseRouting();
app.UseHttpMetrics();
app.UseAuthentication();
app.UseAuthorization();
KestrelMetricServer metricServer = new KestrelMetricServer(config.GetValueOrDefault<int>(nameof(MareConfigurationBase.MetricsPort), 4985));
metricServer.Start();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHealthChecks("/healthz").WithMetadata(new AllowAnonymousAttribute());
foreach (var source in endpoints.DataSources.SelectMany(e => e.Endpoints).Cast<RouteEndpoint>())
{
if (source == null) continue;
_logger.LogInformation("Endpoint: {url} ", source.RoutePattern.RawText);
}
});
}
public void ConfigureServices(IServiceCollection services)
{
var mareConfig = _configuration.GetRequiredSection("MareSynchronos");
services.AddHttpContextAccessor();
ConfigureRedis(services, mareConfig);
services.AddScoped<SecretKeyAuthenticatorService>();
services.AddScoped<AccountRegistrationService>();
services.AddSingleton<GeoIPService>();
services.AddHostedService(provider => provider.GetRequiredService<GeoIPService>());
services.Configure<AuthServiceConfiguration>(_configuration.GetRequiredSection("MareSynchronos"));
services.Configure<MareConfigurationBase>(_configuration.GetRequiredSection("MareSynchronos"));
services.AddSingleton<ServerTokenGenerator>();
// Nearby discovery services (well-known + presence)
services.AddSingleton<DiscoveryWellKnownProvider>();
services.AddHostedService(p => p.GetRequiredService<DiscoveryWellKnownProvider>());
// Presence store selection
var discoveryStore = _configuration.GetValue<string>("NearbyDiscovery:Store") ?? "memory";
TimeSpan presenceTtl = TimeSpan.FromMinutes(_configuration.GetValue<int>("NearbyDiscovery:PresenceTtlMinutes", 5));
TimeSpan tokenTtl = TimeSpan.FromSeconds(_configuration.GetValue<int>("NearbyDiscovery:TokenTtlSeconds", 120));
if (string.Equals(discoveryStore, "redis", StringComparison.OrdinalIgnoreCase))
{
services.AddSingleton<MareSynchronosAuthService.Services.Discovery.IDiscoveryPresenceStore>(sp =>
{
var logger = sp.GetRequiredService<ILogger<MareSynchronosAuthService.Services.Discovery.RedisPresenceStore>>();
var mux = sp.GetRequiredService<IConnectionMultiplexer>();
return new MareSynchronosAuthService.Services.Discovery.RedisPresenceStore(logger, mux, presenceTtl, tokenTtl);
});
}
else
{
services.AddSingleton<MareSynchronosAuthService.Services.Discovery.IDiscoveryPresenceStore>(sp => new MareSynchronosAuthService.Services.Discovery.InMemoryPresenceStore(presenceTtl, tokenTtl));
}
services.AddSingleton<DiscoveryPresenceService>();
services.AddHostedService(p => p.GetRequiredService<DiscoveryPresenceService>());
ConfigureAuthorization(services);
ConfigureDatabase(services, mareConfig);
ConfigureConfigServices(services);
ConfigureMetrics(services);
services.AddHealthChecks();
services.AddControllers().ConfigureApplicationPartManager(a =>
{
a.FeatureProviders.Remove(a.FeatureProviders.OfType<ControllerFeatureProvider>().First());
a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(JwtController), typeof(WellKnownController), typeof(DiscoveryController)));
});
services.AddSingleton<DiscoveryWellKnownProvider>();
services.AddHostedService(p => p.GetRequiredService<DiscoveryWellKnownProvider>());
}
private static void ConfigureAuthorization(IServiceCollection services)
{
services.AddTransient<IAuthorizationHandler, UserRequirementHandler>();
services.AddOptions<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme)
.Configure<IConfigurationService<MareConfigurationBase>>((options, config) =>
{
options.TokenValidationParameters = new()
{
ValidateIssuer = false,
ValidateLifetime = true,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.GetValue<string>(nameof(MareConfigurationBase.Jwt)))),
};
});
services.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer();
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser().Build();
options.AddPolicy("Authenticated", policy =>
{
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
});
options.AddPolicy("Identified", policy =>
{
policy.AddRequirements(new UserRequirement(UserRequirements.Identified));
});
options.AddPolicy("Admin", policy =>
{
policy.AddRequirements(new UserRequirement(UserRequirements.Identified | UserRequirements.Administrator));
});
options.AddPolicy("Moderator", policy =>
{
policy.AddRequirements(new UserRequirement(UserRequirements.Identified | UserRequirements.Moderator | UserRequirements.Administrator));
});
options.AddPolicy("Internal", new AuthorizationPolicyBuilder().RequireClaim(MareClaimTypes.Internal, "true").Build());
});
}
private static void ConfigureMetrics(IServiceCollection services)
{
services.AddSingleton<MareMetrics>(m => new MareMetrics(m.GetService<ILogger<MareMetrics>>(), new List<string>
{
MetricsAPI.CounterAuthenticationCacheHits,
MetricsAPI.CounterAuthenticationFailures,
MetricsAPI.CounterAuthenticationRequests,
MetricsAPI.CounterAuthenticationSuccesses,
MetricsAPI.CounterAccountsCreated,
}, new List<string>
{
}));
}
private static void ConfigureRedis(IServiceCollection services, IConfigurationSection mareConfig)
{
// configure redis for SignalR
var redisConnection = mareConfig.GetValue(nameof(ServerConfiguration.RedisConnectionString), string.Empty);
var options = ConfigurationOptions.Parse(redisConnection);
var endpoint = options.EndPoints[0];
string address = "";
int port = 0;
if (endpoint is DnsEndPoint dnsEndPoint) { address = dnsEndPoint.Host; port = dnsEndPoint.Port; }
if (endpoint is IPEndPoint ipEndPoint) { address = ipEndPoint.Address.ToString(); port = ipEndPoint.Port; }
var redisConfiguration = new RedisConfiguration()
{
AbortOnConnectFail = true,
KeyPrefix = "",
Hosts = new RedisHost[]
{
new RedisHost(){ Host = address, Port = port },
},
AllowAdmin = true,
ConnectTimeout = options.ConnectTimeout,
Database = 0,
Ssl = false,
Password = options.Password,
ServerEnumerationStrategy = new ServerEnumerationStrategy()
{
Mode = ServerEnumerationStrategy.ModeOptions.All,
TargetRole = ServerEnumerationStrategy.TargetRoleOptions.Any,
UnreachableServerAction = ServerEnumerationStrategy.UnreachableServerActionOptions.Throw,
},
MaxValueLength = 1024,
PoolSize = mareConfig.GetValue(nameof(ServerConfiguration.RedisPool), 50),
SyncTimeout = options.SyncTimeout,
};
services.AddStackExchangeRedisExtensions<SystemTextJsonSerializer>(redisConfiguration);
// Also expose raw multiplexer for custom Redis usage (discovery presence)
services.AddSingleton<IConnectionMultiplexer>(_ => ConnectionMultiplexer.Connect(options));
}
private void ConfigureConfigServices(IServiceCollection services)
{
services.AddSingleton<IConfigurationService<AuthServiceConfiguration>, MareConfigurationServiceServer<AuthServiceConfiguration>>();
services.AddSingleton<IConfigurationService<MareConfigurationBase>, MareConfigurationServiceServer<MareConfigurationBase>>();
}
private void ConfigureDatabase(IServiceCollection services, IConfigurationSection mareConfig)
{
services.AddDbContextPool<MareDbContext>(options =>
{
options.UseNpgsql(_configuration.GetConnectionString("DefaultConnection"), builder =>
{
builder.MigrationsHistoryTable("_efmigrationshistory", "public");
builder.MigrationsAssembly("MareSynchronosShared");
}).UseSnakeCaseNamingConvention();
options.EnableThreadSafetyChecks(false);
}, mareConfig.GetValue(nameof(MareConfigurationBase.DbContextPoolSize), 1024));
services.AddDbContextFactory<MareDbContext>(options =>
{
options.UseNpgsql(_configuration.GetConnectionString("DefaultConnection"), builder =>
{
builder.MigrationsHistoryTable("_efmigrationshistory", "public");
builder.MigrationsAssembly("MareSynchronosShared");
}).UseSnakeCaseNamingConvention();
options.EnableThreadSafetyChecks(false);
});
}
}

View File

@@ -1,18 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=umbra_dev"
},
"MareSynchronos": {
"Jwt": "dev-secret-umbra-abcdefghijklmnopqrstuvwxyz123456",
"RedisConnectionString": "localhost:6379,connectTimeout=5000,syncTimeout=5000",
"MetricsPort": 4985
}
}

View File

@@ -1,9 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -1,60 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.2.32602.215
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronosServer", "MareSynchronosServer\MareSynchronosServer.csproj", "{029CA97F-E0BA-4172-A191-EA21FB61AD0F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronos.API", "..\MareAPI\MareSynchronosAPI\MareSynchronos.API.csproj", "{326BFB1B-5571-47A6-8513-1FFDB32D53B0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronosShared", "MareSynchronosShared\MareSynchronosShared.csproj", "{67B1461D-E215-4BA8-A64D-E1836724D5E6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronosStaticFilesServer", "MareSynchronosStaticFilesServer\MareSynchronosStaticFilesServer.csproj", "{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}"
EndProject
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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MareSynchronosAuthService", "MareSynchronosAuthService\MareSynchronosAuthService.csproj", "{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Release|Any CPU.Build.0 = Release|Any CPU
{326BFB1B-5571-47A6-8513-1FFDB32D53B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{326BFB1B-5571-47A6-8513-1FFDB32D53B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{326BFB1B-5571-47A6-8513-1FFDB32D53B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{326BFB1B-5571-47A6-8513-1FFDB32D53B0}.Release|Any CPU.Build.0 = Release|Any CPU
{67B1461D-E215-4BA8-A64D-E1836724D5E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{67B1461D-E215-4BA8-A64D-E1836724D5E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{67B1461D-E215-4BA8-A64D-E1836724D5E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{67B1461D-E215-4BA8-A64D-E1836724D5E6}.Release|Any CPU.Build.0 = Release|Any CPU
{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Release|Any CPU.Build.0 = Release|Any CPU
{E29C8677-AB44-4950-9EB1-D8E70B710A56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E29C8677-AB44-4950-9EB1-D8E70B710A56}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E29C8677-AB44-4950-9EB1-D8E70B710A56}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E29C8677-AB44-4950-9EB1-D8E70B710A56}.Release|Any CPU.Build.0 = Release|Any CPU
{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {78C476A5-6E88-449B-828D-A2465D9D3295}
EndGlobalSection
EndGlobal

View File

@@ -1,12 +0,0 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "6.0.9",
"commands": [
"dotnet-ef"
]
}
}
}

View File

@@ -1,42 +0,0 @@
using MareSynchronos.API.SignalR;
using MareSynchronosServer.Hubs;
using MareSynchronosShared.Utils;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
namespace MareSynchronosServer.Controllers;
[Route("/msgc")]
[Authorize(Policy = "Internal")]
public class ClientMessageController : Controller
{
private ILogger<ClientMessageController> _logger;
private IHubContext<MareHub, IMareHub> _hubContext;
public ClientMessageController(ILogger<ClientMessageController> logger, IHubContext<MareHub, IMareHub> hubContext)
{
_logger = logger;
_hubContext = hubContext;
}
[Route("sendMessage")]
[HttpPost]
public async Task<IActionResult> SendMessage([FromBody] ClientMessage msg)
{
bool hasUid = !string.IsNullOrEmpty(msg.UID);
if (!hasUid)
{
_logger.LogInformation("Sending Message of severity {severity} to all online users: {message}", msg.Severity, msg.Message);
await _hubContext.Clients.All.Client_ReceiveServerMessage(msg.Severity, msg.Message).ConfigureAwait(false);
}
else
{
_logger.LogInformation("Sending Message of severity {severity} to user {uid}: {message}", msg.Severity, msg.UID, msg.Message);
await _hubContext.Clients.User(msg.UID).Client_ReceiveServerMessage(msg.Severity, msg.Message).ConfigureAwait(false);
}
return Empty;
}
}

View File

@@ -1,58 +0,0 @@
using MareSynchronos.API.SignalR;
using MareSynchronosShared.Utils;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using System.Text.Json.Serialization;
namespace MareSynchronosServer.Controllers;
[Route("/main/discovery")]
[Authorize(Policy = "Internal")]
public class DiscoveryNotifyController : Controller
{
private readonly ILogger<DiscoveryNotifyController> _logger;
private readonly IHubContext<Hubs.MareHub, IMareHub> _hub;
public DiscoveryNotifyController(ILogger<DiscoveryNotifyController> logger, IHubContext<Hubs.MareHub, IMareHub> hub)
{
_logger = logger;
_hub = hub;
}
public sealed class NotifyRequestDto
{
[JsonPropertyName("targetUid")] public string TargetUid { get; set; } = string.Empty;
[JsonPropertyName("fromUid")] public string FromUid { get; set; } = string.Empty;
[JsonPropertyName("fromAlias")] public string? FromAlias { get; set; }
}
[HttpPost("notifyRequest")]
public async Task<IActionResult> NotifyRequest([FromBody] NotifyRequestDto dto)
{
if (string.IsNullOrEmpty(dto.TargetUid)) return BadRequest();
var name = string.IsNullOrEmpty(dto.FromAlias) ? dto.FromUid : dto.FromAlias;
var msg = $"Nearby Request: {name} [{dto.FromUid}]";
_logger.LogInformation("Discovery notify request to {target} from {from}", dto.TargetUid, name);
await _hub.Clients.User(dto.TargetUid).Client_ReceiveServerMessage(MareSynchronos.API.Data.Enum.MessageSeverity.Information, msg);
return Accepted();
}
public sealed class NotifyAcceptDto
{
[JsonPropertyName("targetUid")] public string TargetUid { get; set; } = string.Empty;
[JsonPropertyName("fromUid")] public string FromUid { get; set; } = string.Empty;
[JsonPropertyName("fromAlias")] public string? FromAlias { get; set; }
}
[HttpPost("notifyAccept")]
public async Task<IActionResult> NotifyAccept([FromBody] NotifyAcceptDto dto)
{
if (string.IsNullOrEmpty(dto.TargetUid)) return BadRequest();
var name = string.IsNullOrEmpty(dto.FromAlias) ? dto.FromUid : dto.FromAlias;
var msg = $"Nearby Accept: {name} [{dto.FromUid}]";
_logger.LogInformation("Discovery notify accept to {target} from {from}", dto.TargetUid, name);
await _hub.Clients.User(dto.TargetUid).Client_ReceiveServerMessage(MareSynchronos.API.Data.Enum.MessageSeverity.Information, msg);
return Accepted();
}
}

View File

@@ -1,31 +0,0 @@
using MareSynchronos.API.Routes;
using MareSynchronos.API.SignalR;
using MareSynchronosServer.Hubs;
using MareSynchronosServer.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
namespace MareSynchronosServer.Controllers;
[Route(MareFiles.Main)]
public class MainController : Controller
{
private IHubContext<MareHub, IMareHub> _hubContext;
public MainController(ILogger<MainController> logger, IHubContext<MareHub, IMareHub> hubContext)
{
_hubContext = hubContext;
}
[HttpGet(MareFiles.Main_SendReady)]
[Authorize(Policy = "Internal")]
public IActionResult SendReadyToClients(string uid, Guid requestId)
{
_ = Task.Run(async () =>
{
await _hubContext.Clients.User(uid).Client_DownloadReady(requestId);
});
return Ok();
}
}

View File

@@ -1,638 +0,0 @@
using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.CharaData;
using MareSynchronosServer.Utils;
using MareSynchronosShared.Models;
using MareSynchronosShared.Utils;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
namespace MareSynchronosServer.Hubs;
public partial class MareHub
{
[Authorize(Policy = "Identified")]
public async Task<CharaDataFullDto?> CharaDataCreate()
{
_logger.LogCallInfo();
int uploadCount = DbContext.CharaData.Count(c => c.UploaderUID == UserUID);
User user = DbContext.Users.Single(u => u.UID == UserUID);
int maximumUploads = _maxCharaDataByUser;
if (uploadCount >= maximumUploads)
{
return null;
}
string charaDataId = null;
while (charaDataId == null)
{
charaDataId = StringUtils.GenerateRandomString(10, "abcdefghijklmnopqrstuvwxyzABCDEFHIJKLMNOPQRSTUVWXYZ");
bool idExists = await DbContext.CharaData.AnyAsync(c => c.UploaderUID == UserUID && c.Id == charaDataId).ConfigureAwait(false);
if (idExists)
{
charaDataId = null;
}
}
DateTime createdDate = DateTime.UtcNow;
CharaData charaData = new()
{
Id = charaDataId,
UploaderUID = UserUID,
CreatedDate = createdDate,
UpdatedDate = createdDate,
AccessType = CharaDataAccess.Individuals,
ShareType = CharaDataShare.Private,
CustomizeData = string.Empty,
GlamourerData = string.Empty,
ExpiryDate = DateTime.MaxValue,
Description = string.Empty,
};
await DbContext.CharaData.AddAsync(charaData).ConfigureAwait(false);
await DbContext.SaveChangesAsync().ConfigureAwait(false);
_logger.LogCallInfo(MareHubLogger.Args("SUCCESS", charaDataId));
return GetCharaDataFullDto(charaData);
}
[Authorize(Policy = "Identified")]
public async Task<bool> CharaDataDelete(string id)
{
var existingData = await DbContext.CharaData.SingleOrDefaultAsync(u => u.Id == id && u.UploaderUID == UserUID).ConfigureAwait(false);
if (existingData == null)
return false;
try
{
_logger.LogCallInfo(MareHubLogger.Args("SUCCESS", id));
DbContext.Remove(existingData);
await DbContext.SaveChangesAsync().ConfigureAwait(false);
return true;
}
catch (Exception ex)
{
_logger.LogCallWarning(MareHubLogger.Args("FAILURE", id, ex.Message));
return false;
}
}
[Authorize(Policy = "Identified")]
public async Task<CharaDataDownloadDto?> CharaDataDownload(string id)
{
CharaData charaData = await GetCharaDataById(id, nameof(CharaDataDownload)).ConfigureAwait(false);
if (!string.Equals(charaData.UploaderUID, UserUID, StringComparison.Ordinal))
{
charaData.DownloadCount++;
await DbContext.SaveChangesAsync().ConfigureAwait(false);
}
_logger.LogCallInfo(MareHubLogger.Args("SUCCESS", id));
return GetCharaDataDownloadDto(charaData);
}
[Authorize(Policy = "Identified")]
public async Task<CharaDataMetaInfoDto?> CharaDataGetMetainfo(string id)
{
var charaData = await GetCharaDataById(id, nameof(CharaDataGetMetainfo)).ConfigureAwait(false);
_logger.LogCallInfo(MareHubLogger.Args("SUCCESS", id));
return GetCharaDataMetaInfoDto(charaData);
}
[Authorize(Policy = "Identified")]
public async Task<List<CharaDataFullDto>> CharaDataGetOwn()
{
var ownCharaData = await DbContext.CharaData
.Include(u => u.Files)
.Include(u => u.FileSwaps)
.Include(u => u.OriginalFiles)
.Include(u => u.AllowedIndividiuals)
.ThenInclude(u => u.AllowedUser)
.Include(u => u.AllowedIndividiuals)
.ThenInclude(u => u.AllowedGroup)
.Include(u => u.Poses)
.AsSplitQuery()
.Where(c => c.UploaderUID == UserUID).ToListAsync().ConfigureAwait(false);
_logger.LogCallInfo(MareHubLogger.Args("SUCCESS"));
return [.. ownCharaData.Select(GetCharaDataFullDto)];
}
[Authorize(Policy = "Identified")]
public async Task<CharaDataFullDto?> CharaDataAttemptRestore(string id)
{
_logger.LogCallInfo(MareHubLogger.Args(id));
var charaData = await DbContext.CharaData
.Include(u => u.Files)
.Include(u => u.FileSwaps)
.Include(u => u.OriginalFiles)
.Include(u => u.AllowedIndividiuals)
.ThenInclude(u => u.AllowedUser)
.Include(u => u.AllowedIndividiuals)
.ThenInclude(u => u.AllowedGroup)
.Include(u => u.Poses)
.AsSplitQuery()
.SingleOrDefaultAsync(s => s.Id == id && s.UploaderUID == UserUID)
.ConfigureAwait(false);
if (charaData == null)
return null;
var currentHashes = charaData.Files.Select(f => f.FileCacheHash).ToList();
var missingFiles = charaData.OriginalFiles.Where(c => !currentHashes.Contains(c.Hash, StringComparer.Ordinal)).ToList();
// now let's see what's on the db still
var existingDbFiles = await DbContext.Files
.Where(f => missingFiles.Select(k => k.Hash).Distinct().Contains(f.Hash))
.ToListAsync()
.ConfigureAwait(false);
// now shove it all back into the db
foreach (var dbFile in existingDbFiles)
{
var missingFileEntry = missingFiles.First(f => string.Equals(f.Hash, dbFile.Hash, StringComparison.Ordinal));
charaData.Files.Add(new CharaDataFile()
{
FileCache = dbFile,
GamePath = missingFileEntry.GamePath,
Parent = charaData
});
missingFiles.Remove(missingFileEntry);
}
if (existingDbFiles.Any())
{
await DbContext.SaveChangesAsync().ConfigureAwait(false);
}
return GetCharaDataFullDto(charaData);
}
[Authorize(Policy = "Identified")]
public async Task<List<CharaDataMetaInfoDto>> CharaDataGetShared()
{
_logger.LogCallInfo();
List<CharaData> sharedCharaData = [];
var groups = await DbContext.GroupPairs
.Where(u => u.GroupUserUID == UserUID)
.Select(k => k.GroupGID)
.AsNoTracking()
.ToListAsync()
.ConfigureAwait(false);
var individualPairs = await GetDirectPairedUnpausedUsers().ConfigureAwait(false);
var allPairs = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
var allSharedDataByPair = await DbContext.CharaData
.Include(u => u.Files)
.Include(u => u.OriginalFiles)
.Include(u => u.AllowedIndividiuals)
.Include(u => u.Poses)
.Include(u => u.Uploader)
.Where(p => p.UploaderUID != UserUID && p.ShareType == CharaDataShare.Shared)
.Where(p =>
(individualPairs.Contains(p.UploaderUID) && p.AccessType == CharaDataAccess.ClosePairs)
|| (allPairs.Contains(p.UploaderUID) && p.AccessType == CharaDataAccess.AllPairs)
|| (p.AllowedIndividiuals.Any(u => u.AllowedUserUID == UserUID || (u.AllowedGroupGID != null && groups.Contains(u.AllowedGroupGID)))))
.AsSplitQuery()
.AsNoTracking()
.ToListAsync()
.ConfigureAwait(false);
foreach (var charaData in allSharedDataByPair)
{
sharedCharaData.Add(charaData);
}
_logger.LogCallInfo(MareHubLogger.Args("SUCCESS", sharedCharaData.Count));
return [.. sharedCharaData.Select(GetCharaDataMetaInfoDto)];
}
[Authorize(Policy = "Identified")]
public async Task<CharaDataFullDto?> CharaDataUpdate(CharaDataUpdateDto updateDto)
{
var charaData = await DbContext.CharaData
.Include(u => u.Files)
.Include(u => u.OriginalFiles)
.Include(u => u.AllowedIndividiuals)
.ThenInclude(u => u.AllowedUser)
.Include(u => u.AllowedIndividiuals)
.ThenInclude(u => u.AllowedGroup)
.Include(u => u.FileSwaps)
.Include(u => u.Poses)
.AsSplitQuery()
.SingleOrDefaultAsync(u => u.Id == updateDto.Id && u.UploaderUID == UserUID).ConfigureAwait(false);
if (charaData == null)
return null;
bool anyChanges = false;
if (updateDto.Description != null)
{
charaData.Description = updateDto.Description;
anyChanges = true;
}
if (updateDto.ExpiryDate != null)
{
charaData.ExpiryDate = updateDto.ExpiryDate;
anyChanges = true;
}
if (updateDto.GlamourerData != null)
{
charaData.GlamourerData = updateDto.GlamourerData;
anyChanges = true;
}
if (updateDto.CustomizeData != null)
{
charaData.CustomizeData = updateDto.CustomizeData;
anyChanges = true;
}
if (updateDto.ManipulationData != null)
{
charaData.ManipulationData = updateDto.ManipulationData;
anyChanges = true;
}
if (updateDto.AccessType != null)
{
charaData.AccessType = GetAccessType(updateDto.AccessType.Value);
anyChanges = true;
}
if (updateDto.ShareType != null)
{
charaData.ShareType = GetShareType(updateDto.ShareType.Value);
anyChanges = true;
}
if (updateDto.AllowedUsers != null)
{
var individuals = charaData.AllowedIndividiuals.Where(k => k.AllowedGroup == null).ToList();
var allowedUserList = updateDto.AllowedUsers.ToList();
foreach (var user in updateDto.AllowedUsers)
{
if (charaData.AllowedIndividiuals.Any(k => k.AllowedUser != null && (string.Equals(k.AllowedUser.UID, user, StringComparison.Ordinal) || string.Equals(k.AllowedUser.Alias, user, StringComparison.Ordinal))))
{
continue;
}
else
{
var dbUser = await DbContext.Users.SingleOrDefaultAsync(u => u.UID == user || u.Alias == user).ConfigureAwait(false);
if (dbUser != null)
{
charaData.AllowedIndividiuals.Add(new CharaDataAllowance()
{
AllowedUser = dbUser,
Parent = charaData
});
}
}
}
foreach (var dataUser in individuals.Where(k => !updateDto.AllowedUsers.Contains(k.AllowedUser.UID, StringComparer.Ordinal) && !updateDto.AllowedUsers.Contains(k.AllowedUser.Alias, StringComparer.Ordinal)))
{
DbContext.Remove(dataUser);
charaData.AllowedIndividiuals.Remove(dataUser);
}
anyChanges = true;
}
if (updateDto.AllowedGroups != null)
{
var individualGroups = charaData.AllowedIndividiuals.Where(k => k.AllowedUser == null).ToList();
var allowedGroups = updateDto.AllowedGroups.ToList();
foreach (var group in updateDto.AllowedGroups)
{
if (charaData.AllowedIndividiuals.Any(k => k.AllowedGroup != null && (string.Equals(k.AllowedGroup.GID, group, StringComparison.Ordinal) || string.Equals(k.AllowedGroup.Alias, group, StringComparison.Ordinal))))
{
continue;
}
else
{
var groupUser = await DbContext.GroupPairs.Include(u => u.Group).SingleOrDefaultAsync(u => (u.Group.GID == group || u.Group.Alias == group) && u.GroupUserUID == UserUID).ConfigureAwait(false);
if (groupUser != null)
{
charaData.AllowedIndividiuals.Add(new CharaDataAllowance()
{
AllowedGroup = groupUser.Group,
Parent = charaData
});
}
}
}
foreach (var dataGroup in individualGroups.Where(k => !updateDto.AllowedGroups.Contains(k.AllowedGroup.GID, StringComparer.Ordinal) && !updateDto.AllowedGroups.Contains(k.AllowedGroup.Alias, StringComparer.Ordinal)))
{
DbContext.Remove(dataGroup);
charaData.AllowedIndividiuals.Remove(dataGroup);
}
anyChanges = true;
}
if (updateDto.FileGamePaths != null)
{
var originalFiles = charaData.OriginalFiles.ToList();
charaData.OriginalFiles.Clear();
DbContext.RemoveRange(originalFiles);
var files = charaData.Files.ToList();
charaData.Files.Clear();
DbContext.RemoveRange(files);
foreach (var file in updateDto.FileGamePaths)
{
charaData.Files.Add(new CharaDataFile()
{
FileCacheHash = file.HashOrFileSwap,
GamePath = file.GamePath,
Parent = charaData
});
charaData.OriginalFiles.Add(new CharaDataOriginalFile()
{
Hash = file.HashOrFileSwap,
Parent = charaData,
GamePath = file.GamePath
});
}
anyChanges = true;
}
if (updateDto.FileSwaps != null)
{
var fileSwaps = charaData.FileSwaps.ToList();
charaData.FileSwaps.Clear();
DbContext.RemoveRange(fileSwaps);
foreach (var file in updateDto.FileSwaps)
{
charaData.FileSwaps.Add(new CharaDataFileSwap()
{
FilePath = file.HashOrFileSwap,
GamePath = file.GamePath,
Parent = charaData
});
}
anyChanges = true;
}
if (updateDto.Poses != null)
{
foreach (var pose in updateDto.Poses)
{
if (pose.Id == null)
{
charaData.Poses.Add(new CharaDataPose()
{
Description = pose.Description,
Parent = charaData,
ParentUploaderUID = UserUID,
PoseData = pose.PoseData,
WorldData = pose.WorldData == null ? string.Empty : JsonSerializer.Serialize(pose.WorldData),
});
anyChanges = true;
}
else
{
var associatedPose = charaData.Poses.FirstOrDefault(p => p.Id == pose.Id);
if (associatedPose == null)
continue;
if (pose.Description == null && pose.PoseData == null && pose.WorldData == null)
{
charaData.Poses.Remove(associatedPose);
DbContext.Remove(associatedPose);
}
else
{
if (pose.Description != null)
associatedPose.Description = pose.Description;
if (pose.WorldData != null)
{
if (pose.WorldData.Value == default) associatedPose.WorldData = string.Empty;
else associatedPose.WorldData = JsonSerializer.Serialize(pose.WorldData.Value);
}
if (pose.PoseData != null)
associatedPose.PoseData = pose.PoseData;
}
anyChanges = true;
}
var overflowingPoses = charaData.Poses.Skip(10).ToList();
foreach (var overflowing in overflowingPoses)
{
charaData.Poses.Remove(overflowing);
DbContext.Remove(overflowing);
}
}
}
if (anyChanges)
{
charaData.UpdatedDate = DateTime.UtcNow;
await DbContext.SaveChangesAsync().ConfigureAwait(false);
_logger.LogCallInfo(MareHubLogger.Args("SUCCESS", anyChanges));
}
return GetCharaDataFullDto(charaData);
}
private static CharaDataAccess GetAccessType(AccessTypeDto dataAccess) => dataAccess switch
{
AccessTypeDto.Public => CharaDataAccess.Public,
AccessTypeDto.AllPairs => CharaDataAccess.AllPairs,
AccessTypeDto.ClosePairs => CharaDataAccess.ClosePairs,
AccessTypeDto.Individuals => CharaDataAccess.Individuals,
_ => throw new NotSupportedException(),
};
private static AccessTypeDto GetAccessTypeDto(CharaDataAccess dataAccess) => dataAccess switch
{
CharaDataAccess.Public => AccessTypeDto.Public,
CharaDataAccess.AllPairs => AccessTypeDto.AllPairs,
CharaDataAccess.ClosePairs => AccessTypeDto.ClosePairs,
CharaDataAccess.Individuals => AccessTypeDto.Individuals,
_ => throw new NotSupportedException(),
};
private static CharaDataDownloadDto GetCharaDataDownloadDto(CharaData charaData)
{
return new CharaDataDownloadDto(charaData.Id, charaData.Uploader.ToUserData())
{
CustomizeData = charaData.CustomizeData,
Description = charaData.Description,
FileGamePaths = charaData.Files.Select(k => new GamePathEntry(k.FileCacheHash, k.GamePath)).ToList(),
GlamourerData = charaData.GlamourerData,
FileSwaps = charaData.FileSwaps.Select(k => new GamePathEntry(k.FilePath, k.GamePath)).ToList(),
ManipulationData = charaData.ManipulationData,
};
}
private CharaDataFullDto GetCharaDataFullDto(CharaData charaData)
{
return new CharaDataFullDto(charaData.Id, new(UserUID))
{
AccessType = GetAccessTypeDto(charaData.AccessType),
ShareType = GetShareTypeDto(charaData.ShareType),
AllowedUsers = [.. charaData.AllowedIndividiuals.Where(k => !string.IsNullOrEmpty(k.AllowedUserUID)).Select(u => new UserData(u.AllowedUser.UID, u.AllowedUser.Alias))],
AllowedGroups = [.. charaData.AllowedIndividiuals.Where(k => !string.IsNullOrEmpty(k.AllowedGroupGID)).Select(k => new GroupData(k.AllowedGroup.GID, k.AllowedGroup.Alias))],
CustomizeData = charaData.CustomizeData,
Description = charaData.Description,
ExpiryDate = charaData.ExpiryDate ?? DateTime.MaxValue,
OriginalFiles = charaData.OriginalFiles.Select(k => new GamePathEntry(k.Hash, k.GamePath)).ToList(),
FileGamePaths = charaData.Files.Select(k => new GamePathEntry(k.FileCacheHash, k.GamePath)).ToList(),
FileSwaps = charaData.FileSwaps.Select(k => new GamePathEntry(k.FilePath, k.GamePath)).ToList(),
GlamourerData = charaData.GlamourerData,
CreatedDate = charaData.CreatedDate,
UpdatedDate = charaData.UpdatedDate,
ManipulationData = charaData.ManipulationData,
DownloadCount = charaData.DownloadCount,
PoseData = [.. charaData.Poses.OrderBy(p => p.Id).Select(k =>
{
WorldData data = default;
if(!string.IsNullOrEmpty(k.WorldData)) data = JsonSerializer.Deserialize<WorldData>(k.WorldData);
return new PoseEntry(k.Id)
{
Description = k.Description,
PoseData = k.PoseData,
WorldData = data
};
})],
};
}
private static CharaDataMetaInfoDto GetCharaDataMetaInfoDto(CharaData charaData)
{
var allOrigHashes = charaData.OriginalFiles.Select(k => k.Hash).ToList();
var allFileHashes = charaData.Files.Select(f => f.FileCacheHash).ToList();
var allHashesPresent = allOrigHashes.TrueForAll(h => allFileHashes.Contains(h, StringComparer.Ordinal));
var canBeDownloaded = allHashesPresent &= !string.IsNullOrEmpty(charaData.GlamourerData);
return new CharaDataMetaInfoDto(charaData.Id, charaData.Uploader.ToUserData())
{
CanBeDownloaded = canBeDownloaded,
Description = charaData.Description,
UpdatedDate = charaData.UpdatedDate,
PoseData = [.. charaData.Poses.OrderBy(p => p.Id).Select(k =>
{
WorldData data = default;
if(!string.IsNullOrEmpty(k.WorldData)) data = JsonSerializer.Deserialize<WorldData>(k.WorldData);
return new PoseEntry(k.Id)
{
Description = k.Description,
PoseData = k.PoseData,
WorldData = data
};
})],
};
}
private static CharaDataShare GetShareType(ShareTypeDto dataShare) => dataShare switch
{
ShareTypeDto.Shared => CharaDataShare.Shared,
ShareTypeDto.Private => CharaDataShare.Private,
_ => throw new NotSupportedException(),
};
private static ShareTypeDto GetShareTypeDto(CharaDataShare dataShare) => dataShare switch
{
CharaDataShare.Shared => ShareTypeDto.Shared,
CharaDataShare.Private => ShareTypeDto.Private,
_ => throw new NotSupportedException(),
};
private async Task<bool> CheckCharaDataAllowance(CharaData charaData, List<string> joinedGroups)
{
// check for self
if (string.Equals(charaData.UploaderUID, UserUID, StringComparison.Ordinal))
return true;
// check for public access
if (charaData.AccessType == CharaDataAccess.Public)
return true;
// check for individuals
if (charaData.AllowedIndividiuals.Any(u => string.Equals(u.AllowedUserUID, UserUID, StringComparison.Ordinal)))
return true;
var pairInfoUploader = await GetAllPairedUnpausedUsers(charaData.UploaderUID).ConfigureAwait(false);
// check for all pairs
if (charaData.AccessType == CharaDataAccess.AllPairs)
{
if (pairInfoUploader.Any(pair => string.Equals(pair, UserUID, StringComparison.Ordinal)))
return true;
return false;
}
// check for individual pairs
if (charaData.AccessType == CharaDataAccess.ClosePairs)
{
if (pairInfoUploader.Any(pair => string.Equals(pair, UserUID, StringComparison.Ordinal)))
{
ClientPair callerPair =
await DbContext.ClientPairs.AsNoTracking().SingleOrDefaultAsync(w => w.UserUID == UserUID && w.OtherUserUID == charaData.UploaderUID).ConfigureAwait(false);
ClientPair uploaderPair =
await DbContext.ClientPairs.AsNoTracking().SingleOrDefaultAsync(w => w.UserUID == charaData.UploaderUID && w.OtherUserUID == UserUID).ConfigureAwait(false);
return (callerPair != null && uploaderPair != null);
}
return false;
}
return false;
}
private async Task<CharaData> GetCharaDataById(string id, string methodName)
{
var splitid = id.Split(":", StringSplitOptions.None);
if (splitid.Length != 2)
{
_logger.LogCallWarning(MareHubLogger.Args("INVALID", id));
throw new InvalidOperationException($"Id {id} not in expected format");
}
var charaData = await DbContext.CharaData
.Include(u => u.Files)
.Include(u => u.FileSwaps)
.Include(u => u.AllowedIndividiuals)
.Include(u => u.Poses)
.Include(u => u.Uploader)
.AsSplitQuery()
.SingleOrDefaultAsync(c => c.Id == splitid[1] && c.UploaderUID == splitid[0]).ConfigureAwait(false);
if (charaData == null)
{
_logger.LogCallWarning(MareHubLogger.Args("NOT FOUND", id));
throw new InvalidDataException($"No chara data with {id} found");
}
var groups = await DbContext.GroupPairs.Where(u => u.GroupUserUID == UserUID).Select(k => k.GroupGID).ToListAsync()
.ConfigureAwait(false);
if (!await CheckCharaDataAllowance(charaData, groups).ConfigureAwait(false))
{
_logger.LogCallWarning(MareHubLogger.Args("UNAUTHORIZED", id));
throw new UnauthorizedAccessException($"User is not allowed to download {id}");
}
return charaData;
}
}

View File

@@ -1,52 +0,0 @@
using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.API.Dto.Group;
using MareSynchronos.API.Dto.User;
using MareSynchronosServer.Utils;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
namespace MareSynchronosServer.Hubs;
public partial class MareHub
{
[Authorize(Policy = "Identified")]
public Task UserChatSendMsg(UserDto dto, ChatMessage message)
{
_logger.LogCallInfo(MareHubLogger.Args(dto));
// TODO
return Task.CompletedTask;
}
[Authorize(Policy = "Identified")]
public async Task GroupChatSendMsg(GroupDto dto, ChatMessage message)
{
_logger.LogCallInfo(MareHubLogger.Args(dto));
var (userExists, groupPair) = await TryValidateUserInGroup(dto.GID, UserUID).ConfigureAwait(false);
if (!userExists) return;
var group = await DbContext.Groups.AsNoTracking().SingleAsync(g => g.GID == dto.GID).ConfigureAwait(false);
var sender = await DbContext.Users.AsNoTracking().SingleAsync(u => u.UID == UserUID).ConfigureAwait(false);
var groupPairs = await DbContext.GroupPairs.AsNoTracking().Include(p => p.GroupUser).Where(p => p.GroupGID == dto.Group.GID).ToListAsync().ConfigureAwait(false);
if (group == null || sender == null) return;
// TODO: Add and check chat permissions
if (group.Alias?.Equals("Umbra", StringComparison.Ordinal) ?? false)
{
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, $"Chat is disabled for syncshell '{dto.GroupAliasOrGID}'.").ConfigureAwait(false);
return;
}
// TOOO: Sign the message
var signedMessage = new SignedChatMessage(message, sender.ToUserData())
{
Timestamp = 0,
Signature = "",
};
await Clients.Users(groupPairs.Select(p => p.GroupUserUID)).Client_GroupChatMsg(new(new(group.ToGroupData()), signedMessage)).ConfigureAwait(false);
}
}

Some files were not shown because too many files have changed in this diff Show More