Правка архитектуры по бэку

This commit is contained in:
Халимов Рустам
2026-03-17 17:45:45 +03:00
parent e7cdc544d0
commit 900ca6f8b8
24 changed files with 511 additions and 239 deletions

View File

@@ -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
});
}
}

View File

@@ -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<string>();
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;
}

View File

@@ -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<IActionResult> 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<JsonElement>();
_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<IActionResult> 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<JsonElement>();
_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 });
}
}
}

View File

@@ -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;
/// <summary>
/// Регистрация эндпоинтов для конфигурации приложения.
/// </summary>
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);
});
}
}

View File

@@ -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;
/// <summary>
/// Регистрация эндпоинтов федерации.
/// </summary>
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);
}
});
}
}

View File

@@ -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;
/// <summary>
/// Регистрация эндпоинтов для сервиса Klipy.
/// </summary>
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);
}
});
}
}

View File

@@ -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;
/// <summary>
/// Регистрация эндпоинтов WebRTC
/// </summary>
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();
}
}

View File

@@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="AngleSharp" Version="1.4.0" />
<PackageReference Include="Carter" Version="10.0.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
<PackageReference Include="Mapster" Version="7.4.0" />

View File

@@ -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();
}
});
});

View File

@@ -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);
}
}

View File

@@ -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<HandshakeResponse> 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<string>();
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);
}
}

View File

@@ -0,0 +1,11 @@
using Knot.Shared.Kernel.Configuration;
namespace Host.Services;
/// <summary>
/// Сервис конфигурации приложения.
/// </summary>
public interface IConfigService
{
IPublicConfig GetPublicConfig();
}

View File

@@ -0,0 +1,24 @@
using System.Threading.Tasks;
namespace Host.Services;
/// <summary>
/// Сервис федерации (связь с другими узлами).
/// </summary>
public interface IFederationService
{
Task<HandshakeResponse> 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;
}

View File

@@ -0,0 +1,14 @@
using System.Text.Json;
using System.Threading.Tasks;
namespace Host.Services;
/// <summary>
/// Сервис интеграции с Klipy GIF.
/// </summary>
public interface IKlipyService
{
Task<JsonElement?> GetTrendingAsync();
Task<JsonElement?> SearchAsync(string query);
}

View File

@@ -0,0 +1,14 @@
namespace Host.Services;
/// <summary>
/// Интерфейс публичной конфигурации приложения.
/// </summary>
public interface IPublicConfig
{
bool EnableCalls { get; }
bool EnableKlipy { get; }
int MaxFileSizeMb { get; }
int MaxGroupMembers { get; }
bool EnableConfederation { get; }
bool EnableRegistration { get; }
}

View File

@@ -0,0 +1,17 @@
using Knot.Shared.Kernel;
using Microsoft.AspNetCore.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Host.Services;
/// <summary>
/// Сервис для работы с WebRTC конфигурацией.
/// </summary>
public interface IWebRtcService
{
/// <summary>
/// Получить список ICE-серверов.
/// </summary>
IResult GetIceServers();
}

View File

@@ -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<JsonElement?> 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<JsonElement>();
_cache.Set(cacheKeyTrending, result, TimeSpan.FromMinutes(Klipy.TrendingCacheMinutes));
return result;
}
public async Task<JsonElement?> 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<JsonElement>();
_cache.Set(cacheKey, result, TimeSpan.FromMinutes(Klipy.SearchCacheMinutes));
return result;
}
}

View File

@@ -0,0 +1,16 @@
using Knot.Shared.Kernel.Configuration;
namespace Host.Services;
/// <summary>
/// Реализация публичной конфигурации приложения.
/// </summary>
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;
}

View File

@@ -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
/// <summary>
/// Реализация сервиса управления WebRTC настройками.
/// </summary>
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<object>();
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 });
}
}

View File

@@ -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 = "Пустой запрос недопустим.";
}

View File

@@ -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";
}

View File

@@ -0,0 +1,6 @@
namespace Knot.Shared.Kernel.Constants;
public enum Policies
{
AdminOnly
}

View File

@@ -0,0 +1,7 @@
namespace Knot.Shared.Kernel.Constants;
public enum Roles
{
Admin,
User
}

View File

@@ -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";
}