Reworked image handling, added banner for profiles. Made functions to start on xivauth. Refactored some code.
This commit is contained in:
Submodule LightlessAPI updated: 0bc7abb274...7d51340b0b
@@ -24,6 +24,8 @@ public class OAuthController : AuthControllerBase
|
|||||||
{
|
{
|
||||||
private const string _discordOAuthCall = "discordCall";
|
private const string _discordOAuthCall = "discordCall";
|
||||||
private const string _discordOAuthCallback = "discordCallback";
|
private const string _discordOAuthCallback = "discordCallback";
|
||||||
|
private const string _xivauthCall = "xivauthCall";
|
||||||
|
private const string _xivauthCallBack = "xivauthCallBack";
|
||||||
private static readonly ConcurrentDictionary<string, string> _cookieOAuthResponse = [];
|
private static readonly ConcurrentDictionary<string, string> _cookieOAuthResponse = [];
|
||||||
|
|
||||||
public OAuthController(ILogger<OAuthController> logger,
|
public OAuthController(ILogger<OAuthController> logger,
|
||||||
@@ -36,6 +38,21 @@ public class OAuthController : AuthControllerBase
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpGet(_xivauthCall)]
|
||||||
|
public IActionResult XIVAuthOAuthSetCookieAndRedirect([FromQuery] string sessionId)
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpGet(_xivauthCallBack)]
|
||||||
|
public IActionResult XIVAuthOAuthCallback([FromQuery] string code)
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpGet(_discordOAuthCall)]
|
[HttpGet(_discordOAuthCall)]
|
||||||
public IActionResult DiscordOAuthSetCookieAndRedirect([FromQuery] string sessionId)
|
public IActionResult DiscordOAuthSetCookieAndRedirect([FromQuery] string sessionId)
|
||||||
|
|||||||
@@ -4,14 +4,17 @@ 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.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;
|
||||||
@@ -751,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
|
||||||
@@ -764,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
|
||||||
@@ -779,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -800,36 +803,30 @@ public partial class LightlessHub
|
|||||||
cancellationToken)
|
cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
ImageCheckService.ImageLoadResult profileResult = null;
|
||||||
|
ImageCheckService.ImageLoadResult bannerResult = null;
|
||||||
|
|
||||||
|
//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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -842,7 +839,7 @@ public partial class LightlessHub
|
|||||||
IsNSFW = dto.IsNsfw ?? false,
|
IsNSFW = dto.IsNsfw ?? false,
|
||||||
};
|
};
|
||||||
|
|
||||||
groupProfileDb.UpdateProfileFromDto(dto);
|
groupProfileDb.UpdateProfileFromDto(dto, profileResult.Base64Image, bannerResult.Base64Image);
|
||||||
await DbContext.GroupProfiles.AddAsync(groupProfileDb, cancellationToken).ConfigureAwait(false);
|
await DbContext.GroupProfiles.AddAsync(groupProfileDb, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -853,7 +850,7 @@ public partial class LightlessHub
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
groupProfileDb.UpdateProfileFromDto(dto);
|
groupProfileDb.UpdateProfileFromDto(dto, profileResult.Base64Image, bannerResult.Base64Image);
|
||||||
|
|
||||||
var userIds = await DbContext.GroupPairs
|
var userIds = await DbContext.GroupPairs
|
||||||
.Where(p => p.GroupGID == groupProfileDb.GroupGID)
|
.Where(p => p.GroupGID == groupProfileDb.GroupGID)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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.Models;
|
||||||
|
using LightlessSyncServer.Services;
|
||||||
using LightlessSyncServer.Utils;
|
using LightlessSyncServer.Utils;
|
||||||
using LightlessSyncShared.Metrics;
|
using LightlessSyncShared.Metrics;
|
||||||
using LightlessSyncShared.Models;
|
using LightlessSyncShared.Models;
|
||||||
@@ -809,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")]
|
||||||
@@ -896,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")]
|
||||||
@@ -942,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1115,36 +1130,30 @@ public partial class LightlessHub
|
|||||||
|
|
||||||
var existingData = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == dto.User.UID, cancellationToken: RequestAbortedToken).ConfigureAwait(false);
|
var existingData = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == dto.User.UID, cancellationToken: RequestAbortedToken).ConfigureAwait(false);
|
||||||
|
|
||||||
//Image Check of size/format
|
ImageCheckService.ImageLoadResult profileResult = null;
|
||||||
|
ImageCheckService.ImageLoadResult bannerResult = null;
|
||||||
|
|
||||||
|
//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);
|
//Banner image validation
|
||||||
await using (ms.ConfigureAwait(false))
|
if (!string.IsNullOrEmpty(dto.BannerPictureBase64))
|
||||||
|
{
|
||||||
|
bannerResult = await ImageCheckService.ValidateImageAsync(dto.BannerPictureBase64, banner: true, RequestAbortedToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!bannerResult.Success)
|
||||||
{
|
{
|
||||||
var format = await Image.DetectFormatAsync(ms, RequestAbortedToken).ConfigureAwait(false);
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1162,7 +1171,7 @@ public partial class LightlessHub
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
existingData.UpdateProfileFromDto(dto);
|
existingData.UpdateProfileFromDto(dto, profileResult.Base64Image, bannerResult.Base64Image);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -1174,7 +1183,7 @@ public partial class LightlessHub
|
|||||||
IsNSFW = dto.IsNSFW ?? false,
|
IsNSFW = dto.IsNSFW ?? false,
|
||||||
};
|
};
|
||||||
|
|
||||||
newUserProfileData.UpdateProfileFromDto(dto);
|
existingData.UpdateProfileFromDto(dto, profileResult.Base64Image, bannerResult.Base64Image);
|
||||||
|
|
||||||
await DbContext.UserProfileData.AddAsync(newUserProfileData, cancellationToken).ConfigureAwait(false);
|
await DbContext.UserProfileData.AddAsync(newUserProfileData, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
profile.Base64GroupProfileImage = string.IsNullOrWhiteSpace(base64PictureString) ? null : base64PictureString;
|
||||||
|
profile.Base64GroupBannerImage = string.IsNullOrWhiteSpace(base64BannerString) ? null : 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, 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;
|
profile.Base64ProfileImage = string.IsNullOrWhiteSpace(base64PictureString) ? null : base64PictureString;
|
||||||
|
profile.Base64BannerImage = string.IsNullOrWhiteSpace(base64BannerString) ? null : 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,7 +42,7 @@ 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();
|
||||||
@@ -44,6 +52,7 @@ public static class Extensions
|
|||||||
groupProfile.Description,
|
groupProfile.Description,
|
||||||
groupProfile.Tags,
|
groupProfile.Tags,
|
||||||
groupProfile.Base64GroupProfileImage,
|
groupProfile.Base64GroupProfileImage,
|
||||||
|
groupProfile.Base64GroupBannerImage,
|
||||||
groupProfile.IsNSFW,
|
groupProfile.IsNSFW,
|
||||||
groupProfile.ProfileDisabled
|
groupProfile.ProfileDisabled
|
||||||
);
|
);
|
||||||
@@ -53,7 +62,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 +72,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
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
1189
LightlessSyncServer/LightlessSyncShared/Migrations/20251020181150_AddBannerForProfiles.Designer.cs
generated
Normal file
1189
LightlessSyncServer/LightlessSyncShared/Migrations/20251020181150_AddBannerForProfiles.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
Reference in New Issue
Block a user