Compare commits

...

21 Commits

Author SHA1 Message Date
b231d80101 "Ajout de la gestion des états de frappe avec TypingScope et d'une nouvelle méthode UserSetTypingState." 2025-11-02 17:35:20 +01:00
f5d2a95b97 MareAPI link 2025-11-02 12:31:47 +01:00
97c009aef6 "Add MessagePack annotations to MCDF Share DTOs for serialization support" 2025-11-02 12:25:12 +01:00
8527e67af1 Hotfix Migration failed 2025-11-02 10:50:15 +01:00
073464e6c8 Update MareAPI 2025-11-02 00:26:52 +01:00
b2b9ecad7b Préparation feature 2.0 2025-11-01 22:46:55 +01:00
e484616ecd Fix TypingDetection 2025-10-04 09:59:30 +02:00
331cb612ad Préparation serveur pour TypingState 2025-09-28 22:32:05 +02:00
f696c1d400 Ajout healthCheck pour le serveur Auth 2025-09-28 10:57:45 +02:00
cd5248e4e6 Fix delet syncshell 2025-09-20 12:13:45 +02:00
79ff8fad7a Mise à jour deploy perma/temp syncshell 2025-09-20 09:51:39 +02:00
f9df60dc88 Ajout des syncshell perma & temporaire 2025-09-20 09:02:53 +02:00
74dac9f506 Enforcing Unique groupe aliases 2025-09-19 23:25:20 +02:00
54e66b2387 Reverse statut & Fix Server Salt Time & Clean Code & Update API 2025-09-19 23:14:58 +02:00
6852869313 Fix TTL 2025-09-13 20:08:52 +02:00
693d3c6af7 Deploy AutoDetect 2025-09-13 13:40:32 +02:00
8d0c3aafb3 Fix AutoDetect 2025-09-11 22:07:46 +02:00
5366cfbc60 Support AutoDetect 2025-09-11 15:43:11 +02:00
c23a43c84c Update MareAPI submodule to origin/main 2025-09-05 12:10:09 +02:00
b2352514ee Replace UmbraAPI with submodule, remove old refs, upgrade to .NET 9 and apply patches 2025-09-05 12:03:15 +02:00
e43979da54 Replace UmbraAPI with submodule, remove old refs, upgrade to .NET 9 and apply patches 2025-09-05 12:03:12 +02:00
104 changed files with 4232 additions and 1705 deletions

6
.gitignore vendored
View File

@@ -9,8 +9,6 @@
*.user *.user
*.userosscache *.userosscache
*.sln.docstates *.sln.docstates
# Mac
.DS_Store .DS_Store
# User-specific files (MonoDevelop/Xamarin Studio) # User-specific files (MonoDevelop/Xamarin Studio)
@@ -354,3 +352,7 @@ MigrationBackup/
# docker run data # docker run data
Docker/run/data/ Docker/run/data/
**/obj/
**/bin/
**/.idea/

7
.gitmodules vendored
View File

@@ -1,3 +1,4 @@
[submodule "UmbraAPI"] [submodule "MareAPI"]
path = UmbraAPI path = MareAPI
url = https://git.umbra-sync.net/SirConstance/UmbraAPI.git url = git@git.umbra-sync.net:Keda/UmbraAPI.git
branch = main

1
MareAPI Submodule

Submodule MareAPI added at d105d20507

View File

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

View File

@@ -113,7 +113,8 @@ public class JwtController : Controller
} }
var existingIdent = await _redis.GetAsync<string>("UID:" + authResult.Uid); var existingIdent = await _redis.GetAsync<string>("UID:" + authResult.Uid);
if (!string.IsNullOrEmpty(existingIdent)) return Unauthorized("Already logged in to this account. Reconnect in 60 seconds. If you keep seeing this issue, restart your game."); 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>() var token = CreateToken(new List<Claim>()
{ {
@@ -134,10 +135,13 @@ public class JwtController : Controller
var tokenContent = tokenResponse as ContentResult; var tokenContent = tokenResponse as ContentResult;
if (tokenContent == null) if (tokenContent == null)
return tokenResponse; 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 return Json(new AuthReplyDto
{ {
Token = tokenContent.Content, Token = tokenContent.Content,
WellKnown = _configuration.GetValueOrDefault(nameof(AuthServiceConfiguration.WellKnown), string.Empty), WellKnown = wk,
}); });
} }
@@ -190,4 +194,3 @@ public class JwtController : Controller
return handler.CreateJwtSecurityToken(token); return handler.CreateJwtSecurityToken(token);
} }
} }

View File

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

View File

@@ -1,21 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<EnableDefaultContentItems>false</EnableDefaultContentItems>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Content Remove="appsettings.Development.json" /> <Content Include="appsettings.Development.json">
<Content Remove="appsettings.json" /> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</ItemGroup> </Content>
<Content Include="appsettings.json">
<ItemGroup> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
<None Include="appsettings.Development.json" /> </Content>
<None Include="appsettings.json">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -29,6 +27,9 @@
</PackageReference> </PackageReference>
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" /> <PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="8.0.1" /> <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>
<ItemGroup> <ItemGroup>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,6 +16,7 @@ using System.Text;
using MareSynchronosShared.Data; using MareSynchronosShared.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Prometheus; using Prometheus;
using Microsoft.AspNetCore.HttpOverrides;
using MareSynchronosShared.Utils.Configuration; using MareSynchronosShared.Utils.Configuration;
namespace MareSynchronosAuthService; namespace MareSynchronosAuthService;
@@ -35,6 +36,12 @@ public class Startup
{ {
var config = app.ApplicationServices.GetRequiredService<IConfigurationService<MareConfigurationBase>>(); 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.UseRouting();
app.UseHttpMetrics(); app.UseHttpMetrics();
@@ -48,6 +55,7 @@ public class Startup
app.UseEndpoints(endpoints => app.UseEndpoints(endpoints =>
{ {
endpoints.MapControllers(); endpoints.MapControllers();
endpoints.MapHealthChecks("/healthz").WithMetadata(new AllowAnonymousAttribute());
foreach (var source in endpoints.DataSources.SelectMany(e => e.Endpoints).Cast<RouteEndpoint>()) foreach (var source in endpoints.DataSources.SelectMany(e => e.Endpoints).Cast<RouteEndpoint>())
{ {
@@ -65,8 +73,8 @@ public class Startup
ConfigureRedis(services, mareConfig); ConfigureRedis(services, mareConfig);
services.AddSingleton<SecretKeyAuthenticatorService>(); services.AddScoped<SecretKeyAuthenticatorService>();
services.AddSingleton<AccountRegistrationService>(); services.AddScoped<AccountRegistrationService>();
services.AddSingleton<GeoIPService>(); services.AddSingleton<GeoIPService>();
services.AddHostedService(provider => provider.GetRequiredService<GeoIPService>()); services.AddHostedService(provider => provider.GetRequiredService<GeoIPService>());
@@ -75,6 +83,30 @@ public class Startup
services.Configure<MareConfigurationBase>(_configuration.GetRequiredSection("MareSynchronos")); services.Configure<MareConfigurationBase>(_configuration.GetRequiredSection("MareSynchronos"));
services.AddSingleton<ServerTokenGenerator>(); 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); ConfigureAuthorization(services);
@@ -88,8 +120,11 @@ public class Startup
services.AddControllers().ConfigureApplicationPartManager(a => services.AddControllers().ConfigureApplicationPartManager(a =>
{ {
a.FeatureProviders.Remove(a.FeatureProviders.OfType<ControllerFeatureProvider>().First()); a.FeatureProviders.Remove(a.FeatureProviders.OfType<ControllerFeatureProvider>().First());
a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(JwtController))); 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) private static void ConfigureAuthorization(IServiceCollection services)
@@ -195,6 +230,8 @@ public class Startup
}; };
services.AddStackExchangeRedisExtensions<SystemTextJsonSerializer>(redisConfiguration); 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) private void ConfigureConfigServices(IServiceCollection services)
{ {

View File

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

View File

@@ -5,6 +5,8 @@ VisualStudioVersion = 17.2.32602.215
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronosServer", "MareSynchronosServer\MareSynchronosServer.csproj", "{029CA97F-E0BA-4172-A191-EA21FB61AD0F}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronosServer", "MareSynchronosServer\MareSynchronosServer.csproj", "{029CA97F-E0BA-4172-A191-EA21FB61AD0F}"
EndProject 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}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronosShared", "MareSynchronosShared\MareSynchronosShared.csproj", "{67B1461D-E215-4BA8-A64D-E1836724D5E6}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronosStaticFilesServer", "MareSynchronosStaticFilesServer\MareSynchronosStaticFilesServer.csproj", "{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronosStaticFilesServer", "MareSynchronosStaticFilesServer\MareSynchronosStaticFilesServer.csproj", "{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}"
@@ -18,90 +20,36 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MareSynchronosAuthService", "MareSynchronosAuthService\MareSynchronosAuthService.csproj", "{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MareSynchronosAuthService", "MareSynchronosAuthService\MareSynchronosAuthService.csproj", "{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MareSynchronos.API", "..\UmbraAPI\MareSynchronosAPI\MareSynchronos.API.csproj", "{FA97F4D1-8F15-4002-A2C1-70EF1367B879}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {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}.Debug|Any CPU.Build.0 = Debug|Any CPU
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Debug|x64.ActiveCfg = Debug|Any CPU
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Debug|x64.Build.0 = Debug|Any CPU
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Debug|x86.ActiveCfg = Debug|Any CPU
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Debug|x86.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.ActiveCfg = Release|Any CPU
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Release|Any CPU.Build.0 = Release|Any CPU {029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Release|Any CPU.Build.0 = Release|Any CPU
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Release|x64.ActiveCfg = Release|Any CPU {326BFB1B-5571-47A6-8513-1FFDB32D53B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Release|x64.Build.0 = Release|Any CPU {326BFB1B-5571-47A6-8513-1FFDB32D53B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Release|x86.ActiveCfg = Release|Any CPU {326BFB1B-5571-47A6-8513-1FFDB32D53B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Release|x86.Build.0 = 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.ActiveCfg = Debug|Any CPU
{67B1461D-E215-4BA8-A64D-E1836724D5E6}.Debug|Any CPU.Build.0 = Debug|Any CPU {67B1461D-E215-4BA8-A64D-E1836724D5E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{67B1461D-E215-4BA8-A64D-E1836724D5E6}.Debug|x64.ActiveCfg = Debug|Any CPU
{67B1461D-E215-4BA8-A64D-E1836724D5E6}.Debug|x64.Build.0 = Debug|Any CPU
{67B1461D-E215-4BA8-A64D-E1836724D5E6}.Debug|x86.ActiveCfg = Debug|Any CPU
{67B1461D-E215-4BA8-A64D-E1836724D5E6}.Debug|x86.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.ActiveCfg = Release|Any CPU
{67B1461D-E215-4BA8-A64D-E1836724D5E6}.Release|Any CPU.Build.0 = Release|Any CPU {67B1461D-E215-4BA8-A64D-E1836724D5E6}.Release|Any CPU.Build.0 = Release|Any CPU
{67B1461D-E215-4BA8-A64D-E1836724D5E6}.Release|x64.ActiveCfg = Release|Any CPU
{67B1461D-E215-4BA8-A64D-E1836724D5E6}.Release|x64.Build.0 = Release|Any CPU
{67B1461D-E215-4BA8-A64D-E1836724D5E6}.Release|x86.ActiveCfg = Release|Any CPU
{67B1461D-E215-4BA8-A64D-E1836724D5E6}.Release|x86.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.ActiveCfg = Debug|Any CPU
{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Debug|Any CPU.Build.0 = Debug|Any CPU {3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Debug|x64.ActiveCfg = Debug|Any CPU
{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Debug|x64.Build.0 = Debug|Any CPU
{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Debug|x86.ActiveCfg = Debug|Any CPU
{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Debug|x86.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.ActiveCfg = Release|Any CPU
{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Release|Any CPU.Build.0 = Release|Any CPU {3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Release|Any CPU.Build.0 = Release|Any CPU
{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Release|x64.ActiveCfg = Release|Any CPU
{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Release|x64.Build.0 = Release|Any CPU
{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Release|x86.ActiveCfg = Release|Any CPU
{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}.Release|x86.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.ActiveCfg = Debug|Any CPU
{E29C8677-AB44-4950-9EB1-D8E70B710A56}.Debug|Any CPU.Build.0 = Debug|Any CPU {E29C8677-AB44-4950-9EB1-D8E70B710A56}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E29C8677-AB44-4950-9EB1-D8E70B710A56}.Debug|x64.ActiveCfg = Debug|Any CPU
{E29C8677-AB44-4950-9EB1-D8E70B710A56}.Debug|x64.Build.0 = Debug|Any CPU
{E29C8677-AB44-4950-9EB1-D8E70B710A56}.Debug|x86.ActiveCfg = Debug|Any CPU
{E29C8677-AB44-4950-9EB1-D8E70B710A56}.Debug|x86.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.ActiveCfg = Release|Any CPU
{E29C8677-AB44-4950-9EB1-D8E70B710A56}.Release|Any CPU.Build.0 = Release|Any CPU {E29C8677-AB44-4950-9EB1-D8E70B710A56}.Release|Any CPU.Build.0 = Release|Any CPU
{E29C8677-AB44-4950-9EB1-D8E70B710A56}.Release|x64.ActiveCfg = Release|Any CPU
{E29C8677-AB44-4950-9EB1-D8E70B710A56}.Release|x64.Build.0 = Release|Any CPU
{E29C8677-AB44-4950-9EB1-D8E70B710A56}.Release|x86.ActiveCfg = Release|Any CPU
{E29C8677-AB44-4950-9EB1-D8E70B710A56}.Release|x86.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.ActiveCfg = Debug|Any CPU
{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}.Debug|x64.ActiveCfg = Debug|Any CPU
{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}.Debug|x64.Build.0 = Debug|Any CPU
{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}.Debug|x86.ActiveCfg = Debug|Any CPU
{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}.Debug|x86.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.ActiveCfg = Release|Any CPU
{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}.Release|Any CPU.Build.0 = Release|Any CPU {D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}.Release|Any CPU.Build.0 = Release|Any CPU
{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}.Release|x64.ActiveCfg = Release|Any CPU
{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}.Release|x64.Build.0 = Release|Any CPU
{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}.Release|x86.ActiveCfg = Release|Any CPU
{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}.Release|x86.Build.0 = Release|Any CPU
{FA97F4D1-8F15-4002-A2C1-70EF1367B879}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FA97F4D1-8F15-4002-A2C1-70EF1367B879}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FA97F4D1-8F15-4002-A2C1-70EF1367B879}.Debug|x64.ActiveCfg = Debug|Any CPU
{FA97F4D1-8F15-4002-A2C1-70EF1367B879}.Debug|x64.Build.0 = Debug|Any CPU
{FA97F4D1-8F15-4002-A2C1-70EF1367B879}.Debug|x86.ActiveCfg = Debug|Any CPU
{FA97F4D1-8F15-4002-A2C1-70EF1367B879}.Debug|x86.Build.0 = Debug|Any CPU
{FA97F4D1-8F15-4002-A2C1-70EF1367B879}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FA97F4D1-8F15-4002-A2C1-70EF1367B879}.Release|Any CPU.Build.0 = Release|Any CPU
{FA97F4D1-8F15-4002-A2C1-70EF1367B879}.Release|x64.ActiveCfg = Release|Any CPU
{FA97F4D1-8F15-4002-A2C1-70EF1367B879}.Release|x64.Build.0 = Release|Any CPU
{FA97F4D1-8F15-4002-A2C1-70EF1367B879}.Release|x86.ActiveCfg = Release|Any CPU
{FA97F4D1-8F15-4002-A2C1-70EF1367B879}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

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

View File

@@ -54,6 +54,8 @@ namespace MareSynchronosServer.Hubs
public Task Client_UserUpdateSelfPairPermissions(UserPermissionsDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); public Task Client_UserUpdateSelfPairPermissions(UserPermissionsDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
public Task Client_UserTypingState(TypingStateDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
public Task Client_GposeLobbyJoin(UserData userData) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); public Task Client_GposeLobbyJoin(UserData userData) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
public Task Client_GposeLobbyLeave(UserData userData) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); public Task Client_GposeLobbyLeave(UserData userData) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
public Task Client_GposeLobbyPushCharacterData(CharaDataDownloadDto charaDownloadDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); public Task Client_GposeLobbyPushCharacterData(CharaDataDownloadDto charaDownloadDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");

View File

@@ -259,7 +259,13 @@ public partial class MareHub
var user = await DbContext.Users.SingleAsync(u => u.UID == groupHasMigrated.Item2).ConfigureAwait(false); var user = await DbContext.Users.SingleAsync(u => u.UID == groupHasMigrated.Item2).ConfigureAwait(false);
await Clients.Users(groupPairsWithoutSelf.Select(p => p.GroupUserUID)).Client_GroupSendInfo(new GroupInfoDto(group.ToGroupData(), await Clients.Users(groupPairsWithoutSelf.Select(p => p.GroupUserUID)).Client_GroupSendInfo(new GroupInfoDto(group.ToGroupData(),
user.ToUserData(), group.GetGroupPermissions())).ConfigureAwait(false); user.ToUserData(), group.GetGroupPermissions())
{
IsTemporary = group.IsTemporary,
ExpiresAt = group.ExpiresAt,
AutoDetectVisible = group.AutoDetectVisible,
PasswordTemporarilyDisabled = group.PasswordTemporarilyDisabled,
}).ConfigureAwait(false);
} }
else else
{ {

View File

@@ -144,7 +144,13 @@ public partial class MareHub
var groupPairs = await DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).AsNoTracking().ToListAsync().ConfigureAwait(false); var groupPairs = await DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).AsNoTracking().ToListAsync().ConfigureAwait(false);
await Clients.Users(groupPairs).Client_GroupSendInfo(new GroupInfoDto(group.ToGroupData(), newOwnerPair.GroupUser.ToUserData(), group.GetGroupPermissions())).ConfigureAwait(false); await Clients.Users(groupPairs).Client_GroupSendInfo(new GroupInfoDto(group.ToGroupData(), newOwnerPair.GroupUser.ToUserData(), group.GetGroupPermissions())
{
IsTemporary = group.IsTemporary,
ExpiresAt = group.ExpiresAt,
AutoDetectVisible = group.AutoDetectVisible,
PasswordTemporarilyDisabled = group.PasswordTemporarilyDisabled,
}).ConfigureAwait(false);
} }
[Authorize(Policy = "Identified")] [Authorize(Policy = "Identified")]
@@ -202,7 +208,7 @@ public partial class MareHub
} }
[Authorize(Policy = "Identified")] [Authorize(Policy = "Identified")]
public async Task<GroupPasswordDto> GroupCreate() public async Task<GroupPasswordDto> GroupCreate(string? alias)
{ {
_logger.LogCallInfo(); _logger.LogCallInfo();
var existingGroupsByUser = await DbContext.Groups.CountAsync(u => u.OwnerUID == UserUID).ConfigureAwait(false); var existingGroupsByUser = await DbContext.Groups.CountAsync(u => u.OwnerUID == UserUID).ConfigureAwait(false);
@@ -223,12 +229,114 @@ public partial class MareHub
var sha = SHA256.Create(); var sha = SHA256.Create();
var hashedPw = StringUtils.Sha256String(passwd); var hashedPw = StringUtils.Sha256String(passwd);
string? sanitizedAlias = null;
if (!string.IsNullOrWhiteSpace(alias))
{
sanitizedAlias = alias.Trim();
if (sanitizedAlias.Length > 50)
{
sanitizedAlias = sanitizedAlias[..50];
}
var aliasExists = await DbContext.Groups
.AnyAsync(g => g.Alias != null && EF.Functions.ILike(g.Alias!, sanitizedAlias))
.ConfigureAwait(false);
if (aliasExists)
{
throw new System.Exception("Syncshell name is already in use.");
}
}
Group newGroup = new()
{
GID = gid,
HashedPassword = hashedPw,
InvitesEnabled = true,
OwnerUID = UserUID,
Alias = sanitizedAlias,
IsTemporary = false,
ExpiresAt = null,
};
GroupPair initialPair = new()
{
GroupGID = newGroup.GID,
GroupUserUID = UserUID,
IsPaused = false,
IsPinned = true,
};
await DbContext.Groups.AddAsync(newGroup).ConfigureAwait(false);
await DbContext.GroupPairs.AddAsync(initialPair).ConfigureAwait(false);
await DbContext.SaveChangesAsync().ConfigureAwait(false);
var self = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false);
await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(newGroup.ToGroupData(), self.ToUserData(), GroupPermissions.NoneSet, GroupUserPermissions.NoneSet, GroupUserInfo.None)
{
IsTemporary = newGroup.IsTemporary,
ExpiresAt = newGroup.ExpiresAt,
AutoDetectVisible = newGroup.AutoDetectVisible,
PasswordTemporarilyDisabled = newGroup.PasswordTemporarilyDisabled,
}).ConfigureAwait(false);
_logger.LogCallInfo(MareHubLogger.Args(gid));
return new GroupPasswordDto(newGroup.ToGroupData(), passwd)
{
IsTemporary = newGroup.IsTemporary,
ExpiresAt = newGroup.ExpiresAt,
};
}
[Authorize(Policy = "Identified")]
public async Task<GroupPasswordDto> GroupCreateTemporary(DateTime expiresAtUtc)
{
_logger.LogCallInfo();
var now = DateTime.UtcNow;
if (expiresAtUtc.Kind == DateTimeKind.Unspecified)
{
expiresAtUtc = DateTime.SpecifyKind(expiresAtUtc, DateTimeKind.Utc);
}
if (expiresAtUtc <= now)
{
throw new System.Exception("Expiration must be in the future.");
}
if (expiresAtUtc > now.AddDays(7))
{
throw new System.Exception("Temporary syncshells may not last longer than 7 days.");
}
var existingGroupsByUser = await DbContext.Groups.CountAsync(u => u.OwnerUID == UserUID).ConfigureAwait(false);
var existingJoinedGroups = await DbContext.GroupPairs.CountAsync(u => u.GroupUserUID == UserUID).ConfigureAwait(false);
if (existingGroupsByUser >= _maxExistingGroupsByUser || existingJoinedGroups >= _maxJoinedGroupsByUser)
{
throw new System.Exception($"Max groups for user is {_maxExistingGroupsByUser}, max joined groups is {_maxJoinedGroupsByUser}.");
}
var gid = StringUtils.GenerateRandomString(9);
while (await DbContext.Groups.AnyAsync(g => g.GID == "UMB-" + gid).ConfigureAwait(false))
{
gid = StringUtils.GenerateRandomString(9);
}
gid = "UMB-" + gid;
var passwd = StringUtils.GenerateRandomString(16);
var sha = SHA256.Create();
var hashedPw = StringUtils.Sha256String(passwd);
Group newGroup = new() Group newGroup = new()
{ {
GID = gid, GID = gid,
HashedPassword = hashedPw, HashedPassword = hashedPw,
InvitesEnabled = true, InvitesEnabled = true,
OwnerUID = UserUID, OwnerUID = UserUID,
Alias = null,
IsTemporary = true,
ExpiresAt = expiresAtUtc,
}; };
GroupPair initialPair = new() GroupPair initialPair = new()
@@ -245,12 +353,21 @@ public partial class MareHub
var self = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); var self = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false);
await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(newGroup.ToGroupData(), self.ToUserData(), GroupPermissions.NoneSet, GroupUserPermissions.NoneSet, GroupUserInfo.None)) await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(newGroup.ToGroupData(), self.ToUserData(), GroupPermissions.NoneSet, GroupUserPermissions.NoneSet, GroupUserInfo.None)
.ConfigureAwait(false); {
IsTemporary = newGroup.IsTemporary,
ExpiresAt = newGroup.ExpiresAt,
AutoDetectVisible = newGroup.AutoDetectVisible,
PasswordTemporarilyDisabled = newGroup.PasswordTemporarilyDisabled,
}).ConfigureAwait(false);
_logger.LogCallInfo(MareHubLogger.Args(gid)); _logger.LogCallInfo(MareHubLogger.Args(gid, "Temporary", expiresAtUtc));
return new GroupPasswordDto(newGroup.ToGroupData(), passwd); return new GroupPasswordDto(newGroup.ToGroupData(), passwd)
{
IsTemporary = newGroup.IsTemporary,
ExpiresAt = newGroup.ExpiresAt,
};
} }
[Authorize(Policy = "Identified")] [Authorize(Policy = "Identified")]
@@ -338,22 +455,130 @@ public partial class MareHub
_logger.LogCallInfo(MareHubLogger.Args(dto.Group)); _logger.LogCallInfo(MareHubLogger.Args(dto.Group));
var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid).ConfigureAwait(false); var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid).ConfigureAwait(false);
var groupGid = group?.GID ?? string.Empty;
var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID).ConfigureAwait(false);
var hashedPw = StringUtils.Sha256String(dto.Password); var hashedPw = StringUtils.Sha256String(dto.Password);
return await JoinGroupInternal(group, aliasOrGid, hashedPw, allowPasswordless: false, skipInviteCheck: false).ConfigureAwait(false);
}
[Authorize(Policy = "Identified")]
public async Task<bool> SyncshellDiscoveryJoin(GroupDto dto)
{
var gid = dto.Group.GID.Trim();
_logger.LogCallInfo(MareHubLogger.Args(dto.Group));
var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false);
return await JoinGroupInternal(group, gid, hashedPassword: null, allowPasswordless: true, skipInviteCheck: true).ConfigureAwait(false);
}
[Authorize(Policy = "Identified")]
public async Task<List<SyncshellDiscoveryEntryDto>> SyncshellDiscoveryList()
{
_logger.LogCallInfo();
var groups = await DbContext.Groups.AsNoTracking()
.Include(g => g.Owner)
.Where(g => g.AutoDetectVisible && (!g.IsTemporary || g.ExpiresAt == null || g.ExpiresAt > DateTime.UtcNow))
.ToListAsync().ConfigureAwait(false);
var groupIds = groups.Select(g => g.GID).ToArray();
var memberCounts = await DbContext.GroupPairs.AsNoTracking()
.Where(p => groupIds.Contains(p.GroupGID))
.GroupBy(p => p.GroupGID)
.Select(g => new { g.Key, Count = g.Count() })
.ToDictionaryAsync(k => k.Key, k => k.Count, StringComparer.OrdinalIgnoreCase)
.ConfigureAwait(false);
return groups.Select(g => new SyncshellDiscoveryEntryDto
{
GID = g.GID,
Alias = g.Alias,
OwnerUID = g.OwnerUID,
OwnerAlias = g.Owner.Alias,
MemberCount = memberCounts.TryGetValue(g.GID, out var count) ? count : 0,
AutoAcceptPairs = g.InvitesEnabled,
Description = null,
}).ToList();
}
[Authorize(Policy = "Identified")]
public async Task<SyncshellDiscoveryStateDto?> SyncshellDiscoveryGetState(GroupDto dto)
{
_logger.LogCallInfo(MareHubLogger.Args(dto.Group));
var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
if (!hasRights) return null;
return new SyncshellDiscoveryStateDto
{
GID = group.GID,
AutoDetectVisible = group.AutoDetectVisible,
PasswordTemporarilyDisabled = group.PasswordTemporarilyDisabled,
};
}
[Authorize(Policy = "Identified")]
public async Task<bool> SyncshellDiscoverySetVisibility(SyncshellDiscoveryVisibilityRequestDto dto)
{
_logger.LogCallInfo(MareHubLogger.Args(dto));
var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.GID).ConfigureAwait(false);
if (!hasRights) return false;
group.AutoDetectVisible = dto.AutoDetectVisible;
group.PasswordTemporarilyDisabled = dto.AutoDetectVisible;
await DbContext.SaveChangesAsync().ConfigureAwait(false);
await DbContext.Entry(group).Reference(g => g.Owner).LoadAsync().ConfigureAwait(false);
var groupPairs = await DbContext.GroupPairs.AsNoTracking().Where(p => p.GroupGID == group.GID).Select(p => p.GroupUserUID).ToListAsync().ConfigureAwait(false);
await Clients.Users(groupPairs).Client_GroupSendInfo(new GroupInfoDto(group.ToGroupData(), group.Owner.ToUserData(), group.GetGroupPermissions())
{
IsTemporary = group.IsTemporary,
ExpiresAt = group.ExpiresAt,
AutoDetectVisible = group.AutoDetectVisible,
PasswordTemporarilyDisabled = group.PasswordTemporarilyDisabled,
}).ConfigureAwait(false);
return true;
}
private async Task<bool> JoinGroupInternal(Group? group, string aliasOrGid, string? hashedPassword, bool allowPasswordless, bool skipInviteCheck)
{
if (group == null) return false;
var groupGid = group.GID;
var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID).ConfigureAwait(false);
var existingUserCount = await DbContext.GroupPairs.AsNoTracking().CountAsync(g => g.GroupGID == groupGid).ConfigureAwait(false); var existingUserCount = await DbContext.GroupPairs.AsNoTracking().CountAsync(g => g.GroupGID == groupGid).ConfigureAwait(false);
var joinedGroups = await DbContext.GroupPairs.CountAsync(g => g.GroupUserUID == UserUID).ConfigureAwait(false); var joinedGroups = await DbContext.GroupPairs.CountAsync(g => g.GroupUserUID == UserUID).ConfigureAwait(false);
var isBanned = await DbContext.GroupBans.AnyAsync(g => g.GroupGID == groupGid && g.BannedUserUID == UserUID).ConfigureAwait(false); var isBanned = await DbContext.GroupBans.AnyAsync(g => g.GroupGID == groupGid && g.BannedUserUID == UserUID).ConfigureAwait(false);
var oneTimeInvite = await DbContext.GroupTempInvites.SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == hashedPw).ConfigureAwait(false);
if (group == null GroupTempInvite? oneTimeInvite = null;
|| (!string.Equals(group.HashedPassword, hashedPw, StringComparison.Ordinal) && oneTimeInvite == null) if (!string.IsNullOrEmpty(hashedPassword))
{
oneTimeInvite = await DbContext.GroupTempInvites.SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == hashedPassword).ConfigureAwait(false);
}
if (allowPasswordless && !group.AutoDetectVisible)
{
return false;
}
bool passwordBypass = group.PasswordTemporarilyDisabled || allowPasswordless;
bool passwordMatches = !string.IsNullOrEmpty(hashedPassword) && string.Equals(group.HashedPassword, hashedPassword, StringComparison.Ordinal);
bool hasValidCredential = passwordBypass || passwordMatches || oneTimeInvite != null;
if (!hasValidCredential
|| existingPair != null || existingPair != null
|| existingUserCount >= _maxGroupUserCount || existingUserCount >= _maxGroupUserCount
|| !group.InvitesEnabled || (!skipInviteCheck && !group.InvitesEnabled)
|| joinedGroups >= _maxJoinedGroupsByUser || joinedGroups >= _maxJoinedGroupsByUser
|| isBanned) || isBanned)
{
return false; return false;
}
if (oneTimeInvite != null) if (oneTimeInvite != null)
{ {
@@ -375,9 +600,17 @@ public partial class MareHub
_logger.LogCallInfo(MareHubLogger.Args(aliasOrGid, "Success")); _logger.LogCallInfo(MareHubLogger.Args(aliasOrGid, "Success"));
await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(group.ToGroupData(), group.Owner.ToUserData(), group.GetGroupPermissions(), newPair.GetGroupPairPermissions(), newPair.GetGroupPairUserInfo())).ConfigureAwait(false); var owner = group.Owner ?? await DbContext.Users.AsNoTracking().SingleAsync(u => u.UID == group.OwnerUID).ConfigureAwait(false);
var self = DbContext.Users.Single(u => u.UID == UserUID); await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(group.ToGroupData(), owner.ToUserData(), group.GetGroupPermissions(), newPair.GetGroupPairPermissions(), newPair.GetGroupPairUserInfo())
{
IsTemporary = group.IsTemporary,
ExpiresAt = group.ExpiresAt,
AutoDetectVisible = group.AutoDetectVisible,
PasswordTemporarilyDisabled = group.PasswordTemporarilyDisabled,
}).ConfigureAwait(false);
var self = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false);
var groupPairs = await DbContext.GroupPairs.Include(p => p.GroupUser).Where(p => p.GroupGID == group.GID && p.GroupUserUID != UserUID).ToListAsync().ConfigureAwait(false); var groupPairs = await DbContext.GroupPairs.Include(p => p.GroupUser).Where(p => p.GroupGID == group.GID && p.GroupUserUID != UserUID).ToListAsync().ConfigureAwait(false);
@@ -527,7 +760,13 @@ public partial class MareHub
var groups = await DbContext.GroupPairs.Include(g => g.Group).Include(g => g.Group.Owner).Where(g => g.GroupUserUID == UserUID).AsNoTracking().ToListAsync().ConfigureAwait(false); var groups = await DbContext.GroupPairs.Include(g => g.Group).Include(g => g.Group.Owner).Where(g => g.GroupUserUID == UserUID).AsNoTracking().ToListAsync().ConfigureAwait(false);
return groups.Select(g => new GroupFullInfoDto(g.Group.ToGroupData(), g.Group.Owner.ToUserData(), return groups.Select(g => new GroupFullInfoDto(g.Group.ToGroupData(), g.Group.Owner.ToUserData(),
g.Group.GetGroupPermissions(), g.GetGroupPairPermissions(), g.GetGroupPairUserInfo())).ToList(); g.Group.GetGroupPermissions(), g.GetGroupPairPermissions(), g.GetGroupPairUserInfo())
{
IsTemporary = g.Group.IsTemporary,
ExpiresAt = g.Group.ExpiresAt,
AutoDetectVisible = g.Group.AutoDetectVisible,
PasswordTemporarilyDisabled = g.Group.PasswordTemporarilyDisabled,
}).ToList();
} }
[Authorize(Policy = "Identified")] [Authorize(Policy = "Identified")]

View File

@@ -0,0 +1,264 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MareSynchronos.API.Dto.McdfShare;
using MareSynchronosServer.Utils;
using MareSynchronosShared.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
namespace MareSynchronosServer.Hubs;
public partial class MareHub
{
[Authorize(Policy = "Identified")]
public async Task<List<McdfShareEntryDto>> McdfShareGetOwn()
{
_logger.LogCallInfo();
var shares = await DbContext.McdfShares.AsNoTracking()
.Include(s => s.Owner)
.Include(s => s.AllowedIndividuals)
.Include(s => s.AllowedSyncshells)
.Where(s => s.OwnerUID == UserUID)
.OrderByDescending(s => s.CreatedUtc)
.ToListAsync().ConfigureAwait(false);
return shares.Select(s => MapShareEntryDto(s, true)).ToList();
}
[Authorize(Policy = "Identified")]
public async Task<List<McdfShareEntryDto>> McdfShareGetShared()
{
_logger.LogCallInfo();
var userGroups = await DbContext.GroupPairs.AsNoTracking()
.Where(p => p.GroupUserUID == UserUID)
.Select(p => p.GroupGID.ToUpperInvariant())
.ToListAsync().ConfigureAwait(false);
var shares = await DbContext.McdfShares.AsNoTracking()
.Include(s => s.Owner)
.Include(s => s.AllowedIndividuals)
.Include(s => s.AllowedSyncshells)
.Where(s => s.OwnerUID != UserUID)
.OrderByDescending(s => s.CreatedUtc)
.ToListAsync().ConfigureAwait(false);
var now = DateTime.UtcNow;
var accessible = shares.Where(s => ShareAccessibleToUser(s, userGroups) && (!s.ExpiresAtUtc.HasValue || s.ExpiresAtUtc > now)).ToList();
return accessible.Select(s => MapShareEntryDto(s, false)).ToList();
}
[Authorize(Policy = "Identified")]
public async Task<bool> McdfShareUpload(McdfShareUploadRequestDto dto)
{
_logger.LogCallInfo(MareHubLogger.Args(dto.ShareId));
var normalizedUsers = dto.AllowedIndividuals
.Where(s => !string.IsNullOrWhiteSpace(s))
.Select(NormalizeUid)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
var normalizedGroups = dto.AllowedSyncshells
.Where(s => !string.IsNullOrWhiteSpace(s))
.Select(NormalizeGroup)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
var share = await DbContext.McdfShares
.Include(s => s.AllowedIndividuals)
.Include(s => s.AllowedSyncshells)
.SingleOrDefaultAsync(s => s.Id == dto.ShareId)
.ConfigureAwait(false);
if (share != null && !string.Equals(share.OwnerUID, UserUID, StringComparison.Ordinal))
{
return false;
}
var now = DateTime.UtcNow;
if (share == null)
{
share = new McdfShare
{
Id = dto.ShareId,
OwnerUID = UserUID,
CreatedUtc = now,
};
DbContext.McdfShares.Add(share);
}
share.Description = dto.Description ?? string.Empty;
share.CipherData = dto.CipherData ?? Array.Empty<byte>();
share.Nonce = dto.Nonce ?? Array.Empty<byte>();
share.Salt = dto.Salt ?? Array.Empty<byte>();
share.Tag = dto.Tag ?? Array.Empty<byte>();
share.ExpiresAtUtc = dto.ExpiresAtUtc;
share.UpdatedUtc = now;
share.AllowedIndividuals.Clear();
foreach (var uid in normalizedUsers)
{
share.AllowedIndividuals.Add(new McdfShareAllowedUser
{
ShareId = share.Id,
AllowedIndividualUid = uid,
});
}
share.AllowedSyncshells.Clear();
foreach (var gid in normalizedGroups)
{
share.AllowedSyncshells.Add(new McdfShareAllowedGroup
{
ShareId = share.Id,
AllowedGroupGid = gid,
});
}
await DbContext.SaveChangesAsync().ConfigureAwait(false);
return true;
}
[Authorize(Policy = "Identified")]
public async Task<McdfShareEntryDto?> McdfShareUpdate(McdfShareUpdateRequestDto dto)
{
_logger.LogCallInfo(MareHubLogger.Args(dto.ShareId));
var share = await DbContext.McdfShares
.Include(s => s.AllowedIndividuals)
.Include(s => s.AllowedSyncshells)
.Include(s => s.Owner)
.SingleOrDefaultAsync(s => s.Id == dto.ShareId && s.OwnerUID == UserUID)
.ConfigureAwait(false);
if (share == null) return null;
share.Description = dto.Description ?? string.Empty;
share.ExpiresAtUtc = dto.ExpiresAtUtc;
share.UpdatedUtc = DateTime.UtcNow;
var normalizedUsers = dto.AllowedIndividuals
.Where(s => !string.IsNullOrWhiteSpace(s))
.Select(NormalizeUid)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
var normalizedGroups = dto.AllowedSyncshells
.Where(s => !string.IsNullOrWhiteSpace(s))
.Select(NormalizeGroup)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
share.AllowedIndividuals.Clear();
foreach (var uid in normalizedUsers)
{
share.AllowedIndividuals.Add(new McdfShareAllowedUser
{
ShareId = share.Id,
AllowedIndividualUid = uid,
});
}
share.AllowedSyncshells.Clear();
foreach (var gid in normalizedGroups)
{
share.AllowedSyncshells.Add(new McdfShareAllowedGroup
{
ShareId = share.Id,
AllowedGroupGid = gid,
});
}
await DbContext.SaveChangesAsync().ConfigureAwait(false);
return MapShareEntryDto(share, true);
}
[Authorize(Policy = "Identified")]
public async Task<bool> McdfShareDelete(Guid shareId)
{
_logger.LogCallInfo(MareHubLogger.Args(shareId));
var share = await DbContext.McdfShares.SingleOrDefaultAsync(s => s.Id == shareId && s.OwnerUID == UserUID).ConfigureAwait(false);
if (share == null) return false;
DbContext.McdfShares.Remove(share);
await DbContext.SaveChangesAsync().ConfigureAwait(false);
return true;
}
[Authorize(Policy = "Identified")]
public async Task<McdfSharePayloadDto?> McdfShareDownload(Guid shareId)
{
_logger.LogCallInfo(MareHubLogger.Args(shareId));
var share = await DbContext.McdfShares
.Include(s => s.AllowedIndividuals)
.Include(s => s.AllowedSyncshells)
.SingleOrDefaultAsync(s => s.Id == shareId)
.ConfigureAwait(false);
if (share == null) return null;
var userGroups = await DbContext.GroupPairs.AsNoTracking()
.Where(p => p.GroupUserUID == UserUID)
.Select(p => p.GroupGID.ToUpperInvariant())
.ToListAsync().ConfigureAwait(false);
bool isOwner = string.Equals(share.OwnerUID, UserUID, StringComparison.Ordinal);
if (!isOwner && (!ShareAccessibleToUser(share, userGroups) || (share.ExpiresAtUtc.HasValue && share.ExpiresAtUtc.Value <= DateTime.UtcNow)))
{
return null;
}
share.DownloadCount++;
await DbContext.SaveChangesAsync().ConfigureAwait(false);
return new McdfSharePayloadDto
{
ShareId = share.Id,
Description = share.Description,
CipherData = share.CipherData,
Nonce = share.Nonce,
Salt = share.Salt,
Tag = share.Tag,
CreatedUtc = share.CreatedUtc,
ExpiresAtUtc = share.ExpiresAtUtc,
};
}
private static string NormalizeUid(string candidate) => candidate.Trim().ToUpperInvariant();
private static string NormalizeGroup(string candidate) => candidate.Trim().ToUpperInvariant();
private static McdfShareEntryDto MapShareEntryDto(McdfShare share, bool isOwner)
{
return new McdfShareEntryDto
{
Id = share.Id,
Description = share.Description,
CreatedUtc = share.CreatedUtc,
UpdatedUtc = share.UpdatedUtc,
ExpiresAtUtc = share.ExpiresAtUtc,
DownloadCount = share.DownloadCount,
IsOwner = isOwner,
OwnerUid = share.OwnerUID,
OwnerAlias = share.Owner?.Alias ?? string.Empty,
AllowedIndividuals = share.AllowedIndividuals.Select(i => i.AllowedIndividualUid).OrderBy(s => s, StringComparer.OrdinalIgnoreCase).ToList(),
AllowedSyncshells = share.AllowedSyncshells.Select(g => g.AllowedGroupGid).OrderBy(s => s, StringComparer.OrdinalIgnoreCase).ToList(),
};
}
private bool ShareAccessibleToUser(McdfShare share, IReadOnlyCollection<string> userGroups)
{
if (string.Equals(share.OwnerUID, UserUID, StringComparison.Ordinal)) return true;
bool allowedByUser = share.AllowedIndividuals.Any(i => string.Equals(i.AllowedIndividualUid, UserUID, StringComparison.OrdinalIgnoreCase));
if (allowedByUser) return true;
if (share.AllowedSyncshells.Count == 0) return false;
var allowedGroups = share.AllowedSyncshells.Select(g => g.AllowedGroupGid).ToHashSet(StringComparer.OrdinalIgnoreCase);
return userGroups.Any(g => allowedGroups.Contains(g));
}
}

View File

@@ -0,0 +1,72 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.API.Data.Extensions;
using MareSynchronos.API.Dto.User;
using MareSynchronosServer.Utils;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
namespace MareSynchronosServer.Hubs;
public partial class MareHub
{
[Authorize(Policy = "Identified")]
public async Task UserSetTypingState(bool isTyping)
{
_logger.LogCallInfo(MareHubLogger.Args(isTyping));
var pairedEntries = await GetAllPairedClientsWithPauseState().ConfigureAwait(false);
var sender = await DbContext.Users.AsNoTracking()
.SingleAsync(u => u.UID == UserUID)
.ConfigureAwait(false);
var typingDto = new TypingStateDto(sender.ToUserData(), isTyping, TypingScope.Proximity);
await Clients.Caller.Client_UserTypingState(typingDto).ConfigureAwait(false);
var recipients = pairedEntries
.Where(p => !p.IsPaused)
.Select(p => p.UID)
.Distinct(StringComparer.Ordinal)
.ToList();
if (recipients.Count == 0)
return;
await Clients.Users(recipients).Client_UserTypingState(typingDto).ConfigureAwait(false);
}
[Authorize(Policy = "Identified")]
public async Task UserSetTypingState(bool isTyping, TypingScope scope)
{
_logger.LogCallInfo(MareHubLogger.Args(isTyping, scope));
var sender = await DbContext.Users.AsNoTracking()
.SingleAsync(u => u.UID == UserUID)
.ConfigureAwait(false);
var typingDto = new TypingStateDto(sender.ToUserData(), isTyping, scope);
await Clients.Caller.Client_UserTypingState(typingDto).ConfigureAwait(false);
if (scope == TypingScope.Party || scope == TypingScope.CrossParty)
{
_logger.LogCallInfo(MareHubLogger.Args("Typing scope is party-based; server-side party routing not yet implemented, not broadcasting to non-caller."));
return;
}
var pairedEntries = await GetAllPairedClientsWithPauseState().ConfigureAwait(false);
var recipients = pairedEntries
.Where(p => !p.IsPaused)
.Select(p => p.UID)
.Distinct(StringComparer.Ordinal)
.ToList();
if (recipients.Count == 0) return;
await Clients.Users(recipients).Client_UserTypingState(typingDto).ConfigureAwait(false);
}
}

View File

@@ -80,7 +80,7 @@ public partial class MareHub : Hub<IMareHub>, IMareHub
dbUser.LastLoggedIn = DateTime.UtcNow; dbUser.LastLoggedIn = DateTime.UtcNow;
await DbContext.SaveChangesAsync().ConfigureAwait(false); await DbContext.SaveChangesAsync().ConfigureAwait(false);
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, "Welcome to Umbra! Current Online Users: " + _systemInfoService.SystemInfoDto.OnlineUsers).ConfigureAwait(false); await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, "Bienvenue sur UmbraSync ! Utilisateurs en ligne : " + _systemInfoService.SystemInfoDto.OnlineUsers).ConfigureAwait(false);
return new ConnectionDto(new UserData(dbUser.UID, string.IsNullOrWhiteSpace(dbUser.Alias) ? null : dbUser.Alias)) return new ConnectionDto(new UserData(dbUser.UID, string.IsNullOrWhiteSpace(dbUser.Alias) ? null : dbUser.Alias))
{ {

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<UserSecretsId>aspnet-MareSynchronosServer-BA82A12A-0B30-463C-801D-B7E81318CD50</UserSecretsId> <UserSecretsId>aspnet-MareSynchronosServer-BA82A12A-0B30-463C-801D-B7E81318CD50</UserSecretsId>
<AssemblyVersion>1.1.0.0</AssemblyVersion> <AssemblyVersion>1.1.0.0</AssemblyVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
@@ -29,13 +29,17 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.19">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="8.0.1" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="7.7.1" /> <PackageReference Include="Microsoft.IdentityModel.Tokens" Version="7.7.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\UmbraAPI\MareSynchronosAPI\MareSynchronos.API.csproj" /> <ProjectReference Include="..\..\MareAPI\MareSynchronosAPI\MareSynchronos.API.csproj" />
<ProjectReference Include="..\MareSynchronosShared\MareSynchronosShared.csproj" /> <ProjectReference Include="..\MareSynchronosShared\MareSynchronosShared.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -57,6 +57,7 @@ public class SystemInfoService : IHostedService, IDisposable
SystemInfoDto = new SystemInfoDto() SystemInfoDto = new SystemInfoDto()
{ {
OnlineUsers = onlineUsers, OnlineUsers = onlineUsers,
SupportsTypingState = true,
}; };
if (_config.IsMain) if (_config.IsMain)

View File

@@ -5,6 +5,7 @@ using MareSynchronosShared.Services;
using MareSynchronosShared.Utils; using MareSynchronosShared.Utils;
using MareSynchronosShared.Utils.Configuration; using MareSynchronosShared.Utils.Configuration;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace MareSynchronosServer.Services; namespace MareSynchronosServer.Services;
@@ -46,20 +47,41 @@ public class UserCleanupService : IHostedService
await PurgeUnusedAccounts(dbContext).ConfigureAwait(false); await PurgeUnusedAccounts(dbContext).ConfigureAwait(false);
await PurgeTempInvites(dbContext).ConfigureAwait(false); await PurgeTempInvites(dbContext).ConfigureAwait(false);
await PurgeExpiredTemporaryGroups(dbContext).ConfigureAwait(false);
dbContext.SaveChanges(); dbContext.SaveChanges();
} }
var now = DateTime.Now; var span = TimeSpan.FromMinutes(1);
TimeOnly currentTime = new(now.Hour, now.Minute, now.Second); var nextRun = DateTime.Now.Add(span);
TimeOnly futureTime = new(now.Hour, now.Minute - now.Minute % 10, 0);
var span = futureTime.AddMinutes(10) - currentTime;
_logger.LogInformation("User Cleanup Complete, next run at {date}", now.Add(span)); _logger.LogInformation("User Cleanup Complete, next run at {date}", nextRun);
await Task.Delay(span, ct).ConfigureAwait(false); await Task.Delay(span, ct).ConfigureAwait(false);
} }
} }
private async Task PurgeExpiredTemporaryGroups(MareDbContext dbContext)
{
try
{
var now = DateTime.UtcNow;
var expiredGroups = await dbContext.Groups
.Where(g => g.IsTemporary && g.ExpiresAt != null && g.ExpiresAt <= now)
.ToListAsync()
.ConfigureAwait(false);
if (expiredGroups.Count == 0) return;
_logger.LogInformation("Cleaning up {count} expired temporary syncshells", expiredGroups.Count);
dbContext.Groups.RemoveRange(expiredGroups);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error during temporary syncshell purge");
}
}
private async Task PurgeTempInvites(MareDbContext dbContext) private async Task PurgeTempInvites(MareDbContext dbContext)
{ {
try try

View File

@@ -13,6 +13,7 @@ using Prometheus;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using System.Text; using System.Text;
using Microsoft.AspNetCore.HttpOverrides;
using StackExchange.Redis; using StackExchange.Redis;
using StackExchange.Redis.Extensions.Core.Configuration; using StackExchange.Redis.Extensions.Core.Configuration;
using System.Net; using System.Net;
@@ -71,7 +72,7 @@ public class Startup
a.FeatureProviders.Remove(a.FeatureProviders.OfType<ControllerFeatureProvider>().First()); a.FeatureProviders.Remove(a.FeatureProviders.OfType<ControllerFeatureProvider>().First());
if (mareConfig.GetValue<Uri>(nameof(ServerConfiguration.MainServerAddress), defaultValue: null) == null) if (mareConfig.GetValue<Uri>(nameof(ServerConfiguration.MainServerAddress), defaultValue: null) == null)
{ {
a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(MareServerConfigurationController), typeof(MareBaseConfigurationController), typeof(ClientMessageController), typeof(MainController))); a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(MareServerConfigurationController), typeof(MareBaseConfigurationController), typeof(ClientMessageController), typeof(MainController), typeof(DiscoveryNotifyController)));
} }
else else
{ {
@@ -198,6 +199,21 @@ public class Startup
ValidateIssuerSigningKey = true, ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.GetValue<string>(nameof(MareConfigurationBase.Jwt)))), IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.GetValue<string>(nameof(MareConfigurationBase.Jwt)))),
}; };
// Allow SignalR WebSocket connections to authenticate via access_token query on the hub path
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"].ToString();
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments(IMareHub.Path))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
}); });
services.AddAuthentication(o => services.AddAuthentication(o =>
@@ -307,6 +323,12 @@ public class Startup
var config = app.ApplicationServices.GetRequiredService<IConfigurationService<MareConfigurationBase>>(); var config = app.ApplicationServices.GetRequiredService<IConfigurationService<MareConfigurationBase>>();
// Respect X-Forwarded-* headers from reverse proxies (required for correct scheme/host)
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedFor
});
app.UseIpRateLimiting(); app.UseIpRateLimiting();
app.UseRouting(); app.UseRouting();

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
@@ -31,7 +31,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\UmbraAPI\MareSynchronosAPI\MareSynchronos.API.csproj" /> <ProjectReference Include="..\..\MareAPI\MareSynchronosAPI\MareSynchronos.API.csproj" />
<ProjectReference Include="..\MareSynchronosShared\MareSynchronosShared.csproj" /> <ProjectReference Include="..\MareSynchronosShared\MareSynchronosShared.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -51,6 +51,9 @@ public class MareDbContext : DbContext
public DbSet<CharaDataOriginalFile> CharaDataOriginalFiles { get; set; } public DbSet<CharaDataOriginalFile> CharaDataOriginalFiles { get; set; }
public DbSet<CharaDataPose> CharaDataPoses { get; set; } public DbSet<CharaDataPose> CharaDataPoses { get; set; }
public DbSet<CharaDataAllowance> CharaDataAllowances { get; set; } public DbSet<CharaDataAllowance> CharaDataAllowances { get; set; }
public DbSet<McdfShare> McdfShares { get; set; }
public DbSet<McdfShareAllowedUser> McdfShareAllowedUsers { get; set; }
public DbSet<McdfShareAllowedGroup> McdfShareAllowedGroups { get; set; }
protected override void OnModelCreating(ModelBuilder mb) protected override void OnModelCreating(ModelBuilder mb)
{ {
@@ -127,5 +130,28 @@ public class MareDbContext : DbContext
mb.Entity<CharaDataAllowance>().HasIndex(c => c.ParentId); mb.Entity<CharaDataAllowance>().HasIndex(c => c.ParentId);
mb.Entity<CharaDataAllowance>().HasOne(u => u.AllowedGroup).WithMany().HasForeignKey(u => u.AllowedGroupGID).OnDelete(DeleteBehavior.Cascade); mb.Entity<CharaDataAllowance>().HasOne(u => u.AllowedGroup).WithMany().HasForeignKey(u => u.AllowedGroupGID).OnDelete(DeleteBehavior.Cascade);
mb.Entity<CharaDataAllowance>().HasOne(u => u.AllowedUser).WithMany().HasForeignKey(u => u.AllowedUserUID).OnDelete(DeleteBehavior.Cascade); mb.Entity<CharaDataAllowance>().HasOne(u => u.AllowedUser).WithMany().HasForeignKey(u => u.AllowedUserUID).OnDelete(DeleteBehavior.Cascade);
mb.Entity<McdfShare>().ToTable("mcdf_shares");
mb.Entity<McdfShare>().HasIndex(s => s.OwnerUID);
mb.Entity<McdfShare>().HasOne(s => s.Owner).WithMany().HasForeignKey(s => s.OwnerUID).OnDelete(DeleteBehavior.Cascade);
mb.Entity<McdfShare>().Property(s => s.Description).HasColumnType("text");
mb.Entity<McdfShare>().Property(s => s.CipherData).HasColumnType("bytea");
mb.Entity<McdfShare>().Property(s => s.Nonce).HasColumnType("bytea");
mb.Entity<McdfShare>().Property(s => s.Salt).HasColumnType("bytea");
mb.Entity<McdfShare>().Property(s => s.Tag).HasColumnType("bytea");
mb.Entity<McdfShare>().Property(s => s.CreatedUtc).HasColumnType("timestamp with time zone");
mb.Entity<McdfShare>().Property(s => s.UpdatedUtc).HasColumnType("timestamp with time zone");
mb.Entity<McdfShare>().Property(s => s.ExpiresAtUtc).HasColumnType("timestamp with time zone");
mb.Entity<McdfShare>().Property(s => s.DownloadCount).HasColumnType("integer");
mb.Entity<McdfShare>().HasMany(s => s.AllowedIndividuals).WithOne(a => a.Share).HasForeignKey(a => a.ShareId).OnDelete(DeleteBehavior.Cascade);
mb.Entity<McdfShare>().HasMany(s => s.AllowedSyncshells).WithOne(a => a.Share).HasForeignKey(a => a.ShareId).OnDelete(DeleteBehavior.Cascade);
mb.Entity<McdfShareAllowedUser>().ToTable("mcdf_share_allowed_users");
mb.Entity<McdfShareAllowedUser>().HasKey(u => new { u.ShareId, u.AllowedIndividualUid });
mb.Entity<McdfShareAllowedUser>().HasIndex(u => u.AllowedIndividualUid);
mb.Entity<McdfShareAllowedGroup>().ToTable("mcdf_share_allowed_groups");
mb.Entity<McdfShareAllowedGroup>().HasKey(g => new { g.ShareId, g.AllowedGroupGid });
mb.Entity<McdfShareAllowedGroup>().HasIndex(g => g.AllowedGroupGid);
} }
} }

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
@@ -53,7 +53,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\UmbraAPI\MareSynchronosAPI\MareSynchronos.API.csproj" /> <ProjectReference Include="..\..\MareAPI\MareSynchronosAPI\MareSynchronos.API.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,40 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MareSynchronosServer.Migrations
{
/// <inheritdoc />
public partial class GroupTemporary : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "expires_at",
table: "groups",
type: "timestamp with time zone",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "is_temporary",
table: "groups",
type: "boolean",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "expires_at",
table: "groups");
migrationBuilder.DropColumn(
name: "is_temporary",
table: "groups");
}
}
}

View File

@@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MareSynchronosServer.Migrations
{
/// <inheritdoc />
public partial class SyncshellDiscovery : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "auto_detect_visible",
table: "groups",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "password_temporarily_disabled",
table: "groups",
type: "boolean",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "auto_detect_visible",
table: "groups");
migrationBuilder.DropColumn(
name: "password_temporarily_disabled",
table: "groups");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,106 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MareSynchronosServer.Migrations
{
/// <inheritdoc />
public partial class McdfShare : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "mcdf_shares",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
owner_uid = table.Column<string>(type: "character varying(10)", nullable: false),
description = table.Column<string>(type: "text", nullable: false),
cipher_data = table.Column<byte[]>(type: "bytea", nullable: false),
nonce = table.Column<byte[]>(type: "bytea", nullable: false),
salt = table.Column<byte[]>(type: "bytea", nullable: false),
tag = table.Column<byte[]>(type: "bytea", nullable: false),
created_utc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
updated_utc = table.Column<DateTime?>(type: "timestamp with time zone", nullable: true),
expires_at_utc = table.Column<DateTime?>(type: "timestamp with time zone", nullable: true),
download_count = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_mcdf_shares", x => x.id);
table.ForeignKey(
name: "fk_mcdf_shares_users_owner_uid",
column: x => x.owner_uid,
principalTable: "users",
principalColumn: "uid",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "mcdf_share_allowed_groups",
columns: table => new
{
share_id = table.Column<Guid>(type: "uuid", nullable: false),
allowed_group_gid = table.Column<string>(type: "character varying(20)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_mcdf_share_allowed_groups", x => new { x.share_id, x.allowed_group_gid });
table.ForeignKey(
name: "fk_mcdf_share_allowed_groups_mcdf_shares_share_id",
column: x => x.share_id,
principalTable: "mcdf_shares",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "mcdf_share_allowed_users",
columns: table => new
{
share_id = table.Column<Guid>(type: "uuid", nullable: false),
allowed_individual_uid = table.Column<string>(type: "character varying(10)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_mcdf_share_allowed_users", x => new { x.share_id, x.allowed_individual_uid });
table.ForeignKey(
name: "fk_mcdf_share_allowed_users_mcdf_shares_share_id",
column: x => x.share_id,
principalTable: "mcdf_shares",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_mcdf_share_allowed_groups_allowed_group_gid",
table: "mcdf_share_allowed_groups",
column: "allowed_group_gid");
migrationBuilder.CreateIndex(
name: "ix_mcdf_share_allowed_users_allowed_individual_uid",
table: "mcdf_share_allowed_users",
column: "allowed_individual_uid");
migrationBuilder.CreateIndex(
name: "ix_mcdf_shares_owner_uid",
table: "mcdf_shares",
column: "owner_uid");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "mcdf_share_allowed_groups");
migrationBuilder.DropTable(
name: "mcdf_share_allowed_users");
migrationBuilder.DropTable(
name: "mcdf_shares");
}
}
}

View File

@@ -17,7 +17,7 @@ namespace MareSynchronosServer.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "8.0.4") .HasAnnotation("ProductVersion", "8.0.19")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -438,6 +438,10 @@ namespace MareSynchronosServer.Migrations
.HasColumnType("character varying(50)") .HasColumnType("character varying(50)")
.HasColumnName("alias"); .HasColumnName("alias");
b.Property<bool>("AutoDetectVisible")
.HasColumnType("boolean")
.HasColumnName("auto_detect_visible");
b.Property<bool>("DisableAnimations") b.Property<bool>("DisableAnimations")
.HasColumnType("boolean") .HasColumnType("boolean")
.HasColumnName("disable_animations"); .HasColumnName("disable_animations");
@@ -450,6 +454,10 @@ namespace MareSynchronosServer.Migrations
.HasColumnType("boolean") .HasColumnType("boolean")
.HasColumnName("disable_vfx"); .HasColumnName("disable_vfx");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expires_at");
b.Property<string>("HashedPassword") b.Property<string>("HashedPassword")
.HasColumnType("text") .HasColumnType("text")
.HasColumnName("hashed_password"); .HasColumnName("hashed_password");
@@ -458,10 +466,18 @@ namespace MareSynchronosServer.Migrations
.HasColumnType("boolean") .HasColumnType("boolean")
.HasColumnName("invites_enabled"); .HasColumnName("invites_enabled");
b.Property<bool>("IsTemporary")
.HasColumnType("boolean")
.HasColumnName("is_temporary");
b.Property<string>("OwnerUID") b.Property<string>("OwnerUID")
.HasColumnType("character varying(10)") .HasColumnType("character varying(10)")
.HasColumnName("owner_uid"); .HasColumnName("owner_uid");
b.Property<bool>("PasswordTemporarilyDisabled")
.HasColumnType("boolean")
.HasColumnName("password_temporarily_disabled");
b.HasKey("GID") b.HasKey("GID")
.HasName("pk_groups"); .HasName("pk_groups");
@@ -615,6 +631,103 @@ namespace MareSynchronosServer.Migrations
b.ToTable("lodestone_auth", (string)null); b.ToTable("lodestone_auth", (string)null);
}); });
modelBuilder.Entity("MareSynchronosShared.Models.McdfShare", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<byte[]>("CipherData")
.HasColumnType("bytea")
.HasColumnName("cipher_data");
b.Property<DateTime>("CreatedUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_utc");
b.Property<string>("Description")
.HasColumnType("text")
.HasColumnName("description");
b.Property<int>("DownloadCount")
.HasColumnType("integer")
.HasColumnName("download_count");
b.Property<DateTime?>("ExpiresAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("expires_at_utc");
b.Property<byte[]>("Nonce")
.HasColumnType("bytea")
.HasColumnName("nonce");
b.Property<string>("OwnerUID")
.HasMaxLength(10)
.HasColumnType("character varying(10)")
.HasColumnName("owner_uid");
b.Property<byte[]>("Salt")
.HasColumnType("bytea")
.HasColumnName("salt");
b.Property<byte[]>("Tag")
.HasColumnType("bytea")
.HasColumnName("tag");
b.Property<DateTime?>("UpdatedUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_utc");
b.HasKey("Id")
.HasName("pk_mcdf_shares");
b.HasIndex("OwnerUID")
.HasDatabaseName("ix_mcdf_shares_owner_uid");
b.ToTable("mcdf_shares", (string)null);
});
modelBuilder.Entity("MareSynchronosShared.Models.McdfShareAllowedGroup", b =>
{
b.Property<Guid>("ShareId")
.HasColumnType("uuid")
.HasColumnName("share_id");
b.Property<string>("AllowedGroupGid")
.HasMaxLength(20)
.HasColumnType("character varying(20)")
.HasColumnName("allowed_group_gid");
b.HasKey("ShareId", "AllowedGroupGid")
.HasName("pk_mcdf_share_allowed_groups");
b.HasIndex("AllowedGroupGid")
.HasDatabaseName("ix_mcdf_share_allowed_groups_allowed_group_gid");
b.ToTable("mcdf_share_allowed_groups", (string)null);
});
modelBuilder.Entity("MareSynchronosShared.Models.McdfShareAllowedUser", b =>
{
b.Property<Guid>("ShareId")
.HasColumnType("uuid")
.HasColumnName("share_id");
b.Property<string>("AllowedIndividualUid")
.HasMaxLength(10)
.HasColumnType("character varying(10)")
.HasColumnName("allowed_individual_uid");
b.HasKey("ShareId", "AllowedIndividualUid")
.HasName("pk_mcdf_share_allowed_users");
b.HasIndex("AllowedIndividualUid")
.HasDatabaseName("ix_mcdf_share_allowed_users_allowed_individual_uid");
b.ToTable("mcdf_share_allowed_users", (string)null);
});
modelBuilder.Entity("MareSynchronosShared.Models.User", b => modelBuilder.Entity("MareSynchronosShared.Models.User", b =>
{ {
b.Property<string>("UID") b.Property<string>("UID")
@@ -945,6 +1058,41 @@ namespace MareSynchronosServer.Migrations
b.Navigation("User"); b.Navigation("User");
}); });
modelBuilder.Entity("MareSynchronosShared.Models.McdfShare", b =>
{
b.HasOne("MareSynchronosShared.Models.User", "Owner")
.WithMany()
.HasForeignKey("OwnerUID")
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("fk_mcdf_shares_users_owner_uid");
b.Navigation("Owner");
});
modelBuilder.Entity("MareSynchronosShared.Models.McdfShareAllowedGroup", b =>
{
b.HasOne("MareSynchronosShared.Models.McdfShare", "Share")
.WithMany("AllowedSyncshells")
.HasForeignKey("ShareId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_mcdf_share_allowed_groups_mcdf_shares_share_id");
b.Navigation("Share");
});
modelBuilder.Entity("MareSynchronosShared.Models.McdfShareAllowedUser", b =>
{
b.HasOne("MareSynchronosShared.Models.McdfShare", "Share")
.WithMany("AllowedIndividuals")
.HasForeignKey("ShareId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_mcdf_share_allowed_users_mcdf_shares_share_id");
b.Navigation("Share");
});
modelBuilder.Entity("MareSynchronosShared.Models.UserProfileData", b => modelBuilder.Entity("MareSynchronosShared.Models.UserProfileData", b =>
{ {
b.HasOne("MareSynchronosShared.Models.User", "User") b.HasOne("MareSynchronosShared.Models.User", "User")
@@ -986,6 +1134,13 @@ namespace MareSynchronosServer.Migrations
b.Navigation("Poses"); b.Navigation("Poses");
}); });
modelBuilder.Entity("MareSynchronosShared.Models.McdfShare", b =>
{
b.Navigation("AllowedIndividuals");
b.Navigation("AllowedSyncshells");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }

View File

@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations; using System;
using System.ComponentModel.DataAnnotations;
namespace MareSynchronosShared.Models; namespace MareSynchronosShared.Models;
@@ -16,4 +17,8 @@ public class Group
public bool DisableSounds { get; set; } public bool DisableSounds { get; set; }
public bool DisableAnimations { get; set; } public bool DisableAnimations { get; set; }
public bool DisableVFX { get; set; } public bool DisableVFX { get; set; }
public bool IsTemporary { get; set; }
public DateTime? ExpiresAt { get; set; }
public bool AutoDetectVisible { get; set; }
public bool PasswordTemporarilyDisabled { get; set; }
} }

View File

@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace MareSynchronosShared.Models;
public class McdfShare
{
[Key]
public Guid Id { get; set; }
[MaxLength(10)]
public string OwnerUID { get; set; } = string.Empty;
public User Owner { get; set; } = null!;
public string Description { get; set; } = string.Empty;
public byte[] CipherData { get; set; } = Array.Empty<byte>();
public byte[] Nonce { get; set; } = Array.Empty<byte>();
public byte[] Salt { get; set; } = Array.Empty<byte>();
public byte[] Tag { get; set; } = Array.Empty<byte>();
public DateTime CreatedUtc { get; set; }
public DateTime? UpdatedUtc { get; set; }
public DateTime? ExpiresAtUtc { get; set; }
public int DownloadCount { get; set; }
public ICollection<McdfShareAllowedUser> AllowedIndividuals { get; set; } = new HashSet<McdfShareAllowedUser>();
public ICollection<McdfShareAllowedGroup> AllowedSyncshells { get; set; } = new HashSet<McdfShareAllowedGroup>();
}
public class McdfShareAllowedUser
{
public Guid ShareId { get; set; }
public McdfShare Share { get; set; } = null!;
[MaxLength(10)]
public string AllowedIndividualUid { get; set; } = string.Empty;
}
public class McdfShareAllowedGroup
{
public Guid ShareId { get; set; }
public McdfShare Share { get; set; } = null!;
[MaxLength(20)]
public string AllowedGroupGid { get; set; } = string.Empty;
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
@@ -31,7 +31,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\UmbraAPI\MareSynchronosAPI\MareSynchronos.API.csproj" /> <ProjectReference Include="..\..\MareAPI\MareSynchronosAPI\MareSynchronos.API.csproj" />
<ProjectReference Include="..\MareSynchronosShared\MareSynchronosShared.csproj" /> <ProjectReference Include="..\MareSynchronosShared\MareSynchronosShared.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -1,2 +1,9 @@
# UmbraServer # UmbraServer
🇫🇷
Ce projet est basé sur Mare Synchronos de DarkArchon. Le code original est sous licence MIT ; voir le fichier
LICENSE_MIT pour plus de détails. À partir de ce commit, le code est sous licence AGPL.
🇬🇧
This project is based on Mare Synchronos by DarkArchon. Original code is licensed under the MIT License; see the
LICENSE_MIT file for details. From this commit onwards, the code is licensed under AGPL.

350
UmbraAPI/.gitignore vendored
View File

@@ -1,350 +0,0 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +0,0 @@
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 string OwnerUID => Owner.UID;
public string? OwnerAlias => Owner.Alias;
public string OwnerAliasOrUID => Owner.AliasOrUID;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +0,0 @@
using MareSynchronos.API.Data;
using MessagePack;
namespace MareSynchronos.API.Dto.Group;
[MessagePackObject(keyAsPropertyName: true)]
public record GroupPasswordDto(GroupData Group, string Password) : GroupDto(Group);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MessagePack.Annotations" Version="2.5.198" />
</ItemGroup>
</Project>

View File

@@ -1,25 +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}") = "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

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