diff --git a/LightlessAPI b/LightlessAPI index 0bc7abb..bb92cd4 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 0bc7abb274548bcde36c65ef1cf9f1a143d6492c +Subproject commit bb92cd477d76f24fd28200ade00076bc77fe299d diff --git a/LightlessSyncServer/LightlessSyncAuthService/Controllers/AuthControllerBase.cs b/LightlessSyncServer/LightlessSyncAuthService/Controllers/AuthControllerBase.cs index b47f38d..63a97be 100644 --- a/LightlessSyncServer/LightlessSyncAuthService/Controllers/AuthControllerBase.cs +++ b/LightlessSyncServer/LightlessSyncAuthService/Controllers/AuthControllerBase.cs @@ -100,14 +100,15 @@ public abstract class AuthControllerBase : Controller protected async Task CreateJwtFromId(string uid, string charaIdent, string alias) { - var token = CreateJwt(new List() - { + var token = CreateJwt( + [ new Claim(LightlessClaimTypes.Uid, uid), new Claim(LightlessClaimTypes.CharaIdent, charaIdent), new Claim(LightlessClaimTypes.Alias, alias), new Claim(LightlessClaimTypes.Expires, DateTime.UtcNow.AddHours(6).Ticks.ToString(CultureInfo.InvariantCulture)), - new Claim(LightlessClaimTypes.Continent, await _geoIPProvider.GetCountryFromIP(HttpAccessor)) - }); + new Claim(LightlessClaimTypes.Continent, await _geoIPProvider.GetContinentFromIP(HttpAccessor)), + new Claim(LightlessClaimTypes.Country, await _geoIPProvider.GetCountryFromIP(HttpAccessor)), + ]); return Content(token.RawData); } diff --git a/LightlessSyncServer/LightlessSyncAuthService/Controllers/JwtController.cs b/LightlessSyncServer/LightlessSyncAuthService/Controllers/JwtController.cs index f89946b..adb8d98 100644 --- a/LightlessSyncServer/LightlessSyncAuthService/Controllers/JwtController.cs +++ b/LightlessSyncServer/LightlessSyncAuthService/Controllers/JwtController.cs @@ -1,5 +1,6 @@ using LightlessSync.API.Routes; using LightlessSyncAuthService.Services; +using LightlessSyncAuthService.Utils; using LightlessSyncShared; using LightlessSyncShared.Data; using LightlessSyncShared.Services; diff --git a/LightlessSyncServer/LightlessSyncAuthService/Controllers/OAuthController.cs b/LightlessSyncServer/LightlessSyncAuthService/Controllers/OAuthController.cs index e3c4916..5d7215f 100644 --- a/LightlessSyncServer/LightlessSyncAuthService/Controllers/OAuthController.cs +++ b/LightlessSyncServer/LightlessSyncAuthService/Controllers/OAuthController.cs @@ -1,5 +1,6 @@ using LightlessSync.API.Routes; using LightlessSyncAuthService.Services; +using LightlessSyncAuthService.Utils; using LightlessSyncShared; using LightlessSyncShared.Data; using LightlessSyncShared.Services; diff --git a/LightlessSyncServer/LightlessSyncAuthService/Services/GeoIPService.cs b/LightlessSyncServer/LightlessSyncAuthService/Services/GeoIPService.cs index 2ea048f..a60a20f 100644 --- a/LightlessSyncServer/LightlessSyncAuthService/Services/GeoIPService.cs +++ b/LightlessSyncServer/LightlessSyncAuthService/Services/GeoIPService.cs @@ -1,7 +1,8 @@ -using LightlessSyncShared; +using LightlessSyncAuthService.Utils; using LightlessSyncShared.Services; using LightlessSyncShared.Utils.Configuration; using MaxMind.GeoIP2; +using System.Net; namespace LightlessSyncAuthService.Services; @@ -23,7 +24,7 @@ public class GeoIPService : IHostedService _lightlessConfiguration = lightlessConfiguration; } - public async Task GetCountryFromIP(IHttpContextAccessor httpContextAccessor) + public async Task GetContinentFromIP(IHttpContextAccessor httpContextAccessor) { if (!_useGeoIP) { @@ -32,7 +33,9 @@ public class GeoIPService : IHostedService try { - var ip = httpContextAccessor.GetIpAddress(); + var ip = httpContextAccessor.GetClientIpAddress(); + if (ip is null || IPAddress.IsLoopback(ip)) + return "*"; using CancellationTokenSource waitCts = new(); waitCts.CancelAfter(TimeSpan.FromSeconds(5)); @@ -41,6 +44,7 @@ public class GeoIPService : IHostedService if (_dbReader!.TryCity(ip, out var response)) { string? continent = response?.Continent.Code; + if (!string.IsNullOrEmpty(continent) && string.Equals(continent, "NA", StringComparison.Ordinal) && response?.Location.Longitude != null) @@ -140,4 +144,34 @@ public class GeoIPService : IHostedService _dbReader?.Dispose(); return Task.CompletedTask; } + + internal async Task GetCountryFromIP(IHttpContextAccessor httpContextAccessor) + { + if (!_useGeoIP) + return "*"; + + var ip = httpContextAccessor.GetClientIpAddress(); + if (ip is null || IPAddress.IsLoopback(ip)) + return "*"; + + try + { + using CancellationTokenSource waitCts = new(TimeSpan.FromSeconds(5)); + while (_processingReload) + await Task.Delay(100, waitCts.Token).ConfigureAwait(false); + + if (_dbReader!.TryCity(ip, out var response)) + { + var country = response?.Country?.IsoCode; + return country ?? "*"; + } + + return "*"; + } + catch (Exception ex) + { + _logger.LogError(ex, "GeoIP lookup failed for {Ip}", ip); + return "*"; + } + } } diff --git a/LightlessSyncServer/LightlessSyncAuthService/Utils/HttpContextAccessorExtensions.cs b/LightlessSyncServer/LightlessSyncAuthService/Utils/HttpContextAccessorExtensions.cs new file mode 100644 index 0000000..8d3e11d --- /dev/null +++ b/LightlessSyncServer/LightlessSyncAuthService/Utils/HttpContextAccessorExtensions.cs @@ -0,0 +1,26 @@ +using System.Net; + +namespace LightlessSyncAuthService.Utils +{ + public static class HttpContextAccessorExtensions + { + public static IPAddress? GetClientIpAddress(this IHttpContextAccessor accessor) + { + var context = accessor.HttpContext; + if (context == null) return null; + + string[] headerKeys = { "CF-Connecting-IP", "X-Forwarded-For", "X-Real-IP" }; + foreach (var key in headerKeys) + { + if (context.Request.Headers.TryGetValue(key, out var values)) + { + var ipCandidate = values.FirstOrDefault()?.Split(',').FirstOrDefault()?.Trim(); + if (IPAddress.TryParse(ipCandidate, out var parsed)) + return parsed; + } + } + + return context.Connection?.RemoteIpAddress; + } + } +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs index 82e757a..c5fd851 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs @@ -1,5 +1,6 @@ using LightlessSync.API.Data; using LightlessSync.API.Dto.Group; +using LightlessSyncServer.Models; using LightlessSyncServer.Utils; using LightlessSyncShared.Metrics; using LightlessSyncShared.Models; @@ -8,7 +9,6 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using StackExchange.Redis; using System.Text.Json; -using System.Threading; namespace LightlessSyncServer.Hubs; @@ -20,6 +20,8 @@ public partial class LightlessHub public string Continent => Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, LightlessClaimTypes.Continent, StringComparison.Ordinal))?.Value ?? "UNK"; + public string Country => Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, LightlessClaimTypes.Country, StringComparison.Ordinal))?.Value ?? "UNK"; + private async Task DeleteUser(User user) { var ownPairData = await DbContext.ClientPairs.Where(u => u.User.UID == user.UID).ToListAsync().ConfigureAwait(false); diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs index fe6ac8f..80f8d5f 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs @@ -3,14 +3,18 @@ using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.User; +using LightlessSyncServer.Models; +using LightlessSyncServer.Services; using LightlessSyncServer.Utils; using LightlessSyncShared.Models; using LightlessSyncShared.Utils; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json.Linq; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; +using System.Reflection; using System.Security.Cryptography; namespace LightlessSyncServer.Hubs; @@ -750,7 +754,7 @@ public partial class LightlessHub if (dto?.Group == null) { _logger.LogCallWarning(LightlessHubLogger.Args("GroupGetProfile: dto.Group is null")); - return new GroupProfileDto(Group: null, Description: null, Tags: null, PictureBase64: null, IsNsfw: false, IsDisabled: false); + return new GroupProfileDto(Group: null, Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: false, IsDisabled: false); } var data = await DbContext.GroupProfiles @@ -763,12 +767,12 @@ public partial class LightlessHub if (data == null) { - return new GroupProfileDto(dto.Group, Description: null, Tags: null, PictureBase64: null, IsNsfw: false, IsDisabled: false); + return new GroupProfileDto(dto.Group, Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: false, IsDisabled: false); } if (data.ProfileDisabled) { - return new GroupProfileDto(Group: dto.Group, Description: "This profile was permanently disabled", Tags: [], PictureBase64: null, IsNsfw: false, IsDisabled: true); + return new GroupProfileDto(Group: dto.Group, Description: "This profile was permanently disabled", Tags: [], PictureBase64: null, BannerBase64: null, IsNsfw: false, IsDisabled: true); } try @@ -778,7 +782,7 @@ public partial class LightlessHub catch (Exception ex) { _logger.LogCallWarning(LightlessHubLogger.Args(ex, "GroupGetProfile: failed to map GroupProfileDto for {Group}", dto.Group.GID ?? dto.Group.AliasOrGID)); - return new GroupProfileDto(dto.Group, Description: null, Tags: null, PictureBase64: null, IsNsfw: false, IsDisabled: false); + return new GroupProfileDto(dto.Group, Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: false, IsDisabled: false); } } @@ -799,36 +803,30 @@ public partial class LightlessHub cancellationToken) .ConfigureAwait(false); + ImageCheckService.ImageLoadResult profileResult = null; + ImageCheckService.ImageLoadResult bannerResult = null; + //Avatar image validation if (!string.IsNullOrEmpty(dto.PictureBase64)) { - byte[] imageData; - try + profileResult = await ImageCheckService.ValidateImageAsync(dto.PictureBase64, banner: false, RequestAbortedToken).ConfigureAwait(false); + + if (!profileResult.Success) { - imageData = Convert.FromBase64String(dto.PictureBase64); - } - catch (FormatException) - { - await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "The provided image is not a valid Base64 string.").ConfigureAwait(false); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, profileResult.ErrorMessage).ConfigureAwait(false); return; } + } - MemoryStream ms = new(imageData); - await using (ms.ConfigureAwait(false)) + //Banner image validation + if (!string.IsNullOrEmpty(dto.BannerBase64)) + { + bannerResult = await ImageCheckService.ValidateImageAsync(dto.BannerBase64, banner: true, RequestAbortedToken).ConfigureAwait(false); + + if (!bannerResult.Success) { - var format = await Image.DetectFormatAsync(ms, RequestAbortedToken).ConfigureAwait(false); - if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase)) - { - await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is not in PNG format").ConfigureAwait(false); - return; - } - using var image = Image.Load(imageData); - - if (image.Width > 512 || image.Height > 512 || (imageData.Length > 2000 * 1024)) - { - await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is larger than 512x512 or more than 2MiB").ConfigureAwait(false); - return; - } + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, bannerResult.ErrorMessage).ConfigureAwait(false); + return; } } @@ -841,7 +839,7 @@ public partial class LightlessHub IsNSFW = dto.IsNsfw ?? false, }; - groupProfileDb.UpdateProfileFromDto(dto); + groupProfileDb.UpdateProfileFromDto(dto, profileResult.Base64Image, bannerResult.Base64Image); await DbContext.GroupProfiles.AddAsync(groupProfileDb, cancellationToken).ConfigureAwait(false); } else @@ -852,7 +850,7 @@ public partial class LightlessHub return; } - groupProfileDb.UpdateProfileFromDto(dto); + groupProfileDb.UpdateProfileFromDto(dto, profileResult.Base64Image, bannerResult.Base64Image); var userIds = await DbContext.GroupPairs .Where(p => p.GroupGID == groupProfileDb.GroupGID) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs index 838118c..b40cc57 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs @@ -3,6 +3,8 @@ using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.User; +using LightlessSyncServer.Models; +using LightlessSyncServer.Services; using LightlessSyncServer.Utils; using LightlessSyncShared.Metrics; using LightlessSyncShared.Models; @@ -204,8 +206,8 @@ public partial class LightlessHub return; } - var sender = await _pairService.TryAddPairAsync(UserUID, payload.UID); - var receiver = await _pairService.TryAddPairAsync(payload.UID, UserUID); + var sender = await _pairService.TryAddPairAsync(UserUID, payload.UID).ConfigureAwait(false); + var receiver = await _pairService.TryAddPairAsync(payload.UID, UserUID).ConfigureAwait(false); var user = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false); var otherUser = await DbContext.Users.SingleAsync(u => u.UID == payload.UID).ConfigureAwait(false); @@ -304,7 +306,6 @@ public partial class LightlessHub } } - private async Task NotifyBroadcastOwnerOfPairRequest(string targetHashedCid) { var myHashedCid = UserCharaIdent; @@ -360,23 +361,6 @@ public partial class LightlessHub await Clients.User(entry.OwnerUID).Client_ReceiveBroadcastPairRequest(dto).ConfigureAwait(false); } - private class PairingPayload - { - public string UID { get; set; } = string.Empty; - public string HashedCid { get; set; } = string.Empty; - public DateTime Timestamp { get; set; } - } - - public class BroadcastRedisEntry - { - public string HashedCID { get; set; } = string.Empty; - public string OwnerUID { get; set; } = string.Empty; - public string? GID { get; set; } - - public bool OwnedBy(string userUid) => !string.IsNullOrEmpty(userUid) && string.Equals(OwnerUID, userUid, StringComparison.Ordinal); - - public bool HasOwner() => !string.IsNullOrEmpty(OwnerUID); - } [Authorize(Policy = "Identified")] public async Task SetBroadcastStatus(bool enabled, GroupBroadcastRequestDto? groupDto = null) @@ -826,16 +810,16 @@ public partial class LightlessHub if (!allUserPairs.Contains(user.User.UID, StringComparer.Ordinal) && !string.Equals(user.User.UID, UserUID, StringComparison.Ordinal)) { - return new UserProfileDto(user.User, false, null, null, "Due to the pause status you cannot access this users profile.", []); + return new UserProfileDto(user.User, Disabled: false, IsNSFW: null, ProfilePictureBase64: null, BannerPictureBase64: null, Description: "Due to the pause status you cannot access this users profile.", Tags: []); } var data = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == user.User.UID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); - if (data == null) return new UserProfileDto(user.User, false, null, null, null, []); + if (data == null) return new UserProfileDto(user.User, Disabled: false, IsNSFW: null, ProfilePictureBase64: null, BannerPictureBase64: null, Description: null, Tags: []); - if (data.FlaggedForReport) return new UserProfileDto(user.User, true, null, null, "This profile is flagged for report and pending evaluation", []); - if (data.ProfileDisabled) return new UserProfileDto(user.User, true, null, null, "This profile was permanently disabled", []); + if (data.FlaggedForReport) return new UserProfileDto(user.User, Disabled: true, IsNSFW: null, ProfilePictureBase64: null, BannerPictureBase64: null, Description: "This profile is flagged for report and pending evaluation", Tags: []); + if (data.ProfileDisabled) return new UserProfileDto(user.User, Disabled: true, IsNSFW: null, ProfilePictureBase64: null, BannerPictureBase64: null, Description: "This profile was permanently disabled", Tags: []); - return new UserProfileDto(user.User, false, data.IsNSFW, data.Base64ProfileImage, data.UserDescription, data.Tags); + return data.ToDTO(); } [Authorize(Policy = "Identified")] @@ -913,20 +897,20 @@ public partial class LightlessHub if (profile == null) { - return new UserProfileDto(userData, false, null, null, null, []); + return new UserProfileDto(userData, Disabled: false, IsNSFW: null, ProfilePictureBase64: null, BannerPictureBase64: null, Description: null, Tags: []); } if (profile.FlaggedForReport) { - return new UserProfileDto(userData, true, null, null, "This profile is flagged for report and pending evaluation", []); + return new UserProfileDto(userData, Disabled: true, IsNSFW: null, ProfilePictureBase64: null, BannerPictureBase64: null, Description: "This profile is flagged for report and pending evaluation", Tags: []); } if (profile.ProfileDisabled) { - return new UserProfileDto(userData, true, null, null, "This profile was permanently disabled", []); + return new UserProfileDto(userData, Disabled: true, IsNSFW: null, ProfilePictureBase64: null, BannerPictureBase64: null, Description: "This profile was permanently disabled", Tags: []); } - return new UserProfileDto(userData, false, profile.IsNSFW, profile.Base64ProfileImage, profile.UserDescription, profile.Tags); + return profile.ToDTO(); } [Authorize(Policy = "Identified")] @@ -959,22 +943,36 @@ public partial class LightlessHub } bool hadInvalidData = false; - List invalidGamePaths = new(); - List invalidFileSwapPaths = new(); + List invalidGamePaths = []; + List invalidFileSwapPaths = []; + + var gamePathRegex = GamePathRegex(); + var hashRegex = HashRegex(); + foreach (var replacement in dto.CharaData.FileReplacements.SelectMany(p => p.Value)) { - var invalidPaths = replacement.GamePaths.Where(p => !GamePathRegex().IsMatch(p)).ToList(); - invalidPaths.AddRange(replacement.GamePaths.Where(p => !AllowedExtensionsForGamePaths.Any(e => p.EndsWith(e, StringComparison.OrdinalIgnoreCase)))); - replacement.GamePaths = replacement.GamePaths.Where(p => !invalidPaths.Contains(p, StringComparer.OrdinalIgnoreCase)).ToArray(); - bool validGamePaths = replacement.GamePaths.Any(); - bool validHash = string.IsNullOrEmpty(replacement.Hash) || HashRegex().IsMatch(replacement.Hash); - bool validFileSwapPath = string.IsNullOrEmpty(replacement.FileSwapPath) || GamePathRegex().IsMatch(replacement.FileSwapPath); - if (!validGamePaths || !validHash || !validFileSwapPath) + var validGamePaths = replacement.GamePaths + .Where(p => gamePathRegex.IsMatch(p) && + AllowedExtensionsForGamePaths.Any(e => p.EndsWith(e, StringComparison.OrdinalIgnoreCase))) + .ToArray(); + + var invalidPaths = replacement.GamePaths.Except(validGamePaths, StringComparer.OrdinalIgnoreCase).ToArray(); + + replacement.GamePaths = validGamePaths; + + bool validHash = string.IsNullOrEmpty(replacement.Hash) || hashRegex.IsMatch(replacement.Hash); + bool validFileSwapPath = string.IsNullOrEmpty(replacement.FileSwapPath) || gamePathRegex.IsMatch(replacement.FileSwapPath); + bool validGamePathsFlag = validGamePaths.Length + != 0; + + if (!validGamePathsFlag || !validHash || !validFileSwapPath) { - _logger.LogCallWarning(LightlessHubLogger.Args("Invalid Data", "GamePaths", validGamePaths, string.Join(",", invalidPaths), "Hash", validHash, replacement.Hash, "FileSwap", validFileSwapPath, replacement.FileSwapPath)); + _logger.LogCallWarning(LightlessHubLogger.Args("Invalid Data", "GamePaths", validGamePathsFlag, string.Join(',', invalidPaths), "Hash", validHash, replacement.Hash, "FileSwap", validFileSwapPath, replacement.FileSwapPath)); + hadInvalidData = true; + if (!validFileSwapPath) invalidFileSwapPaths.Add(replacement.FileSwapPath); - if (!validGamePaths) invalidGamePaths.AddRange(replacement.GamePaths); + if (!validGamePathsFlag) invalidGamePaths.AddRange(invalidPaths); if (!validHash) invalidFileSwapPaths.Add(replacement.Hash); } } @@ -1132,36 +1130,30 @@ public partial class LightlessHub var existingData = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == dto.User.UID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); - //Image Check of size/format + ImageCheckService.ImageLoadResult profileResult = new(); + ImageCheckService.ImageLoadResult bannerResult = new(); + + //Avatar image validation if (!string.IsNullOrEmpty(dto.ProfilePictureBase64)) { - byte[] imageData; - try + profileResult = await ImageCheckService.ValidateImageAsync(dto.ProfilePictureBase64, banner: false, RequestAbortedToken).ConfigureAwait(false); + + if (!profileResult.Success) { - imageData = Convert.FromBase64String(dto.ProfilePictureBase64); - } - catch (FormatException) - { - await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "The provided image is not a valid Base64 string.").ConfigureAwait(false); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, profileResult.ErrorMessage).ConfigureAwait(false); return; } + } - MemoryStream ms = new(imageData); - await using (ms.ConfigureAwait(false)) + //Banner image validation + if (!string.IsNullOrEmpty(dto.BannerPictureBase64)) + { + bannerResult = await ImageCheckService.ValidateImageAsync(dto.BannerPictureBase64, banner: true, RequestAbortedToken).ConfigureAwait(false); + + if (!bannerResult.Success) { - var format = await Image.DetectFormatAsync(ms, RequestAbortedToken).ConfigureAwait(false); - if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase)) - { - await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is not in PNG format").ConfigureAwait(false); - return; - } - using var image = Image.Load(imageData); - - if (image.Width > 512 || image.Height > 512 || (imageData.Length > 2000 * 1024)) - { - await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is larger than 512x512 or more than 2MiB.").ConfigureAwait(false); - return; - } + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, bannerResult.ErrorMessage).ConfigureAwait(false); + return; } } @@ -1179,7 +1171,7 @@ public partial class LightlessHub return; } - existingData.UpdateProfileFromDto(dto); + existingData.UpdateProfileFromDto(dto, profileResult.Base64Image, bannerResult.Base64Image); } else { @@ -1191,7 +1183,7 @@ public partial class LightlessHub IsNSFW = dto.IsNSFW ?? false, }; - newUserProfileData.UpdateProfileFromDto(dto); + existingData.UpdateProfileFromDto(dto, profileResult.Base64Image, bannerResult.Base64Image); await DbContext.UserProfileData.AddAsync(newUserProfileData, cancellationToken).ConfigureAwait(false); } diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.cs index 318fc99..b366bf2 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.cs @@ -16,7 +16,6 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using StackExchange.Redis.Extensions.Core.Abstractions; using System.Collections.Concurrent; -using System.Threading; namespace LightlessSyncServer.Hubs; @@ -161,8 +160,13 @@ public partial class LightlessHub : Hub, ILightlessHub } else { - _lightlessMetrics.IncGaugeWithLabels(MetricsAPI.GaugeConnections, labels: Continent); + var ResultLabels = new List + { + Continent, + Country, + }; + _lightlessMetrics.IncGaugeWithLabels(MetricsAPI.GaugeConnections, labels: [.. ResultLabels]); try { _logger.LogCallInfo(LightlessHubLogger.Args(_contextAccessor.GetIpAddress(), Context.ConnectionId, UserCharaIdent)); @@ -185,7 +189,12 @@ public partial class LightlessHub : Hub, ILightlessHub if (_userConnections.TryGetValue(UserUID, out var connectionId) && string.Equals(connectionId, Context.ConnectionId, StringComparison.Ordinal)) { - _lightlessMetrics.DecGaugeWithLabels(MetricsAPI.GaugeConnections, labels: Continent); + var ResultLabels = new List + { + Continent, + Country, + }; + _lightlessMetrics.DecGaugeWithLabels(MetricsAPI.GaugeConnections, labels: [.. ResultLabels]); try { diff --git a/LightlessSyncServer/LightlessSyncServer/Models/BroadcastRedisEntry.cs b/LightlessSyncServer/LightlessSyncServer/Models/BroadcastRedisEntry.cs new file mode 100644 index 0000000..b5fe478 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Models/BroadcastRedisEntry.cs @@ -0,0 +1,12 @@ +namespace LightlessSyncServer.Models; + +public class BroadcastRedisEntry() +{ + public string? GID { get; set; } + public string HashedCID { get; set; } = string.Empty; + public string OwnerUID { get; set; } = string.Empty; + + public bool OwnedBy(string userUid) => !string.IsNullOrEmpty(userUid) && string.Equals(OwnerUID, userUid, StringComparison.Ordinal); + + public bool HasOwner() => !string.IsNullOrEmpty(OwnerUID); +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncServer/Models/PairingPayload.cs b/LightlessSyncServer/LightlessSyncServer/Models/PairingPayload.cs new file mode 100644 index 0000000..89cda50 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Models/PairingPayload.cs @@ -0,0 +1,8 @@ +namespace LightlessSyncServer.Models; + +public class PairingPayload +{ + public string UID { get; set; } = string.Empty; + public string HashedCid { get; set; } = string.Empty; + public DateTime Timestamp { get; set; } +} \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncServer/Program.cs b/LightlessSyncServer/LightlessSyncServer/Program.cs index 426d520..d55e08a 100644 --- a/LightlessSyncServer/LightlessSyncServer/Program.cs +++ b/LightlessSyncServer/LightlessSyncServer/Program.cs @@ -41,7 +41,6 @@ public class Program metrics.SetGaugeTo(MetricsAPI.GaugeUsersRegistered, context.Users.AsNoTracking().Count()); metrics.SetGaugeTo(MetricsAPI.GaugePairs, context.ClientPairs.AsNoTracking().Count()); metrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, context.Permissions.AsNoTracking().Where(p=>p.IsPaused).Count()); - } if (args.Length == 0 || !string.Equals(args[0], "dry", StringComparison.Ordinal)) diff --git a/LightlessSyncServer/LightlessSyncServer/Services/ImageCheckService.cs b/LightlessSyncServer/LightlessSyncServer/Services/ImageCheckService.cs new file mode 100644 index 0000000..c36dc25 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Services/ImageCheckService.cs @@ -0,0 +1,105 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace LightlessSyncServer.Services +{ + public class ImageCheckService + { + private static readonly int _imageWidthAvatar = 512; + private static readonly int _imageHeightAvatar = 512; + private static readonly int _imageWidthBanner = 840; + private static readonly int _imageHeightBanner = 260; + private static readonly int _imageSize = 2000; + + public class ImageLoadResult + { + public bool Success { get; init; } + public string? Base64Image { get; init; } + public string? ErrorMessage { get; init; } + + public static ImageLoadResult Fail(string message) => new() + { + Success = false, + ErrorMessage = message, + }; + + public static ImageLoadResult Ok(string base64) => new() + { + Success = true, + Base64Image = base64, + }; + } + + + public static async Task ValidateImageAsync(string base64String, bool banner, CancellationToken token) + { + if (token.IsCancellationRequested) + return ImageLoadResult.Fail("Operation cancelled."); + + byte[] imageData; + try + { + imageData = Convert.FromBase64String(base64String); + } + catch (FormatException) + { + return ImageLoadResult.Fail("The provided image is not a valid Base64 string."); + } + + Image? image = null; + bool imageLoaded = false; + IImageFormat? format = null; + + try + { + using (var ms = new MemoryStream(imageData)) + { + format = await Image.DetectFormatAsync(ms, token).ConfigureAwait(false); + } + + if (format == null) + { + return ImageLoadResult.Fail("Unable to detect image format."); + } + + using (image = Image.Load(imageData)) + { + imageLoaded = true; + + int maxWidth = banner ? _imageWidthBanner : _imageWidthAvatar; + int maxHeight = banner ? _imageHeightBanner : _imageHeightAvatar; + + if (image.Width > maxWidth || image.Height > maxHeight) + { + var ratio = Math.Min((double)maxWidth / image.Width, (double)maxHeight / image.Height); + int newWidth = (int)(image.Width * ratio); + int newHeight = (int)(image.Height * ratio); + + image.Mutate(x => x.Resize(newWidth, newHeight)); + } + + using var memoryStream = new MemoryStream(); + + await image.SaveAsPngAsync(memoryStream, token).ConfigureAwait(false); + + if (memoryStream.Length > _imageSize * 1024) + { + return ImageLoadResult.Fail("Your image exceeds 2 MiB after resizing/conversion."); + } + + string base64Png = Convert.ToBase64String(memoryStream.GetBuffer(), 0, (int)memoryStream.Length); + return ImageLoadResult.Ok(base64Png); + } + } + catch + { + if (imageLoaded) + image?.Dispose(); + + return ImageLoadResult.Fail("Failed to load or process the image. It may be corrupted or unsupported."); + } + } + } +} diff --git a/LightlessSyncServer/LightlessSyncServer/Services/MareCensus.cs b/LightlessSyncServer/LightlessSyncServer/Services/LightlessCensus.cs similarity index 100% rename from LightlessSyncServer/LightlessSyncServer/Services/MareCensus.cs rename to LightlessSyncServer/LightlessSyncServer/Services/LightlessCensus.cs diff --git a/LightlessSyncServer/LightlessSyncServer/Services/SystemInfoService.cs b/LightlessSyncServer/LightlessSyncServer/Services/SystemInfoService.cs index 255fdb0..8778613 100644 --- a/LightlessSyncServer/LightlessSyncServer/Services/SystemInfoService.cs +++ b/LightlessSyncServer/LightlessSyncServer/Services/SystemInfoService.cs @@ -1,15 +1,14 @@ using LightlessSync.API.Dto; using LightlessSync.API.SignalR; using LightlessSyncServer.Hubs; +using LightlessSyncServer.Models; using LightlessSyncShared.Data; using LightlessSyncShared.Metrics; using LightlessSyncShared.Services; using LightlessSyncShared.Utils.Configuration; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; -using StackExchange.Redis; using StackExchange.Redis.Extensions.Core.Abstractions; -using static LightlessSyncServer.Hubs.LightlessHub; namespace LightlessSyncServer.Services; diff --git a/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs b/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs index 1a1130f..e0d09d9 100644 --- a/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs +++ b/LightlessSyncServer/LightlessSyncServer/Utils/Extensions.cs @@ -10,21 +10,29 @@ namespace LightlessSyncServer.Utils; public static class Extensions { - public static void UpdateProfileFromDto(this GroupProfile profile, GroupProfileDto dto) + public static void UpdateProfileFromDto(this GroupProfile profile, GroupProfileDto dto, string? base64PictureString = null, string? base64BannerString = null) { + ArgumentNullException.ThrowIfNull(profile); + ArgumentNullException.ThrowIfNull(dto); + if (profile == null || dto == null) return; - profile.Base64GroupProfileImage = string.IsNullOrWhiteSpace(dto.PictureBase64) ? null : dto.PictureBase64; + profile.Base64GroupProfileImage = string.IsNullOrWhiteSpace(base64PictureString) ? null : base64PictureString; + profile.Base64GroupBannerImage = string.IsNullOrWhiteSpace(base64BannerString) ? null : base64BannerString; if (dto.Tags != null) profile.Tags = dto.Tags; if (dto.Description != null) profile.Description = dto.Description; if (dto.IsNsfw.HasValue) profile.IsNSFW = dto.IsNsfw.Value; } - public static void UpdateProfileFromDto(this UserProfileData profile, UserProfileDto dto) + public static void UpdateProfileFromDto(this UserProfileData profile, UserProfileDto dto, string? base64PictureString, string? base64BannerString = null) { + ArgumentNullException.ThrowIfNull(profile); + ArgumentNullException.ThrowIfNull(dto); + if (profile == null || dto == null) return; - profile.Base64ProfileImage = string.IsNullOrWhiteSpace(dto.ProfilePictureBase64) ? null : dto.ProfilePictureBase64; + profile.Base64ProfileImage = string.IsNullOrWhiteSpace(base64PictureString) ? null : base64PictureString; + profile.Base64BannerImage = string.IsNullOrWhiteSpace(base64BannerString) ? null : base64BannerString; if (dto.Tags != null) profile.Tags = dto.Tags; if (dto.Description != null) profile.UserDescription = dto.Description; if (dto.IsNSFW.HasValue) profile.IsNSFW = dto.IsNSFW.Value; @@ -34,7 +42,7 @@ public static class Extensions { if (groupProfile == null) { - return new GroupProfileDto(Group: null, Description: null, Tags: null, PictureBase64: null, IsNsfw: false, IsDisabled: false); + return new GroupProfileDto(Group: null, Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: false, IsDisabled: false); } var groupData = groupProfile.Group?.ToGroupData(); @@ -44,6 +52,7 @@ public static class Extensions groupProfile.Description, groupProfile.Tags, groupProfile.Base64GroupProfileImage, + groupProfile.Base64GroupBannerImage, groupProfile.IsNSFW, groupProfile.ProfileDisabled ); @@ -53,7 +62,7 @@ public static class Extensions { if (userProfileData == null) { - return new UserProfileDto(User: null, Disabled: false, IsNSFW: null, ProfilePictureBase64: null, Description: null, Tags: []); + return new UserProfileDto(User: null, Disabled: false, IsNSFW: null, ProfilePictureBase64: null, BannerPictureBase64: null, Description: null, Tags: []); } var userData = userProfileData.User?.ToUserData(); @@ -63,6 +72,7 @@ public static class Extensions userProfileData.ProfileDisabled, userProfileData.IsNSFW, userProfileData.Base64ProfileImage, + userProfileData.Base64BannerImage, userProfileData.UserDescription, userProfileData.Tags ); diff --git a/LightlessSyncServer/LightlessSyncServer/Utils/MareHubLogger.cs b/LightlessSyncServer/LightlessSyncServer/Utils/LightlessHubLogger.cs similarity index 93% rename from LightlessSyncServer/LightlessSyncServer/Utils/MareHubLogger.cs rename to LightlessSyncServer/LightlessSyncServer/Utils/LightlessHubLogger.cs index 03d92d9..bb73bfa 100644 --- a/LightlessSyncServer/LightlessSyncServer/Utils/MareHubLogger.cs +++ b/LightlessSyncServer/LightlessSyncServer/Utils/LightlessHubLogger.cs @@ -1,5 +1,4 @@ -using LightlessSync.API.SignalR; -using LightlessSyncServer.Hubs; +using LightlessSyncServer.Hubs; using System.Runtime.CompilerServices; namespace LightlessSyncServer.Utils; diff --git a/LightlessSyncServer/LightlessSyncShared/Metrics/MareMetrics.cs b/LightlessSyncServer/LightlessSyncShared/Metrics/MareMetrics.cs index 046f8c0..51ac245 100644 --- a/LightlessSyncServer/LightlessSyncShared/Metrics/MareMetrics.cs +++ b/LightlessSyncServer/LightlessSyncShared/Metrics/MareMetrics.cs @@ -20,7 +20,7 @@ public class LightlessMetrics if (!string.Equals(gauge, MetricsAPI.GaugeConnections, StringComparison.OrdinalIgnoreCase)) _gauges.Add(gauge, Prometheus.Metrics.CreateGauge(gauge, gauge)); else - _gauges.Add(gauge, Prometheus.Metrics.CreateGauge(gauge, gauge, new[] { "continent" })); + _gauges.Add(gauge, Prometheus.Metrics.CreateGauge(gauge, gauge, ["continent", "country"])); } } diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20251020181150_AddBannerForProfiles.Designer.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20251020181150_AddBannerForProfiles.Designer.cs new file mode 100644 index 0000000..58f2efd --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20251020181150_AddBannerForProfiles.Designer.cs @@ -0,0 +1,1189 @@ +// +using System; +using LightlessSyncShared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LightlessSyncServer.Migrations +{ + [DbContext(typeof(LightlessDbContext))] + [Migration("20251020181150_AddBannerForProfiles")] + partial class AddBannerForProfiles + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LightlessSyncShared.Models.Auth", b => + { + b.Property("HashedKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("hashed_key"); + + b.Property("IsBanned") + .HasColumnType("boolean") + .HasColumnName("is_banned"); + + b.Property("MarkForBan") + .HasColumnType("boolean") + .HasColumnName("mark_for_ban"); + + b.Property("PrimaryUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("primary_user_uid"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("HashedKey") + .HasName("pk_auth"); + + b.HasIndex("PrimaryUserUID") + .HasDatabaseName("ix_auth_primary_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_auth_user_uid"); + + b.ToTable("auth", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Banned", b => + { + b.Property("CharacterIdentification") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("character_identification"); + + b.Property("BannedUid") + .HasColumnType("text") + .HasColumnName("banned_uid"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("CharacterIdentification") + .HasName("pk_banned_users"); + + b.ToTable("banned_users", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.BannedRegistrations", b => + { + b.Property("DiscordIdOrLodestoneAuth") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("discord_id_or_lodestone_auth"); + + b.HasKey("DiscordIdOrLodestoneAuth") + .HasName("pk_banned_registrations"); + + b.ToTable("banned_registrations", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaData", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("UploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.Property("AccessType") + .HasColumnType("integer") + .HasColumnName("access_type"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_date"); + + b.Property("CustomizeData") + .HasColumnType("text") + .HasColumnName("customize_data"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("DownloadCount") + .HasColumnType("integer") + .HasColumnName("download_count"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property("GlamourerData") + .HasColumnType("text") + .HasColumnName("glamourer_data"); + + b.Property("ManipulationData") + .HasColumnType("text") + .HasColumnName("manipulation_data"); + + b.Property("ShareType") + .HasColumnType("integer") + .HasColumnName("share_type"); + + b.Property("UpdatedDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_date"); + + b.HasKey("Id", "UploaderUID") + .HasName("pk_chara_data"); + + b.HasIndex("Id") + .HasDatabaseName("ix_chara_data_id"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_chara_data_uploader_uid"); + + b.ToTable("chara_data", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataAllowance", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowedGroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("allowed_group_gid"); + + b.Property("AllowedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("allowed_user_uid"); + + b.HasKey("ParentId", "ParentUploaderUID", "Id") + .HasName("pk_chara_data_allowance"); + + b.HasIndex("AllowedGroupGID") + .HasDatabaseName("ix_chara_data_allowance_allowed_group_gid"); + + b.HasIndex("AllowedUserUID") + .HasDatabaseName("ix_chara_data_allowance_allowed_user_uid"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_allowance_parent_id"); + + b.ToTable("chara_data_allowance", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataFile", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("GamePath") + .HasColumnType("text") + .HasColumnName("game_path"); + + b.Property("FileCacheHash") + .HasColumnType("character varying(40)") + .HasColumnName("file_cache_hash"); + + b.HasKey("ParentId", "ParentUploaderUID", "GamePath") + .HasName("pk_chara_data_files"); + + b.HasIndex("FileCacheHash") + .HasDatabaseName("ix_chara_data_files_file_cache_hash"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_files_parent_id"); + + b.ToTable("chara_data_files", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataFileSwap", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("GamePath") + .HasColumnType("text") + .HasColumnName("game_path"); + + b.Property("FilePath") + .HasColumnType("text") + .HasColumnName("file_path"); + + b.HasKey("ParentId", "ParentUploaderUID", "GamePath") + .HasName("pk_chara_data_file_swaps"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_file_swaps_parent_id"); + + b.ToTable("chara_data_file_swaps", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataOriginalFile", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("GamePath") + .HasColumnType("text") + .HasColumnName("game_path"); + + b.Property("Hash") + .HasColumnType("text") + .HasColumnName("hash"); + + b.HasKey("ParentId", "ParentUploaderUID", "GamePath") + .HasName("pk_chara_data_orig_files"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_orig_files_parent_id"); + + b.ToTable("chara_data_orig_files", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataPose", b => + { + b.Property("ParentId") + .HasColumnType("text") + .HasColumnName("parent_id"); + + b.Property("ParentUploaderUID") + .HasColumnType("character varying(10)") + .HasColumnName("parent_uploader_uid"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("PoseData") + .HasColumnType("text") + .HasColumnName("pose_data"); + + b.Property("WorldData") + .HasColumnType("text") + .HasColumnName("world_data"); + + b.HasKey("ParentId", "ParentUploaderUID", "Id") + .HasName("pk_chara_data_poses"); + + b.HasIndex("ParentId") + .HasDatabaseName("ix_chara_data_poses_parent_id"); + + b.ToTable("chara_data_poses", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_client_pairs"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_client_pairs_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_client_pairs_user_uid"); + + b.ToTable("client_pairs", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.FileCache", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("RawSize") + .HasColumnType("bigint") + .HasColumnName("raw_size"); + + b.Property("Size") + .HasColumnType("bigint") + .HasColumnName("size"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.Property("UploadDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("upload_date"); + + b.Property("Uploaded") + .HasColumnType("boolean") + .HasColumnName("uploaded"); + + b.Property("UploaderUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uploader_uid"); + + b.HasKey("Hash") + .HasName("pk_file_caches"); + + b.HasIndex("UploaderUID") + .HasDatabaseName("ix_file_caches_uploader_uid"); + + b.ToTable("file_caches", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ForbiddenUploadEntry", b => + { + b.Property("Hash") + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("hash"); + + b.Property("ForbiddenBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("forbidden_by"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("Hash") + .HasName("pk_forbidden_upload_entries"); + + b.ToTable("forbidden_upload_entries", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.Property("GID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("gid"); + + b.Property("Alias") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("alias"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_date") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("HashedPassword") + .HasColumnType("text") + .HasColumnName("hashed_password"); + + b.Property("InvitesEnabled") + .HasColumnType("boolean") + .HasColumnName("invites_enabled"); + + b.Property("OwnerUID") + .HasColumnType("character varying(10)") + .HasColumnName("owner_uid"); + + b.Property("PreferDisableAnimations") + .HasColumnType("boolean") + .HasColumnName("prefer_disable_animations"); + + b.Property("PreferDisableSounds") + .HasColumnType("boolean") + .HasColumnName("prefer_disable_sounds"); + + b.Property("PreferDisableVFX") + .HasColumnType("boolean") + .HasColumnName("prefer_disable_vfx"); + + b.HasKey("GID") + .HasName("pk_groups"); + + b.HasIndex("OwnerUID") + .HasDatabaseName("ix_groups_owner_uid"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("BannedUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_user_uid"); + + b.Property("BannedByUID") + .HasColumnType("character varying(10)") + .HasColumnName("banned_by_uid"); + + b.Property("BannedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("banned_on"); + + b.Property("BannedReason") + .HasColumnType("text") + .HasColumnName("banned_reason"); + + b.HasKey("GroupGID", "BannedUserUID") + .HasName("pk_group_bans"); + + b.HasIndex("BannedByUID") + .HasDatabaseName("ix_group_bans_banned_by_uid"); + + b.HasIndex("BannedUserUID") + .HasDatabaseName("ix_group_bans_banned_user_uid"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_bans_group_gid"); + + b.ToTable("group_bans", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("GroupUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("group_user_uid"); + + b.Property("FromFinder") + .HasColumnType("boolean") + .HasColumnName("from_finder"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("IsPinned") + .HasColumnType("boolean") + .HasColumnName("is_pinned"); + + b.Property("JoinedGroupOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("joined_group_on"); + + b.HasKey("GroupGID", "GroupUserUID") + .HasName("pk_group_pairs"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pairs_group_gid"); + + b.HasIndex("GroupUserUID") + .HasDatabaseName("ix_group_pairs_group_user_uid"); + + b.ToTable("group_pairs", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupPairPreferredPermission", b => + { + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("DisableVFX") + .HasColumnType("boolean") + .HasColumnName("disable_vfx"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.HasKey("UserUID", "GroupGID") + .HasName("pk_group_pair_preferred_permissions"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_pair_preferred_permissions_group_gid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_group_pair_preferred_permissions_user_uid"); + + b.ToTable("group_pair_preferred_permissions", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupProfile", b => + { + b.Property("GroupGID") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Base64GroupBannerImage") + .HasColumnType("text") + .HasColumnName("base64group_banner_image"); + + b.Property("Base64GroupProfileImage") + .HasColumnType("text") + .HasColumnName("base64group_profile_image"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsNSFW") + .HasColumnType("boolean") + .HasColumnName("is_nsfw"); + + b.Property("ProfileDisabled") + .HasColumnType("boolean") + .HasColumnName("profile_disabled"); + + b.PrimitiveCollection("Tags") + .HasColumnType("integer[]") + .HasColumnName("tags"); + + b.HasKey("GroupGID") + .HasName("pk_group_profiles"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_profiles_group_gid"); + + b.ToTable("group_profiles", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.Property("GroupGID") + .HasColumnType("character varying(20)") + .HasColumnName("group_gid"); + + b.Property("Invite") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("invite"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_date"); + + b.HasKey("GroupGID", "Invite") + .HasName("pk_group_temp_invites"); + + b.HasIndex("GroupGID") + .HasDatabaseName("ix_group_temp_invites_group_gid"); + + b.HasIndex("Invite") + .HasDatabaseName("ix_group_temp_invites_invite"); + + b.ToTable("group_temp_invites", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("HashedLodestoneId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("hashed_lodestone_id"); + + b.Property("LodestoneAuthString") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("lodestone_auth_string"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.HasKey("DiscordId") + .HasName("pk_lodestone_auth"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_lodestone_auth_user_uid"); + + b.ToTable("lodestone_auth", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.User", b => + { + b.Property("UID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("uid"); + + b.Property("Alias") + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("alias"); + + b.Property("HasVanity") + .HasColumnType("boolean") + .HasColumnName("has_vanity"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("is_admin"); + + b.Property("IsModerator") + .HasColumnType("boolean") + .HasColumnName("is_moderator"); + + b.Property("LastLoggedIn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_in"); + + b.Property("TextColorHex") + .HasMaxLength(9) + .HasColumnType("character varying(9)") + .HasColumnName("text_color_hex"); + + b.Property("TextGlowColorHex") + .HasMaxLength(9) + .HasColumnType("character varying(9)") + .HasColumnName("text_glow_color_hex"); + + b.Property("Timestamp") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasColumnName("timestamp"); + + b.HasKey("UID") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserDefaultPreferredPermission", b => + { + b.Property("UserUID") + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("DisableGroupAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_group_animations"); + + b.Property("DisableGroupSounds") + .HasColumnType("boolean") + .HasColumnName("disable_group_sounds"); + + b.Property("DisableGroupVFX") + .HasColumnType("boolean") + .HasColumnName("disable_group_vfx"); + + b.Property("DisableIndividualAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_individual_animations"); + + b.Property("DisableIndividualSounds") + .HasColumnType("boolean") + .HasColumnName("disable_individual_sounds"); + + b.Property("DisableIndividualVFX") + .HasColumnType("boolean") + .HasColumnName("disable_individual_vfx"); + + b.Property("IndividualIsSticky") + .HasColumnType("boolean") + .HasColumnName("individual_is_sticky"); + + b.HasKey("UserUID") + .HasName("pk_user_default_preferred_permissions"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_user_default_preferred_permissions_user_uid"); + + b.ToTable("user_default_preferred_permissions", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserPermissionSet", b => + { + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("OtherUserUID") + .HasColumnType("character varying(10)") + .HasColumnName("other_user_uid"); + + b.Property("DisableAnimations") + .HasColumnType("boolean") + .HasColumnName("disable_animations"); + + b.Property("DisableSounds") + .HasColumnType("boolean") + .HasColumnName("disable_sounds"); + + b.Property("DisableVFX") + .HasColumnType("boolean") + .HasColumnName("disable_vfx"); + + b.Property("IsPaused") + .HasColumnType("boolean") + .HasColumnName("is_paused"); + + b.Property("Sticky") + .HasColumnType("boolean") + .HasColumnName("sticky"); + + b.HasKey("UserUID", "OtherUserUID") + .HasName("pk_user_permission_sets"); + + b.HasIndex("OtherUserUID") + .HasDatabaseName("ix_user_permission_sets_other_user_uid"); + + b.HasIndex("UserUID") + .HasDatabaseName("ix_user_permission_sets_user_uid"); + + b.HasIndex("UserUID", "OtherUserUID", "IsPaused") + .HasDatabaseName("ix_user_permission_sets_user_uid_other_user_uid_is_paused"); + + b.ToTable("user_permission_sets", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserProfileData", b => + { + b.Property("UserUID") + .HasColumnType("character varying(10)") + .HasColumnName("user_uid"); + + b.Property("Base64BannerImage") + .HasColumnType("text") + .HasColumnName("base64banner_image"); + + b.Property("Base64ProfileImage") + .HasColumnType("text") + .HasColumnName("base64profile_image"); + + b.Property("FlaggedForReport") + .HasColumnType("boolean") + .HasColumnName("flagged_for_report"); + + b.Property("IsNSFW") + .HasColumnType("boolean") + .HasColumnName("is_nsfw"); + + b.Property("ProfileDisabled") + .HasColumnType("boolean") + .HasColumnName("profile_disabled"); + + b.PrimitiveCollection("Tags") + .HasColumnType("integer[]") + .HasColumnName("tags"); + + b.Property("UserDescription") + .HasColumnType("text") + .HasColumnName("user_description"); + + b.HasKey("UserUID") + .HasName("pk_user_profile_data"); + + b.ToTable("user_profile_data", (string)null); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Auth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "PrimaryUser") + .WithMany() + .HasForeignKey("PrimaryUserUID") + .HasConstraintName("fk_auth_users_primary_user_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_auth_users_user_uid"); + + b.Navigation("PrimaryUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataAllowance", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "AllowedGroup") + .WithMany() + .HasForeignKey("AllowedGroupGID") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_allowance_groups_allowed_group_gid"); + + b.HasOne("LightlessSyncShared.Models.User", "AllowedUser") + .WithMany() + .HasForeignKey("AllowedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_allowance_users_allowed_user_uid"); + + b.HasOne("LightlessSyncShared.Models.CharaData", "Parent") + .WithMany("AllowedIndividiuals") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_allowance_chara_data_parent_id_parent_uploader_u"); + + b.Navigation("AllowedGroup"); + + b.Navigation("AllowedUser"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataFile", b => + { + b.HasOne("LightlessSyncShared.Models.FileCache", "FileCache") + .WithMany() + .HasForeignKey("FileCacheHash") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_chara_data_files_files_file_cache_hash"); + + b.HasOne("LightlessSyncShared.Models.CharaData", "Parent") + .WithMany("Files") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_files_chara_data_parent_id_parent_uploader_uid"); + + b.Navigation("FileCache"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataFileSwap", b => + { + b.HasOne("LightlessSyncShared.Models.CharaData", "Parent") + .WithMany("FileSwaps") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_file_swaps_chara_data_parent_id_parent_uploader_"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataOriginalFile", b => + { + b.HasOne("LightlessSyncShared.Models.CharaData", "Parent") + .WithMany("OriginalFiles") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_orig_files_chara_data_parent_id_parent_uploader_"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaDataPose", b => + { + b.HasOne("LightlessSyncShared.Models.CharaData", "Parent") + .WithMany("Poses") + .HasForeignKey("ParentId", "ParentUploaderUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chara_data_poses_chara_data_parent_id_parent_uploader_uid"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.ClientPair", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_client_pairs_users_user_uid"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.FileCache", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderUID") + .HasConstraintName("fk_file_caches_users_uploader_uid"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.HasOne("LightlessSyncShared.Models.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUID") + .HasConstraintName("fk_groups_users_owner_uid"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupBan", b => + { + b.HasOne("LightlessSyncShared.Models.User", "BannedBy") + .WithMany() + .HasForeignKey("BannedByUID") + .HasConstraintName("fk_group_bans_users_banned_by_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "BannedUser") + .WithMany() + .HasForeignKey("BannedUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_users_banned_user_uid"); + + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_bans_groups_group_gid"); + + b.Navigation("BannedBy"); + + b.Navigation("BannedUser"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupPair", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_groups_group_gid"); + + b.HasOne("LightlessSyncShared.Models.User", "GroupUser") + .WithMany() + .HasForeignKey("GroupUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pairs_users_group_user_uid"); + + b.Navigation("Group"); + + b.Navigation("GroupUser"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupPairPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_groups_group_gid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_pair_preferred_permissions_users_user_uid"); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupProfile", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithOne("Profile") + .HasForeignKey("LightlessSyncShared.Models.GroupProfile", "GroupGID") + .HasConstraintName("fk_group_profiles_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.GroupTempInvite", b => + { + b.HasOne("LightlessSyncShared.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupGID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_group_temp_invites_groups_group_gid"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.LodeStoneAuth", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .HasConstraintName("fk_lodestone_auth_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserDefaultPreferredPermission", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_default_preferred_permissions_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserPermissionSet", b => + { + b.HasOne("LightlessSyncShared.Models.User", "OtherUser") + .WithMany() + .HasForeignKey("OtherUserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_permission_sets_users_other_user_uid"); + + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_permission_sets_users_user_uid"); + + b.Navigation("OtherUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.UserProfileData", b => + { + b.HasOne("LightlessSyncShared.Models.User", "User") + .WithMany() + .HasForeignKey("UserUID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_profile_data_users_user_uid"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.CharaData", b => + { + b.Navigation("AllowedIndividiuals"); + + b.Navigation("FileSwaps"); + + b.Navigation("Files"); + + b.Navigation("OriginalFiles"); + + b.Navigation("Poses"); + }); + + modelBuilder.Entity("LightlessSyncShared.Models.Group", b => + { + b.Navigation("Profile"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/20251020181150_AddBannerForProfiles.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/20251020181150_AddBannerForProfiles.cs new file mode 100644 index 0000000..b5ba3e4 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/20251020181150_AddBannerForProfiles.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LightlessSyncServer.Migrations +{ + /// + public partial class AddBannerForProfiles : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "base64banner_image", + table: "user_profile_data", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "base64group_banner_image", + table: "group_profiles", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "base64banner_image", + table: "user_profile_data"); + + migrationBuilder.DropColumn( + name: "base64group_banner_image", + table: "group_profiles"); + } + } +} diff --git a/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs b/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs index ba9a000..992dc19 100644 --- a/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs +++ b/LightlessSyncServer/LightlessSyncShared/Migrations/LightlessDbContextModelSnapshot.cs @@ -589,6 +589,10 @@ namespace LightlessSyncServer.Migrations .HasColumnType("character varying(20)") .HasColumnName("group_gid"); + b.Property("Base64GroupBannerImage") + .HasColumnType("text") + .HasColumnName("base64group_banner_image"); + b.Property("Base64GroupProfileImage") .HasColumnType("text") .HasColumnName("base64group_profile_image"); @@ -824,6 +828,10 @@ namespace LightlessSyncServer.Migrations .HasColumnType("character varying(10)") .HasColumnName("user_uid"); + b.Property("Base64BannerImage") + .HasColumnType("text") + .HasColumnName("base64banner_image"); + b.Property("Base64ProfileImage") .HasColumnType("text") .HasColumnName("base64profile_image"); diff --git a/LightlessSyncServer/LightlessSyncShared/Models/GroupProfile.cs b/LightlessSyncServer/LightlessSyncShared/Models/GroupProfile.cs index b61b450..034975d 100644 --- a/LightlessSyncServer/LightlessSyncShared/Models/GroupProfile.cs +++ b/LightlessSyncServer/LightlessSyncShared/Models/GroupProfile.cs @@ -15,6 +15,7 @@ public class GroupProfile public string Description { get; set; } public int[] Tags { get; set; } public string Base64GroupProfileImage { get; set; } + public string Base64GroupBannerImage { get; set; } public bool IsNSFW { get; set; } = false; public bool ProfileDisabled { get; set; } = false; } diff --git a/LightlessSyncServer/LightlessSyncShared/Models/UserProfileData.cs b/LightlessSyncServer/LightlessSyncShared/Models/UserProfileData.cs index ec7ba9b..b8b9761 100644 --- a/LightlessSyncServer/LightlessSyncShared/Models/UserProfileData.cs +++ b/LightlessSyncServer/LightlessSyncShared/Models/UserProfileData.cs @@ -6,6 +6,7 @@ namespace LightlessSyncShared.Models; public class UserProfileData { public string Base64ProfileImage { get; set; } + public string Base64BannerImage { get; set; } public bool FlaggedForReport { get; set; } public bool IsNSFW { get; set; } public bool ProfileDisabled { get; set; } diff --git a/LightlessSyncServer/LightlessSyncShared/Utils/MareClaimTypes.cs b/LightlessSyncServer/LightlessSyncShared/Utils/MareClaimTypes.cs index f832cfa..2c172da 100644 --- a/LightlessSyncServer/LightlessSyncShared/Utils/MareClaimTypes.cs +++ b/LightlessSyncServer/LightlessSyncShared/Utils/MareClaimTypes.cs @@ -8,6 +8,7 @@ public static class LightlessClaimTypes public const string Internal = "internal"; public const string Expires = "expiration_date"; public const string Continent = "continent"; + public const string Country = "country"; public const string DiscordUser = "discord_user"; public const string DiscordId = "discord_user_id"; public const string OAuthLoginToken = "oauth_login_token";