Compare commits
10 Commits
f696c1d400
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
d105d20507
|
|||
|
f817187088
|
|||
|
7728ef2390
|
|||
|
f75f16fb13
|
|||
|
deb911cb0a
|
|||
|
0abb078c21
|
|||
|
5fc7969adf
|
|||
|
3b175900c1
|
|||
|
fdc0f04cbb
|
|||
|
9c087cb4f8
|
1
MareAPI
1
MareAPI
Submodule MareAPI deleted from fa9b7bce43
36
MareSynchronosAPI/Data/CharacterData.cs
Normal file
36
MareSynchronosAPI/Data/CharacterData.cs
Normal 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;
|
||||
}
|
||||
11
MareSynchronosAPI/Data/ChatMessage.cs
Normal file
11
MareSynchronosAPI/Data/ChatMessage.cs
Normal 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; } = [];
|
||||
}
|
||||
19
MareSynchronosAPI/Data/Comparer/GroupDataComparer.cs
Normal file
19
MareSynchronosAPI/Data/Comparer/GroupDataComparer.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
23
MareSynchronosAPI/Data/Comparer/GroupDtoComparer.cs
Normal file
23
MareSynchronosAPI/Data/Comparer/GroupDtoComparer.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
20
MareSynchronosAPI/Data/Comparer/GroupPairDtoComparer.cs
Normal file
20
MareSynchronosAPI/Data/Comparer/GroupPairDtoComparer.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
20
MareSynchronosAPI/Data/Comparer/UserDataComparer.cs
Normal file
20
MareSynchronosAPI/Data/Comparer/UserDataComparer.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
20
MareSynchronosAPI/Data/Comparer/UserDtoComparer.cs
Normal file
20
MareSynchronosAPI/Data/Comparer/UserDtoComparer.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
11
MareSynchronosAPI/Data/Enum/GroupPermissions.cs
Normal file
11
MareSynchronosAPI/Data/Enum/GroupPermissions.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace MareSynchronos.API.Data.Enum;
|
||||
|
||||
[Flags]
|
||||
public enum GroupPermissions
|
||||
{
|
||||
NoneSet = 0x0,
|
||||
DisableAnimations = 0x1,
|
||||
DisableSounds = 0x2,
|
||||
DisableInvites = 0x4,
|
||||
DisableVFX = 0x8,
|
||||
}
|
||||
9
MareSynchronosAPI/Data/Enum/GroupUserInfo.cs
Normal file
9
MareSynchronosAPI/Data/Enum/GroupUserInfo.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace MareSynchronos.API.Data.Enum;
|
||||
|
||||
[Flags]
|
||||
public enum GroupUserInfo
|
||||
{
|
||||
None = 0x0,
|
||||
IsModerator = 0x2,
|
||||
IsPinned = 0x4
|
||||
}
|
||||
11
MareSynchronosAPI/Data/Enum/GroupUserPermissions.cs
Normal file
11
MareSynchronosAPI/Data/Enum/GroupUserPermissions.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace MareSynchronos.API.Data.Enum;
|
||||
|
||||
[Flags]
|
||||
public enum GroupUserPermissions
|
||||
{
|
||||
NoneSet = 0x0,
|
||||
Paused = 0x1,
|
||||
DisableAnimations = 0x2,
|
||||
DisableSounds = 0x4,
|
||||
DisableVFX = 0x8,
|
||||
}
|
||||
8
MareSynchronosAPI/Data/Enum/MessageSeverity.cs
Normal file
8
MareSynchronosAPI/Data/Enum/MessageSeverity.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace MareSynchronos.API.Data.Enum;
|
||||
|
||||
public enum MessageSeverity
|
||||
{
|
||||
Information,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
9
MareSynchronosAPI/Data/Enum/ObjectKind.cs
Normal file
9
MareSynchronosAPI/Data/Enum/ObjectKind.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace MareSynchronos.API.Data.Enum;
|
||||
|
||||
public enum ObjectKind
|
||||
{
|
||||
Player = 0,
|
||||
MinionOrMount = 1,
|
||||
Companion = 2,
|
||||
Pet = 3,
|
||||
}
|
||||
9
MareSynchronosAPI/Data/Enum/TypingScope.cs
Normal file
9
MareSynchronosAPI/Data/Enum/TypingScope.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace MareSynchronos.API.Data.Enum;
|
||||
|
||||
public enum TypingScope
|
||||
{
|
||||
Unknown = 0,
|
||||
Proximity = 1, // Parler/Crier/Hurler
|
||||
Party = 2,
|
||||
CrossParty = 3
|
||||
}
|
||||
12
MareSynchronosAPI/Data/Enum/UserPermissions.cs
Normal file
12
MareSynchronosAPI/Data/Enum/UserPermissions.cs
Normal 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,
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
28
MareSynchronosAPI/Data/Extensions/GroupUserInfoExtensions.cs
Normal file
28
MareSynchronosAPI/Data/Extensions/GroupUserInfoExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
30
MareSynchronosAPI/Data/FileReplacementData.cs
Normal file
30
MareSynchronosAPI/Data/FileReplacementData.cs
Normal 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;
|
||||
}
|
||||
10
MareSynchronosAPI/Data/GroupData.cs
Normal file
10
MareSynchronosAPI/Data/GroupData.cs
Normal 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;
|
||||
}
|
||||
14
MareSynchronosAPI/Data/SignedChatMessage.cs
Normal file
14
MareSynchronosAPI/Data/SignedChatMessage.cs
Normal 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;
|
||||
}
|
||||
10
MareSynchronosAPI/Data/UserData.cs
Normal file
10
MareSynchronosAPI/Data/UserData.cs
Normal 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;
|
||||
}
|
||||
12
MareSynchronosAPI/Dto/Account/RegisterReplyDto.cs
Normal file
12
MareSynchronosAPI/Dto/Account/RegisterReplyDto.cs
Normal 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;
|
||||
}
|
||||
11
MareSynchronosAPI/Dto/Account/RegisterReplyV2Dto.cs
Normal file
11
MareSynchronosAPI/Dto/Account/RegisterReplyV2Dto.cs
Normal 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;
|
||||
}
|
||||
11
MareSynchronosAPI/Dto/AuthReplyDto.cs
Normal file
11
MareSynchronosAPI/Dto/AuthReplyDto.cs
Normal 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; }
|
||||
}
|
||||
9
MareSynchronosAPI/Dto/CharaData/AccessTypeDto.cs
Normal file
9
MareSynchronosAPI/Dto/CharaData/AccessTypeDto.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace MareSynchronos.API.Dto.CharaData;
|
||||
|
||||
public enum AccessTypeDto
|
||||
{
|
||||
Individuals,
|
||||
ClosePairs,
|
||||
AllPairs,
|
||||
Public
|
||||
}
|
||||
14
MareSynchronosAPI/Dto/CharaData/CharaDataDownloadDto.cs
Normal file
14
MareSynchronosAPI/Dto/CharaData/CharaDataDownloadDto.cs
Normal 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; } = [];
|
||||
}
|
||||
9
MareSynchronosAPI/Dto/CharaData/CharaDataDto.cs
Normal file
9
MareSynchronosAPI/Dto/CharaData/CharaDataDto.cs
Normal 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; }
|
||||
}
|
||||
88
MareSynchronosAPI/Dto/CharaData/CharaDataFullDto.cs
Normal file
88
MareSynchronosAPI/Dto/CharaData/CharaDataFullDto.cs
Normal 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; }
|
||||
}
|
||||
11
MareSynchronosAPI/Dto/CharaData/CharaDataMetaInfoDto.cs
Normal file
11
MareSynchronosAPI/Dto/CharaData/CharaDataMetaInfoDto.cs
Normal 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; } = [];
|
||||
}
|
||||
20
MareSynchronosAPI/Dto/CharaData/CharaDataUpdateDto.cs
Normal file
20
MareSynchronosAPI/Dto/CharaData/CharaDataUpdateDto.cs
Normal 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; }
|
||||
}
|
||||
7
MareSynchronosAPI/Dto/CharaData/ShareTypeDto.cs
Normal file
7
MareSynchronosAPI/Dto/CharaData/ShareTypeDto.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace MareSynchronos.API.Dto.CharaData;
|
||||
|
||||
public enum ShareTypeDto
|
||||
{
|
||||
Private,
|
||||
Shared
|
||||
}
|
||||
13
MareSynchronosAPI/Dto/Chat/GroupChatMsgDto.cs
Normal file
13
MareSynchronosAPI/Dto/Chat/GroupChatMsgDto.cs
Normal 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;
|
||||
}
|
||||
11
MareSynchronosAPI/Dto/Chat/UserChatMsgDto.cs
Normal file
11
MareSynchronosAPI/Dto/Chat/UserChatMsgDto.cs
Normal 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;
|
||||
}
|
||||
25
MareSynchronosAPI/Dto/ConnectionDto.cs
Normal file
25
MareSynchronosAPI/Dto/ConnectionDto.cs
Normal 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; }
|
||||
}
|
||||
14
MareSynchronosAPI/Dto/Files/DownloadFileDto.cs
Normal file
14
MareSynchronosAPI/Dto/Files/DownloadFileDto.cs
Normal 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;
|
||||
}
|
||||
13
MareSynchronosAPI/Dto/Files/FilesSendDto.cs
Normal file
13
MareSynchronosAPI/Dto/Files/FilesSendDto.cs
Normal 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();
|
||||
}
|
||||
8
MareSynchronosAPI/Dto/Files/ITransferFileDto.cs
Normal file
8
MareSynchronosAPI/Dto/Files/ITransferFileDto.cs
Normal 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; }
|
||||
}
|
||||
11
MareSynchronosAPI/Dto/Files/UploadFileDto.cs
Normal file
11
MareSynchronosAPI/Dto/Files/UploadFileDto.cs
Normal 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;
|
||||
}
|
||||
19
MareSynchronosAPI/Dto/Group/BannedGroupUserDto.cs
Normal file
19
MareSynchronosAPI/Dto/Group/BannedGroupUserDto.cs
Normal 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; }
|
||||
}
|
||||
13
MareSynchronosAPI/Dto/Group/GroupDto.cs
Normal file
13
MareSynchronosAPI/Dto/Group/GroupDto.cs
Normal 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;
|
||||
}
|
||||
12
MareSynchronosAPI/Dto/Group/GroupFullInfoDto.cs
Normal file
12
MareSynchronosAPI/Dto/Group/GroupFullInfoDto.cs
Normal 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;
|
||||
}
|
||||
22
MareSynchronosAPI/Dto/Group/GroupInfoDto.cs
Normal file
22
MareSynchronosAPI/Dto/Group/GroupInfoDto.cs
Normal 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; }
|
||||
}
|
||||
12
MareSynchronosAPI/Dto/Group/GroupPairDto.cs
Normal file
12
MareSynchronosAPI/Dto/Group/GroupPairDto.cs
Normal 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;
|
||||
}
|
||||
12
MareSynchronosAPI/Dto/Group/GroupPairFullInfoDto.cs
Normal file
12
MareSynchronosAPI/Dto/Group/GroupPairFullInfoDto.cs
Normal 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;
|
||||
}
|
||||
8
MareSynchronosAPI/Dto/Group/GroupPairUserInfoDto.cs
Normal file
8
MareSynchronosAPI/Dto/Group/GroupPairUserInfoDto.cs
Normal 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);
|
||||
@@ -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);
|
||||
12
MareSynchronosAPI/Dto/Group/GroupPasswordDto.cs
Normal file
12
MareSynchronosAPI/Dto/Group/GroupPasswordDto.cs
Normal 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; }
|
||||
}
|
||||
8
MareSynchronosAPI/Dto/Group/GroupPermissionDto.cs
Normal file
8
MareSynchronosAPI/Dto/Group/GroupPermissionDto.cs
Normal 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);
|
||||
15
MareSynchronosAPI/Dto/Group/SyncshellDiscoveryEntryDto.cs
Normal file
15
MareSynchronosAPI/Dto/Group/SyncshellDiscoveryEntryDto.cs
Normal 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; }
|
||||
}
|
||||
11
MareSynchronosAPI/Dto/Group/SyncshellDiscoveryStateDto.cs
Normal file
11
MareSynchronosAPI/Dto/Group/SyncshellDiscoveryStateDto.cs
Normal 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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
21
MareSynchronosAPI/Dto/McdfShare/McdfShareEntryDto.cs
Normal file
21
MareSynchronosAPI/Dto/McdfShare/McdfShareEntryDto.cs
Normal 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; } = [];
|
||||
}
|
||||
17
MareSynchronosAPI/Dto/McdfShare/McdfSharePayloadDto.cs
Normal file
17
MareSynchronosAPI/Dto/McdfShare/McdfSharePayloadDto.cs
Normal 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; }
|
||||
}
|
||||
15
MareSynchronosAPI/Dto/McdfShare/McdfShareUpdateRequestDto.cs
Normal file
15
MareSynchronosAPI/Dto/McdfShare/McdfShareUpdateRequestDto.cs
Normal 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; } = [];
|
||||
}
|
||||
19
MareSynchronosAPI/Dto/McdfShare/McdfShareUploadRequestDto.cs
Normal file
19
MareSynchronosAPI/Dto/McdfShare/McdfShareUploadRequestDto.cs
Normal 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; } = [];
|
||||
}
|
||||
10
MareSynchronosAPI/Dto/SystemInfoDto.cs
Normal file
10
MareSynchronosAPI/Dto/SystemInfoDto.cs
Normal 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; }
|
||||
}
|
||||
7
MareSynchronosAPI/Dto/User/OnlineUserCharaDataDto.cs
Normal file
7
MareSynchronosAPI/Dto/User/OnlineUserCharaDataDto.cs
Normal 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);
|
||||
7
MareSynchronosAPI/Dto/User/OnlineUserIdentDto.cs
Normal file
7
MareSynchronosAPI/Dto/User/OnlineUserIdentDto.cs
Normal 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);
|
||||
13
MareSynchronosAPI/Dto/User/TypingStateDto.cs
Normal file
13
MareSynchronosAPI/Dto/User/TypingStateDto.cs
Normal 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;
|
||||
}
|
||||
7
MareSynchronosAPI/Dto/User/UserCharaDataMessageDto.cs
Normal file
7
MareSynchronosAPI/Dto/User/UserCharaDataMessageDto.cs
Normal 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);
|
||||
7
MareSynchronosAPI/Dto/User/UserDto.cs
Normal file
7
MareSynchronosAPI/Dto/User/UserDto.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using MareSynchronos.API.Data;
|
||||
using MessagePack;
|
||||
|
||||
namespace MareSynchronos.API.Dto.User;
|
||||
|
||||
[MessagePackObject(keyAsPropertyName: true)]
|
||||
public record UserDto(UserData User);
|
||||
12
MareSynchronosAPI/Dto/User/UserPairDto.cs
Normal file
12
MareSynchronosAPI/Dto/User/UserPairDto.cs
Normal 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;
|
||||
}
|
||||
8
MareSynchronosAPI/Dto/User/UserPermissionsDto.cs
Normal file
8
MareSynchronosAPI/Dto/User/UserPermissionsDto.cs
Normal 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);
|
||||
7
MareSynchronosAPI/Dto/User/UserProfileDto.cs
Normal file
7
MareSynchronosAPI/Dto/User/UserProfileDto.cs
Normal 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);
|
||||
7
MareSynchronosAPI/Dto/User/UserProfileReportDto.cs
Normal file
7
MareSynchronosAPI/Dto/User/UserProfileReportDto.cs
Normal 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);
|
||||
14
MareSynchronosAPI/MareSynchronos.API.csproj
Normal file
14
MareSynchronosAPI/MareSynchronos.API.csproj
Normal 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>
|
||||
25
MareSynchronosAPI/MareSynchronosAPI.sln
Normal file
25
MareSynchronosAPI/MareSynchronosAPI.sln
Normal 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
|
||||
14
MareSynchronosAPI/Routes/MareAuth.cs
Normal file
14
MareSynchronosAPI/Routes/MareAuth.cs
Normal 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);
|
||||
}
|
||||
45
MareSynchronosAPI/Routes/MareFiles.cs
Normal file
45
MareSynchronosAPI/Routes/MareFiles.cs
Normal 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());
|
||||
}
|
||||
150
MareSynchronosAPI/SignalR/IMareHub.cs
Normal file
150
MareSynchronosAPI/SignalR/IMareHub.cs
Normal 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);
|
||||
}
|
||||
64
MareSynchronosAPI/SignalR/IMareHubClient.cs
Normal file
64
MareSynchronosAPI/SignalR/IMareHubClient.cs
Normal 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);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
[*.cs]
|
||||
|
||||
# MA0048: File name must match type name
|
||||
dotnet_diagnostic.MA0048.severity = suggestion
|
||||
@@ -1,3 +0,0 @@
|
||||
namespace MareSynchronosAuthService.Authentication;
|
||||
|
||||
public record SecretKeyAuthReply(bool Success, string Uid, string Alias, bool TempBan, bool Permaban);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "6.0.9",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user