Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd4cb73b9b | ||
|
|
ab9cdeb682 | ||
| 63211b2e8b | |||
|
|
a1280d58bf | ||
| 34f0223a85 | |||
| 69f06f5868 | |||
| 066f56e5a2 | |||
|
|
287f72b6ad | ||
| ef13566b7a |
@@ -995,11 +995,11 @@ public partial class LightlessHub
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var (isOwner, _) = await TryValidateOwner(dto.GID).ConfigureAwait(false);
|
var (isOwner, _) = await TryValidateGroupModeratorOrOwner(dto.GID).ConfigureAwait(false);
|
||||||
if (!isOwner)
|
if (!isOwner)
|
||||||
{
|
{
|
||||||
_logger.LogCallWarning(LightlessHubLogger.Args("Unauthorized syncshell broadcast change", "User", UserUID, "GID", dto.GID));
|
_logger.LogCallWarning(LightlessHubLogger.Args("Unauthorized syncshell broadcast change", "User", UserUID, "GID", dto.GID));
|
||||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "You must be the owner of the syncshell to broadcast it.");
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "You must be the owner or moderator of the syncshell to broadcast it.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
|
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
|
||||||
|
<PackageReference Include="Blake3" Version="2.0.0" />
|
||||||
<PackageReference Include="IDisposableAnalyzers" Version="4.0.8">
|
<PackageReference Include="IDisposableAnalyzers" Version="4.0.8">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ 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{8})/?$");
|
var regex = new Regex(@"^https:\/\/(na|eu|de|fr|jp)\.finalfantasyxiv\.com\/lodestone\/character\/(\d+)/?$");
|
||||||
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;
|
||||||
|
|||||||
@@ -91,6 +91,12 @@ public class LightlessDbContext : DbContext
|
|||||||
mb.Entity<GroupProfile>().ToTable("group_profiles");
|
mb.Entity<GroupProfile>().ToTable("group_profiles");
|
||||||
mb.Entity<GroupProfile>().HasKey(u => u.GroupGID);
|
mb.Entity<GroupProfile>().HasKey(u => u.GroupGID);
|
||||||
mb.Entity<GroupProfile>().HasIndex(c => c.GroupGID);
|
mb.Entity<GroupProfile>().HasIndex(c => c.GroupGID);
|
||||||
|
mb.Entity<Group>()
|
||||||
|
.HasOne(g => g.Profile)
|
||||||
|
.WithOne(p => p.Group)
|
||||||
|
.HasForeignKey<GroupProfile>(p => p.GroupGID)
|
||||||
|
.IsRequired(false)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
mb.Entity<GroupTempInvite>().ToTable("group_temp_invites");
|
mb.Entity<GroupTempInvite>().ToTable("group_temp_invites");
|
||||||
mb.Entity<GroupTempInvite>().HasKey(u => new { u.GroupGID, u.Invite });
|
mb.Entity<GroupTempInvite>().HasKey(u => new { u.GroupGID, u.Invite });
|
||||||
mb.Entity<GroupTempInvite>().HasIndex(c => c.GroupGID);
|
mb.Entity<GroupTempInvite>().HasIndex(c => c.GroupGID);
|
||||||
|
|||||||
1319
LightlessSyncServer/LightlessSyncShared/Migrations/20251117153226_FixForeignKeyGroupProfiles.Designer.cs
generated
Normal file
1319
LightlessSyncServer/LightlessSyncShared/Migrations/20251117153226_FixForeignKeyGroupProfiles.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace LightlessSyncServer.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class FixForeignKeyGroupProfiles : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "fk_group_profiles_groups_group_gid",
|
||||||
|
table: "group_profiles");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "fk_group_profiles_groups_group_gid",
|
||||||
|
table: "group_profiles",
|
||||||
|
column: "group_gid",
|
||||||
|
principalTable: "groups",
|
||||||
|
principalColumn: "gid",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "fk_group_profiles_groups_group_gid",
|
||||||
|
table: "group_profiles");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "fk_group_profiles_groups_group_gid",
|
||||||
|
table: "group_profiles",
|
||||||
|
column: "group_gid",
|
||||||
|
principalTable: "groups",
|
||||||
|
principalColumn: "gid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -820,14 +820,14 @@ namespace LightlessSyncServer.Migrations
|
|||||||
.HasColumnType("character varying(15)")
|
.HasColumnType("character varying(15)")
|
||||||
.HasColumnName("alias");
|
.HasColumnName("alias");
|
||||||
|
|
||||||
b.Property<bool>("HasVanity")
|
|
||||||
.HasColumnType("boolean")
|
|
||||||
.HasColumnName("has_vanity");
|
|
||||||
|
|
||||||
b.Property<bool>("ChatBanned")
|
b.Property<bool>("ChatBanned")
|
||||||
.HasColumnType("boolean")
|
.HasColumnType("boolean")
|
||||||
.HasColumnName("chat_banned");
|
.HasColumnName("chat_banned");
|
||||||
|
|
||||||
|
b.Property<bool>("HasVanity")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("has_vanity");
|
||||||
|
|
||||||
b.Property<bool>("IsAdmin")
|
b.Property<bool>("IsAdmin")
|
||||||
.HasColumnType("boolean")
|
.HasColumnType("boolean")
|
||||||
.HasColumnName("is_admin");
|
.HasColumnName("is_admin");
|
||||||
@@ -1220,6 +1220,7 @@ namespace LightlessSyncServer.Migrations
|
|||||||
b.HasOne("LightlessSyncShared.Models.Group", "Group")
|
b.HasOne("LightlessSyncShared.Models.Group", "Group")
|
||||||
.WithOne("Profile")
|
.WithOne("Profile")
|
||||||
.HasForeignKey("LightlessSyncShared.Models.GroupProfile", "GroupGID")
|
.HasForeignKey("LightlessSyncShared.Models.GroupProfile", "GroupGID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.HasConstraintName("fk_group_profiles_groups_group_gid");
|
.HasConstraintName("fk_group_profiles_groups_group_gid");
|
||||||
|
|
||||||
b.Navigation("Group");
|
b.Navigation("Group");
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using K4os.Compression.LZ4.Legacy;
|
using Blake3;
|
||||||
|
using K4os.Compression.LZ4.Legacy;
|
||||||
using LightlessSync.API.Dto.Files;
|
using LightlessSync.API.Dto.Files;
|
||||||
using LightlessSync.API.Routes;
|
using LightlessSync.API.Routes;
|
||||||
using LightlessSync.API.SignalR;
|
using LightlessSync.API.SignalR;
|
||||||
@@ -208,11 +209,14 @@ public class ServerFilesController : ControllerBase
|
|||||||
[RequestSizeLimit(200 * 1024 * 1024)]
|
[RequestSizeLimit(200 * 1024 * 1024)]
|
||||||
public async Task<IActionResult> UploadFile(string hash, CancellationToken requestAborted)
|
public async Task<IActionResult> UploadFile(string hash, CancellationToken requestAborted)
|
||||||
{
|
{
|
||||||
using var dbContext = await _lightlessDbContext.CreateDbContextAsync();
|
await using var dbContext = await _lightlessDbContext.CreateDbContextAsync();
|
||||||
|
|
||||||
_logger.LogInformation("{user}|{file}: Uploading", LightlessUser, hash);
|
_logger.LogInformation("{user}|{file}: Uploading", LightlessUser, hash);
|
||||||
|
if (hash.Length == 40)
|
||||||
|
{
|
||||||
|
hash = hash.ToUpperInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
hash = hash.ToUpperInvariant();
|
|
||||||
var existingFile = await dbContext.Files.SingleOrDefaultAsync(f => f.Hash == hash);
|
var existingFile = await dbContext.Files.SingleOrDefaultAsync(f => f.Hash == hash);
|
||||||
if (existingFile != null) return Ok();
|
if (existingFile != null) return Ok();
|
||||||
|
|
||||||
@@ -263,10 +267,14 @@ public class ServerFilesController : ControllerBase
|
|||||||
[RequestSizeLimit(200 * 1024 * 1024)]
|
[RequestSizeLimit(200 * 1024 * 1024)]
|
||||||
public async Task<IActionResult> UploadFileMunged(string hash, CancellationToken requestAborted)
|
public async Task<IActionResult> UploadFileMunged(string hash, CancellationToken requestAborted)
|
||||||
{
|
{
|
||||||
using var dbContext = await _lightlessDbContext.CreateDbContextAsync();
|
await using var dbContext = await _lightlessDbContext.CreateDbContextAsync();
|
||||||
|
|
||||||
_logger.LogInformation("{user}|{file}: Uploading munged", LightlessUser, hash);
|
_logger.LogInformation("{user}|{file}: Uploading munged", LightlessUser, hash);
|
||||||
hash = hash.ToUpperInvariant();
|
if (hash.Length == 40)
|
||||||
|
{
|
||||||
|
hash = hash.ToUpperInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
var existingFile = await dbContext.Files.SingleOrDefaultAsync(f => f.Hash == hash);
|
var existingFile = await dbContext.Files.SingleOrDefaultAsync(f => f.Hash == hash);
|
||||||
if (existingFile != null) return Ok();
|
if (existingFile != null) return Ok();
|
||||||
|
|
||||||
@@ -319,20 +327,26 @@ public class ServerFilesController : ControllerBase
|
|||||||
private async Task StoreData(string hash, LightlessDbContext dbContext, MemoryStream compressedFileStream)
|
private async Task StoreData(string hash, LightlessDbContext dbContext, MemoryStream compressedFileStream)
|
||||||
{
|
{
|
||||||
var decompressedData = LZ4Wrapper.Unwrap(compressedFileStream.ToArray());
|
var decompressedData = LZ4Wrapper.Unwrap(compressedFileStream.ToArray());
|
||||||
// reset streams
|
|
||||||
compressedFileStream.Seek(0, SeekOrigin.Begin);
|
compressedFileStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
// compute hash to verify
|
bool valid;
|
||||||
var hashString = BitConverter.ToString(SHA1.HashData(decompressedData))
|
|
||||||
.Replace("-", "", StringComparison.Ordinal).ToUpperInvariant();
|
|
||||||
if (!string.Equals(hashString, hash, StringComparison.Ordinal))
|
|
||||||
throw new InvalidOperationException($"{LightlessUser}|{hash}: Hash does not match file, computed: {hashString}, expected: {hash}");
|
|
||||||
|
|
||||||
// save file
|
if (hash.Length == 40)
|
||||||
var path = FilePathUtil.GetFilePath(_basePath, hash);
|
{
|
||||||
using var fileStream = new FileStream(path, FileMode.Create);
|
var sha1Hex = Convert.ToHexString(SHA1.HashData(decompressedData));
|
||||||
await compressedFileStream.CopyToAsync(fileStream).ConfigureAwait(false);
|
valid = string.Equals(sha1Hex, hash, StringComparison.OrdinalIgnoreCase);
|
||||||
_logger.LogDebug("{user}|{file}: Uploaded file saved to {path}", LightlessUser, hash, path);
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var blakeHash = Hasher.Hash(decompressedData);
|
||||||
|
var blakeHex = Convert.ToHexString(blakeHash.AsSpan());
|
||||||
|
valid = string.Equals(blakeHex, hash, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!valid)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"{LightlessUser}|{hash}: Hash does not match file, computed mismatch."
|
||||||
|
);
|
||||||
|
|
||||||
// update on db
|
// update on db
|
||||||
await dbContext.Files.AddAsync(new FileCache()
|
await dbContext.Files.AddAsync(new FileCache()
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Blake3" Version="2.0.0" />
|
||||||
<PackageReference Include="IDisposableAnalyzers" Version="4.0.8">
|
<PackageReference Include="IDisposableAnalyzers" Version="4.0.8">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
|||||||
Reference in New Issue
Block a user