Compare commits

..

31 Commits

Author SHA1 Message Date
0f95f26c1c Implemented match group instead of tinkering with the URL string
We're using regex already anyways, so might as well take advantage of matching groups. Group 1 will always be the country code and group 2 always the ID
2025-11-01 22:47:05 +01:00
8e36b062fd Updated Lodestone URL regex
Made it match the lodestone URL scheme exactly, with optional trailing "/" and nothing before or after the URL
2025-11-01 22:29:09 +01:00
ee69df8081 Merge pull request 'Fixed some issues on group/user profiles' (#25) from syncshells-images-combined into master
Reviewed-on: #25
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-10-27 20:19:27 +01:00
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
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
28 changed files with 1589 additions and 149 deletions

View File

@@ -100,14 +100,15 @@ public abstract class AuthControllerBase : Controller
protected async Task<IActionResult> CreateJwtFromId(string uid, string charaIdent, string alias) 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.Uid, uid),
new Claim(LightlessClaimTypes.CharaIdent, charaIdent), new Claim(LightlessClaimTypes.CharaIdent, charaIdent),
new Claim(LightlessClaimTypes.Alias, alias), new Claim(LightlessClaimTypes.Alias, alias),
new Claim(LightlessClaimTypes.Expires, DateTime.UtcNow.AddHours(6).Ticks.ToString(CultureInfo.InvariantCulture)), 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); return Content(token.RawData);
} }

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
using LightlessSyncShared; using LightlessSyncAuthService.Utils;
using LightlessSyncShared.Services; using LightlessSyncShared.Services;
using LightlessSyncShared.Utils.Configuration; using LightlessSyncShared.Utils.Configuration;
using MaxMind.GeoIP2; using MaxMind.GeoIP2;
using System.Net;
namespace LightlessSyncAuthService.Services; namespace LightlessSyncAuthService.Services;
@@ -23,7 +24,7 @@ public class GeoIPService : IHostedService
_lightlessConfiguration = lightlessConfiguration; _lightlessConfiguration = lightlessConfiguration;
} }
public async Task<string> GetCountryFromIP(IHttpContextAccessor httpContextAccessor) public async Task<string> GetContinentFromIP(IHttpContextAccessor httpContextAccessor)
{ {
if (!_useGeoIP) if (!_useGeoIP)
{ {
@@ -32,7 +33,9 @@ public class GeoIPService : IHostedService
try try
{ {
var ip = httpContextAccessor.GetIpAddress(); var ip = httpContextAccessor.GetClientIpAddress();
if (ip is null || IPAddress.IsLoopback(ip))
return "*";
using CancellationTokenSource waitCts = new(); using CancellationTokenSource waitCts = new();
waitCts.CancelAfter(TimeSpan.FromSeconds(5)); waitCts.CancelAfter(TimeSpan.FromSeconds(5));
@@ -41,6 +44,7 @@ public class GeoIPService : IHostedService
if (_dbReader!.TryCity(ip, out var response)) if (_dbReader!.TryCity(ip, out var response))
{ {
string? continent = response?.Continent.Code; string? continent = response?.Continent.Code;
if (!string.IsNullOrEmpty(continent) && if (!string.IsNullOrEmpty(continent) &&
string.Equals(continent, "NA", StringComparison.Ordinal) string.Equals(continent, "NA", StringComparison.Ordinal)
&& response?.Location.Longitude != null) && response?.Location.Longitude != null)
@@ -140,4 +144,34 @@ public class GeoIPService : IHostedService
_dbReader?.Dispose(); _dbReader?.Dispose();
return Task.CompletedTask; 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.Data;
using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.Group;
using LightlessSyncServer.Models;
using LightlessSyncServer.Utils; using LightlessSyncServer.Utils;
using LightlessSyncShared.Metrics; using LightlessSyncShared.Metrics;
using LightlessSyncShared.Models; using LightlessSyncShared.Models;
@@ -8,7 +9,6 @@ using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using StackExchange.Redis; using StackExchange.Redis;
using System.Text.Json; using System.Text.Json;
using System.Threading;
namespace LightlessSyncServer.Hubs; 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 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) private async Task DeleteUser(User user)
{ {
var ownPairData = await DbContext.ClientPairs.Where(u => u.User.UID == user.UID).ToListAsync().ConfigureAwait(false); 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); 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); if (groupPairSelf == null || !groupPairSelf.IsModerator) return (false, null);
return (true, isOwnerResult.ReferredGroup); return (true, isOwnerResult.ReferredGroup);

View File

@@ -3,14 +3,18 @@ using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions; using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.Group;
using LightlessSync.API.Dto.User; using LightlessSync.API.Dto.User;
using LightlessSyncServer.Models;
using LightlessSyncServer.Services;
using LightlessSyncServer.Utils; using LightlessSyncServer.Utils;
using LightlessSyncShared.Models; using LightlessSyncShared.Models;
using LightlessSyncShared.Utils; using LightlessSyncShared.Utils;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json.Linq;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using System.Reflection;
using System.Security.Cryptography; using System.Security.Cryptography;
namespace LightlessSyncServer.Hubs; namespace LightlessSyncServer.Hubs;
@@ -750,7 +754,7 @@ public partial class LightlessHub
if (dto?.Group == null) if (dto?.Group == null)
{ {
_logger.LogCallWarning(LightlessHubLogger.Args("GroupGetProfile: dto.Group is 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 var data = await DbContext.GroupProfiles
@@ -763,12 +767,12 @@ public partial class LightlessHub
if (data == null) 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) 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 try
@@ -778,7 +782,7 @@ public partial class LightlessHub
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogCallWarning(LightlessHubLogger.Args(ex, "GroupGetProfile: failed to map GroupProfileDto for {Group}", dto.Group.GID ?? dto.Group.AliasOrGID)); _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);
} }
} }
@@ -795,77 +799,77 @@ public partial class LightlessHub
if (!hasRights) return; if (!hasRights) return;
var groupProfileDb = await DbContext.GroupProfiles var groupProfileDb = await DbContext.GroupProfiles
.FirstOrDefaultAsync(g => g.Group.GID == dto.Group.GID || g.Group.Alias == dto.Group.GID, .Include(g => g.Group)
cancellationToken) .FirstOrDefaultAsync(g => g.GroupGID == dto.Group.GID, cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
ImageCheckService.ImageLoadResult profileResult = new();
ImageCheckService.ImageLoadResult bannerResult = new();
//Avatar image validation
if (!string.IsNullOrEmpty(dto.PictureBase64)) if (!string.IsNullOrEmpty(dto.PictureBase64))
{ {
byte[] imageData; profileResult = await ImageCheckService.ValidateImageAsync(dto.PictureBase64, banner: false, RequestAbortedToken).ConfigureAwait(false);
try
if (!profileResult.Success)
{ {
imageData = Convert.FromBase64String(dto.PictureBase64); await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, profileResult.ErrorMessage).ConfigureAwait(false);
}
catch (FormatException)
{
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "The provided image is not a valid Base64 string.").ConfigureAwait(false);
return; return;
} }
}
MemoryStream ms = new(imageData); //Banner image validation
await using (ms.ConfigureAwait(false)) 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); await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, bannerResult.ErrorMessage).ConfigureAwait(false);
if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase)) return;
{
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);
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;
}
} }
} }
var sanitizedProfileImage = profileResult?.Base64Image;
var sanitizedBannerImage = bannerResult?.Base64Image;
if (groupProfileDb == null) if (groupProfileDb == null)
{ {
groupProfileDb = new GroupProfile groupProfileDb = new GroupProfile
{ {
GroupGID = dto.Group.GID, GroupGID = dto.Group.GID,
Group = group,
ProfileDisabled = false, ProfileDisabled = false,
IsNSFW = dto.IsNsfw ?? false, IsNSFW = dto.IsNsfw ?? false,
}; };
groupProfileDb.UpdateProfileFromDto(dto); groupProfileDb.UpdateProfileFromDto(dto, sanitizedProfileImage, sanitizedBannerImage);
await DbContext.GroupProfiles.AddAsync(groupProfileDb, cancellationToken).ConfigureAwait(false); await DbContext.GroupProfiles.AddAsync(groupProfileDb, cancellationToken).ConfigureAwait(false);
} }
else else
{ {
groupProfileDb.Group ??= group;
if (groupProfileDb?.ProfileDisabled ?? false) if (groupProfileDb?.ProfileDisabled ?? false)
{ {
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile was permanently disabled and cannot be edited").ConfigureAwait(false); await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile was permanently disabled and cannot be edited").ConfigureAwait(false);
return; return;
} }
groupProfileDb.UpdateProfileFromDto(dto); groupProfileDb.UpdateProfileFromDto(dto, sanitizedProfileImage, sanitizedBannerImage);
}
var userIds = await DbContext.GroupPairs var userIds = await DbContext.GroupPairs
.Where(p => p.GroupGID == groupProfileDb.GroupGID) .Where(p => p.GroupGID == groupProfileDb.GroupGID)
.Select(p => p.GroupUserUID) .Select(p => p.GroupUserUID)
.ToListAsync(cancellationToken) .ToListAsync(cancellationToken)
.ConfigureAwait(false);
if (userIds.Count > 0)
{
var profileDto = groupProfileDb.ToDTO();
await Clients.Users(userIds).Client_GroupSendProfile(profileDto)
.ConfigureAwait(false); .ConfigureAwait(false);
if (userIds.Count > 0)
{
var profileDto = groupProfileDb.ToDTO();
await Clients.Users(userIds).Client_GroupSendProfile(profileDto)
.ConfigureAwait(false);
}
} }
await DbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); await DbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);

View File

@@ -3,6 +3,8 @@ using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions; using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.Group;
using LightlessSync.API.Dto.User; using LightlessSync.API.Dto.User;
using LightlessSyncServer.Models;
using LightlessSyncServer.Services;
using LightlessSyncServer.Utils; using LightlessSyncServer.Utils;
using LightlessSyncShared.Metrics; using LightlessSyncShared.Metrics;
using LightlessSyncShared.Models; using LightlessSyncShared.Models;
@@ -204,8 +206,8 @@ public partial class LightlessHub
return; return;
} }
var sender = await _pairService.TryAddPairAsync(UserUID, payload.UID); var sender = await _pairService.TryAddPairAsync(UserUID, payload.UID).ConfigureAwait(false);
var receiver = await _pairService.TryAddPairAsync(payload.UID, UserUID); var receiver = await _pairService.TryAddPairAsync(payload.UID, UserUID).ConfigureAwait(false);
var user = await DbContext.Users.SingleAsync(u => u.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); 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) private async Task NotifyBroadcastOwnerOfPairRequest(string targetHashedCid)
{ {
var myHashedCid = UserCharaIdent; var myHashedCid = UserCharaIdent;
@@ -360,23 +361,6 @@ public partial class LightlessHub
await Clients.User(entry.OwnerUID).Client_ReceiveBroadcastPairRequest(dto).ConfigureAwait(false); 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")] [Authorize(Policy = "Identified")]
public async Task SetBroadcastStatus(bool enabled, GroupBroadcastRequestDto? groupDto = null) 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)) 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); 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.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, true, null, null, "This profile was permanently disabled", []); 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")] [Authorize(Policy = "Identified")]
@@ -913,20 +897,20 @@ public partial class LightlessHub
if (profile == null) 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) 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) 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")] [Authorize(Policy = "Identified")]
@@ -959,22 +943,36 @@ public partial class LightlessHub
} }
bool hadInvalidData = false; bool hadInvalidData = false;
List<string> invalidGamePaths = new(); List<string> invalidGamePaths = [];
List<string> invalidFileSwapPaths = new(); List<string> invalidFileSwapPaths = [];
var gamePathRegex = GamePathRegex();
var hashRegex = HashRegex();
foreach (var replacement in dto.CharaData.FileReplacements.SelectMany(p => p.Value)) foreach (var replacement in dto.CharaData.FileReplacements.SelectMany(p => p.Value))
{ {
var invalidPaths = replacement.GamePaths.Where(p => !GamePathRegex().IsMatch(p)).ToList(); var validGamePaths = replacement.GamePaths
invalidPaths.AddRange(replacement.GamePaths.Where(p => !AllowedExtensionsForGamePaths.Any(e => p.EndsWith(e, StringComparison.OrdinalIgnoreCase)))); .Where(p => gamePathRegex.IsMatch(p) &&
replacement.GamePaths = replacement.GamePaths.Where(p => !invalidPaths.Contains(p, StringComparer.OrdinalIgnoreCase)).ToArray(); AllowedExtensionsForGamePaths.Any(e => p.EndsWith(e, StringComparison.OrdinalIgnoreCase)))
bool validGamePaths = replacement.GamePaths.Any(); .ToArray();
bool validHash = string.IsNullOrEmpty(replacement.Hash) || HashRegex().IsMatch(replacement.Hash);
bool validFileSwapPath = string.IsNullOrEmpty(replacement.FileSwapPath) || GamePathRegex().IsMatch(replacement.FileSwapPath); var invalidPaths = replacement.GamePaths.Except(validGamePaths, StringComparer.OrdinalIgnoreCase).ToArray();
if (!validGamePaths || !validHash || !validFileSwapPath)
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; hadInvalidData = true;
if (!validFileSwapPath) invalidFileSwapPaths.Add(replacement.FileSwapPath); if (!validFileSwapPath) invalidFileSwapPaths.Add(replacement.FileSwapPath);
if (!validGamePaths) invalidGamePaths.AddRange(replacement.GamePaths); if (!validGamePathsFlag) invalidGamePaths.AddRange(invalidPaths);
if (!validHash) invalidFileSwapPaths.Add(replacement.Hash); if (!validHash) invalidFileSwapPaths.Add(replacement.Hash);
} }
} }
@@ -1130,70 +1128,62 @@ public partial class LightlessHub
if (!string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal)) throw new HubException("Cannot modify profile data for anyone but yourself"); 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);
//Image Check of size/format ImageCheckService.ImageLoadResult profileResult = new();
ImageCheckService.ImageLoadResult bannerResult = new();
//Avatar image validation
if (!string.IsNullOrEmpty(dto.ProfilePictureBase64)) if (!string.IsNullOrEmpty(dto.ProfilePictureBase64))
{ {
byte[] imageData; profileResult = await ImageCheckService.ValidateImageAsync(dto.ProfilePictureBase64, banner: false, RequestAbortedToken).ConfigureAwait(false);
try
if (!profileResult.Success)
{ {
imageData = Convert.FromBase64String(dto.ProfilePictureBase64); await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, profileResult.ErrorMessage).ConfigureAwait(false);
}
catch (FormatException)
{
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "The provided image is not a valid Base64 string.").ConfigureAwait(false);
return; return;
} }
MemoryStream ms = new(imageData);
await using (ms.ConfigureAwait(false))
{
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<Rgba32>(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;
}
}
} }
if (existingData != null) //Banner image validation
if (!string.IsNullOrEmpty(dto.BannerPictureBase64))
{ {
if (existingData.FlaggedForReport) bannerResult = await ImageCheckService.ValidateImageAsync(dto.BannerPictureBase64, banner: true, RequestAbortedToken).ConfigureAwait(false);
if (!bannerResult.Success)
{
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, bannerResult.ErrorMessage).ConfigureAwait(false);
return;
}
}
if (profileData != null)
{
if (profileData.FlaggedForReport)
{ {
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile is currently flagged for report and cannot be edited").ConfigureAwait(false); await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile is currently flagged for report and cannot be edited").ConfigureAwait(false);
return; return;
} }
if (existingData.ProfileDisabled) if (profileData.ProfileDisabled)
{ {
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile was permanently disabled and cannot be edited").ConfigureAwait(false); await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile was permanently disabled and cannot be edited").ConfigureAwait(false);
return; return;
} }
existingData.UpdateProfileFromDto(dto); profileData.UpdateProfileFromDto(dto, profileResult.Base64Image, bannerResult.Base64Image);
} }
else else
{ {
UserProfileData newUserProfileData = new() profileData = new()
{ {
UserUID = dto.User.UID, UserUID = dto.User.UID,
Base64ProfileImage = dto.ProfilePictureBase64 ?? null,
UserDescription = dto.Description ?? null,
IsNSFW = dto.IsNSFW ?? false, IsNSFW = dto.IsNSFW ?? false,
}; };
newUserProfileData.UpdateProfileFromDto(dto); profileData.UpdateProfileFromDto(dto, profileResult.Base64Image, bannerResult.Base64Image);
await DbContext.UserProfileData.AddAsync(newUserProfileData, cancellationToken).ConfigureAwait(false); await DbContext.UserProfileData.AddAsync(profileData, cancellationToken).ConfigureAwait(false);
} }

View File

@@ -16,7 +16,6 @@ using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using StackExchange.Redis.Extensions.Core.Abstractions; using StackExchange.Redis.Extensions.Core.Abstractions;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Threading;
namespace LightlessSyncServer.Hubs; namespace LightlessSyncServer.Hubs;
@@ -161,8 +160,13 @@ public partial class LightlessHub : Hub<ILightlessHub>, ILightlessHub
} }
else else
{ {
_lightlessMetrics.IncGaugeWithLabels(MetricsAPI.GaugeConnections, labels: Continent); var ResultLabels = new List<string>
{
Continent,
Country,
};
_lightlessMetrics.IncGaugeWithLabels(MetricsAPI.GaugeConnections, labels: [.. ResultLabels]);
try try
{ {
_logger.LogCallInfo(LightlessHubLogger.Args(_contextAccessor.GetIpAddress(), Context.ConnectionId, UserCharaIdent)); _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) if (_userConnections.TryGetValue(UserUID, out var connectionId)
&& string.Equals(connectionId, Context.ConnectionId, StringComparison.Ordinal)) && 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 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.GaugeUsersRegistered, context.Users.AsNoTracking().Count());
metrics.SetGaugeTo(MetricsAPI.GaugePairs, context.ClientPairs.AsNoTracking().Count()); metrics.SetGaugeTo(MetricsAPI.GaugePairs, context.ClientPairs.AsNoTracking().Count());
metrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, context.Permissions.AsNoTracking().Where(p=>p.IsPaused).Count()); metrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, context.Permissions.AsNoTracking().Where(p=>p.IsPaused).Count());
} }
if (args.Length == 0 || !string.Equals(args[0], "dry", StringComparison.Ordinal)) 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.Dto;
using LightlessSync.API.SignalR; using LightlessSync.API.SignalR;
using LightlessSyncServer.Hubs; using LightlessSyncServer.Hubs;
using LightlessSyncServer.Models;
using LightlessSyncShared.Data; using LightlessSyncShared.Data;
using LightlessSyncShared.Metrics; using LightlessSyncShared.Metrics;
using LightlessSyncShared.Services; using LightlessSyncShared.Services;
using LightlessSyncShared.Utils.Configuration; using LightlessSyncShared.Utils.Configuration;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using StackExchange.Redis;
using StackExchange.Redis.Extensions.Core.Abstractions; using StackExchange.Redis.Extensions.Core.Abstractions;
using static LightlessSyncServer.Hubs.LightlessHub;
namespace LightlessSyncServer.Services; namespace LightlessSyncServer.Services;

View File

@@ -10,21 +10,29 @@ namespace LightlessSyncServer.Utils;
public static class Extensions 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; if (profile == null || dto == null) return;
profile.Base64GroupProfileImage = string.IsNullOrWhiteSpace(dto.PictureBase64) ? null : dto.PictureBase64; if (base64PictureString != null) profile.Base64GroupProfileImage = base64PictureString;
if (base64BannerString != null) profile.Base64GroupBannerImage = base64BannerString;
if (dto.Tags != null) profile.Tags = dto.Tags; if (dto.Tags != null) profile.Tags = dto.Tags;
if (dto.Description != null) profile.Description = dto.Description; if (dto.Description != null) profile.Description = dto.Description;
if (dto.IsNsfw.HasValue) profile.IsNSFW = dto.IsNsfw.Value; 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 = null, string? base64BannerString = null)
{ {
ArgumentNullException.ThrowIfNull(profile);
ArgumentNullException.ThrowIfNull(dto);
if (profile == null || dto == null) return; if (profile == null || dto == null) return;
profile.Base64ProfileImage = string.IsNullOrWhiteSpace(dto.ProfilePictureBase64) ? null : dto.ProfilePictureBase64; if (base64PictureString != null) profile.Base64ProfileImage = base64PictureString;
if (base64BannerString != null) profile.Base64BannerImage = base64BannerString;
if (dto.Tags != null) profile.Tags = dto.Tags; if (dto.Tags != null) profile.Tags = dto.Tags;
if (dto.Description != null) profile.UserDescription = dto.Description; if (dto.Description != null) profile.UserDescription = dto.Description;
if (dto.IsNSFW.HasValue) profile.IsNSFW = dto.IsNSFW.Value; if (dto.IsNSFW.HasValue) profile.IsNSFW = dto.IsNSFW.Value;
@@ -34,16 +42,18 @@ public static class Extensions
{ {
if (groupProfile == null) 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(); var groupData = groupProfile.Group?.ToGroupData()
?? (!string.IsNullOrWhiteSpace(groupProfile.GroupGID) ? new GroupData(groupProfile.GroupGID) : null);
return new GroupProfileDto( return new GroupProfileDto(
groupData, groupData,
groupProfile.Description, groupProfile.Description,
groupProfile.Tags, groupProfile.Tags,
groupProfile.Base64GroupProfileImage, groupProfile.Base64GroupProfileImage,
groupProfile.Base64GroupBannerImage,
groupProfile.IsNSFW, groupProfile.IsNSFW,
groupProfile.ProfileDisabled groupProfile.ProfileDisabled
); );
@@ -53,7 +63,7 @@ public static class Extensions
{ {
if (userProfileData == null) 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(); var userData = userProfileData.User?.ToUserData();
@@ -63,6 +73,7 @@ public static class Extensions
userProfileData.ProfileDisabled, userProfileData.ProfileDisabled,
userProfileData.IsNSFW, userProfileData.IsNSFW,
userProfileData.Base64ProfileImage, userProfileData.Base64ProfileImage,
userProfileData.Base64BannerImage,
userProfileData.UserDescription, userProfileData.UserDescription,
userProfileData.Tags userProfileData.Tags
); );

View File

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

View File

@@ -329,13 +329,12 @@ public partial class LightlessWizardModule : InteractionModuleBase
private int? ParseCharacterIdFromLodestoneUrl(string lodestoneUrl) private int? ParseCharacterIdFromLodestoneUrl(string lodestoneUrl)
{ {
var regex = new Regex(@"https:\/\/(na|eu|de|fr|jp)\.finalfantasyxiv\.com\/lodestone\/character\/\d+"); var regex = new Regex(@"^https:\/\/(na|eu|de|fr|jp)\.finalfantasyxiv\.com\/lodestone\/character\/(\d{8})/?$");
var matches = regex.Match(lodestoneUrl); var matches = regex.Match(lodestoneUrl);
var isLodestoneUrl = matches.Success; var isLodestoneUrl = matches.Success;
if (!isLodestoneUrl || matches.Groups.Count < 1) return null; if (!isLodestoneUrl || matches.Groups.Count < 1) return null;
var stringId = matches.Groups[2].ToString();
lodestoneUrl = matches.Groups[0].ToString();
var stringId = lodestoneUrl.Split('/', StringSplitOptions.RemoveEmptyEntries).Last();
if (!int.TryParse(stringId, out int lodestoneId)) if (!int.TryParse(stringId, out int lodestoneId))
{ {
return null; return null;

View File

@@ -20,7 +20,7 @@ public class LightlessMetrics
if (!string.Equals(gauge, MetricsAPI.GaugeConnections, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(gauge, MetricsAPI.GaugeConnections, StringComparison.OrdinalIgnoreCase))
_gauges.Add(gauge, Prometheus.Metrics.CreateGauge(gauge, gauge)); _gauges.Add(gauge, Prometheus.Metrics.CreateGauge(gauge, gauge));
else else
_gauges.Add(gauge, Prometheus.Metrics.CreateGauge(gauge, gauge, new[] { "continent" })); _gauges.Add(gauge, Prometheus.Metrics.CreateGauge(gauge, gauge, ["continent", "country"]));
} }
} }

View File

@@ -16,7 +16,9 @@ namespace LightlessSyncServer.Migrations
type: "integer[]", type: "integer[]",
nullable: true); nullable: true);
migrationBuilder.Sql("UPDATE group_profiles SET tags = NULL;"); migrationBuilder.Sql(
"ALTER TABLE group_profiles ALTER COLUMN tags TYPE integer[] USING string_to_array(tags, ',')::integer[];"
);
migrationBuilder.AlterColumn<int[]>( migrationBuilder.AlterColumn<int[]>(
name: "tags", name: "tags",

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)") .HasColumnType("character varying(20)")
.HasColumnName("group_gid"); .HasColumnName("group_gid");
b.Property<string>("Base64GroupBannerImage")
.HasColumnType("text")
.HasColumnName("base64group_banner_image");
b.Property<string>("Base64GroupProfileImage") b.Property<string>("Base64GroupProfileImage")
.HasColumnType("text") .HasColumnType("text")
.HasColumnName("base64group_profile_image"); .HasColumnName("base64group_profile_image");
@@ -824,6 +828,10 @@ namespace LightlessSyncServer.Migrations
.HasColumnType("character varying(10)") .HasColumnType("character varying(10)")
.HasColumnName("user_uid"); .HasColumnName("user_uid");
b.Property<string>("Base64BannerImage")
.HasColumnType("text")
.HasColumnName("base64banner_image");
b.Property<string>("Base64ProfileImage") b.Property<string>("Base64ProfileImage")
.HasColumnType("text") .HasColumnType("text")
.HasColumnName("base64profile_image"); .HasColumnName("base64profile_image");

View File

@@ -15,6 +15,7 @@ public class GroupProfile
public string Description { get; set; } public string Description { get; set; }
public int[] Tags { get; set; } public int[] Tags { get; set; }
public string Base64GroupProfileImage { get; set; } public string Base64GroupProfileImage { get; set; }
public string Base64GroupBannerImage { get; set; }
public bool IsNSFW { get; set; } = false; public bool IsNSFW { get; set; } = false;
public bool ProfileDisabled { get; set; } = false; public bool ProfileDisabled { get; set; } = false;
} }

View File

@@ -6,6 +6,7 @@ namespace LightlessSyncShared.Models;
public class UserProfileData public class UserProfileData
{ {
public string Base64ProfileImage { get; set; } public string Base64ProfileImage { get; set; }
public string Base64BannerImage { get; set; }
public bool FlaggedForReport { get; set; } public bool FlaggedForReport { get; set; }
public bool IsNSFW { get; set; } public bool IsNSFW { get; set; }
public bool ProfileDisabled { get; set; } public bool ProfileDisabled { get; set; }

View File

@@ -8,6 +8,7 @@ public static class LightlessClaimTypes
public const string Internal = "internal"; public const string Internal = "internal";
public const string Expires = "expiration_date"; public const string Expires = "expiration_date";
public const string Continent = "continent"; public const string Continent = "continent";
public const string Country = "country";
public const string DiscordUser = "discord_user"; public const string DiscordUser = "discord_user";
public const string DiscordId = "discord_user_id"; public const string DiscordId = "discord_user_id";
public const string OAuthLoginToken = "oauth_login_token"; public const string OAuthLoginToken = "oauth_login_token";

View File

@@ -211,7 +211,7 @@ public class Startup
} }
else 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(); }).AddJwtBearer();
services.AddAuthorization(options => services.AddAuthorization(options =>
{ {
options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
options.AddPolicy("Internal", new AuthorizationPolicyBuilder().RequireClaim(LightlessClaimTypes.Internal, "true").Build()); options.AddPolicy("Internal", new AuthorizationPolicyBuilder().RequireClaim(LightlessClaimTypes.Internal, "true").Build());
}); });
services.AddSingleton<IUserIdProvider, IdBasedUserIdProvider>(); services.AddSingleton<IUserIdProvider, IdBasedUserIdProvider>();