Compare commits
21 Commits
0b337e362e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
b231d80101
|
|||
|
f5d2a95b97
|
|||
|
97c009aef6
|
|||
|
8527e67af1
|
|||
|
073464e6c8
|
|||
|
b2b9ecad7b
|
|||
|
e484616ecd
|
|||
|
331cb612ad
|
|||
|
f696c1d400
|
|||
|
cd5248e4e6
|
|||
|
79ff8fad7a
|
|||
|
f9df60dc88
|
|||
|
74dac9f506
|
|||
|
54e66b2387
|
|||
|
6852869313
|
|||
|
693d3c6af7
|
|||
|
8d0c3aafb3
|
|||
|
5366cfbc60
|
|||
|
c23a43c84c
|
|||
|
b2352514ee
|
|||
|
e43979da54
|
6
.gitignore
vendored
6
.gitignore
vendored
@@ -9,8 +9,6 @@
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# Mac
|
||||
.DS_Store
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
@@ -354,3 +352,7 @@ MigrationBackup/
|
||||
|
||||
# docker run data
|
||||
Docker/run/data/
|
||||
|
||||
**/obj/
|
||||
**/bin/
|
||||
**/.idea/
|
||||
7
.gitmodules
vendored
7
.gitmodules
vendored
@@ -1,3 +1,4 @@
|
||||
[submodule "UmbraAPI"]
|
||||
path = UmbraAPI
|
||||
url = https://git.umbra-sync.net/SirConstance/UmbraAPI.git
|
||||
[submodule "MareAPI"]
|
||||
path = MareAPI
|
||||
url = git@git.umbra-sync.net:Keda/UmbraAPI.git
|
||||
branch = main
|
||||
|
||||
1
MareAPI
Submodule
1
MareAPI
Submodule
Submodule MareAPI added at d105d20507
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -113,7 +113,8 @@ public class JwtController : Controller
|
||||
}
|
||||
|
||||
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>()
|
||||
{
|
||||
@@ -134,10 +135,13 @@ public class JwtController : Controller
|
||||
var tokenContent = tokenResponse as ContentResult;
|
||||
if (tokenContent == null)
|
||||
return tokenResponse;
|
||||
var provider = HttpContext.RequestServices.GetService<DiscoveryWellKnownProvider>();
|
||||
var wk = provider?.GetWellKnownJson(Request.Scheme, Request.Host.Value)
|
||||
?? _configuration.GetValueOrDefault(nameof(AuthServiceConfiguration.WellKnown), string.Empty);
|
||||
return Json(new AuthReplyDto
|
||||
{
|
||||
Token = tokenContent.Content,
|
||||
WellKnown = _configuration.GetValueOrDefault(nameof(AuthServiceConfiguration.WellKnown), string.Empty),
|
||||
WellKnown = wk,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -190,4 +194,3 @@ public class JwtController : Controller
|
||||
return handler.CreateJwtSecurityToken(token);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<EnableDefaultContentItems>false</EnableDefaultContentItems>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Remove="appsettings.Development.json" />
|
||||
<Content Remove="appsettings.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="appsettings.Development.json" />
|
||||
<None Include="appsettings.json">
|
||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||
</None>
|
||||
<Content Include="appsettings.Development.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="appsettings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -29,6 +27,9 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="8.0.1" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.9.11" />
|
||||
<PackageReference Include="StackExchange.Redis.Extensions.Core" Version="10.2.0" />
|
||||
<PackageReference Include="StackExchange.Redis.Extensions.System.Text.Json" Version="10.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ using System.Text;
|
||||
using MareSynchronosShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Prometheus;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using MareSynchronosShared.Utils.Configuration;
|
||||
|
||||
namespace MareSynchronosAuthService;
|
||||
@@ -35,6 +36,12 @@ public class Startup
|
||||
{
|
||||
var config = app.ApplicationServices.GetRequiredService<IConfigurationService<MareConfigurationBase>>();
|
||||
|
||||
// Respect X-Forwarded-* headers from the reverse proxy so generated links use the public scheme/host
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedFor
|
||||
});
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseHttpMetrics();
|
||||
@@ -48,6 +55,7 @@ public class Startup
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapControllers();
|
||||
endpoints.MapHealthChecks("/healthz").WithMetadata(new AllowAnonymousAttribute());
|
||||
|
||||
foreach (var source in endpoints.DataSources.SelectMany(e => e.Endpoints).Cast<RouteEndpoint>())
|
||||
{
|
||||
@@ -65,8 +73,8 @@ public class Startup
|
||||
|
||||
ConfigureRedis(services, mareConfig);
|
||||
|
||||
services.AddSingleton<SecretKeyAuthenticatorService>();
|
||||
services.AddSingleton<AccountRegistrationService>();
|
||||
services.AddScoped<SecretKeyAuthenticatorService>();
|
||||
services.AddScoped<AccountRegistrationService>();
|
||||
services.AddSingleton<GeoIPService>();
|
||||
|
||||
services.AddHostedService(provider => provider.GetRequiredService<GeoIPService>());
|
||||
@@ -75,6 +83,30 @@ public class Startup
|
||||
services.Configure<MareConfigurationBase>(_configuration.GetRequiredSection("MareSynchronos"));
|
||||
|
||||
services.AddSingleton<ServerTokenGenerator>();
|
||||
// Nearby discovery services (well-known + presence)
|
||||
services.AddSingleton<DiscoveryWellKnownProvider>();
|
||||
services.AddHostedService(p => p.GetRequiredService<DiscoveryWellKnownProvider>());
|
||||
|
||||
// Presence store selection
|
||||
var discoveryStore = _configuration.GetValue<string>("NearbyDiscovery:Store") ?? "memory";
|
||||
TimeSpan presenceTtl = TimeSpan.FromMinutes(_configuration.GetValue<int>("NearbyDiscovery:PresenceTtlMinutes", 5));
|
||||
TimeSpan tokenTtl = TimeSpan.FromSeconds(_configuration.GetValue<int>("NearbyDiscovery:TokenTtlSeconds", 120));
|
||||
if (string.Equals(discoveryStore, "redis", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
services.AddSingleton<MareSynchronosAuthService.Services.Discovery.IDiscoveryPresenceStore>(sp =>
|
||||
{
|
||||
var logger = sp.GetRequiredService<ILogger<MareSynchronosAuthService.Services.Discovery.RedisPresenceStore>>();
|
||||
var mux = sp.GetRequiredService<IConnectionMultiplexer>();
|
||||
return new MareSynchronosAuthService.Services.Discovery.RedisPresenceStore(logger, mux, presenceTtl, tokenTtl);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<MareSynchronosAuthService.Services.Discovery.IDiscoveryPresenceStore>(sp => new MareSynchronosAuthService.Services.Discovery.InMemoryPresenceStore(presenceTtl, tokenTtl));
|
||||
}
|
||||
|
||||
services.AddSingleton<DiscoveryPresenceService>();
|
||||
services.AddHostedService(p => p.GetRequiredService<DiscoveryPresenceService>());
|
||||
|
||||
ConfigureAuthorization(services);
|
||||
|
||||
@@ -88,8 +120,11 @@ public class Startup
|
||||
services.AddControllers().ConfigureApplicationPartManager(a =>
|
||||
{
|
||||
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)
|
||||
@@ -195,6 +230,8 @@ public class Startup
|
||||
};
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -4,5 +4,15 @@
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=umbra_dev"
|
||||
},
|
||||
"MareSynchronos": {
|
||||
"Jwt": "dev-secret-umbra-abcdefghijklmnopqrstuvwxyz123456",
|
||||
"RedisConnectionString": "localhost:6379,connectTimeout=5000,syncTimeout=5000",
|
||||
"MetricsPort": 4985
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ VisualStudioVersion = 17.2.32602.215
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronosServer", "MareSynchronosServer\MareSynchronosServer.csproj", "{029CA97F-E0BA-4172-A191-EA21FB61AD0F}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronos.API", "..\MareAPI\MareSynchronosAPI\MareSynchronos.API.csproj", "{326BFB1B-5571-47A6-8513-1FFDB32D53B0}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronosShared", "MareSynchronosShared\MareSynchronosShared.csproj", "{67B1461D-E215-4BA8-A64D-E1836724D5E6}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronosStaticFilesServer", "MareSynchronosStaticFilesServer\MareSynchronosStaticFilesServer.csproj", "{3C7F43BB-FE4C-48BC-BF42-D24E70E8FCB7}"
|
||||
@@ -18,90 +20,36 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MareSynchronosAuthService", "MareSynchronosAuthService\MareSynchronosAuthService.csproj", "{D7D4041C-DCD9-4B7A-B423-0F458DFFF3D6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MareSynchronos.API", "..\UmbraAPI\MareSynchronosAPI\MareSynchronos.API.csproj", "{FA97F4D1-8F15-4002-A2C1-70EF1367B879}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.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.Build.0 = Release|Any CPU
|
||||
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Release|x64.Build.0 = Release|Any CPU
|
||||
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{029CA97F-E0BA-4172-A191-EA21FB61AD0F}.Release|x86.Build.0 = Release|Any CPU
|
||||
{326BFB1B-5571-47A6-8513-1FFDB32D53B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{326BFB1B-5571-47A6-8513-1FFDB32D53B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{326BFB1B-5571-47A6-8513-1FFDB32D53B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{326BFB1B-5571-47A6-8513-1FFDB32D53B0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{67B1461D-E215-4BA8-A64D-E1836724D5E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{67B1461D-E215-4BA8-A64D-E1836724D5E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{67B1461D-E215-4BA8-A64D-E1836724D5E6}.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.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.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.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.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.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.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.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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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_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_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");
|
||||
|
||||
@@ -259,7 +259,13 @@ public partial class MareHub
|
||||
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(),
|
||||
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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
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")]
|
||||
@@ -202,7 +208,7 @@ public partial class MareHub
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<GroupPasswordDto> GroupCreate()
|
||||
public async Task<GroupPasswordDto> GroupCreate(string? alias)
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
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 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()
|
||||
{
|
||||
GID = gid,
|
||||
HashedPassword = hashedPw,
|
||||
InvitesEnabled = true,
|
||||
OwnerUID = UserUID,
|
||||
Alias = null,
|
||||
IsTemporary = true,
|
||||
ExpiresAt = expiresAtUtc,
|
||||
};
|
||||
|
||||
GroupPair initialPair = new()
|
||||
@@ -245,12 +353,21 @@ public partial class MareHub
|
||||
|
||||
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))
|
||||
.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));
|
||||
_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")]
|
||||
@@ -338,22 +455,130 @@ public partial class MareHub
|
||||
_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 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);
|
||||
|
||||
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 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 oneTimeInvite = await DbContext.GroupTempInvites.SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == hashedPw).ConfigureAwait(false);
|
||||
|
||||
if (group == null
|
||||
|| (!string.Equals(group.HashedPassword, hashedPw, StringComparison.Ordinal) && oneTimeInvite == null)
|
||||
GroupTempInvite? 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
|
||||
|| existingUserCount >= _maxGroupUserCount
|
||||
|| !group.InvitesEnabled
|
||||
|| (!skipInviteCheck && !group.InvitesEnabled)
|
||||
|| joinedGroups >= _maxJoinedGroupsByUser
|
||||
|| isBanned)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (oneTimeInvite != null)
|
||||
{
|
||||
@@ -375,9 +600,17 @@ public partial class MareHub
|
||||
|
||||
_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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
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")]
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ public partial class MareHub : Hub<IMareHub>, IMareHub
|
||||
dbUser.LastLoggedIn = DateTime.UtcNow;
|
||||
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))
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<UserSecretsId>aspnet-MareSynchronosServer-BA82A12A-0B30-463C-801D-B7E81318CD50</UserSecretsId>
|
||||
<AssemblyVersion>1.1.0.0</AssemblyVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
@@ -29,13 +29,17 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</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.IdentityModel.Tokens" Version="7.7.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\UmbraAPI\MareSynchronosAPI\MareSynchronos.API.csproj" />
|
||||
<ProjectReference Include="..\..\MareAPI\MareSynchronosAPI\MareSynchronos.API.csproj" />
|
||||
<ProjectReference Include="..\MareSynchronosShared\MareSynchronosShared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ public class SystemInfoService : IHostedService, IDisposable
|
||||
SystemInfoDto = new SystemInfoDto()
|
||||
{
|
||||
OnlineUsers = onlineUsers,
|
||||
SupportsTypingState = true,
|
||||
};
|
||||
|
||||
if (_config.IsMain)
|
||||
|
||||
@@ -5,6 +5,7 @@ using MareSynchronosShared.Services;
|
||||
using MareSynchronosShared.Utils;
|
||||
using MareSynchronosShared.Utils.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Linq;
|
||||
|
||||
namespace MareSynchronosServer.Services;
|
||||
|
||||
@@ -46,20 +47,41 @@ public class UserCleanupService : IHostedService
|
||||
await PurgeUnusedAccounts(dbContext).ConfigureAwait(false);
|
||||
|
||||
await PurgeTempInvites(dbContext).ConfigureAwait(false);
|
||||
await PurgeExpiredTemporaryGroups(dbContext).ConfigureAwait(false);
|
||||
|
||||
dbContext.SaveChanges();
|
||||
}
|
||||
|
||||
var now = DateTime.Now;
|
||||
TimeOnly currentTime = new(now.Hour, now.Minute, now.Second);
|
||||
TimeOnly futureTime = new(now.Hour, now.Minute - now.Minute % 10, 0);
|
||||
var span = futureTime.AddMinutes(10) - currentTime;
|
||||
var span = TimeSpan.FromMinutes(1);
|
||||
var nextRun = DateTime.Now.Add(span);
|
||||
|
||||
_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);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -13,6 +13,7 @@ using Prometheus;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using StackExchange.Redis;
|
||||
using StackExchange.Redis.Extensions.Core.Configuration;
|
||||
using System.Net;
|
||||
@@ -71,7 +72,7 @@ public class Startup
|
||||
a.FeatureProviders.Remove(a.FeatureProviders.OfType<ControllerFeatureProvider>().First());
|
||||
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
|
||||
{
|
||||
@@ -198,6 +199,21 @@ public class Startup
|
||||
ValidateIssuerSigningKey = true,
|
||||
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 =>
|
||||
@@ -307,6 +323,12 @@ public class Startup
|
||||
|
||||
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.UseRouting();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\UmbraAPI\MareSynchronosAPI\MareSynchronos.API.csproj" />
|
||||
<ProjectReference Include="..\..\MareAPI\MareSynchronosAPI\MareSynchronos.API.csproj" />
|
||||
<ProjectReference Include="..\MareSynchronosShared\MareSynchronosShared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -51,6 +51,9 @@ public class MareDbContext : DbContext
|
||||
public DbSet<CharaDataOriginalFile> CharaDataOriginalFiles { get; set; }
|
||||
public DbSet<CharaDataPose> CharaDataPoses { 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)
|
||||
{
|
||||
@@ -127,5 +130,28 @@ public class MareDbContext : DbContext
|
||||
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.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);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\UmbraAPI\MareSynchronosAPI\MareSynchronos.API.csproj" />
|
||||
<ProjectReference Include="..\..\MareAPI\MareSynchronosAPI\MareSynchronos.API.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
1150
MareSynchronosServer/MareSynchronosShared/Migrations/20251102085631_SyncshellDiscovery.Designer.cs
generated
Normal file
1150
MareSynchronosServer/MareSynchronosShared/Migrations/20251102085631_SyncshellDiscovery.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
1150
MareSynchronosServer/MareSynchronosShared/Migrations/20251102085639_McdfShare.Designer.cs
generated
Normal file
1150
MareSynchronosServer/MareSynchronosShared/Migrations/20251102085639_McdfShare.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ namespace MareSynchronosServer.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.4")
|
||||
.HasAnnotation("ProductVersion", "8.0.19")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
@@ -438,6 +438,10 @@ namespace MareSynchronosServer.Migrations
|
||||
.HasColumnType("character varying(50)")
|
||||
.HasColumnName("alias");
|
||||
|
||||
b.Property<bool>("AutoDetectVisible")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("auto_detect_visible");
|
||||
|
||||
b.Property<bool>("DisableAnimations")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("disable_animations");
|
||||
@@ -450,6 +454,10 @@ namespace MareSynchronosServer.Migrations
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("disable_vfx");
|
||||
|
||||
b.Property<DateTime?>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expires_at");
|
||||
|
||||
b.Property<string>("HashedPassword")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("hashed_password");
|
||||
@@ -458,10 +466,18 @@ namespace MareSynchronosServer.Migrations
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("invites_enabled");
|
||||
|
||||
b.Property<bool>("IsTemporary")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_temporary");
|
||||
|
||||
b.Property<string>("OwnerUID")
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("owner_uid");
|
||||
|
||||
b.Property<bool>("PasswordTemporarilyDisabled")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("password_temporarily_disabled");
|
||||
|
||||
b.HasKey("GID")
|
||||
.HasName("pk_groups");
|
||||
|
||||
@@ -615,6 +631,103 @@ namespace MareSynchronosServer.Migrations
|
||||
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 =>
|
||||
{
|
||||
b.Property<string>("UID")
|
||||
@@ -945,6 +1058,41 @@ namespace MareSynchronosServer.Migrations
|
||||
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 =>
|
||||
{
|
||||
b.HasOne("MareSynchronosShared.Models.User", "User")
|
||||
@@ -986,6 +1134,13 @@ namespace MareSynchronosServer.Migrations
|
||||
|
||||
b.Navigation("Poses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MareSynchronosShared.Models.McdfShare", b =>
|
||||
{
|
||||
b.Navigation("AllowedIndividuals");
|
||||
|
||||
b.Navigation("AllowedSyncshells");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace MareSynchronosShared.Models;
|
||||
|
||||
@@ -16,4 +17,8 @@ public class Group
|
||||
public bool DisableSounds { get; set; }
|
||||
public bool DisableAnimations { 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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\UmbraAPI\MareSynchronosAPI\MareSynchronos.API.csproj" />
|
||||
<ProjectReference Include="..\..\MareAPI\MareSynchronosAPI\MareSynchronos.API.csproj" />
|
||||
<ProjectReference Include="..\MareSynchronosShared\MareSynchronosShared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,2 +1,9 @@
|
||||
# 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
350
UmbraAPI/.gitignore
vendored
@@ -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/
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
namespace MareSynchronos.API.Data.Enum;
|
||||
|
||||
[Flags]
|
||||
public enum GroupPermissions
|
||||
{
|
||||
NoneSet = 0x0,
|
||||
DisableAnimations = 0x1,
|
||||
DisableSounds = 0x2,
|
||||
DisableInvites = 0x4,
|
||||
DisableVFX = 0x8,
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace MareSynchronos.API.Data.Enum;
|
||||
|
||||
[Flags]
|
||||
public enum GroupUserInfo
|
||||
{
|
||||
None = 0x0,
|
||||
IsModerator = 0x2,
|
||||
IsPinned = 0x4
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
namespace MareSynchronos.API.Data.Enum;
|
||||
|
||||
[Flags]
|
||||
public enum GroupUserPermissions
|
||||
{
|
||||
NoneSet = 0x0,
|
||||
Paused = 0x1,
|
||||
DisableAnimations = 0x2,
|
||||
DisableSounds = 0x4,
|
||||
DisableVFX = 0x8,
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace MareSynchronos.API.Data.Enum;
|
||||
|
||||
public enum MessageSeverity
|
||||
{
|
||||
Information,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace MareSynchronos.API.Data.Enum;
|
||||
|
||||
public enum ObjectKind
|
||||
{
|
||||
Player = 0,
|
||||
MinionOrMount = 1,
|
||||
Companion = 2,
|
||||
Pet = 3,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace MareSynchronos.API.Dto.CharaData;
|
||||
|
||||
public enum AccessTypeDto
|
||||
{
|
||||
Individuals,
|
||||
ClosePairs,
|
||||
AllPairs,
|
||||
Public
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace MareSynchronos.API.Dto.CharaData;
|
||||
|
||||
public enum ShareTypeDto
|
||||
{
|
||||
Private,
|
||||
Shared
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace MareSynchronos.API.Dto.Files;
|
||||
|
||||
public interface ITransferFileDto
|
||||
{
|
||||
string Hash { get; set; }
|
||||
bool IsForbidden { get; set; }
|
||||
string ForbiddenBy { get; set; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -1,9 +0,0 @@
|
||||
using MessagePack;
|
||||
|
||||
namespace MareSynchronos.API.Dto;
|
||||
|
||||
[MessagePackObject(keyAsPropertyName: true)]
|
||||
public record SystemInfoDto
|
||||
{
|
||||
public int OnlineUsers { get; set; }
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -1,7 +0,0 @@
|
||||
using MareSynchronos.API.Data;
|
||||
using MessagePack;
|
||||
|
||||
namespace MareSynchronos.API.Dto.User;
|
||||
|
||||
[MessagePackObject(keyAsPropertyName: true)]
|
||||
public record UserDto(UserData User);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
@@ -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
Reference in New Issue
Block a user