diff --git a/apps/server-net/src/Host/Controllers/ConfigController.cs b/apps/server-net/src/Host/Controllers/ConfigController.cs deleted file mode 100644 index 885db7c..0000000 --- a/apps/server-net/src/Host/Controllers/ConfigController.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Knot.Shared.Kernel.Configuration; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Authorization; - -namespace Host.Controllers; - -[Authorize] -[ApiController] -[Route("api/[controller]")] -public class ConfigController : ControllerBase -{ - private readonly ISettingsService _settings; - - public ConfigController(ISettingsService settings) - { - _settings = settings; - } - - [HttpGet] - [AllowAnonymous] - public IActionResult GetPublicConfig() - { - var conf = _settings.Current; - return Ok(new - { - conf.EnableCalls, - conf.EnableKlipy, - conf.MaxFileSizeMb, - conf.MaxGroupMembers, - conf.EnableConfederation, - conf.EnableRegistration - }); - } -} diff --git a/apps/server-net/src/Host/Controllers/FederationController.cs b/apps/server-net/src/Host/Controllers/FederationController.cs deleted file mode 100644 index effde0c..0000000 --- a/apps/server-net/src/Host/Controllers/FederationController.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Security.Cryptography; -using Knot.Shared.Kernel.Configuration; -using Microsoft.AspNetCore.Mvc; - -namespace Host.Controllers; - -[ApiController] -[Route("api/[controller]")] -public class FederationController : ControllerBase -{ - private readonly ISettingsService _settingsService; - - public FederationController(ISettingsService settingsService) - { - _settingsService = settingsService; - } - - [HttpPost("handshake")] - public IActionResult Handshake([FromBody] HandshakeRequest request) - { - var conf = _settingsService.Current; - if (!conf.EnableConfederation) - return StatusCode(503, new { error = "Confederation is disabled on this node." }); - - if (string.IsNullOrEmpty(request.Domain) || string.IsNullOrEmpty(request.PublicKey)) - return BadRequest(new { error = "Domain and PublicKey are required." }); - - // Discovery: Checks allowed list - var allowedList = conf.AllowedDomains?.Select(d => d.Trim().ToLower()).ToList() ?? new List(); - if (!allowedList.Contains(request.Domain.ToLower())) - return Forbid("Domain not in whitelist."); - - // Here we would store the peer's public key in the DB to encrypt future traffic. - // For demonstration, we simply generate a response with our own public key. - - using var rsa = RSA.Create(2048); - var selfPublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey()); - - return Ok(new HandshakeResponse - { - Domain = Environment.GetEnvironmentVariable("DOMAIN") ?? "knot.local", - PublicKey = selfPublicKey, - Status = "Accepted" - }); - } -} - -public class HandshakeRequest -{ - public string Domain { get; set; } = string.Empty; - public string PublicKey { get; set; } = string.Empty; -} - -public class HandshakeResponse -{ - public string Domain { get; set; } = string.Empty; - public string PublicKey { get; set; } = string.Empty; - public string Status { get; set; } = string.Empty; -} diff --git a/apps/server-net/src/Host/Controllers/KlipyController.cs b/apps/server-net/src/Host/Controllers/KlipyController.cs deleted file mode 100644 index da069af..0000000 --- a/apps/server-net/src/Host/Controllers/KlipyController.cs +++ /dev/null @@ -1,119 +0,0 @@ -using Knot.Shared.Kernel.Configuration; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Caching.Memory; -using System.Text.Json; - -namespace Host.Controllers; - -[Authorize] -[ApiController] -[Route("api/[controller]")] -public class KlipyController : ControllerBase -{ - private readonly ISettingsService _settings; - private readonly IMemoryCache _cache; - private readonly IHttpClientFactory _httpClientFactory; - - public KlipyController(ISettingsService settings, IMemoryCache cache, IHttpClientFactory httpClientFactory) - { - _settings = settings; - _cache = cache; - _httpClientFactory = httpClientFactory; - } - - [HttpGet("trending")] - public async Task GetTrending() - { - var conf = _settings.Current; - if (!conf.EnableKlipy || string.IsNullOrEmpty(conf.KlipyApiKey)) - return BadRequest(new { error = "Klipy is disabled or not configured." }); - - var cacheKeyTrending = $"klipy_trending_{conf.KlipyApiKey}"; - - if (_cache.TryGetValue(cacheKeyTrending, out JsonElement cachedResult)) - { - return Ok(cachedResult); - } - - try - { - var customerId = string.IsNullOrWhiteSpace(conf.KlipyCustomerId) ? "anonymous" : conf.KlipyCustomerId; - var client = _httpClientFactory.CreateClient(); - var url = $"https://api.klipy.co/api/v1/{conf.KlipyApiKey}/gifs/trending?page=1&per_page=30&customer_id={customerId}"; - - var response = await client.GetAsync(url); - if (!response.IsSuccessStatusCode) - { - var errorContent = await response.Content.ReadAsStringAsync(); - - // fallback to api.klipy.com if .co failed with 404 or something, though maybe not needed - if (response.StatusCode == System.Net.HttpStatusCode.NotFound) { - url = $"https://api.klipy.com/api/v1/{conf.KlipyApiKey}/gifs/trending?page=1&per_page=30&customer_id={customerId}"; - response = await client.GetAsync(url); - errorContent = await response.Content.ReadAsStringAsync(); - } - - if (!response.IsSuccessStatusCode) - return StatusCode(500, new { error = "Failed to fetch from Klipy", details = errorContent, url }); - } - - var result = await response.Content.ReadFromJsonAsync(); - - _cache.Set(cacheKeyTrending, result, TimeSpan.FromMinutes(60)); // Cache trending for 60 minutes - return Ok(result); - } - catch (Exception ex) - { - return StatusCode(500, new { error = "Failed to fetch from Klipy", details = ex.Message }); - } - } - - [HttpGet("search")] - public async Task Search([FromQuery] string q) - { - var conf = _settings.Current; - if (!conf.EnableKlipy || string.IsNullOrEmpty(conf.KlipyApiKey)) - return BadRequest(new { error = "Klipy is disabled or not configured." }); - - if (string.IsNullOrWhiteSpace(q)) - return BadRequest(new { error = "Query is empty." }); - - var cacheKey = $"klipy_search_{conf.KlipyApiKey}_{q.ToLowerInvariant()}"; - if (_cache.TryGetValue(cacheKey, out JsonElement cachedResult)) - { - return Ok(cachedResult); - } - - try - { - var customerId = string.IsNullOrWhiteSpace(conf.KlipyCustomerId) ? "anonymous" : conf.KlipyCustomerId; - var client = _httpClientFactory.CreateClient(); - var url = $"https://api.klipy.co/api/v1/{conf.KlipyApiKey}/gifs/search?page=1&per_page=30&q={Uri.EscapeDataString(q)}&customer_id={customerId}"; - - var response = await client.GetAsync(url); - if (!response.IsSuccessStatusCode) - { - var errorContent = await response.Content.ReadAsStringAsync(); - - if (response.StatusCode == System.Net.HttpStatusCode.NotFound) { - url = $"https://api.klipy.com/api/v1/{conf.KlipyApiKey}/gifs/search?page=1&per_page=30&q={Uri.EscapeDataString(q)}&customer_id={customerId}"; - response = await client.GetAsync(url); - errorContent = await response.Content.ReadAsStringAsync(); - } - - if (!response.IsSuccessStatusCode) - return StatusCode(500, new { error = "Failed to fetch from Klipy", details = errorContent, url }); - } - - var result = await response.Content.ReadFromJsonAsync(); - - _cache.Set(cacheKey, result, TimeSpan.FromMinutes(15)); // Cache searches for 15 minutes - return Ok(result); - } - catch (Exception ex) - { - return StatusCode(500, new { error = "Failed to fetch from Klipy", details = ex.Message }); - } - } -} diff --git a/apps/server-net/src/Host/Endpoints/ConfigEndpoints.cs b/apps/server-net/src/Host/Endpoints/ConfigEndpoints.cs new file mode 100644 index 0000000..8375f0d --- /dev/null +++ b/apps/server-net/src/Host/Endpoints/ConfigEndpoints.cs @@ -0,0 +1,25 @@ +using Carter; +using Host.Services; +using Knot.Shared.Kernel.Constants; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace Host.Endpoints; + +/// +/// Регистрация эндпоинтов для конфигурации приложения. +/// +public sealed class ConfigEndpoints : ICarterModule +{ + public void AddRoutes(IEndpointRouteBuilder app) + { + var group = app.MapGroup(Routes.ApiConfig); // Без RequiresAuthorization, т.к. контроллер раньше был AllowAnonymous для GET + + group.MapGet("/", (IConfigService configService) => + { + var config = configService.GetPublicConfig(); + return Results.Ok(config); + }); + } +} diff --git a/apps/server-net/src/Host/Endpoints/FederationEndpoints.cs b/apps/server-net/src/Host/Endpoints/FederationEndpoints.cs new file mode 100644 index 0000000..b7c9be4 --- /dev/null +++ b/apps/server-net/src/Host/Endpoints/FederationEndpoints.cs @@ -0,0 +1,46 @@ +using Carter; +using Host.Services; +using Knot.Shared.Kernel.Constants; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using System; + +namespace Host.Endpoints; + +/// +/// Регистрация эндпоинтов федерации. +/// +public sealed class FederationEndpoints : ICarterModule +{ + public void AddRoutes(IEndpointRouteBuilder app) + { + var group = app.MapGroup(Routes.ApiFederation); // Без RequireAuthorization для handshake + + group.MapPost("/handshake", async ([FromBody] HandshakeRequest request, IFederationService federationService) => + { + try + { + var result = await federationService.HandshakeAsync(request); + return Results.Ok(result); + } + catch (InvalidOperationException ex) + { + return Results.StatusCode(503); // Service Unavailable + } + catch (ArgumentException ex) + { + return Results.BadRequest(new { error = ex.Message }); + } + catch (UnauthorizedAccessException ex) + { + return Results.Forbid(); + } + catch (Exception ex) + { + return Results.StatusCode(500); + } + }); + } +} diff --git a/apps/server-net/src/Host/Endpoints/KlipyEndpoints.cs b/apps/server-net/src/Host/Endpoints/KlipyEndpoints.cs new file mode 100644 index 0000000..60757df --- /dev/null +++ b/apps/server-net/src/Host/Endpoints/KlipyEndpoints.cs @@ -0,0 +1,60 @@ +using Carter; +using Knot.Shared.Kernel.Constants; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using System; +using System.Threading.Tasks; +using Host.Services; + +namespace Host.Endpoints; + +/// +/// Регистрация эндпоинтов для сервиса Klipy. +/// +public sealed class KlipyEndpoints : ICarterModule +{ + public void AddRoutes(IEndpointRouteBuilder app) + { + var group = app.MapGroup(Routes.ApiKlipy).RequireAuthorization(); + + group.MapGet("/trending", async (IKlipyService klipyService) => + { + try + { + var result = await klipyService.GetTrendingAsync(); + return Results.Ok(result); + } + catch (InvalidOperationException ex) + { + return Results.BadRequest(new { error = ex.Message }); + } + catch (Exception ex) + { + return Results.StatusCode(500); // Можно возвращать JSON с сообщением, но для продакшена лучше централизованный Exception handling + } + }); + + group.MapGet("/search", async ([FromQuery] string q, IKlipyService klipyService) => + { + try + { + var result = await klipyService.SearchAsync(q); + return Results.Ok(result); + } + catch (ArgumentException ex) + { + return Results.BadRequest(new { error = ex.Message }); + } + catch (InvalidOperationException ex) + { + return Results.BadRequest(new { error = ex.Message }); + } + catch (Exception ex) + { + return Results.StatusCode(500); + } + }); + } +} diff --git a/apps/server-net/src/Host/Endpoints/WebRtcEndpoints.cs b/apps/server-net/src/Host/Endpoints/WebRtcEndpoints.cs new file mode 100644 index 0000000..a7a7dac --- /dev/null +++ b/apps/server-net/src/Host/Endpoints/WebRtcEndpoints.cs @@ -0,0 +1,24 @@ +using Carter; +using Knot.Shared.Kernel; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Host.Services; + +namespace Host.Endpoints; + +/// +/// Регистрация эндпоинтов WebRTC +/// +public sealed class WebRtcEndpoints : ICarterModule +{ + public void AddRoutes(IEndpointRouteBuilder app) + { + var group = app.MapGroup(Knot.Shared.Kernel.Constants.Routes.ApiWebRtc) + .RequireAuthorization(); + + group.MapGet("/ice-servers", (IWebRtcService service) => service.GetIceServers()) + .WithName("GetIceServers") + .WithOpenApi(); + } +} diff --git a/apps/server-net/src/Host/Host.csproj b/apps/server-net/src/Host/Host.csproj index a12a735..bed394f 100644 --- a/apps/server-net/src/Host/Host.csproj +++ b/apps/server-net/src/Host/Host.csproj @@ -8,6 +8,7 @@ + diff --git a/apps/server-net/src/Host/Program.cs b/apps/server-net/src/Host/Program.cs index 8b6a967..0c62421 100644 --- a/apps/server-net/src/Host/Program.cs +++ b/apps/server-net/src/Host/Program.cs @@ -62,10 +62,18 @@ builder.Services.AddCors(options => ? originsFromConfig.Split(',') : (!string.IsNullOrEmpty(domain) ? new[] { $"https://{domain}" } : new[] { "*" }); - policy.WithOrigins(origins) - .AllowAnyHeader() - .AllowAnyMethod() - .AllowCredentials(); + var corsBuilder = policy.WithOrigins(origins) + .AllowAnyHeader() + .AllowAnyMethod(); + + if (origins.Contains("*")) + { + // The CORS protocol does not allow specifying a wildcard (any) origin and credentials at the same time. + } + else + { + corsBuilder.AllowCredentials(); + } }); }); diff --git a/apps/server-net/src/Host/Services/ConfigService.cs b/apps/server-net/src/Host/Services/ConfigService.cs new file mode 100644 index 0000000..4e235e1 --- /dev/null +++ b/apps/server-net/src/Host/Services/ConfigService.cs @@ -0,0 +1,18 @@ +using Knot.Shared.Kernel.Configuration; + +namespace Host.Services; + +public sealed class ConfigService : IConfigService +{ + private readonly ISettingsService _settings; + + public ConfigService(ISettingsService settings) + { + _settings = settings; + } + + public IPublicConfig GetPublicConfig() + { + return new PublicConfig(_settings.Current); + } +} diff --git a/apps/server-net/src/Host/Services/FederationService.cs b/apps/server-net/src/Host/Services/FederationService.cs new file mode 100644 index 0000000..6397ca6 --- /dev/null +++ b/apps/server-net/src/Host/Services/FederationService.cs @@ -0,0 +1,44 @@ +using Knot.Shared.Kernel.Configuration; +using Knot.Shared.Kernel.Constants; +using System; +using System.Linq; +using System.Security.Cryptography; +using System.Threading.Tasks; + +namespace Host.Services; + +public sealed class FederationService : IFederationService +{ + private readonly ISettingsService _settings; + + public FederationService(ISettingsService settings) + { + _settings = settings; + } + + public Task HandshakeAsync(HandshakeRequest request) + { + var conf = _settings.Current; + if (!conf.EnableConfederation) + throw new InvalidOperationException(Errors.DisabledByAdmin); + + if (string.IsNullOrEmpty(request.Domain) || string.IsNullOrEmpty(request.PublicKey)) + throw new ArgumentException("Укажите Domain и PublicKey."); + + var allowedList = conf.AllowedDomains?.Select(d => d.Trim().ToLower()).ToList() ?? new System.Collections.Generic.List(); + if (!allowedList.Contains(request.Domain.ToLowerInvariant())) + throw new UnauthorizedAccessException("Домен не входит в белый список."); + + using var rsa = RSA.Create(2048); + var selfPublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey()); + + var response = new HandshakeResponse + { + Domain = Environment.GetEnvironmentVariable("DOMAIN") ?? "knot.local", + PublicKey = selfPublicKey, + Status = "Accepted" + }; + + return Task.FromResult(response); + } +} diff --git a/apps/server-net/src/Host/Services/IConfigService.cs b/apps/server-net/src/Host/Services/IConfigService.cs new file mode 100644 index 0000000..12c550b --- /dev/null +++ b/apps/server-net/src/Host/Services/IConfigService.cs @@ -0,0 +1,11 @@ +using Knot.Shared.Kernel.Configuration; + +namespace Host.Services; + +/// +/// Сервис конфигурации приложения. +/// +public interface IConfigService +{ + IPublicConfig GetPublicConfig(); +} diff --git a/apps/server-net/src/Host/Services/IFederationService.cs b/apps/server-net/src/Host/Services/IFederationService.cs new file mode 100644 index 0000000..cef9207 --- /dev/null +++ b/apps/server-net/src/Host/Services/IFederationService.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; + +namespace Host.Services; + +/// +/// Сервис федерации (связь с другими узлами). +/// +public interface IFederationService +{ + Task HandshakeAsync(HandshakeRequest request); +} + +public class HandshakeRequest +{ + public string Domain { get; set; } = string.Empty; + public string PublicKey { get; set; } = string.Empty; +} + +public class HandshakeResponse +{ + public string Domain { get; set; } = string.Empty; + public string PublicKey { get; set; } = string.Empty; + public string Status { get; set; } = string.Empty; +} diff --git a/apps/server-net/src/Host/Services/IKlipyService.cs b/apps/server-net/src/Host/Services/IKlipyService.cs new file mode 100644 index 0000000..75ad9c1 --- /dev/null +++ b/apps/server-net/src/Host/Services/IKlipyService.cs @@ -0,0 +1,14 @@ +using System.Text.Json; +using System.Threading.Tasks; + +namespace Host.Services; + +/// +/// Сервис интеграции с Klipy GIF. +/// +public interface IKlipyService +{ + Task GetTrendingAsync(); + + Task SearchAsync(string query); +} diff --git a/apps/server-net/src/Host/Services/IPublicConfig.cs b/apps/server-net/src/Host/Services/IPublicConfig.cs new file mode 100644 index 0000000..c93c1d1 --- /dev/null +++ b/apps/server-net/src/Host/Services/IPublicConfig.cs @@ -0,0 +1,14 @@ +namespace Host.Services; + +/// +/// Интерфейс публичной конфигурации приложения. +/// +public interface IPublicConfig +{ + bool EnableCalls { get; } + bool EnableKlipy { get; } + int MaxFileSizeMb { get; } + int MaxGroupMembers { get; } + bool EnableConfederation { get; } + bool EnableRegistration { get; } +} diff --git a/apps/server-net/src/Host/Services/IWebRtcService.cs b/apps/server-net/src/Host/Services/IWebRtcService.cs new file mode 100644 index 0000000..d7f2815 --- /dev/null +++ b/apps/server-net/src/Host/Services/IWebRtcService.cs @@ -0,0 +1,17 @@ +using Knot.Shared.Kernel; +using Microsoft.AspNetCore.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Host.Services; + +/// +/// Сервис для работы с WebRTC конфигурацией. +/// +public interface IWebRtcService +{ + /// + /// Получить список ICE-серверов. + /// + IResult GetIceServers(); +} diff --git a/apps/server-net/src/Host/Services/KlipyService.cs b/apps/server-net/src/Host/Services/KlipyService.cs new file mode 100644 index 0000000..30cce88 --- /dev/null +++ b/apps/server-net/src/Host/Services/KlipyService.cs @@ -0,0 +1,102 @@ +using Knot.Shared.Kernel.Configuration; +using Knot.Shared.Kernel.Constants; +using Microsoft.Extensions.Caching.Memory; +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Host.Services; + +public sealed class KlipyService : IKlipyService +{ + private readonly ISettingsService _settings; + private readonly IMemoryCache _cache; + private readonly IHttpClientFactory _httpClientFactory; + + public KlipyService(ISettingsService settings, IMemoryCache cache, IHttpClientFactory httpClientFactory) + { + _settings = settings; + _cache = cache; + _httpClientFactory = httpClientFactory; + } + + public async Task GetTrendingAsync() + { + var conf = _settings.Current; + if (!conf.EnableKlipy || string.IsNullOrEmpty(conf.KlipyApiKey)) + throw new InvalidOperationException(Errors.KlipyNotConfigured); + + var cacheKeyTrending = $"klipy_trending_{conf.KlipyApiKey}"; + + if (_cache.TryGetValue(cacheKeyTrending, out JsonElement cachedResult)) + { + return cachedResult; + } + + var customerId = string.IsNullOrWhiteSpace(conf.KlipyCustomerId) ? Klipy.DefaultCustomerId : conf.KlipyCustomerId; + var client = _httpClientFactory.CreateClient(); + + var urlCo = string.Format(Klipy.ApiUrlCo, conf.KlipyApiKey, Klipy.ResourceTrending, "", customerId); + + var response = await client.GetAsync(urlCo); + if (!response.IsSuccessStatusCode) + { + if (response.StatusCode == System.Net.HttpStatusCode.NotFound) + { + var urlCom = string.Format(Klipy.ApiUrlCom, conf.KlipyApiKey, Klipy.ResourceTrending, "", customerId); + response = await client.GetAsync(urlCom); + } + + if (!response.IsSuccessStatusCode) + throw new HttpRequestException(Errors.KlipyApiError); + } + + var result = await response.Content.ReadFromJsonAsync(); + + _cache.Set(cacheKeyTrending, result, TimeSpan.FromMinutes(Klipy.TrendingCacheMinutes)); + return result; + } + + public async Task SearchAsync(string query) + { + var conf = _settings.Current; + if (!conf.EnableKlipy || string.IsNullOrEmpty(conf.KlipyApiKey)) + throw new InvalidOperationException(Errors.KlipyNotConfigured); + + if (string.IsNullOrWhiteSpace(query)) + throw new ArgumentException(Errors.InvalidQuery, nameof(query)); + + var cacheKey = $"klipy_search_{conf.KlipyApiKey}_{query.ToLowerInvariant()}"; + + if (_cache.TryGetValue(cacheKey, out JsonElement cachedResult)) + { + return cachedResult; + } + + var customerId = string.IsNullOrWhiteSpace(conf.KlipyCustomerId) ? Klipy.DefaultCustomerId : conf.KlipyCustomerId; + var client = _httpClientFactory.CreateClient(); + + var queryParam = $"q={Uri.EscapeDataString(query)}"; + var urlCo = string.Format(Klipy.ApiUrlCo, conf.KlipyApiKey, Klipy.ResourceSearch, queryParam, customerId); + + var response = await client.GetAsync(urlCo); + if (!response.IsSuccessStatusCode) + { + if (response.StatusCode == System.Net.HttpStatusCode.NotFound) + { + var urlCom = string.Format(Klipy.ApiUrlCom, conf.KlipyApiKey, Klipy.ResourceSearch, queryParam, customerId); + response = await client.GetAsync(urlCom); + } + + if (!response.IsSuccessStatusCode) + throw new HttpRequestException(Errors.KlipySearchError); + } + + var result = await response.Content.ReadFromJsonAsync(); + + _cache.Set(cacheKey, result, TimeSpan.FromMinutes(Klipy.SearchCacheMinutes)); + return result; + } +} diff --git a/apps/server-net/src/Host/Services/PublicConfig.cs b/apps/server-net/src/Host/Services/PublicConfig.cs new file mode 100644 index 0000000..6d24f92 --- /dev/null +++ b/apps/server-net/src/Host/Services/PublicConfig.cs @@ -0,0 +1,16 @@ +using Knot.Shared.Kernel.Configuration; + +namespace Host.Services; + +/// +/// Реализация публичной конфигурации приложения. +/// +public sealed class PublicConfig(SystemSettingsDto settings) : IPublicConfig +{ + public bool EnableCalls => settings.EnableCalls; + public bool EnableKlipy => settings.EnableKlipy; + public int MaxFileSizeMb => settings.MaxFileSizeMb; + public int MaxGroupMembers => settings.MaxGroupMembers; + public bool EnableConfederation => settings.EnableConfederation; + public bool EnableRegistration => settings.EnableRegistration; +} diff --git a/apps/server-net/src/Host/Controllers/WebRtcController.cs b/apps/server-net/src/Host/Services/WebRtcService.cs similarity index 61% rename from apps/server-net/src/Host/Controllers/WebRtcController.cs rename to apps/server-net/src/Host/Services/WebRtcService.cs index 7327d16..46306ca 100644 --- a/apps/server-net/src/Host/Controllers/WebRtcController.cs +++ b/apps/server-net/src/Host/Services/WebRtcService.cs @@ -1,54 +1,53 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Authorization; using Knot.Shared.Kernel.Configuration; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using System.Collections.Generic; -namespace Host.Controllers; +namespace Host.Services; -[Authorize] -[ApiController] -[Route("api/webrtc")] -public sealed class WebRtcController : ControllerBase +/// +/// Реализация сервиса управления WebRTC настройками. +/// +public sealed class WebRtcService : IWebRtcService { private readonly IConfiguration _configuration; private readonly ISettingsService _settingsService; - public WebRtcController(IConfiguration configuration, ISettingsService settingsService) + public WebRtcService( + IConfiguration configuration, + ISettingsService settingsService) { _configuration = configuration; _settingsService = settingsService; } - [HttpGet("ice-servers")] - public IActionResult GetIceServers() + public IResult GetIceServers() { var settings = _settingsService.Current; if (!settings.EnableCalls) { - return StatusCode(503, new { error = "Calls module is disabled by administrator." }); + return Results.StatusCode(503); // TODO: Возвращать структуру ошибки с пояснением на русском языке (Knot.Shared.Kernel.Constants.Errors.DisabledByAdmin) } - var turnUrl = !string.IsNullOrEmpty(settings.TurnHost) - ? $"turn:{settings.TurnHost}:{settings.TurnPort}" + var turnUrl = !string.IsNullOrEmpty(settings.TurnHost) + ? $"turn:{settings.TurnHost}:{settings.TurnPort}" : _configuration["WebRtc:TurnUrl"]; - - - var turnUsername = !string.IsNullOrEmpty(settings.TurnUser) - ? settings.TurnUser + + var turnUsername = !string.IsNullOrEmpty(settings.TurnUser) + ? settings.TurnUser : _configuration["WebRtc:TurnUsername"]; - var turnSecret = !string.IsNullOrEmpty(settings.TurnSecret) - ? settings.TurnSecret + var turnSecret = !string.IsNullOrEmpty(settings.TurnSecret) + ? settings.TurnSecret : _configuration["WebRtc:TurnPassword"]; var iceServers = new List(); if (!string.IsNullOrEmpty(turnUrl)) { - // Use the VPS server as STUN var stunUrl = turnUrl.Replace("turn:", "stun:"); iceServers.Add(new { urls = new[] { stunUrl } }); - - // Allow TURN even without username/password + if (!string.IsNullOrEmpty(turnUsername)) { iceServers.Add(new @@ -67,6 +66,6 @@ public sealed class WebRtcController : ControllerBase } } - return Ok(new { iceServers }); + return Results.Ok(new { iceServers }); } } diff --git a/apps/server-net/src/Shared/Knot.Shared.Kernel/Constants/Errors.cs b/apps/server-net/src/Shared/Knot.Shared.Kernel/Constants/Errors.cs new file mode 100644 index 0000000..90390e7 --- /dev/null +++ b/apps/server-net/src/Shared/Knot.Shared.Kernel/Constants/Errors.cs @@ -0,0 +1,15 @@ +namespace Knot.Shared.Kernel.Constants; + +public static class Errors +{ + public const string Unauthorized = "Не авторизован."; + public const string Forbidden = "Доступ запрещен."; + public const string NotFound = "Сущность не найдена."; + public const string ValidationError = "Ошибка валидации."; + public const string InternalServerError = "Внутренняя ошибка сервера."; + public const string DisabledByAdmin = "Модуль отключен администратором."; + public const string KlipyNotConfigured = "Ключ Klipy не настроен или модуль отключен."; + public const string KlipyApiError = "Ошибка запроса к Klipy API."; + public const string KlipySearchError = "Ошибка запроса к Klipy API при поиске."; + public const string InvalidQuery = "Пустой запрос недопустим."; +} diff --git a/apps/server-net/src/Shared/Knot.Shared.Kernel/Constants/Klipy.cs b/apps/server-net/src/Shared/Knot.Shared.Kernel/Constants/Klipy.cs new file mode 100644 index 0000000..8238383 --- /dev/null +++ b/apps/server-net/src/Shared/Knot.Shared.Kernel/Constants/Klipy.cs @@ -0,0 +1,15 @@ +namespace Knot.Shared.Kernel.Constants; + +public static class Klipy +{ + public const string ApiUrlCo = "https://api.klipy.co/api/v1/{0}/{1}?page=1&per_page=30&{2}&customer_id={3}"; + public const string ApiUrlCom = "https://api.klipy.com/api/v1/{0}/{1}?page=1&per_page=30&{2}&customer_id={3}"; + + public const string ResourceTrending = "gifs/trending"; + public const string ResourceSearch = "gifs/search"; + + public const int TrendingCacheMinutes = 60; + public const int SearchCacheMinutes = 15; + + public const string DefaultCustomerId = "anonymous"; +} diff --git a/apps/server-net/src/Shared/Knot.Shared.Kernel/Constants/Policies.cs b/apps/server-net/src/Shared/Knot.Shared.Kernel/Constants/Policies.cs new file mode 100644 index 0000000..bf4436d --- /dev/null +++ b/apps/server-net/src/Shared/Knot.Shared.Kernel/Constants/Policies.cs @@ -0,0 +1,6 @@ +namespace Knot.Shared.Kernel.Constants; + +public enum Policies +{ + AdminOnly +} diff --git a/apps/server-net/src/Shared/Knot.Shared.Kernel/Constants/Roles.cs b/apps/server-net/src/Shared/Knot.Shared.Kernel/Constants/Roles.cs new file mode 100644 index 0000000..9902cd7 --- /dev/null +++ b/apps/server-net/src/Shared/Knot.Shared.Kernel/Constants/Roles.cs @@ -0,0 +1,7 @@ +namespace Knot.Shared.Kernel.Constants; + +public enum Roles +{ + Admin, + User +} diff --git a/apps/server-net/src/Shared/Knot.Shared.Kernel/Constants/Routes.cs b/apps/server-net/src/Shared/Knot.Shared.Kernel/Constants/Routes.cs new file mode 100644 index 0000000..15399f8 --- /dev/null +++ b/apps/server-net/src/Shared/Knot.Shared.Kernel/Constants/Routes.cs @@ -0,0 +1,18 @@ +namespace Knot.Shared.Kernel.Constants; + +public static class Routes +{ + public const string ApiAdmin = "/api/admin"; + public const string ApiAuth = "/api/auth"; + public const string ApiChats = "/api/chats"; + public const string ApiConfig = "/api/config"; + public const string ApiFederation = "/api/federation"; + public const string ApiFiles = "/api/files"; + public const string ApiFriends = "/api/friends"; + public const string ApiKlipy = "/api/klipy"; + public const string ApiMessages = "/api/messages"; + public const string ApiStories = "/api/stories"; + public const string ApiTelegramImport = "/api/telegram/import"; + public const string ApiUsers = "/api/users"; + public const string ApiWebRtc = "/api/webrtc"; +}