Compare commits

...

40 Commits

Author SHA1 Message Date
f29c874515 Merge branch 'master' into syncshells-images-combined 2025-10-27 20:19:03 +01:00
cake
b84d6c35d6 Moved notification on groups on new ones, Fixed new creation of profiles. 2025-10-27 19:08:25 +01:00
bd03fa6762 Syncshells Fix -
Reviewed-on: #24
written by: Abel / Cake
2025-10-26 19:48:18 +01:00
cake
8cde3b4933 Fixed image update from dto 2025-10-26 18:59:20 +01:00
defnotken
0c357aaf7c Merge remote-tracking branch 'origin/fix-images' into syncshells-images-combined 2025-10-26 12:31:34 -05:00
cake
ae09d79577 fix image results 2025-10-26 17:43:49 +01:00
azyges
cc24dc067e fix moderators + profiles 2025-10-27 00:59:56 +09:00
6ac56d38c0 Merge pull request 'lets try this' (#23) from sql-thing into master
Reviewed-on: #23
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-10-21 23:26:14 +02:00
defnotken
b7f7381dec lets try this 2025-10-21 16:16:45 -05:00
1ce7a718bb Merge pull request 'Banner Support for profiles, Some cleanup/refactoring. Country for metrics.' (#22) from server-changes into master
Reviewed-on: #22
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-10-21 22:50:54 +02:00
CakeAndBanana
46bb7a4a98 submodule update 2025-10-21 22:48:57 +02:00
CakeAndBanana
8752ce0e62 removal concept 2025-10-21 22:39:05 +02:00
CakeAndBanana
db0115316d Made new imageloadresult instead of null 2025-10-20 20:51:55 +02:00
CakeAndBanana
00d4632510 Reworked image handling, added banner for profiles. Made functions to start on xivauth. Refactored some code. 2025-10-20 20:46:20 +02:00
CakeAndBanana
e61e0db36b Removed logging 2025-10-20 15:52:47 +02:00
CakeAndBanana
8d82365d0e Made logging from information to warning 2025-10-20 15:43:29 +02:00
CakeAndBanana
b142329d09 Added some logging for country 2025-10-20 04:05:27 +02:00
CakeAndBanana
8a329ccbaa Added IP check on loopback 2025-10-20 03:27:47 +02:00
CakeAndBanana
23ee3f98b0 Removed some random characters 2025-10-20 03:15:37 +02:00
CakeAndBanana
f8e711f3c0 Redone array of labels for geoip 2025-10-20 03:12:27 +02:00
CakeAndBanana
73e7bb67bb Fixed metric for country 2025-10-20 03:07:07 +02:00
CakeAndBanana
70500b21e6 Add another label on guage metric 2025-10-20 03:03:59 +02:00
CakeAndBanana
698a9eddf7 Added new jwt claim for country, Moved models to correct folder instead of inside Lightlesshub.Groups 2025-10-20 02:30:40 +02:00
9cab73e8c8 Merge pull request 'Fallback Policy change' (#20) from authorization-shard into master
Reviewed-on: #20
2025-10-19 23:11:31 +02:00
5240beddf4 Merge branch 'master' into authorization-shard 2025-10-19 23:01:59 +02:00
cb4998e960 Merge pull request 'Reworked syncshell profile and user profile calls.' (#21) from syncshell-profiles-attempt-two into master
Reviewed-on: #21
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-10-19 21:41:38 +02:00
CakeAndBanana
884ad25c33 update submodule 2025-10-19 21:21:19 +02:00
CakeAndBanana
3926f3be89 remove blankspace 2025-10-19 21:04:22 +02:00
CakeAndBanana
d28198a9c8 fix migrations 2025-10-19 21:04:12 +02:00
CakeAndBanana
7cc6918b12 updated submodule 2025-10-19 20:58:40 +02:00
CakeAndBanana
dba7536a7f Added tags on call for user profile calls, added disabled on syncshell profiles. reworked the calls 2025-10-19 20:58:07 +02:00
CakeAndBanana
f35c0c4c2a updated submodule 2025-10-19 18:40:49 +02:00
CakeAndBanana
ad00f7b078 Changes in database for tags to be array integers instead of strings 2025-10-19 18:36:08 +02:00
CakeAndBanana
c30190704f Changed get/set profile with more safe handling 2025-10-19 17:53:20 +02:00
CakeAndBanana
bab81aaf51 Added null checks 2025-10-19 17:39:36 +02:00
CakeAndBanana
4fdc2a5c29 FIx to attempt to get group 2025-10-19 17:30:16 +02:00
CakeAndBanana
bbcf98576e Fixed so it can search on alias better 2025-10-19 17:22:25 +02:00
defnotken
1ac92f6da2 move shard controller. 2025-10-18 19:44:02 -05:00
defnotken
e7e4a4527a Testing something 2025-10-18 18:51:58 -05:00
583f1a8957 Merge pull request 'Fixed some issues with profiles on groups' (#15) from fix-profiles-syncshell into master
Reviewed-on: #15
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-10-17 00:09:46 +02:00
28 changed files with 2934 additions and 146 deletions

View File

@@ -100,14 +100,15 @@ public abstract class AuthControllerBase : Controller
protected async Task<IActionResult> CreateJwtFromId(string uid, string charaIdent, string alias)
{
var token = CreateJwt(new List<Claim>()
{
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);
}

View File

@@ -1,5 +1,6 @@
using LightlessSync.API.Routes;
using LightlessSyncAuthService.Services;
using LightlessSyncAuthService.Utils;
using LightlessSyncShared;
using LightlessSyncShared.Data;
using LightlessSyncShared.Services;

View File

@@ -1,5 +1,6 @@
using LightlessSync.API.Routes;
using LightlessSyncAuthService.Services;
using LightlessSyncAuthService.Utils;
using LightlessSyncShared;
using LightlessSyncShared.Data;
using LightlessSyncShared.Services;

View File

@@ -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<string> GetCountryFromIP(IHttpContextAccessor httpContextAccessor)
public async Task<string> 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<string> 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 "*";
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
@@ -209,7 +211,8 @@ public partial class LightlessHub
if (isOwnerResult.ReferredGroup == null) return (false, null);
var groupPairSelf = await DbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid || g.Group.Alias == gid && g.GroupUserUID == UserUID).ConfigureAwait(false);
var groupPairSelf = await DbContext.GroupPairs.SingleOrDefaultAsync(
g => (g.GroupGID == gid || g.Group.Alias == gid) && g.GroupUserUID == UserUID).ConfigureAwait(false);
if (groupPairSelf == null || !groupPairSelf.IsModerator) return (false, null);
return (true, isOwnerResult.ReferredGroup);

View File

@@ -3,12 +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;
@@ -745,18 +751,39 @@ public partial class LightlessHub
var cancellationToken = RequestAbortedToken;
var data = await DbContext.GroupProfiles
.FirstOrDefaultAsync(g => g.Group.GID == dto.Group.GID || g.Group.Alias == dto.Group.GID, cancellationToken)
.ConfigureAwait(false);
var profileDto = new GroupProfileDto(dto.Group, Description: null, Tags: null, PictureBase64: null, IsNsfw: false, IsDisabled: false);
if (data is not null)
if (dto?.Group == null)
{
profileDto = data.ToDTO();
_logger.LogCallWarning(LightlessHubLogger.Args("GroupGetProfile: dto.Group is null"));
return new GroupProfileDto(Group: null, Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: false, IsDisabled: false);
}
return profileDto;
var data = await DbContext.GroupProfiles
.Include(gp => gp.Group)
.FirstOrDefaultAsync(
g => g.Group.GID == dto.Group.GID || g.Group.Alias == dto.Group.AliasOrGID,
cancellationToken
)
.ConfigureAwait(false);
if (data == null)
{
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, BannerBase64: null, IsNsfw: false, IsDisabled: true);
}
try
{
return data.ToDTO();
}
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, BannerBase64: null, IsNsfw: false, IsDisabled: false);
}
}
[Authorize(Policy = "Identified")]
@@ -764,64 +791,88 @@ public partial class LightlessHub
{
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
var cancellationToken = RequestAbortedToken;
if (dto.Group == null) return;
var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
if (!hasRights) return;
var groupProfileDb = await DbContext.GroupProfiles
.FirstOrDefaultAsync(g => g.Group.GID == dto.Group.GID || g.Group.Alias == dto.Group.GID,
RequestAbortedToken)
.Include(g => g.Group)
.FirstOrDefaultAsync(g => g.GroupGID == dto.Group.GID, cancellationToken)
.ConfigureAwait(false);
if (groupProfileDb != null)
ImageCheckService.ImageLoadResult profileResult = new();
ImageCheckService.ImageLoadResult bannerResult = new();
//Avatar image validation
if (!string.IsNullOrEmpty(dto.PictureBase64))
{
var groupPairs = DbContext.GroupPairs.Where(p => p.GroupGID == groupProfileDb.GroupGID).Select(p => p.GroupUserUID).ToList();
profileResult = await ImageCheckService.ValidateImageAsync(dto.PictureBase64, banner: false, RequestAbortedToken).ConfigureAwait(false);
if (string.Equals("", dto.PictureBase64, StringComparison.OrdinalIgnoreCase))
if (!profileResult.Success)
{
groupProfileDb.Base64GroupProfileImage = null;
}
else if (dto.PictureBase64 != null)
{
groupProfileDb.Base64GroupProfileImage = dto.PictureBase64;
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, profileResult.ErrorMessage).ConfigureAwait(false);
return;
}
}
if (dto.Tags != null)
{
groupProfileDb.Tags = dto.Tags;
}
//Banner image validation
if (!string.IsNullOrEmpty(dto.BannerBase64))
{
bannerResult = await ImageCheckService.ValidateImageAsync(dto.BannerBase64, banner: true, RequestAbortedToken).ConfigureAwait(false);
if (dto.Description != null)
if (!bannerResult.Success)
{
groupProfileDb.Description = dto.Description;
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, bannerResult.ErrorMessage).ConfigureAwait(false);
return;
}
}
if (dto.IsNsfw != null)
var sanitizedProfileImage = profileResult?.Base64Image;
var sanitizedBannerImage = bannerResult?.Base64Image;
if (groupProfileDb == null)
{
groupProfileDb = new GroupProfile
{
groupProfileDb.IsNSFW = dto.IsNsfw.Value;
}
GroupGID = dto.Group.GID,
Group = group,
ProfileDisabled = false,
IsNSFW = dto.IsNsfw ?? false,
};
await Clients.Users(groupPairs).Client_GroupSendProfile(groupProfileDb.ToDTO()).ConfigureAwait(false);
groupProfileDb.UpdateProfileFromDto(dto, sanitizedProfileImage, sanitizedBannerImage);
await DbContext.GroupProfiles.AddAsync(groupProfileDb, cancellationToken).ConfigureAwait(false);
}
else
{
var groupProfile = new GroupProfile
{
GroupGID = dto.Group.GID,
Description = dto.Description,
Tags = dto.Tags,
Base64GroupProfileImage = dto.PictureBase64,
IsNSFW = false,
ProfileDisabled = false,
};
groupProfileDb.Group ??= group;
await DbContext.GroupProfiles.AddAsync(groupProfile,
RequestAbortedToken)
if (groupProfileDb?.ProfileDisabled ?? false)
{
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile was permanently disabled and cannot be edited").ConfigureAwait(false);
return;
}
groupProfileDb.UpdateProfileFromDto(dto, sanitizedProfileImage, sanitizedBannerImage);
}
var userIds = await DbContext.GroupPairs
.Where(p => p.GroupGID == groupProfileDb.GroupGID)
.Select(p => p.GroupUserUID)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
if (userIds.Count > 0)
{
var profileDto = groupProfileDb.ToDTO();
await Clients.Users(userIds).Client_GroupSendProfile(profileDto)
.ConfigureAwait(false);
}
await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false);
await DbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
[Authorize(Policy = "Identified")]

View File

@@ -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);
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);
return profile.ToDTO();
}
[Authorize(Policy = "Identified")]
@@ -959,22 +943,36 @@ public partial class LightlessHub
}
bool hadInvalidData = false;
List<string> invalidGamePaths = new();
List<string> invalidFileSwapPaths = new();
List<string> invalidGamePaths = [];
List<string> 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);
}
}
@@ -1126,76 +1124,70 @@ public partial class LightlessHub
{
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
var cancellationToken = RequestAbortedToken;
if (!string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal)) throw new HubException("Cannot modify profile data for anyone but yourself");
var existingData = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == dto.User.UID, cancellationToken: RequestAbortedToken).ConfigureAwait(false);
var profileData = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == dto.User.UID, cancellationToken: RequestAbortedToken).ConfigureAwait(false);
if (existingData?.FlaggedForReport ?? false)
{
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile is currently flagged for report and cannot be edited").ConfigureAwait(false);
return;
}
if (existingData?.ProfileDisabled ?? false)
{
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile was permanently disabled and cannot be edited").ConfigureAwait(false);
return;
}
ImageCheckService.ImageLoadResult profileResult = new();
ImageCheckService.ImageLoadResult bannerResult = new();
//Avatar image validation
if (!string.IsNullOrEmpty(dto.ProfilePictureBase64))
{
byte[] imageData = Convert.FromBase64String(dto.ProfilePictureBase64);
using MemoryStream ms = new(imageData);
var format = await Image.DetectFormatAsync(ms).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<Rgba32>(imageData);
profileResult = await ImageCheckService.ValidateImageAsync(dto.ProfilePictureBase64, banner: false, RequestAbortedToken).ConfigureAwait(false);
if (image.Width > 256 || image.Height > 256 || (imageData.Length > 250 * 1024))
if (!profileResult.Success)
{
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is larger than 256x256 or more than 250KiB.").ConfigureAwait(false);
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, profileResult.ErrorMessage).ConfigureAwait(false);
return;
}
}
if (existingData != null)
//Banner image validation
if (!string.IsNullOrEmpty(dto.BannerPictureBase64))
{
if (string.Equals("", dto.ProfilePictureBase64, StringComparison.OrdinalIgnoreCase))
bannerResult = await ImageCheckService.ValidateImageAsync(dto.BannerPictureBase64, banner: true, RequestAbortedToken).ConfigureAwait(false);
if (!bannerResult.Success)
{
existingData.Base64ProfileImage = null;
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, bannerResult.ErrorMessage).ConfigureAwait(false);
return;
}
else if (dto.ProfilePictureBase64 != null)
}
if (profileData != null)
{
if (profileData.FlaggedForReport)
{
existingData.Base64ProfileImage = dto.ProfilePictureBase64;
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile is currently flagged for report and cannot be edited").ConfigureAwait(false);
return;
}
if (dto.IsNSFW != null)
if (profileData.ProfileDisabled)
{
existingData.IsNSFW = dto.IsNSFW.Value;
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile was permanently disabled and cannot be edited").ConfigureAwait(false);
return;
}
if (dto.Description != null)
{
existingData.UserDescription = dto.Description;
}
profileData.UpdateProfileFromDto(dto, profileResult.Base64Image, bannerResult.Base64Image);
}
else
{
UserProfileData userProfileData = new()
profileData = new()
{
UserUID = dto.User.UID,
Base64ProfileImage = dto.ProfilePictureBase64 ?? null,
UserDescription = dto.Description ?? null,
IsNSFW = dto.IsNSFW ?? false
IsNSFW = dto.IsNSFW ?? false,
};
await DbContext.UserProfileData.AddAsync(userProfileData).ConfigureAwait(false);
profileData.UpdateProfileFromDto(dto, profileResult.Base64Image, bannerResult.Base64Image);
await DbContext.UserProfileData.AddAsync(profileData, cancellationToken).ConfigureAwait(false);
}
await DbContext.SaveChangesAsync().ConfigureAwait(false);
await DbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
var pairs = await GetOnlineUsers(allPairedUsers).ConfigureAwait(false);

View File

@@ -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>, ILightlessHub
}
else
{
_lightlessMetrics.IncGaugeWithLabels(MetricsAPI.GaugeConnections, labels: Continent);
var ResultLabels = new List<string>
{
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>, 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<string>
{
Continent,
Country,
};
_lightlessMetrics.DecGaugeWithLabels(MetricsAPI.GaugeConnections, labels: [.. ResultLabels]);
try
{

View File

@@ -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);
}

View File

@@ -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; }
}

View File

@@ -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))

View File

@@ -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<ImageLoadResult> 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<Rgba32>? 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<Rgba32>(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.");
}
}
}
}

View File

@@ -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;

View File

@@ -2,6 +2,7 @@
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.Group;
using LightlessSync.API.Dto.User;
using LightlessSyncShared.Models;
using static LightlessSyncServer.Hubs.LightlessHub;
@@ -9,23 +10,96 @@ namespace LightlessSyncServer.Utils;
public static class Extensions
{
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;
if (base64PictureString != null) profile.Base64GroupProfileImage = base64PictureString;
if (base64BannerString != null) profile.Base64GroupBannerImage = 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, string? base64PictureString = null, string? base64BannerString = null)
{
ArgumentNullException.ThrowIfNull(profile);
ArgumentNullException.ThrowIfNull(dto);
if (profile == null || dto == null) return;
if (base64PictureString != null) profile.Base64ProfileImage = base64PictureString;
if (base64BannerString != null) profile.Base64BannerImage = 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;
}
public static GroupProfileDto ToDTO(this GroupProfile groupProfile)
{
return new GroupProfileDto(groupProfile.Group.ToGroupData(), groupProfile.Description, groupProfile.Tags, groupProfile.Base64GroupProfileImage, groupProfile.IsNSFW, groupProfile.ProfileDisabled);
if (groupProfile == null)
{
return new GroupProfileDto(Group: null, Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: false, IsDisabled: false);
}
var groupData = groupProfile.Group?.ToGroupData()
?? (!string.IsNullOrWhiteSpace(groupProfile.GroupGID) ? new GroupData(groupProfile.GroupGID) : null);
return new GroupProfileDto(
groupData,
groupProfile.Description,
groupProfile.Tags,
groupProfile.Base64GroupProfileImage,
groupProfile.Base64GroupBannerImage,
groupProfile.IsNSFW,
groupProfile.ProfileDisabled
);
}
public static UserProfileDto ToDTO(this UserProfileData userProfileData)
{
if (userProfileData == null)
{
return new UserProfileDto(User: null, Disabled: false, IsNSFW: null, ProfilePictureBase64: null, BannerPictureBase64: null, Description: null, Tags: []);
}
var userData = userProfileData.User?.ToUserData();
return new UserProfileDto(
userData,
userProfileData.ProfileDisabled,
userProfileData.IsNSFW,
userProfileData.Base64ProfileImage,
userProfileData.Base64BannerImage,
userProfileData.UserDescription,
userProfileData.Tags
);
}
public static GroupData ToGroupData(this Group group)
{
if (group == null)
return null;
return new GroupData(group.GID, group.Alias, group.CreatedDate);
}
public static UserData ToUserData(this GroupPair pair)
{
if (pair == null)
return null;
return new UserData(pair.GroupUser.UID, pair.GroupUser.Alias);
}
public static UserData ToUserData(this User user)
{
if (user == null)
return null;
return new UserData(user.UID, user.Alias);
}

View File

@@ -1,5 +1,4 @@
using LightlessSync.API.SignalR;
using LightlessSyncServer.Hubs;
using LightlessSyncServer.Hubs;
using System.Runtime.CompilerServices;
namespace LightlessSyncServer.Utils;

View File

@@ -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"]));
}
}

View File

@@ -0,0 +1,51 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LightlessSyncServer.Migrations
{
/// <inheritdoc />
public partial class AddAndChangeTagsUserGroupProfile : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int[]>(
name: "tags",
table: "user_profile_data",
type: "integer[]",
nullable: true);
migrationBuilder.Sql(
"ALTER TABLE group_profiles ALTER COLUMN tags TYPE integer[] USING string_to_array(tags, ',')::integer[];"
);
migrationBuilder.AlterColumn<int[]>(
name: "tags",
table: "group_profiles",
type: "integer[]",
nullable: true,
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "tags",
table: "user_profile_data");
migrationBuilder.AlterColumn<string>(
name: "tags",
table: "group_profiles",
type: "text",
nullable: true,
oldClrType: typeof(int[]),
oldType: "integer[]",
oldNullable: true);
}
}
}

View File

@@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LightlessSyncServer.Migrations
{
/// <inheritdoc />
public partial class AddBannerForProfiles : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "base64banner_image",
table: "user_profile_data",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "base64group_banner_image",
table: "group_profiles",
type: "text",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "base64banner_image",
table: "user_profile_data");
migrationBuilder.DropColumn(
name: "base64group_banner_image",
table: "group_profiles");
}
}
}

View File

@@ -589,6 +589,10 @@ namespace LightlessSyncServer.Migrations
.HasColumnType("character varying(20)")
.HasColumnName("group_gid");
b.Property<string>("Base64GroupBannerImage")
.HasColumnType("text")
.HasColumnName("base64group_banner_image");
b.Property<string>("Base64GroupProfileImage")
.HasColumnType("text")
.HasColumnName("base64group_profile_image");
@@ -605,8 +609,8 @@ namespace LightlessSyncServer.Migrations
.HasColumnType("boolean")
.HasColumnName("profile_disabled");
b.Property<string>("Tags")
.HasColumnType("text")
b.PrimitiveCollection<int[]>("Tags")
.HasColumnType("integer[]")
.HasColumnName("tags");
b.HasKey("GroupGID")
@@ -824,6 +828,10 @@ namespace LightlessSyncServer.Migrations
.HasColumnType("character varying(10)")
.HasColumnName("user_uid");
b.Property<string>("Base64BannerImage")
.HasColumnType("text")
.HasColumnName("base64banner_image");
b.Property<string>("Base64ProfileImage")
.HasColumnType("text")
.HasColumnName("base64profile_image");
@@ -840,6 +848,10 @@ namespace LightlessSyncServer.Migrations
.HasColumnType("boolean")
.HasColumnName("profile_disabled");
b.PrimitiveCollection<int[]>("Tags")
.HasColumnType("integer[]")
.HasColumnName("tags");
b.Property<string>("UserDescription")
.HasColumnType("text")
.HasColumnName("user_description");

View File

@@ -13,8 +13,9 @@ public class GroupProfile
public string GroupGID { get; set; }
public Group Group { get; set; }
public string Description { get; set; }
public string Tags { 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;
}

View File

@@ -6,12 +6,14 @@ 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; }
public User User { get; set; }
public string UserDescription { get; set; }
public int[] Tags { get; set; }
[Required]
[Key]

View File

@@ -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";

View File

@@ -211,7 +211,7 @@ public class Startup
}
else
{
a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(CacheController), typeof(RequestController), typeof(SpeedTestController)));
a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(CacheController), typeof(ShardServerFilesController), typeof(RequestController), typeof(SpeedTestController)));
}
});
@@ -236,7 +236,6 @@ public class Startup
}).AddJwtBearer();
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
options.AddPolicy("Internal", new AuthorizationPolicyBuilder().RequireClaim(LightlessClaimTypes.Internal, "true").Build());
});
services.AddSingleton<IUserIdProvider, IdBasedUserIdProvider>();