using PnvPanel.Domain.Common;
namespace PnvPanel.Domain.Inbounds;
///
/// Проекция inbound из 3x-ui. — идентификатор на стороне панели
/// (в ThreeXui.Net это string, а не число — так отдаёт API 3x-ui).
/// AllowedRoleIds хранит только Guid ролей (не навигацию на AppRole — тот живёт в Infrastructure/Identity,
/// Domain не должен на него ссылаться).
///
public sealed class Inbound : Entity
{
public Guid NodeId { get; private set; }
public string RemoteInboundId { get; private set; } = string.Empty;
public VpnProtocol Protocol { get; private set; }
public string Remark { get; private set; } = string.Empty;
public int Port { get; private set; }
public bool IsPublished { get; private set; }
public bool IsAvailable { get; private set; } = true;
public string? DisplayName { get; private set; }
/// ISO 3166-1 alpha-2 (верхний регистр) — страна, флаг которой пользователь видит перед
/// именем инбаунда. Только для отображения, на подключение не влияет.
public string? CountryCode { get; private set; }
public IReadOnlyList AllowedRoleIds { get; private set; } = [];
public DateTimeOffset? LastSyncAt { get; private set; }
private Inbound() { }
public static Inbound FromRemote(
Guid nodeId,
string remoteInboundId,
VpnProtocol protocol,
string remark,
int port
)
{
return new Inbound
{
Id = Guid.NewGuid(),
NodeId = nodeId,
RemoteInboundId = remoteInboundId,
Protocol = protocol,
Remark = remark,
Port = port,
IsPublished = false,
IsAvailable = true,
LastSyncAt = DateTimeOffset.UtcNow,
};
}
public void UpdateFromRemote(VpnProtocol protocol, string remark, int port)
{
Protocol = protocol;
Remark = remark;
Port = port;
LastSyncAt = DateTimeOffset.UtcNow;
// Инбаунд снова пришёл в синхронизации — значит панель его видит, снимаем MarkUnavailable()
// (иначе после ЛЮБОГО временного сбоя синка — не только реального удаления — Revoke навсегда
// пропускал бы обращение к панели для конфигов на этом инбаунде, хотя клиент в панели жив).
IsAvailable = true;
}
public void Publish(
string? displayName,
string? countryCode,
IReadOnlyCollection allowedRoleIds
)
{
IsPublished = true;
IsAvailable = true;
DisplayName = displayName;
CountryCode = string.IsNullOrWhiteSpace(countryCode)
? null
: countryCode.Trim().ToUpperInvariant();
AllowedRoleIds = allowedRoleIds.Distinct().ToList();
}
public void Unpublish() => IsPublished = false;
/// Роль удалена админом — снимаем её из списка разрешённых, чтобы не оставлять ссылку на
/// несуществующую роль (иначе она навсегда "съедала" бы место в списке, ни на что не влияя, но и
/// не позволяя понять, что инбаунд стал доступен меньшему числу ролей, чем задумано).
public void RemoveAllowedRole(Guid roleId) => AllowedRoleIds = AllowedRoleIds.Where(id => id != roleId).ToList();
///
/// Инбаунд пропал на панели (не пришёл в очередной синхронизации), но по нему есть конфиги —
/// удалить запись нельзя (FK), поэтому помечаем недоступной вместо тихого "не опубликован":
/// UI показывает явный статус, а Revoke пропускает обращение к панели как заведомо мёртвое.
///
public void MarkUnavailable()
{
IsAvailable = false;
IsPublished = false;
}
}