Файлы, правки отображения
This commit is contained in:
@@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace Knot.Host.Presentation.Endpoints;
|
||||
namespace Knot.Modules.Admin.Presentation.Endpoints;
|
||||
|
||||
public record KlipyTestDto(
|
||||
[property: JsonPropertyName("apiKey")] string ApiKey,
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Knot.Modules.Conversations.Application.DTOs;
|
||||
public record MediaDto(
|
||||
Guid Id,
|
||||
string Type,
|
||||
string Url,
|
||||
string? Url,
|
||||
string? Filename,
|
||||
long? Size
|
||||
);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System.IO;
|
||||
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Knot.Modules.Conversations.Infrastructure;
|
||||
|
||||
@@ -10,8 +12,24 @@ public class ChatsDbContextFactory : IDesignTimeDbContextFactory<ChatsDbContext>
|
||||
{
|
||||
public ChatsDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var basePath = Path.Combine(Directory.GetCurrentDirectory(), "..", "Host");
|
||||
if (!Directory.Exists(basePath))
|
||||
{
|
||||
basePath = Directory.GetCurrentDirectory();
|
||||
}
|
||||
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(basePath)
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
var optionsBuilder = new DbContextOptionsBuilder<ChatsDbContext>();
|
||||
optionsBuilder.UseNpgsql("Host=localhost;Database=knot_db;Username=knot;Password=knot_pass");
|
||||
var connectionString = configuration.GetConnectionString("DefaultConnection")
|
||||
?? configuration["DATABASE_URL"]
|
||||
?? "Host=localhost;Port=5432;Database=knot_db;Username=knot;Password=knot_pass";
|
||||
|
||||
optionsBuilder.UseNpgsql(connectionString);
|
||||
return new ChatsDbContext(optionsBuilder.Options);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace Host.Endpoints;
|
||||
namespace Knot.Modules.Federation.Presentation.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Эндпоинты федерации для межсерверного взаимодействия.
|
||||
|
||||
@@ -11,7 +11,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
|
||||
namespace Host.Application.Klipy.Queries;
|
||||
namespace Knot.Modules.Klipy.Application.Klipy.Queries;
|
||||
|
||||
public record GetTrendingGifsQuery : IQuery<JsonElement?>;
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediatR;
|
||||
|
||||
namespace Host.Application.Klipy.Queries;
|
||||
namespace Knot.Modules.Klipy.Application.Klipy.Queries;
|
||||
|
||||
public record SearchGifsQuery(string Query) : IQuery<JsonElement?>;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Routing;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Host.Endpoints;
|
||||
namespace Knot.Modules.Klipy.Presentation.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Регистрация эндпоинтов для сервиса Klipy.
|
||||
@@ -20,13 +20,13 @@ public static class KlipyEndpoints
|
||||
|
||||
group.MapGet("/trending", async (ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new Host.Application.Klipy.Queries.GetTrendingGifsQuery(), ct);
|
||||
var result = await sender.Send(new Knot.Modules.Klipy.Application.Klipy.Queries.GetTrendingGifsQuery(), ct);
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(new { error = result.Error.Description });
|
||||
});
|
||||
|
||||
group.MapGet("/search", async ([FromQuery] string q, ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new Host.Application.Klipy.Queries.SearchGifsQuery(q), ct);
|
||||
var result = await sender.Send(new Knot.Modules.Klipy.Application.Klipy.Queries.SearchGifsQuery(q), ct);
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(new { error = result.Error.Description });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ public static class DependencyInjection
|
||||
services.AddDbContext<RelationsDbContext>(options =>
|
||||
options.UseNpgsql(connectionString));
|
||||
|
||||
services.AddScoped<IFriendshipRepository, FriendshipRepository>();
|
||||
services.AddScoped<Knot.Contracts.Relations.Application.Abstractions.IFriendshipRepository, FriendshipRepository>();
|
||||
|
||||
services.AddMediatR(config =>
|
||||
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Modules.Relations.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Knot.Modules.Relations.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Modules.Relations.Infrastructure.Persistence;
|
||||
|
||||
@@ -22,6 +22,7 @@ public sealed class RelationsDbContext : DbContext
|
||||
|
||||
public DbSet<Contact> Contacts => Set<Contact>();
|
||||
public DbSet<UserReplica> UserReplicas => Set<UserReplica>();
|
||||
public DbSet<Friendship> Friendships => Set<Friendship>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -40,6 +41,14 @@ public sealed class RelationsDbContext : DbContext
|
||||
builder.ToTable("UserReplicas");
|
||||
builder.HasKey(r => r.Id);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Friendship>(builder =>
|
||||
{
|
||||
builder.ToTable("Friendships");
|
||||
builder.HasKey(f => f.Id);
|
||||
builder.Property(f => f.Status).HasConversion<string>();
|
||||
builder.HasIndex(f => new { f.UserId, f.FriendId }).IsUnique();
|
||||
});
|
||||
}
|
||||
|
||||
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.IO;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Knot.Modules.Relations.Infrastructure.Persistence;
|
||||
|
||||
public class RelationsDbContextFactory : IDesignTimeDbContextFactory<RelationsDbContext>
|
||||
{
|
||||
public RelationsDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var basePath = Path.Combine(Directory.GetCurrentDirectory(), "..", "Host");
|
||||
if (!Directory.Exists(basePath))
|
||||
{
|
||||
basePath = Directory.GetCurrentDirectory();
|
||||
}
|
||||
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(basePath)
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
var optionsBuilder = new DbContextOptionsBuilder<RelationsDbContext>();
|
||||
var connectionString = configuration.GetConnectionString("DefaultConnection")
|
||||
?? configuration["DATABASE_URL"]
|
||||
?? "Host=localhost;Port=5432;Database=knot_db;Username=knot;Password=knot_pass";
|
||||
|
||||
optionsBuilder.UseNpgsql(connectionString);
|
||||
|
||||
return new RelationsDbContext(optionsBuilder.Options, null!);
|
||||
}
|
||||
}
|
||||
114
backend/src/Modules/Relations/Migrations/20260401112051_AddFriendships.Designer.cs
generated
Normal file
114
backend/src/Modules/Relations/Migrations/20260401112051_AddFriendships.Designer.cs
generated
Normal file
@@ -0,0 +1,114 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Knot.Modules.Relations.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Relations.Migrations
|
||||
{
|
||||
[DbContext(typeof(RelationsDbContext))]
|
||||
[Migration("20260401112051_AddFriendships")]
|
||||
partial class AddFriendships
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("relations")
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Relations.Domain.Contact", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ContactId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId", "ContactId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Contacts", "relations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Relations.Domain.Friendship", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("FriendId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId", "FriendId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Friendships", "relations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Relations.Domain.UserReplica", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Domain")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsExternal")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("UserReplicas", "relations");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Relations.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddFriendships : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.EnsureSchema(
|
||||
name: "relations");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Contacts",
|
||||
schema: "relations",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ContactId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Status = table.Column<string>(type: "text", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Contacts", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Friendships",
|
||||
schema: "relations",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
FriendId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Status = table.Column<string>(type: "text", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Friendships", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserReplicas",
|
||||
schema: "relations",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Username = table.Column<string>(type: "text", nullable: false),
|
||||
DisplayName = table.Column<string>(type: "text", nullable: false),
|
||||
Avatar = table.Column<string>(type: "text", nullable: false),
|
||||
IsExternal = table.Column<bool>(type: "boolean", nullable: false),
|
||||
Domain = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserReplicas", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Contacts_UserId_ContactId",
|
||||
schema: "relations",
|
||||
table: "Contacts",
|
||||
columns: new[] { "UserId", "ContactId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Friendships_UserId_FriendId",
|
||||
schema: "relations",
|
||||
table: "Friendships",
|
||||
columns: new[] { "UserId", "FriendId" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Contacts",
|
||||
schema: "relations");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Friendships",
|
||||
schema: "relations");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserReplicas",
|
||||
schema: "relations");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Knot.Modules.Relations.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Relations.Migrations
|
||||
{
|
||||
[DbContext(typeof(RelationsDbContext))]
|
||||
partial class RelationsDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("relations")
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Relations.Domain.Contact", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ContactId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId", "ContactId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Contacts", "relations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Relations.Domain.Friendship", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("FriendId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId", "FriendId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Friendships", "relations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Relations.Domain.UserReplica", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Domain")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsExternal")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("UserReplicas", "relations");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,17 +9,43 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace Knot.Host.Presentation.Endpoints;
|
||||
namespace Knot.Modules.Storage.Presentation.Endpoints;
|
||||
|
||||
public static class FilesEndpoints
|
||||
{
|
||||
public static void MapFilesEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("api/files");
|
||||
|
||||
group.MapGet("{id}", async (string id, [FromQuery] bool download, [FromServices] IFileStorageService fileStorage, [FromServices] IMemoryCache cache, CancellationToken ct) =>
|
||||
// Federation remote endpoint - most specific, must be first
|
||||
app.MapGet("/api/files/remote/{domain}/{id}", async (string domain, string id, HttpClient httpClient, ISettingsService settingsService, CancellationToken ct) =>
|
||||
{
|
||||
var settings = await settingsService.GetSettingsAsync(ct);
|
||||
if (!settings.Federation.Enabled) return Results.Forbid();
|
||||
|
||||
var targetUrl = $"{domain.TrimEnd('/')}/api/federation/v1/proxy/{id}";
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, targetUrl);
|
||||
request.Headers.Add("X-Knot-Domain", settings.System.DomainUrl);
|
||||
|
||||
using (var rsa = RSA.Create())
|
||||
{
|
||||
rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(settings.Federation.PrivateKey!), out _);
|
||||
var dataToSign = Encoding.UTF8.GetBytes(id + settings.System.DomainUrl);
|
||||
var signature = Convert.ToBase64String(rsa.SignData(dataToSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
|
||||
request.Headers.Add("X-Knot-Signature", signature);
|
||||
}
|
||||
|
||||
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct);
|
||||
if (!response.IsSuccessStatusCode) return Results.NotFound();
|
||||
|
||||
var remoteStream = await response.Content.ReadAsStreamAsync(ct);
|
||||
var contentType = response.Content.Headers.ContentType?.ToString() ?? "application/octet-stream";
|
||||
|
||||
return Results.Stream(remoteStream, contentType, enableRangeProcessing: true);
|
||||
});
|
||||
|
||||
// Main file download endpoint - without group prefix to avoid routing issues
|
||||
app.MapGet("/api/files/{id}", async ([FromRoute] string id, [FromServices] IFileStorageService fileStorage, [FromServices] IMemoryCache cache, HttpContext httpContext, [FromQuery] bool download = false, CancellationToken ct = default) =>
|
||||
{
|
||||
// (Текущая логика локальных файлов остается без изменений)
|
||||
try
|
||||
{
|
||||
var cacheKey = $"file_{id}";
|
||||
@@ -50,39 +76,9 @@ public static class FilesEndpoints
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Results.NotFound(new { error = "Файл не найден или ошибка доступа.", message = ex.Message });
|
||||
return Results.NotFound(new { error = "Файл не найден", message = ex.Message });
|
||||
}
|
||||
});
|
||||
|
||||
// Новый эндпоинт для удаленных (федеративных) файлов
|
||||
group.MapGet("/remote/{domain}/{id}", async (string domain, string id, HttpClient httpClient, ISettingsService settingsService, CancellationToken ct) =>
|
||||
{
|
||||
var settings = await settingsService.GetSettingsAsync(ct);
|
||||
if (!settings.Federation.Enabled) return Results.Forbid();
|
||||
|
||||
// Формируем подписанный запрос к удаленному серверу
|
||||
var targetUrl = $"{domain.TrimEnd('/')}/api/federation/v1/proxy/{id}";
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, targetUrl);
|
||||
request.Headers.Add("X-Knot-Domain", settings.System.DomainUrl);
|
||||
|
||||
// Генерируем подпись запроса своим приватным ключом
|
||||
using (var rsa = RSA.Create())
|
||||
{
|
||||
rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(settings.Federation.PrivateKey!), out _);
|
||||
var dataToSign = Encoding.UTF8.GetBytes(id + settings.System.DomainUrl);
|
||||
var signature = Convert.ToBase64String(rsa.SignData(dataToSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
|
||||
request.Headers.Add("X-Knot-Signature", signature);
|
||||
}
|
||||
|
||||
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct);
|
||||
if (!response.IsSuccessStatusCode) return Results.NotFound();
|
||||
|
||||
var remoteStream = await response.Content.ReadAsStreamAsync(ct);
|
||||
var contentType = response.Content.Headers.ContentType?.ToString() ?? "application/octet-stream";
|
||||
|
||||
return Results.Stream(remoteStream, contentType, enableRangeProcessing: true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Net.Http;
|
||||
using Knot.Contracts.Stories.Infrastructure.Persistence;
|
||||
using Knot.Modules.Stories.Application.Abstractions;
|
||||
using Knot.Modules.Stories.Infrastructure.Database;
|
||||
using Knot.Modules.Stories.Infrastructure.Persistence.Mongo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@@ -14,6 +15,7 @@ public static class DependencyInjection
|
||||
public static IServiceCollection AddStoriesModule(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddScoped<IStoryRepository, StoryRepository>();
|
||||
services.AddScoped<IUserRepository, UserRepository>();
|
||||
services.AddScoped<Knot.Contracts.Stories.Infrastructure.Persistence.IStoryCollection, StoryCollection>();
|
||||
|
||||
var connectionString = configuration.GetConnectionString("DefaultConnection");
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.IO;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Knot.Modules.Stories.Infrastructure.Database;
|
||||
|
||||
public class StoriesDbContextFactory : IDesignTimeDbContextFactory<StoriesDbContext>
|
||||
{
|
||||
public StoriesDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var basePath = Path.Combine(Directory.GetCurrentDirectory(), "..", "Host");
|
||||
if (!Directory.Exists(basePath))
|
||||
{
|
||||
basePath = Directory.GetCurrentDirectory();
|
||||
}
|
||||
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(basePath)
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
var optionsBuilder = new DbContextOptionsBuilder<StoriesDbContext>();
|
||||
var connectionString = configuration.GetConnectionString("DefaultConnection")
|
||||
?? configuration["DATABASE_URL"]
|
||||
?? "Host=localhost;Port=5432;Database=knot_db;Username=knot;Password=knot_pass";
|
||||
|
||||
optionsBuilder.UseNpgsql(connectionString);
|
||||
|
||||
return new StoriesDbContext(optionsBuilder.Options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Contracts.Profiles.Domain;
|
||||
using Knot.Contracts.Relations.Domain;
|
||||
using Knot.Modules.Stories.Application.Abstractions;
|
||||
|
||||
namespace Knot.Modules.Stories.Infrastructure.Database;
|
||||
|
||||
internal sealed class UserRepository : IUserRepository
|
||||
{
|
||||
private readonly IProfileRepository _profileRepository;
|
||||
|
||||
public UserRepository(IProfileRepository profileRepository)
|
||||
{
|
||||
_profileRepository = profileRepository;
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var profile = await _profileRepository.GetAsync(id, cancellationToken);
|
||||
return profile != null;
|
||||
}
|
||||
|
||||
public async Task<UserReplica?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var profile = await _profileRepository.GetAsync(id, cancellationToken);
|
||||
if (profile == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new UserReplica(
|
||||
profile.UserId,
|
||||
profile.Username ?? string.Empty,
|
||||
profile.DisplayName ?? string.Empty,
|
||||
profile.Avatar ?? string.Empty,
|
||||
false,
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Contracts\Profiles\Knot.Contracts.Profiles.csproj" />
|
||||
<ProjectReference Include="..\..\Contracts\Relations\Knot.Contracts.Relations.csproj" />
|
||||
<ProjectReference Include="..\..\Contracts\Conversations\Knot.Contracts.Conversations.csproj" />
|
||||
<ProjectReference Include="..\..\Contracts\Settings\Knot.Contracts.Settings.csproj" />
|
||||
|
||||
54
backend/src/Modules/Stories/Migrations/20260401112239_InitialStories.Designer.cs
generated
Normal file
54
backend/src/Modules/Stories/Migrations/20260401112239_InitialStories.Designer.cs
generated
Normal file
@@ -0,0 +1,54 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Knot.Modules.Stories.Infrastructure.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Stories.Migrations
|
||||
{
|
||||
[DbContext(typeof(StoriesDbContext))]
|
||||
[Migration("20260401112239_InitialStories")]
|
||||
partial class InitialStories
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("stories")
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Stories.Domain.StoryViewer", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("StoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("ViewedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoryId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("StoryViewers", "stories");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Stories.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialStories : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.EnsureSchema(
|
||||
name: "stories");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "StoryViewers",
|
||||
schema: "stories",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
StoryId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ViewedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_StoryViewers", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StoryViewers_StoryId_UserId",
|
||||
schema: "stories",
|
||||
table: "StoryViewers",
|
||||
columns: new[] { "StoryId", "UserId" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "StoryViewers",
|
||||
schema: "stories");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Knot.Modules.Stories.Infrastructure.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Modules.Stories.Migrations
|
||||
{
|
||||
[DbContext(typeof(StoriesDbContext))]
|
||||
partial class StoriesDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("stories")
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Modules.Stories.Domain.StoryViewer", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("StoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("ViewedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoryId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("StoryViewers", "stories");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user